MediaWiki:ClamComponent.js: Difference between revisions
Jump to navigation
Jump to search
RobertWesner (talk | contribs) No edit summary |
RobertWesner (talk | contribs) m Remove now obsolete 7AM UTC status. |
||
| (3 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/** | (() => { | ||
/** | |||
* Reusable XSS-safe html-builder DSL. | |||
* Taken from Roberts userscripts so we can avoid insecure innerHtml nonsense. | |||
* | |||
* Example: | |||
* $builder('div') | |||
* .id('foo') | |||
* .className('a b c') | |||
* .onBuildText('<b>Test</b>') | |||
* .build() | |||
* | |||
* Result: | |||
* ``` | |||
* <div id="foo" class="a b c"> | |||
* <b>Test</b> | |||
* </div>; | |||
* ``` | |||
* | |||
* @license MIT | |||
* @author Robert Wesner | |||
* @see https://github.com/RobertWesner/YouTube-Play-All/blob/main/script.user.js | |||
const { $builder, $style } = (()=>{const $builder=t=>(t=>{const e=t.match(/^[a-zA-Z0-9-]+/);if(!e)throw"Invalid tag supplied to parseQuery.";const n=e[0],r=(t=>{let e=[];const getOperations=()=>{const t=[...e.flat()];return e=[],t},n=new Proxy(t,{get(r,a,s){const P=t=>(...e)=>(t(...e),n),PBO=t=>P(((...n)=>e.push((()=>t(...n)))));switch(a){case"build":return()=>(getOperations().forEach((e=>e(t))),t);case"buildWithSync":return async()=>{for(const e of getOperations()){const n=e(t);n&&"function"==typeof n.then&&await n}return t};case"addClass":return P((e=>t.classList.add(e)));case"onBuild":return P((t=>e.push(t)));case"onBuildAppend":return PBO(((...e)=>t.append(...e)));case"onBuildText":return PBO((e=>t.textContent=e));case"on":return P(((e,n)=>t.addEventListener(e,n)))}const c=["hidden","style"];return e=>(!c.includes(a)&&a in t?t[a]=e:t.setAttribute(a.replaceAll("_","-"),e),n)}});return n})(document.createElement(n));if(!t.match(/^[a-zA-Z0-9-]+$/)){const e=(t=>t.match(/^[a-zA-Z0-9-]+((?:[#.][a-zA-Z0-9-]+)+)?((?:\[[a-zA-Z0-9-]+(?:=".*?")?])+)?/))(t),n=e[1]??"",a=e[2]??"";n.matchAll(/([#.])([a-zA-Z0-9-]+)/g).forEach((([t,e,n])=>{({"#":r.id,".":r.addClass})[e](n)})),a.matchAll(/\[([a-zA-Z0-9-]+)(?:="(.*?)")?]/g).forEach((([t,e,n])=>{r[e](n)}))}return r})(t);return{$builder,$style:(t,e)=>document.head.insertAdjacentElement("beforeend",$builder(`style#${t}`).onBuild((t=>t.textContent=e)).build())}})(); | */ | ||
const { $builder, $style } = (()=>{const $builder=t=>(t=>{const e=t.match(/^[a-zA-Z0-9-]+/);if(!e)throw"Invalid tag supplied to parseQuery.";const n=e[0],r=(t=>{let e=[];const getOperations=()=>{const t=[...e.flat()];return e=[],t},n=new Proxy(t,{get(r,a,s){const P=t=>(...e)=>(t(...e),n),PBO=t=>P(((...n)=>e.push((()=>t(...n)))));switch(a){case"build":return()=>(getOperations().forEach((e=>e(t))),t);case"buildWithSync":return async()=>{for(const e of getOperations()){const n=e(t);n&&"function"==typeof n.then&&await n}return t};case"addClass":return P((e=>t.classList.add(e)));case"onBuild":return P((t=>e.push(t)));case"onBuildAppend":return PBO(((...e)=>t.append(...e)));case"onBuildText":return PBO((e=>t.textContent=e));case"on":return P(((e,n)=>t.addEventListener(e,n)))}const c=["hidden","style"];return e=>(!c.includes(a)&&a in t?t[a]=e:t.setAttribute(a.replaceAll("_","-"),e),n)}});return n})(document.createElement(n));if(!t.match(/^[a-zA-Z0-9-]+$/)){const e=(t=>t.match(/^[a-zA-Z0-9-]+((?:[#.][a-zA-Z0-9-]+)+)?((?:\[[a-zA-Z0-9-]+(?:=".*?")?])+)?/))(t),n=e[1]??"",a=e[2]??"";n.matchAll(/([#.])([a-zA-Z0-9-]+)/g).forEach((([t,e,n])=>{({"#":r.id,".":r.addClass})[e](n)})),a.matchAll(/\[([a-zA-Z0-9-]+)(?:="(.*?)")?]/g).forEach((([t,e,n])=>{r[e](n)}))}return r})(t);return{$builder,$style:(t,e)=>document.head.insertAdjacentElement("beforeend",$builder(`style#${t}`).onBuild((t=>t.textContent=e)).build())}})(); | |||
const $bold = text => $builder('b').onBuildText(text).build(); | |||
$style('clam-components', ` | $style('clam-components', ` | ||
.clam-component---online-badge { | |||
border: 1px solid #2c303624; | |||
background-color: | background-color: white; | ||
color: white; | color: white; | ||
padding: 0.12em 0.4em; | padding: 0.12em 0.4em; | ||
border-radius: 0.32em; | border-radius: 0.32em; | ||
font-weight: bold; | |||
&[data-online="true"] { | |||
color: | &[data-online="true"]::after { | ||
color: #11cc0e; | |||
content: '● online'; | |||
} | } | ||
&[data-online="false"] { | &[data-online="false"]::after { | ||
color: | color: #cc140e; | ||
content: '● offline'; | |||
} | } | ||
} | } | ||
} | |||
.clam-component---server-status { | |||
font-weight: bold; | |||
&[data-status="online"]::after { | |||
color: #11cc0e; | |||
content: '● online'; | |||
} | |||
&[data-status="offline"]::after { | |||
color: #cc140e; | |||
content: '● offline'; | |||
} | |||
} | |||
.clam-component---mc-text > .cc-mc-text { | |||
&[data-color="0"] { color: #000000 } | |||
&[data-color="1"] { color: #0000AA } | |||
&[data-color="2"] { color: #00AA00 } | |||
&[data-color="3"] { color: #00AAAA } | |||
&[data-color="4"] { color: #AA0000 } | |||
&[data-color="5"] { color: #AA00AA } | |||
&[data-color="6"] { color: #FFAA00 } | |||
&[data-color="7"] { color: #AAAAAA } | |||
&[data-color="8"] { color: #555555 } | |||
&[data-color="9"] { color: #5555FF } | |||
&[data-color="a"] { color: #55FF55 } | |||
&[data-color="b"] { color: #55FFFF } | |||
&[data-color="c"] { color: #FF5555 } | |||
&[data-color="d"] { color: #FF55FF } | |||
&[data-color="e"] { color: #FFFF55 } | |||
&[data-color="f"] { color: #FFFFFF } | |||
} | |||
.clam-component---stats { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
gap: 0.6em; | |||
float: right; | |||
padding: 0.6em; | |||
background-color: #69696912; | |||
border: 1px solid #80808080; | |||
border-radius: 1em; | |||
} | |||
`); | |||
const mcText = raw => $builder('span.clam-component---mc-text') | |||
.onBuildAppend( | |||
...raw | |||
.split('§') | |||
.map(part => | |||
$builder('span.cc-mc-text') | |||
.data_color(part.charAt(0)) | |||
.onBuildText(part.slice(1)) | |||
.build(), | |||
), | |||
) | |||
.build(); | |||
const date = timestamp => new Date(timestamp) | |||
.toISOString() | |||
.replace('T', ' ') | |||
.slice(0, 16); | |||
const time = seconds => { | |||
const days = Math.floor(seconds / 86400000); | |||
const hours = Math.floor((seconds % 86400000) / 3600000); | |||
const minutes = Math.floor((seconds % 3600000) / 60000); | |||
let result = ''; | |||
if (days) result += days + 'd '; | |||
if (hours) result += hours + 'h '; | |||
if (minutes) result += minutes + 'm'; | |||
return result.trim(); | |||
}; | |||
const route = (path, callable) => { | |||
const matches = location.pathname.match(path); | |||
if (matches) callable(...matches.slice(1)); | |||
}; | |||
const component = (selector, callable) => | |||
document.querySelectorAll(selector) | |||
.forEach(element => { | |||
const argsMatches = element.className.match(/\(([^()]+?)\)/); | |||
const args = argsMatches | |||
? Object.fromEntries( | |||
argsMatches[1] | |||
.split(',') | |||
.map(arg => { | |||
const parts = arg.trim().split('=', 2); | |||
return [parts[0], parts[1] ?? true]; | |||
}), | |||
) : {}; | |||
element.replaceWith(callable(args)); | |||
}); | |||
route(/^\/index.php\/Main_Page$/, async () => { | |||
let status = 'offline'; | |||
try { | |||
if ((await statsClient.server()).unique_players) status = 'online'; | |||
} catch (e) {} | |||
component('br.cc.-server-status', () => | |||
$builder('span.clam-component---server-status') | |||
.data_status(status) | |||
.build(), | |||
); | |||
}); | |||
route(/^\/index.php\/User:([^\/]+)/, async username => { | |||
const stats = await statsClient.player(username); | |||
component('br.cc.-online', () => | |||
$builder('span.clam-component---online-badge') | |||
.data_online(stats.online) | |||
.build(), | |||
); | |||
component('br.cc.-stats', args => { | |||
const element = (title, value) => $builder('div').onBuildAppend($bold(title), ': ', value).build(); | |||
return $builder('div.clam-component---stats') | |||
.onBuildAppend(...[ | |||
. | $builder('b').onBuildAppend(...[ | ||
mcText(stats.prefix), | |||
stats.name, | |||
args.online && $builder('span.clam-component---online-badge') | |||
.style('margin-left: 0.48em') | |||
.data_online(stats.online) | |||
.build(), | |||
].filter(_ => !!_)).build(), | |||
args.skin === 'face' && $builder(`img[src="https://minotar.net/avatar/${stats.uuid}/128.png"]`).build(), | |||
args.skin === 'full' && $builder(`img[src="https://api.mineatar.io/body/full/${stats.uuid}?scale=8#.png"]`).build(), | |||
$builder('div.cc-stats-container') | |||
.onBuildAppend(...[ | |||
args.firstJoin && element('Joined', date(stats.first_join)), | |||
args.lastSeen && element( | |||
'Seen', | |||
stats.online | |||
? $builder('span').style("color: #11cc0e").onBuildText('online').build() | |||
: date(stats.last_join), | |||
), | |||
args.playTime && element('Played', time(stats.playtime)), | |||
args.score && element('Score', stats.overall.toFixed(2)), | |||
args.balance && element('Balance', stats.balance.toFixed(2)), | |||
args.blocksPlaced && element('Blocks placed', stats.blocks_placed), | |||
args.blocksBroken && element('Blocks broken', stats.blocks_broken), | |||
args.blocksTraveled && element('Traveled', Math.floor(stats.blocks_traveled)), | |||
args.deaths && element('Deaths', stats.deaths), | |||
args.playersKilled && element('Players killed', stats.players_killed), | |||
args.mobsKilled && element('Mobs killed', stats.mobs_killed), | |||
args.damageDealt && element('Damage dealt', stats.damage_dealt), | |||
args.damageTaken && element('Damage taken', stats.damage_taken), | |||
].filter(_ => !!_)) | |||
.build(), | |||
].filter(_ => !!_)) | |||
.build(); | |||
}); | }); | ||
} | }); | ||
})(); | })(); | ||
Latest revision as of 08:48, 15 October 2026
(() => {
/**
* Reusable XSS-safe html-builder DSL.
* Taken from Roberts userscripts so we can avoid insecure innerHtml nonsense.
*
* Example:
* $builder('div')
* .id('foo')
* .className('a b c')
* .onBuildText('<b>Test</b>')
* .build()
*
* Result:
* ```
* <div id="foo" class="a b c">
* <b>Test</b>
* </div>;
* ```
*
* @license MIT
* @author Robert Wesner
* @see https://github.com/RobertWesner/YouTube-Play-All/blob/main/script.user.js
*/
const { $builder, $style } = (()=>{const $builder=t=>(t=>{const e=t.match(/^[a-zA-Z0-9-]+/);if(!e)throw"Invalid tag supplied to parseQuery.";const n=e[0],r=(t=>{let e=[];const getOperations=()=>{const t=[...e.flat()];return e=[],t},n=new Proxy(t,{get(r,a,s){const P=t=>(...e)=>(t(...e),n),PBO=t=>P(((...n)=>e.push((()=>t(...n)))));switch(a){case"build":return()=>(getOperations().forEach((e=>e(t))),t);case"buildWithSync":return async()=>{for(const e of getOperations()){const n=e(t);n&&"function"==typeof n.then&&await n}return t};case"addClass":return P((e=>t.classList.add(e)));case"onBuild":return P((t=>e.push(t)));case"onBuildAppend":return PBO(((...e)=>t.append(...e)));case"onBuildText":return PBO((e=>t.textContent=e));case"on":return P(((e,n)=>t.addEventListener(e,n)))}const c=["hidden","style"];return e=>(!c.includes(a)&&a in t?t[a]=e:t.setAttribute(a.replaceAll("_","-"),e),n)}});return n})(document.createElement(n));if(!t.match(/^[a-zA-Z0-9-]+$/)){const e=(t=>t.match(/^[a-zA-Z0-9-]+((?:[#.][a-zA-Z0-9-]+)+)?((?:\[[a-zA-Z0-9-]+(?:=".*?")?])+)?/))(t),n=e[1]??"",a=e[2]??"";n.matchAll(/([#.])([a-zA-Z0-9-]+)/g).forEach((([t,e,n])=>{({"#":r.id,".":r.addClass})[e](n)})),a.matchAll(/\[([a-zA-Z0-9-]+)(?:="(.*?)")?]/g).forEach((([t,e,n])=>{r[e](n)}))}return r})(t);return{$builder,$style:(t,e)=>document.head.insertAdjacentElement("beforeend",$builder(`style#${t}`).onBuild((t=>t.textContent=e)).build())}})();
const $bold = text => $builder('b').onBuildText(text).build();
$style('clam-components', `
.clam-component---online-badge {
border: 1px solid #2c303624;
background-color: white;
color: white;
padding: 0.12em 0.4em;
border-radius: 0.32em;
font-weight: bold;
&[data-online="true"]::after {
color: #11cc0e;
content: '● online';
}
&[data-online="false"]::after {
color: #cc140e;
content: '● offline';
}
}
.clam-component---server-status {
font-weight: bold;
&[data-status="online"]::after {
color: #11cc0e;
content: '● online';
}
&[data-status="offline"]::after {
color: #cc140e;
content: '● offline';
}
}
.clam-component---mc-text > .cc-mc-text {
&[data-color="0"] { color: #000000 }
&[data-color="1"] { color: #0000AA }
&[data-color="2"] { color: #00AA00 }
&[data-color="3"] { color: #00AAAA }
&[data-color="4"] { color: #AA0000 }
&[data-color="5"] { color: #AA00AA }
&[data-color="6"] { color: #FFAA00 }
&[data-color="7"] { color: #AAAAAA }
&[data-color="8"] { color: #555555 }
&[data-color="9"] { color: #5555FF }
&[data-color="a"] { color: #55FF55 }
&[data-color="b"] { color: #55FFFF }
&[data-color="c"] { color: #FF5555 }
&[data-color="d"] { color: #FF55FF }
&[data-color="e"] { color: #FFFF55 }
&[data-color="f"] { color: #FFFFFF }
}
.clam-component---stats {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6em;
float: right;
padding: 0.6em;
background-color: #69696912;
border: 1px solid #80808080;
border-radius: 1em;
}
`);
const mcText = raw => $builder('span.clam-component---mc-text')
.onBuildAppend(
...raw
.split('§')
.map(part =>
$builder('span.cc-mc-text')
.data_color(part.charAt(0))
.onBuildText(part.slice(1))
.build(),
),
)
.build();
const date = timestamp => new Date(timestamp)
.toISOString()
.replace('T', ' ')
.slice(0, 16);
const time = seconds => {
const days = Math.floor(seconds / 86400000);
const hours = Math.floor((seconds % 86400000) / 3600000);
const minutes = Math.floor((seconds % 3600000) / 60000);
let result = '';
if (days) result += days + 'd ';
if (hours) result += hours + 'h ';
if (minutes) result += minutes + 'm';
return result.trim();
};
const route = (path, callable) => {
const matches = location.pathname.match(path);
if (matches) callable(...matches.slice(1));
};
const component = (selector, callable) =>
document.querySelectorAll(selector)
.forEach(element => {
const argsMatches = element.className.match(/\(([^()]+?)\)/);
const args = argsMatches
? Object.fromEntries(
argsMatches[1]
.split(',')
.map(arg => {
const parts = arg.trim().split('=', 2);
return [parts[0], parts[1] ?? true];
}),
) : {};
element.replaceWith(callable(args));
});
route(/^\/index.php\/Main_Page$/, async () => {
let status = 'offline';
try {
if ((await statsClient.server()).unique_players) status = 'online';
} catch (e) {}
component('br.cc.-server-status', () =>
$builder('span.clam-component---server-status')
.data_status(status)
.build(),
);
});
route(/^\/index.php\/User:([^\/]+)/, async username => {
const stats = await statsClient.player(username);
component('br.cc.-online', () =>
$builder('span.clam-component---online-badge')
.data_online(stats.online)
.build(),
);
component('br.cc.-stats', args => {
const element = (title, value) => $builder('div').onBuildAppend($bold(title), ': ', value).build();
return $builder('div.clam-component---stats')
.onBuildAppend(...[
$builder('b').onBuildAppend(...[
mcText(stats.prefix),
stats.name,
args.online && $builder('span.clam-component---online-badge')
.style('margin-left: 0.48em')
.data_online(stats.online)
.build(),
].filter(_ => !!_)).build(),
args.skin === 'face' && $builder(`img[src="https://minotar.net/avatar/${stats.uuid}/128.png"]`).build(),
args.skin === 'full' && $builder(`img[src="https://api.mineatar.io/body/full/${stats.uuid}?scale=8#.png"]`).build(),
$builder('div.cc-stats-container')
.onBuildAppend(...[
args.firstJoin && element('Joined', date(stats.first_join)),
args.lastSeen && element(
'Seen',
stats.online
? $builder('span').style("color: #11cc0e").onBuildText('online').build()
: date(stats.last_join),
),
args.playTime && element('Played', time(stats.playtime)),
args.score && element('Score', stats.overall.toFixed(2)),
args.balance && element('Balance', stats.balance.toFixed(2)),
args.blocksPlaced && element('Blocks placed', stats.blocks_placed),
args.blocksBroken && element('Blocks broken', stats.blocks_broken),
args.blocksTraveled && element('Traveled', Math.floor(stats.blocks_traveled)),
args.deaths && element('Deaths', stats.deaths),
args.playersKilled && element('Players killed', stats.players_killed),
args.mobsKilled && element('Mobs killed', stats.mobs_killed),
args.damageDealt && element('Damage dealt', stats.damage_dealt),
args.damageTaken && element('Damage taken', stats.damage_taken),
].filter(_ => !!_))
.build(),
].filter(_ => !!_))
.build();
});
});
})();