Ver código fonte

ref: implement StringGen everywhere

Sv443 2 meses atrás
pai
commit
1587a11628

+ 11 - 16
src/components/ExImDialog.ts

@@ -1,3 +1,4 @@
+import { consumeStringGen, type StringGen } from "@sv443-network/userutils";
 import { BytmDialog, type BytmDialogOptions } from "./BytmDialog.js";
 import { t } from "../utils/translations.js";
 import { onInteraction } from "../utils/input.js";
@@ -14,17 +15,17 @@ type ExImDialogOpts =
   & Omit<BytmDialogOptions, "renderHeader" | "renderBody" | "renderFooter">
   & {
     /** Title of the dialog */
-    title: string | (() => (string | Promise<string>));
+    title: StringGen;
     /** Description when importing */
-    descImport: string | (() => (string | Promise<string>));
+    descImport: StringGen;
     /** Description when exporting */
-    descExport: string | (() => (string | Promise<string>));
+    descExport: StringGen;
     /** Function that gets called when the user imports data */
     onImport: (data: string) => void;
     /** The data to export (or a function that returns the data as string, sync or async) */
-    exportData: string | (() => (string | Promise<string>));
+    exportData: StringGen;
     /** Optional variant of the data, used for special cases like when shift-clicking the copy button */
-    exportDataSpecial?: string | (() => (string | Promise<string>));
+    exportDataSpecial?: StringGen;
   };
 
 //#region class
@@ -53,9 +54,7 @@ export class ExImDialog extends BytmDialog {
     headerEl.role = "heading";
     headerEl.ariaLevel = "1";
     headerEl.tabIndex = 0;
-    headerEl.textContent = headerEl.ariaLabel = typeof opts.title === "function"
-      ? await opts.title()
-      : opts.title;
+    headerEl.textContent = headerEl.ariaLabel = await consumeStringGen(opts.title);
 
     return headerEl;
   }
@@ -76,9 +75,7 @@ export class ExImDialog extends BytmDialog {
       descEl.classList.add("bytm-exim-dialog-desc");
       descEl.role = "note";
       descEl.tabIndex = 0;
-      descEl.textContent = descEl.ariaLabel = typeof opts.descExport === "function"
-        ? await opts.descExport()
-        : opts.descExport;
+      descEl.textContent = descEl.ariaLabel = await consumeStringGen(opts.descExport);
 
       const dataEl = document.createElement("textarea");
       dataEl.classList.add("bytm-exim-dialog-data");
@@ -86,7 +83,7 @@ export class ExImDialog extends BytmDialog {
       dataEl.tabIndex = 0;
       dataEl.value = t("click_to_reveal");
       onInteraction(dataEl, async () => {
-        dataEl.value = typeof opts.exportData === "function" ? await opts.exportData() : opts.exportData;
+        dataEl.value = await consumeStringGen(opts.exportData);
         dataEl.setSelectionRange(0, dataEl.value.length);
       });
 
@@ -99,7 +96,7 @@ export class ExImDialog extends BytmDialog {
         resourceName: "icon-copy",
         async onClick({ shiftKey }) {
           const copyData = shiftKey && opts.exportDataSpecial ? opts.exportDataSpecial : opts.exportData;
-          copyToClipboard(typeof copyData === "function" ? await copyData() : copyData);
+          copyToClipboard(await consumeStringGen(copyData));
           await showToast({ message: t("copied_to_clipboard") });
         },
       }));
@@ -118,9 +115,7 @@ export class ExImDialog extends BytmDialog {
       descEl.classList.add("bytm-exim-dialog-desc");
       descEl.role = "note";
       descEl.tabIndex = 0;
-      descEl.textContent = descEl.ariaLabel = typeof opts.descImport === "function"
-        ? await opts.descImport()
-        : opts.descImport;
+      descEl.textContent = descEl.ariaLabel = await consumeStringGen(opts.descImport);
 
       const dataEl = document.createElement("textarea");
       dataEl.classList.add("bytm-exim-dialog-data");

+ 1 - 1
src/components/MarkdownDialog.ts

@@ -1,7 +1,7 @@
 import { marked } from "marked";
+import { consumeStringGen, type StringGen } from "@sv443-network/userutils";
 import { setInnerHtml } from "../utils/dom.js";
 import { BytmDialog, type BytmDialogOptions } from "./BytmDialog.js";
-import { consumeStringGen, type StringGen } from "@sv443-network/userutils";
 
 /** Options for the MarkdownDialog - a `body` prop is required instead of `renderBody` */
 type MarkdownDialogOptions = Omit<BytmDialogOptions, "renderBody"> & {

+ 5 - 3
src/dialogs/prompt.ts

@@ -1,5 +1,5 @@
 import type { Emitter } from "nanoevents";
-import type { Stringifiable } from "@sv443-network/userutils";
+import { consumeStringGen, type StringGen, type Stringifiable } from "@sv443-network/userutils";
 import { getOS, resourceAsString, setInnerHtml, t } from "../utils/index.js";
 import { BytmDialog, type BytmDialogEvents } from "../components/index.js";
 import { addSelectorListener } from "../observers.js";
@@ -23,7 +23,7 @@ type AlertRenderProps = BaseRenderProps & {
 
 type PromptRenderProps = BaseRenderProps & {
   type: "prompt";
-  defaultValue?: string;
+  defaultValue?: StringGen;
 };
 
 type BaseRenderProps = {
@@ -99,7 +99,9 @@ class PromptDialog extends BytmDialog {
       inputElem.type = "text";
       inputElem.autocomplete = "off";
       inputElem.spellcheck = false;
-      inputElem.value = "defaultValue" in rest ? rest.defaultValue ?? "" : "";
+      inputElem.value = "defaultValue" in rest && rest.defaultValue
+        ? await consumeStringGen(rest.defaultValue)
+        : "";
 
       const inputEnterListener = (e: KeyboardEvent) => {
         if(e.key === "Enter") {

+ 7 - 3
src/features/index.ts

@@ -9,6 +9,7 @@ import { getAutoLikeDialog, getPluginListDialog, showPrompt } from "../dialogs/i
 import { showIconToast } from "../components/index.js";
 import { mode } from "../constants.js";
 import { getStoreSerializer } from "../serializer.js";
+import { consumeStringGen, type StringGen } from "@sv443-network/userutils";
 
 //#region re-exports
 
@@ -37,8 +38,11 @@ type AdornmentFunc =
   | Promise<string | undefined>;
 
 /** Creates an HTML string for the given adornment properties */
-const getAdornHtml = async (className: string, title: string | undefined, resource: ResourceKey, extraAttributes?: string) =>
-  `<span class="${className} bytm-adorn-icon" ${title ? `title="${title}" aria-label="${title}"` : ""}${extraAttributes ? ` ${extraAttributes}` : ""}>${await resourceAsString(resource) ?? ""}</span>`;
+const getAdornHtml = async (className: string, title: StringGen | undefined, resource: ResourceKey, extraAttributes?: StringGen) => {
+  title = title ? await consumeStringGen(title) : undefined;
+  extraAttributes = extraAttributes ? await consumeStringGen(extraAttributes) : undefined;
+  return `<span class="${className} bytm-adorn-icon" ${title ? `title="${title}" aria-label="${title}"` : ""}${extraAttributes ? ` ${extraAttributes}` : ""}>${await resourceAsString(resource) ?? ""}</span>`;
+};
 
 /** Combines multiple async functions or promises that resolve with an adornment HTML string into a single string */
 const combineAdornments = (
@@ -68,7 +72,7 @@ const adornments = {
   advanced: async () => getAdornHtml("bytm-advanced-mode-icon", t("advanced_mode"), "icon-advanced_mode"),
   experimental: async () => getAdornHtml("bytm-experimental-icon", t("experimental_feature"), "icon-experimental"),
   globe: async () => getAdornHtml("bytm-locale-icon", undefined, "icon-globe_small"),
-  alert: async (title: string) => getAdornHtml("bytm-warning-icon", title, "icon-error", "role=\"alert\""),
+  alert: async (title: StringGen) => getAdornHtml("bytm-warning-icon", title, "icon-error", "role=\"alert\""),
   reload: async () => getFeature("advancedMode") ? getAdornHtml("bytm-reload-icon", t("feature_requires_reload"), "icon-reload") : undefined,
 } satisfies Record<string, AdornmentFunc>;
 

+ 17 - 17
src/index.ts

@@ -152,17 +152,16 @@ async function onDomLoad() {
   document.body.classList.add(`bytm-dom-${domain}`);
 
   try {
-    initGlobalCssVars();
+    initGlobalCss();
     initObservers();
 
-    await Promise.allSettled([
+    Promise.allSettled([
       injectCssBundle(),
       initVersionCheck(),
     ]);
   }
   catch(err) {
-    error("Fatal error in feature pre-init:", err);
-    return;
+    error("Encountered error in feature pre-init:", err);
   }
 
   log(`DOM loaded and feature pre-init finished, now initializing all features for domain "${domain}"...`);
@@ -323,10 +322,10 @@ async function injectCssBundle() {
     error("Couldn't inject CSS bundle due to an error");
 }
 
-/** Initializes global CSS variables */
-function initGlobalCssVars() {
+/** Initializes global CSS values */
+function initGlobalCss() {
   try {
-    loadFonts();
+    initFonts();
 
     const applyVars = () => {
       setGlobalCssVars({
@@ -341,32 +340,33 @@ function initGlobalCssVars() {
     applyVars();
   }
   catch(err) {
-    error("Couldn't initialize global CSS variables:", err);
+    error("Couldn't initialize global CSS:", err);
   }
 }
 
-async function loadFonts() {
+async function initFonts() {
   const fonts = {
     "Cousine": {
       woff: await getResourceUrl("font-cousine_woff"),
       woff2: await getResourceUrl("font-cousine_woff2"),
-      ttf: await getResourceUrl("font-cousine_ttf"),
+      truetype: await getResourceUrl("font-cousine_ttf"),
     },
   };
 
   let css = "";
-  for(const [font, urls] of Object.entries(fonts))
+  for(const [fontName, urls] of Object.entries(fonts))
     css += `\
 @font-face {
-  font-family: "${font}";
-  src: url("${urls.woff2}") format("woff2"),
-    url("${urls.woff}") format("woff"),
-    url("${urls.ttf}") format("truetype");
+  font-family: "${fontName}";
+  src: ${
+  Object.entries(urls)
+    .map(([type, url]) => `url("${url}") format("${type}")`)
+    .join(", ")
+};
   font-weight: normal;
   font-style: normal;
   font-display: swap;
-}
-`;
+}`;
 
   addStyle(css, "fonts");
 }

+ 6 - 6
src/utils/dom.ts

@@ -1,4 +1,4 @@
-import { addGlobalStyle, getUnsafeWindow, randomId, type Stringifiable } from "@sv443-network/userutils";
+import { addGlobalStyle, consumeStringGen, getUnsafeWindow, randomId, type StringGen, type Stringifiable } from "@sv443-network/userutils";
 import DOMPurify from "dompurify";
 import { error, fetchCss, getDomain, t } from "./index.js";
 import { addSelectorListener } from "../observers.js";
@@ -171,10 +171,10 @@ export function waitVideoElementReady(): Promise<HTMLVideoElement> {
  * @param ref A reference string to identify the style element - defaults to a random 5-character string
  * @param transform A function to transform the CSS before adding it to the DOM
  */
-export async function addStyle(css: string, ref?: string, transform: (css: string) => string | Promise<string> = (c) => c) {
+export async function addStyle(css: StringGen, ref?: string, transform: (css: string) => string | Promise<string> = (c) => c) {
   if(!domLoaded)
     throw new Error("DOM has not finished loading yet");
-  const elem = addGlobalStyle(await transform(css));
+  const elem = addGlobalStyle(await transform(await consumeStringGen(css)));
   elem.id = `bytm-style-${ref ?? randomId(6, 36)}`;
   return elem;
 }
@@ -183,10 +183,10 @@ export async function addStyle(css: string, ref?: string, transform: (css: strin
  * Adds a global style element with the contents fetched from the specified resource starting with `css-`  
  * The CSS can be transformed using the provided function before being added to the DOM.
  */
-export async function addStyleFromResource(key: ResourceKey & `css-${string}`, transform: (css: string) => string = (c) => c) {
+export async function addStyleFromResource(key: ResourceKey & `css-${string}`, transform: (css: string) => Stringifiable = (c) => c) {
   const css = await fetchCss(key);
   if(css) {
-    await addStyle(transform(css), key.slice(4));
+    await addStyle(String(transform(css)), key.slice(4));
     return true;
   }
   return false;
@@ -274,7 +274,7 @@ export function setInnerHtml(element: HTMLElement, html?: string | null) {
 
   if(!ttPolicy && window?.trustedTypes?.createPolicy) {
     ttPolicy = window.trustedTypes.createPolicy("bytm-sanitize-html", {
-      createHTML: (dirty: string) => DOMPurify.sanitize(dirty, {
+      createHTML: (dirty: Stringifiable) => DOMPurify.sanitize(String(dirty), {
         RETURN_TRUSTED_TYPE: true,
       }) as unknown as string,
     });

+ 13 - 4
src/utils/misc.ts

@@ -1,4 +1,4 @@
-import { compress, decompress, fetchAdvanced, getUnsafeWindow, openInNewTab, pauseFor, randomId, randRange, type Prettify } from "@sv443-network/userutils";
+import { compress, consumeStringGen, decompress, fetchAdvanced, getUnsafeWindow, openInNewTab, pauseFor, randomId, randRange, type Prettify, type StringGen } from "@sv443-network/userutils";
 import { marked } from "marked";
 import { assetSource, buildNumber, changelogUrl, compressionFormat, devServerPort, repo, sessionStorageAvailable } from "../constants.js";
 import { type Domain, type NumberLengthFormat, type ResourceKey } from "../types.js";
@@ -165,15 +165,16 @@ export function openInTab(href: string, background = false) {
 }
 
 /** Tries to parse an uncompressed or compressed input string as a JSON object */
-export async function tryToDecompressAndParse<TData = Record<string, unknown>>(input: string): Promise<TData | null> {
+export async function tryToDecompressAndParse<TData = Record<string, unknown>>(input: StringGen): Promise<TData | null> {
   let parsed: TData | null = null;
+  const val = await consumeStringGen(input);
 
   try {
-    parsed = JSON.parse(input);
+    parsed = JSON.parse(val);
   }
   catch {
     try {
-      parsed = JSON.parse(await decompress(input, compressionFormat, "string"));
+      parsed = JSON.parse(await decompress(val, compressionFormat, "string"));
     }
     catch(err) {
       error("Couldn't decompress and parse data due to an error:", err);
@@ -242,6 +243,14 @@ export async function reloadTab() {
   }
 }
 
+/** Checks if the passed value is a {@linkcode StringGen} */
+export function isStringGen(val: unknown): val is StringGen {
+  return typeof val === "string"
+    || typeof val === "function"
+    || (typeof val === "object" && val !== null && "toString" in val && !val.toString().startsWith("[object"))
+    || val instanceof Promise;
+}
+
 //#region resources
 
 /**