Compare commits

..

No commits in common. "e700bb9c309627b944618152a7d8e936ae7a05db" and "7930f961710641f022ef0cd3ad3277ffd4eab7eb" have entirely different histories.

11 changed files with 270 additions and 455 deletions

View file

@ -19,7 +19,6 @@ module.exports = {
"no-param-reassign": "off",
"no-alert": "off",
"no-console": "off",
"func-names": "off",
"@typescript-eslint/ban-ts-comment": [
"error",
{

23
LICENSE
View file

@ -1,23 +0,0 @@
This code is license under ISC, full copy below:
Copyright 2020, Alessandro Gatti
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
This code uses idb-keyval, licensed under Apache 2:
Copyright 2016, Jake Archibald
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -8,4 +8,4 @@
## Development
`yarn dev`
`yarn && yarn dev`

View file

@ -18,7 +18,6 @@
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "2.21.2",
"eslint-plugin-prettier": "^3.1.4",
"idb-keyval": "^3.2.0",
"parcel-bundler": "^1.12.4",
"parcel-plugin-sw-cache": "^0.3.1",
"prettier": "^2.0.5",

View file

@ -1,9 +1,7 @@
// @ts-expect-error: Asset imports are handled by parcel
import speen from "~/assets/images/speen.svg";
import { getPageHTML } from "./wiki";
import { processHTML, bindFunctions, CURRENT_VERSION } from "./userscript";
import cache from "./cache";
import { nextAnimationFrame } from "./utils";
import userscript from "./userscript";
// @ts-expect-error: Parcel image import
import unknown from "~/assets/images/tab-icons/unknown.svg";
@ -24,52 +22,21 @@ function initWaiting(elem: HTMLElement) {
}
async function loadPage(page: string, elem: HTMLElement) {
let html: string | null = null;
const key = `page:${page}`;
// Check cache for pre-processed page
try {
const cachedPage = await cache.get<string>(key);
if (cachedPage) {
if (cachedPage.version === CURRENT_VERSION) {
console.log(`${page}: found cached entry`);
html = cachedPage.value;
} else {
console.log(`${page}: found outdated cache entry`);
}
}
} catch (e) {
console.log(`${page}: failed to retrieve cache entry:`, e);
}
// Fetch page content
if (!html) {
console.log(`${page}: fetching`);
html = await getPageHTML(page);
let html = await getPageHTML(page);
// Convert relative links to absolute (and proxied)
html = html.replace(/"\/wiki/gi, '"//tgproxy.ovo.ovh/wiki');
await nextAnimationFrame();
// Convert relative links to absolute
html = html.replace(/"\/wiki/gi, '"//tgstation13.org/wiki');
// Set as HTML content and run HTML manipulations on it
requestAnimationFrame(() => {
elem.innerHTML = html;
console.log(`${page}: processing`);
processHTML(elem, page);
// Save result to cache
cache.set(key, elem.outerHTML, CURRENT_VERSION).then(() => {
console.log(`${page}: saved to cache`);
});
} else {
// Set cached content as HTML
elem.outerHTML = html;
}
bindFunctions(elem, page);
userscript(elem, page);
console.log(`${page}: userscript applied`);
elem.classList.remove("waiting");
});
}
type TabElements = {

View file

@ -1,110 +0,0 @@
/* eslint-disable no-shadow */
interface CacheEntry<T> {
version: string;
value: T;
}
export class Store {
readonly dbp: Promise<IDBDatabase>;
constructor(dbName = "tg-cache", readonly storeName = "keyval") {
this.dbp = new Promise((resolve, reject) => {
const openreq = indexedDB.open(dbName, 1);
openreq.onerror = () => reject(openreq.error);
openreq.onsuccess = () => resolve(openreq.result);
// First time setup: create an empty object store
openreq.onupgradeneeded = () => {
openreq.result.createObjectStore(storeName);
};
});
}
withIDBStore(
type: IDBTransactionMode,
callback: (store: IDBObjectStore) => void
): Promise<void> {
return this.dbp.then(
(db) =>
new Promise<void>((resolve, reject) => {
const transaction = db.transaction(this.storeName, type);
transaction.oncomplete = () => resolve();
transaction.onabort = () => reject(transaction.error);
transaction.onerror = () => reject(transaction.error);
callback(transaction.objectStore(this.storeName));
})
);
}
}
let defaultStore: Store;
function getDefaultStore() {
if (!defaultStore) defaultStore = new Store();
return defaultStore;
}
export function get<Type>(
key: IDBValidKey,
store = getDefaultStore()
): Promise<CacheEntry<Type>> {
let req: IDBRequest;
return store
.withIDBStore("readonly", (store) => {
req = store.get(key);
})
.then(() => req.result);
}
export function set<Type>(
key: IDBValidKey,
value: Type,
version: string,
store = getDefaultStore()
): Promise<void> {
return store.withIDBStore("readwrite", (store) => {
store.put({ version, value }, key);
});
}
export function del(
key: IDBValidKey,
store = getDefaultStore()
): Promise<void> {
return store.withIDBStore("readwrite", (store) => {
store.delete(key);
});
}
export function clear(store = getDefaultStore()): Promise<void> {
return store.withIDBStore("readwrite", (store) => {
store.clear();
});
}
export function keys(store = getDefaultStore()): Promise<IDBValidKey[]> {
const dbkeys: IDBValidKey[] = [];
return store
.withIDBStore("readonly", (store) => {
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
// And openKeyCursor isn't supported by Safari.
(store.openKeyCursor || store.openCursor).call(
store
).onsuccess = function () {
if (!this.result) return;
dbkeys.push(this.result.key);
this.result.continue();
};
})
.then(() => dbkeys);
}
export default {
get,
set,
keys,
del,
clear,
};

View file

@ -2,145 +2,7 @@ import { darken, ColorFmt, lighten } from "./darkmode";
import searchBox from "./search";
import { findParent } from "./utils";
// This is used for cache busting when userscript changes significantly.
// Only change it when you made changes to the processHTML part!
export const CURRENT_VERSION = "df914edcb5522670309ceb8dfd0195dc70fb81d4";
function chemistryFixups(root: HTMLElement) {
// Enable page-specific CSS rules
root.classList.add("bchem");
// 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
root
.querySelectorAll(
"table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2), .tooltiptext"
)
.forEach((td) => {
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.
// 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) => {
if (el instanceof HTMLParagraphElement) {
tmp.append(...el.childNodes);
} else {
tmp.append(el);
}
});
td.parentNode.replaceChild(tmp, td);
});
// Enrich "x part" with checkboxes and parts
Array.from(root.querySelectorAll("td"))
.filter((el) => el.textContent.indexOf(" part") >= 0)
.forEach((el) => {
el.innerHTML = el.innerHTML.replace(
/((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi,
(match, ...m) =>
`<label class="bgus_part ${
m[2].includes("</a>") ? "bgus_part_tooltip" : ""
}" data-amount="${
m[1]
}"><input type="checkbox" class='bgus_checkbox bgus_hidden'/> <span class="bgus_part_label" data-src="${
m[0]
}">${m[0]}</span></label>${m[2].replace(
/(<a .+?<\/a>)/gi,
'<span class="bgus_nobreak bgus_nested_element">$1<span class="bgus_twistie"></span></span>'
)}`
);
});
// Wrap every recipe with extra metadata
root.querySelectorAll<HTMLElement>(".bgus_part").forEach((el) => {
if ("parts" in el.parentElement.dataset) {
el.parentElement.dataset.parts = (
parseInt(el.parentElement.dataset.parts, 10) +
parseInt(el.dataset.amount, 10)
).toString();
} else {
el.parentElement.dataset.parts = el.dataset.amount;
}
});
// Remove "Removed medicines" section
const remTable = root.querySelector(
"#Non-craftable_Medicines + h4 + p + table"
);
remTable.parentElement.removeChild(remTable);
// Restructure recipes to work in a narrow window
root
.querySelectorAll<HTMLElement>("div[data-name] .wikitable.sortable tr")
.forEach((row) => {
const sectionEl = findParent(
row,
(sel) => "name" in sel.dataset && sel.dataset.name !== ""
);
const section = sectionEl.dataset.name;
if (row.querySelector("td") === null) {
// Remove unused rows if found
const headers = row.querySelectorAll("th");
headers.forEach((th, i) => {
if (i < 2) {
th.classList.add("table-head");
return;
}
th.parentElement.removeChild(th);
});
return;
}
const rows = Array.from(row.querySelectorAll("td")).slice(1);
let treatment = null;
let desc = null;
let metabolism = null;
let overdose = null;
let addiction = null;
// Handle special cases
switch (section) {
case "Components":
case "Virology Recipes":
[desc] = rows;
break;
case "Narcotics":
[desc, metabolism, overdose, addiction] = rows;
break;
case "Explosive Strength":
case "Other Reagents":
case "Mutation Toxins":
[desc, metabolism] = rows;
break;
default:
// All fields
[treatment, desc, metabolism, overdose, addiction] = rows;
}
const title = row.querySelector("th");
let content = `<div class="reagent-header">${title.innerHTML}</div>`;
if (treatment) {
content += `<p class="treatment">${treatment.innerHTML}</p>`;
}
if (metabolism) {
content += `<p class="metabolism">${metabolism.innerHTML}</p>`;
}
if (addiction && addiction.innerHTML.trim() !== "N/A") {
content += `<p class="addiction">${addiction.innerHTML}</p>`;
}
if (overdose && overdose.innerHTML.trim() !== "N/A") {
content += `<p class="overdose">${overdose.innerHTML}</p>`;
}
if (desc) {
content += `<p>${desc.innerHTML}</p>`;
}
title.classList.add("reagent-ext");
title.innerHTML = content;
if (desc) desc.parentElement.removeChild(desc);
if (treatment) treatment.parentElement.removeChild(treatment);
if (metabolism) metabolism.parentElement.removeChild(metabolism);
if (overdose) overdose.parentElement.removeChild(overdose);
if (addiction) addiction.parentElement.removeChild(addiction);
});
}
export function processHTML(root: HTMLElement, docname: string): void {
export default function userscript(root: HTMLElement, docname: string): void {
// Add header
const header = document.createElement("h1");
header.className = "pageheader";
@ -236,15 +98,57 @@ export function processHTML(root: HTMLElement, docname: string): void {
}
});
switch (docname) {
case "Guide_to_chemistry":
chemistryFixups(root);
break;
default:
}
}
// Tell user that better chemistry is loading
const postbody = root;
const statusMessage = document.createElement("div");
statusMessage.innerHTML = `
<table style="background-color: black; margin-bottom:10px;" width="95%" align="center">
<tbody><tr><td align="center">
<b>Hang on...</b> Better guides is loading.
</td></tr></tbody>
</table>`;
postbody.insertBefore(statusMessage, postbody.firstChild);
function chemistryScript(root: HTMLElement) {
function betterChemistry() {
// 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
document
.querySelectorAll(
"table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2), .tooltiptext"
)
.forEach((td) => {
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.
// 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) => {
if (el instanceof HTMLParagraphElement) {
tmp.append(...el.childNodes);
} else {
tmp.append(el);
}
});
td.parentNode.replaceChild(tmp, td);
});
// Enrich "x part" with checkboxes and parts
Array.from(document.querySelectorAll("td"))
.filter((el) => el.textContent.indexOf(" part") >= 0)
.forEach((el) => {
el.innerHTML = el.innerHTML.replace(
/((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi,
(match, ...m) =>
`<label class="bgus_part ${
m[2].includes("</a>") ? "bgus_part_tooltip" : ""
}" data-amount="${
m[1]
}"><input type="checkbox" class='bgus_checkbox bgus_hidden'/> <span class="bgus_part_label" data-src="${
m[0]
}">${m[0]}</span></label>${m[2].replace(
/(<a .+?<\/a>)/gi,
'<span class="bgus_nobreak bgus_nested_element">$1<span class="bgus_twistie"></span></span>'
)}`
);
});
// Add event to autofill child checkboxes
root
.querySelectorAll(".bgus_part_tooltip > .bgus_checkbox")
@ -266,6 +170,18 @@ function chemistryScript(root: HTMLElement) {
});
});
// Wrap every recipe with extra metadata
root.querySelectorAll<HTMLElement>(".bgus_part").forEach((el) => {
if ("parts" in el.parentElement.dataset) {
el.parentElement.dataset.parts = (
parseInt(el.parentElement.dataset.parts, 10) +
parseInt(el.dataset.amount, 10)
).toString();
} else {
el.parentElement.dataset.parts = el.dataset.amount;
}
});
const setPartSize = (labels, ml) => {
labels.forEach((el) => {
const part = el.parentElement.dataset.amount;
@ -293,21 +209,104 @@ function chemistryScript(root: HTMLElement) {
});
};
root.classList.add("bchem");
// Init fuzzy search with elements
const el = Array.from(
root.querySelectorAll<HTMLElement>(
"table.wikitable > tbody > tr:not(:first-child) > th"
)
);
const name = el.map((elem) =>
elem.querySelector(".reagent-header").textContent.trim().replace("▮", "")
);
const name = el.map((elem) => {
let partial = "";
elem.childNodes.forEach((t) => {
if (t instanceof Text) {
partial += t.textContent;
}
});
return partial.trim();
});
const box = searchBox(
el,
name.map((e, i) => ({ id: i, str: e }))
);
document.body.appendChild(box);
// Remove "Removed medicines" section
const remTable = root.querySelector(
"#Non-craftable_Medicines + h4 + p + table"
);
remTable.parentElement.removeChild(remTable);
root
.querySelectorAll<HTMLElement>("div[data-name] .wikitable.sortable tr")
.forEach((row) => {
const sectionEl = findParent(
row,
(sel) => "name" in sel.dataset && sel.dataset.name !== ""
);
const section = sectionEl.dataset.name;
if (row.querySelector("td") === null) {
// Remove unused rows if found
const headers = row.querySelectorAll("th");
headers.forEach((th, i) => {
if (i < 2) {
th.classList.add("table-head");
return;
}
th.parentElement.removeChild(th);
});
return;
}
const rows = Array.from(row.querySelectorAll("td")).slice(1);
let treatment = null;
let desc = null;
let metabolism = null;
let overdose = null;
let addiction = null;
// Handle special cases
switch (section) {
case "Components":
case "Virology Recipes":
[desc] = rows;
break;
case "Narcotics":
[desc, metabolism, overdose, addiction] = rows;
break;
case "Explosive Strength":
case "Other Reagents":
case "Mutation Toxins":
[desc, metabolism] = rows;
break;
default:
// All fields
[treatment, desc, metabolism, overdose, addiction] = rows;
}
const title = row.querySelector("th");
let content = `<div class="reagent-header">${title.innerHTML}</div>`;
if (treatment) {
content += `<p class="treatment">${treatment.innerHTML}</p>`;
}
if (metabolism) {
content += `<p class="metabolism">${metabolism.innerHTML}</p>`;
}
if (addiction && addiction.innerHTML.trim() !== "N/A") {
content += `<p class="addiction">${addiction.innerHTML}</p>`;
}
if (overdose && overdose.innerHTML.trim() !== "N/A") {
content += `<p class="overdose">${overdose.innerHTML}</p>`;
}
if (desc) {
content += `<p>${desc.innerHTML}</p>`;
}
title.classList.add("reagent-ext");
title.innerHTML = content;
if (desc) desc.parentElement.removeChild(desc);
if (treatment) treatment.parentElement.removeChild(treatment);
if (metabolism) metabolism.parentElement.removeChild(metabolism);
if (overdose) overdose.parentElement.removeChild(overdose);
if (addiction) addiction.parentElement.removeChild(addiction);
});
document.body.addEventListener("keydown", (ev) => {
if (ev.shiftKey) {
switch (ev.keyCode) {
@ -349,9 +348,9 @@ function chemistryScript(root: HTMLElement) {
}
}
});
}
}
function genericScript(root: HTMLElement) {
function betterGeneric() {
const el = Array.from(
root.querySelectorAll<HTMLElement>("div.mw-headline-cont[id][data-name]")
);
@ -364,21 +363,16 @@ function genericScript(root: HTMLElement) {
{ alignment: "start" }
);
root.appendChild(box);
}
}
export function bindFunctions(root: HTMLElement, docname: string): void {
switch (docname) {
case "Guide_to_chemistry":
chemistryScript(root);
betterChemistry();
break;
default:
genericScript(root);
betterGeneric();
break;
}
// Everything is loaded, remove loading bar
statusMessage.innerHTML = "";
}
export default {
CURRENT_VERSION,
processHTML,
bindFunctions,
};

View file

@ -12,8 +12,4 @@ export function findParent(
return parent;
}
export function nextAnimationFrame(): Promise<void> {
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
}
export default { findParent, nextAnimationFrame };
export default { findParent };

View file

@ -66,7 +66,6 @@ body {
overflow: hidden;
.page {
//visibility: hidden;
will-change: display;
display: none;
overflow-y: scroll;
grid-row: 1;

View file

@ -2,7 +2,6 @@
"compilerOptions": {
"target": "ES2020",
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"paths": {
"~*": ["./src/*"]
}

View file

@ -3230,11 +3230,6 @@ icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0:
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
idb-keyval@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-3.2.0.tgz#cbbf354deb5684b6cdc84376294fc05932845bd6"
integrity sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ==
ieee754@^1.1.4:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"