webscale #5

Closed
D wants to merge 1 commit from D/tghandbook:load-performance into master
4 changed files with 266 additions and 67 deletions

View file

@ -2,6 +2,7 @@
import speen from "~/assets/images/speen.svg"; import speen from "~/assets/images/speen.svg";
import { getPageHTML } from "./wiki"; import { getPageHTML } from "./wiki";
import userscript from "./userscript"; import userscript from "./userscript";
import { nextAnimationFrame } from "./utils";
function initWaiting(elem: HTMLElement) { function initWaiting(elem: HTMLElement) {
// Add spinner // Add spinner
@ -27,13 +28,11 @@ async function loadPage(page: string, elem: HTMLElement) {
html = html.replace(/"\/wiki/gi, '"//tgstation13.org/wiki'); html = html.replace(/"\/wiki/gi, '"//tgstation13.org/wiki');
// Set as HTML content and run HTML manipulations on it // Set as HTML content and run HTML manipulations on it
requestAnimationFrame(() => { await nextAnimationFrame();
elem.innerHTML = html; elem.innerHTML = html;
console.log(`${page}: processing`); await nextAnimationFrame();
userscript(elem, page); await userscript(elem, page);
console.log(`${page}: userscript applied`);
elem.classList.remove("waiting"); elem.classList.remove("waiting");
});
} }
type TabElements = { tabListItem: HTMLElement; tabContentItem: HTMLElement }; type TabElements = { tabListItem: HTMLElement; tabContentItem: HTMLElement };
@ -50,7 +49,7 @@ export default class TabManager {
this.tabContentContainer = tabcontent; this.tabContentContainer = tabcontent;
} }
openTab(page: string, setActive: boolean): void { async openTab(page: string, setActive: boolean): void {
// Create tab list item // Create tab list item
const tabListItem = document.createElement("div"); const tabListItem = document.createElement("div");
tabListItem.className = "tab"; tabListItem.className = "tab";
@ -72,7 +71,7 @@ export default class TabManager {
this.tabContentContainer.appendChild(tabContentItem); this.tabContentContainer.appendChild(tabContentItem);
// Start loading page for new tab // Start loading page for new tab
loadPage(page, tabContentItem); await loadPage(page, tabContentItem);
// Create tab entry // Create tab entry
this.tabs[page] = { tabListItem, tabContentItem }; this.tabs[page] = { tabListItem, tabContentItem };

View file

@ -1,3 +1,5 @@
import { nextAnimationFrame } from "./utils";
interface ColorRGB { interface ColorRGB {
r: number; r: number;
g: number; g: number;
@ -15,6 +17,166 @@ export enum ColorFmt {
HEX, // #RRGGBB HEX, // #RRGGBB
} }
/* https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
$$('#colors_table tbody tr')
.map((row) => {
const [name, hex] = Array.from(row.children).slice(-3);
return [name.innerText.trim().replace(/\s.+/g, ''), hex.innerText.trim()];
}).sort((a, b) => [a[0], b[0]].sort()[0] === a[0] ? -1 : 1)
.map(([name, hex]) => `${name} : '${hex}',`)
.join('\n')
*/
const namedColors = {
aliceblue : '#f0f8ff',
antiquewhite : '#faebd7',
aqua : '#00ffff',
aquamarine : '#7fffd4',
azure : '#f0ffff',
beige : '#f5f5dc',
bisque : '#ffe4c4',
black : '#000000',
blanchedalmond : '#ffebcd',
blue : '#0000ff',
blueviolet : '#8a2be2',
brown : '#a52a2a',
burlywood : '#deb887',
cadetblue : '#5f9ea0',
chartreuse : '#7fff00',
chocolate : '#d2691e',
coral : '#ff7f50',
cornflowerblue : '#6495ed',
cornsilk : '#fff8dc',
crimson : '#dc143c',
cyan : '#00ffff',
darkblue : '#00008b',
darkcyan : '#008b8b',
darkgoldenrod : '#b8860b',
darkgray : '#a9a9a9',
darkgreen : '#006400',
darkgrey : '#a9a9a9',
darkkhaki : '#bdb76b',
darkmagenta : '#8b008b',
darkolivegreen : '#556b2f',
darkorange : '#ff8c00',
darkorchid : '#9932cc',
darkred : '#8b0000',
darksalmon : '#e9967a',
darkseagreen : '#8fbc8f',
darkslateblue : '#483d8b',
darkslategray : '#2f4f4f',
darkslategrey : '#2f4f4f',
darkturquoise : '#00ced1',
darkviolet : '#9400d3',
deeppink : '#ff1493',
deepskyblue : '#00bfff',
dimgray : '#696969',
dimgrey : '#696969',
dodgerblue : '#1e90ff',
firebrick : '#b22222',
floralwhite : '#fffaf0',
forestgreen : '#228b22',
fuchsia : '#ff00ff',
gainsboro : '#dcdcdc',
ghostwhite : '#f8f8ff',
gold : '#ffd700',
goldenrod : '#daa520',
gray : '#808080',
green : '#008000',
greenyellow : '#adff2f',
grey : '#808080',
honeydew : '#f0fff0',
hotpink : '#ff69b4',
indianred : '#cd5c5c',
indigo : '#4b0082',
ivory : '#fffff0',
khaki : '#f0e68c',
lavender : '#e6e6fa',
lavenderblush : '#fff0f5',
lawngreen : '#7cfc00',
lemonchiffon : '#fffacd',
lightblue : '#add8e6',
lightcoral : '#f08080',
lightcyan : '#e0ffff',
lightgoldenrodyellow : '#fafad2',
lightgray : '#d3d3d3',
lightgreen : '#90ee90',
lightgrey : '#d3d3d3',
lightpink : '#ffb6c1',
lightsalmon : '#ffa07a',
lightseagreen : '#20b2aa',
lightskyblue : '#87cefa',
lightslategray : '#778899',
lightslategrey : '#778899',
lightsteelblue : '#b0c4de',
lightyellow : '#ffffe0',
lime : '#00ff00',
limegreen : '#32cd32',
linen : '#faf0e6',
magenta : '#ff00ff',
maroon : '#800000',
mediumaquamarine : '#66cdaa',
mediumblue : '#0000cd',
mediumorchid : '#ba55d3',
mediumpurple : '#9370db',
mediumseagreen : '#3cb371',
mediumslateblue : '#7b68ee',
mediumspringgreen : '#00fa9a',
mediumturquoise : '#48d1cc',
mediumvioletred : '#c71585',
midnightblue : '#191970',
mintcream : '#f5fffa',
mistyrose : '#ffe4e1',
moccasin : '#ffe4b5',
navajowhite : '#ffdead',
navy : '#000080',
oldlace : '#fdf5e6',
olive : '#808000',
olivedrab : '#6b8e23',
orange : '#ffa500',
orangered : '#ff4500',
orchid : '#da70d6',
palegoldenrod : '#eee8aa',
palegreen : '#98fb98',
paleturquoise : '#afeeee',
palevioletred : '#db7093',
papayawhip : '#ffefd5',
peachpuff : '#ffdab9',
peru : '#cd853f',
pink : '#ffc0cb',
plum : '#dda0dd',
powderblue : '#b0e0e6',
purple : '#800080',
rebeccapurple : '#663399',
red : '#ff0000',
rosybrown : '#bc8f8f',
royalblue : '#4169e1',
saddlebrown : '#8b4513',
salmon : '#fa8072',
sandybrown : '#f4a460',
seagreen : '#2e8b57',
seashell : '#fff5ee',
sienna : '#a0522d',
silver : '#c0c0c0',
skyblue : '#87ceeb',
slateblue : '#6a5acd',
slategray : '#708090',
slategrey : '#708090',
snow : '#fffafa',
springgreen : '#00ff7f',
steelblue : '#4682b4',
tan : '#d2b48c',
teal : '#008080',
thistle : '#d8bfd8',
tomato : '#ff6347',
turquoise : '#40e0d0',
violet : '#ee82ee',
wheat : '#f5deb3',
white : '#ffffff',
whitesmoke : '#f5f5f5',
yellow : '#ffff00',
yellowgreen : '#9acd32',
}
function hsvToRgb({ h, s, v }: ColorHSV): ColorRGB { function hsvToRgb({ h, s, v }: ColorHSV): ColorRGB {
const i = Math.floor(h * 6); const i = Math.floor(h * 6);
const f = h * 6 - i; const f = h * 6 - i;
@ -72,18 +234,30 @@ function rgbToHsv({ r, g, b }: ColorRGB): ColorHSV {
} }
// Hacky way to get RGB values FOR SURE! // Hacky way to get RGB values FOR SURE!
function nameToRGB(name: string): ColorRGB { const nameToRGB = function self(name: string): ColorRGB {
if ( namedColors[name] ) {
return parseColor(namedColors[name]);
}
// Create fake div // Create fake div
const fakeDiv = document.createElement("div"); self.fakeDiv = self.fakeDiv || (() => {
fakeDiv.style.color = name; const tmp = document.createElement("div");
document.body.appendChild(fakeDiv); Object.assign(tmp.style, {
position: 'fixed';
height: 0;
width: 0;
top: -99;
left: -99;
});
document.body.appendChild(tmp);
return tmp;
})();
self.fakeDiv.style.color = name;
// Get color of div // Get color of div
const cs = window.getComputedStyle(fakeDiv); const cs = window.getComputedStyle(self.fakeDiv);
const pv = cs.getPropertyValue("color"); const pv = cs.color; //TODO slow
// Remove div after obtaining desired color value
document.body.removeChild(fakeDiv);
return parseColor(pv); return parseColor(pv);
} }
@ -92,10 +266,10 @@ function nameToRGB(name: string): ColorRGB {
// https://stackoverflow.com/questions/11068240/what-is-the-most-efficient-way-to-parse-a-css-color-in-javascript // https://stackoverflow.com/questions/11068240/what-is-the-most-efficient-way-to-parse-a-css-color-in-javascript
function parseColor(input: string): ColorRGB { function parseColor(input: string): ColorRGB {
// Hex format // Hex format
if (input.substr(0, 1) === "#") { if (input[0] === "#") {
const collen = (input.length - 1) / 3; const collen = (input.length - 1) / 3;
const fact = [17, 1, 0.062272][collen - 1]; const fact = [17, 1, 0.062272][collen - 1];
return { return { //TODO refactor with substring
r: Math.round(parseInt(input.substr(1, collen), 16) * fact) / 256, r: Math.round(parseInt(input.substr(1, collen), 16) * fact) / 256,
g: g:
Math.round(parseInt(input.substr(1 + collen, collen), 16) * fact) / 256, Math.round(parseInt(input.substr(1 + collen, collen), 16) * fact) / 256,

View file

@ -1,10 +1,10 @@
import { darken, ColorFmt, lighten } from "./darkmode"; import { darken, ColorFmt, lighten } from "./darkmode";
import searchBox from "./search"; import searchBox from "./search";
import { findParent } from "./utils"; import { nextAnimationFrame, findParent } from "./utils";
export default function userscript(root: HTMLElement, docname: string): void { export default async function userscript(root: HTMLElement, docname: string): void {
root.querySelectorAll(".mw-editsection").forEach((editLink) => { root.querySelectorAll(".mw-editsection").forEach((editLink) => {
editLink.parentElement.removeChild(editLink); window.requestAnimationFrame(() => editLink.remove())
}); });
// Darken bgcolor // Darken bgcolor
@ -37,6 +37,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
}); });
// Remove fixed widths // Remove fixed widths
await nextAnimationFrame();
root.querySelectorAll("table[width]").forEach((td) => { root.querySelectorAll("table[width]").forEach((td) => {
td.setAttribute("width", "100%"); td.setAttribute("width", "100%");
}); });
@ -47,16 +48,18 @@ export default function userscript(root: HTMLElement, docname: string): void {
}); });
// Fixup spacing on top quotes // Fixup spacing on top quotes
root const tmpFloatRows = Array.from(root.querySelectorAll<HTMLImageElement>("table .floatright > a > img"))
.querySelectorAll<HTMLImageElement>("table .floatright > a > img") .map((img) => {
.forEach((img) => { return findParent(img, (el) => el instanceof HTMLTableRowElement);
const row = findParent(img, (el) => el instanceof HTMLTableRowElement); })
await nextAnimationFrame();
tmpFloatRows.forEach((row) => {
const td = document.createElement("td"); const td = document.createElement("td");
row.appendChild(td); row.appendChild(td);
}); });
// Group headers and content so stickies don't overlap // Group headers and content so stickies don't overlap
root.querySelectorAll("h3,h2").forEach((h3) => { root.querySelectorAll("h3,h2").forEach((h3) => { //NOTE slow
const parent = h3.parentNode; const parent = h3.parentNode;
const div = document.createElement("div"); const div = document.createElement("div");
parent.insertBefore(div, h3); parent.insertBefore(div, h3);
@ -71,17 +74,28 @@ export default function userscript(root: HTMLElement, docname: string): void {
}); });
// Move id from header to container, if one is found // Move id from header to container, if one is found
root.querySelectorAll<HTMLElement>(".mw-headline").forEach((span) => { const tmpHeaders = Array.from(root.querySelectorAll<HTMLElement>(".mw-headline"))
.map((span) => {
// Find nearest container // Find nearest container
const container = findParent(span, (el) => const container = findParent(span, (el) =>
el.classList.contains("mw-headline-cont") el.classList.contains("mw-headline-cont")
); );
if (container) { if (container) {
container.id = span.id; return [container, span, span.id, span.textContent]; //NOTE slow
span.id += "-span"; } else {
container.dataset.name = span.innerText; return null;
}
})
.filter((e) => e !== null);
await nextAnimationFrame();
for (const [container, span, spanId, spanInnerText] of tmpHeaders) {
container.id = spanId;
span.id += "-span";
}
await nextAnimationFrame();
for (const [container, span, spanId, spanInnerText] of tmpHeaders) {
container.dataset.name = spanInnerText;
} }
});
// Tell user that better chemistry is loading // Tell user that better chemistry is loading
const postbody = root; const postbody = root;
@ -94,32 +108,35 @@ export default function userscript(root: HTMLElement, docname: string): void {
</table>`; </table>`;
postbody.insertBefore(statusMessage, postbody.firstChild); postbody.insertBefore(statusMessage, postbody.firstChild);
function betterChemistry() { async function betterChemistry() {
// Fix inconsistencies with <p> on random parts // Fix inconsistencies with <p> on random parts
// Ideally I'd like a <p> or something on every part, wrapping it completely, but for now let's just kill 'em // Ideally I'd like a <p> or something on every part, wrapping it completely, but for now let's just kill 'em
document const tmpTooltiptext = Array.from(document.querySelectorAll(
.querySelectorAll(
"table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2), .tooltiptext" "table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2), .tooltiptext"
) ))
.forEach((td) => { .map((td) => {
const tmp = td.cloneNode() as HTMLElement; const tmp = td.cloneNode() as HTMLElement;
// The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop. // The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop.
// Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index. // Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index.
Array.from(td.childNodes).forEach((el) => { Array.from(td.childNodes).forEach((el) => { //TODO really slow
if (el instanceof HTMLParagraphElement) { if (el instanceof HTMLParagraphElement) {
tmp.append(...el.childNodes); tmp.append(...el.childNodes);
} else { } else {
tmp.append(el); tmp.append(el);
} }
}); });
td.parentNode.replaceChild(tmp, td); return [td, td.parentNode, tmp];
}); });
await nextAnimationFrame();
for (const [td, parent, newTD] of tmpTooltiptext) {
parent.replaceChild(newTD, td);
}
// Enrich "x part" with checkboxes and parts // Enrich "x part" with checkboxes and parts
Array.from(document.querySelectorAll("td")) const tmpParts = Array.from(document.querySelectorAll("td"))
.filter((el) => el.innerText.indexOf(" part") >= 0) .filter((el) => el.textContent.indexOf(" part") >= 0)
.forEach((el) => { .map((el) => {
el.innerHTML = el.innerHTML.replace( const newInnerHTML = el.innerHTML.replace( //TODO slow
/((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi, /((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi,
(match, ...m) => (match, ...m) =>
`<label class="bgus_part ${ `<label class="bgus_part ${
@ -133,7 +150,12 @@ export default function userscript(root: HTMLElement, docname: string): void {
'<span class="bgus_nobreak bgus_nested_element">$1<span class="bgus_twistie"></span></span>' '<span class="bgus_nobreak bgus_nested_element">$1<span class="bgus_twistie"></span></span>'
)}` )}`
); );
return [el, newInnerHTML];
}); });
await nextAnimationFrame();
for (const [el, newInnerHTML] of tmpParts) {
el.innerHTML = newInnerHTML;
}
// Add event to autofill child checkboxes // Add event to autofill child checkboxes
root root
.querySelectorAll(".bgus_part_tooltip > .bgus_checkbox") .querySelectorAll(".bgus_part_tooltip > .bgus_checkbox")
@ -156,7 +178,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
}); });
// Wrap every recipe with extra metadata // Wrap every recipe with extra metadata
root.querySelectorAll<HTMLElement>(".bgus_part").forEach((el) => { root.querySelectorAll<HTMLElement>(".bgus_part").forEach((el) => { //NOTE slow-ish
if ("parts" in el.parentElement.dataset) { if ("parts" in el.parentElement.dataset) {
el.parentElement.dataset.parts = ( el.parentElement.dataset.parts = (
parseInt(el.parentElement.dataset.parts, 10) + parseInt(el.parentElement.dataset.parts, 10) +
@ -220,11 +242,11 @@ export default function userscript(root: HTMLElement, docname: string): void {
const remTable = root.querySelector( const remTable = root.querySelector(
"#Non-craftable_Medicines + h4 + p + table" "#Non-craftable_Medicines + h4 + p + table"
); );
remTable.parentElement.removeChild(remTable); remTable.remove();
root root
.querySelectorAll<HTMLElement>("div[data-name] .wikitable.sortable tr") .querySelectorAll<HTMLElement>("div[data-name] .wikitable.sortable tr")
.forEach((row) => { .forEach((row) => { //TODO slow
const sectionEl = findParent( const sectionEl = findParent(
row, row,
(sel) => "name" in sel.dataset && sel.dataset.name !== "" (sel) => "name" in sel.dataset && sel.dataset.name !== ""
@ -238,7 +260,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
th.classList.add("table-head"); th.classList.add("table-head");
return; return;
} }
th.parentElement.removeChild(th); th.remove();
}); });
return; return;
} }
@ -285,11 +307,11 @@ export default function userscript(root: HTMLElement, docname: string): void {
} }
title.classList.add("reagent-ext"); title.classList.add("reagent-ext");
title.innerHTML = content; title.innerHTML = content;
if (desc) desc.parentElement.removeChild(desc); if (desc) desc.remove();
if (treatment) treatment.parentElement.removeChild(treatment); if (treatment) treatment.remove();
if (metabolism) metabolism.parentElement.removeChild(metabolism); if (metabolism) metabolism.remove();
if (overdose) overdose.parentElement.removeChild(overdose); if (overdose) overdose.remove();
if (addiction) addiction.parentElement.removeChild(addiction); if (addiction) addiction.remove();
}); });
document.body.addEventListener("keydown", (ev) => { document.body.addEventListener("keydown", (ev) => {
@ -335,7 +357,7 @@ export default function userscript(root: HTMLElement, docname: string): void {
}); });
} }
function betterGeneric() { async function betterGeneric() {
const el = Array.from( const el = Array.from(
root.querySelectorAll<HTMLElement>("div.mw-headline-cont[id][data-name]") root.querySelectorAll<HTMLElement>("div.mw-headline-cont[id][data-name]")
); );
@ -352,10 +374,10 @@ export default function userscript(root: HTMLElement, docname: string): void {
switch (docname) { switch (docname) {
case "Guide_to_chemistry": case "Guide_to_chemistry":
betterChemistry(); await betterChemistry();
break; break;
default: default:
betterGeneric(); await betterGeneric();
break; break;
} }
// Everything is loaded, remove loading bar // Everything is loaded, remove loading bar

View file

@ -1,3 +1,7 @@
export function nextAnimationFrame(): Promise {
return new Promise(requestAnimationFrame);
}
export function findParent( export function findParent(
base: HTMLElement, base: HTMLElement,
matchFn: (candidate: HTMLElement) => boolean matchFn: (candidate: HTMLElement) => boolean
@ -12,4 +16,4 @@ export function findParent(
return parent; return parent;
} }
export default { findParent }; export default { nextAnimationFrame, findParent };