diff --git a/VERSION b/VERSION index 589268e..e21e727 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0 \ No newline at end of file +1.4.0 \ No newline at end of file diff --git a/build.sh b/build.sh index 64f3366..9a1506f 100644 --- a/build.sh +++ b/build.sh @@ -16,20 +16,26 @@ copy_files() { local browser="$1"; rm -rf build; mkdir -p build; + npx tsc for file in "${FILES[@]}"; do cp -r "$file" "$BUILD_DIR"; done + # TODO: Improve this. + cp "scripts/purify.min.js" "$BUILD_DIR/scripts/" - find "$BUILD_DIR" -type f -printf "%P\n" | while read -r file; do - if string_in_array "$file" "${VERSION_DISPLAY_FILES[@]}"; then - sed --sandbox -i -E -e "s/#\{\{version\}\}/$WSH_VERSION/" "$BUILD_DIR/$file"; + find "$BUILD_DIR" -type f | while read -r file; do + if string_in_array "$(basename "$file")" "${VERSION_DISPLAY_FILES[@]}"; then + version=$(sed 's/\./\\./g' <<< "$WSH_VERSION"); + sed -E -i "s/#\{\{version\}\}/$version/" "$file"; fi - if string_in_array "$file" "${BROWSER_DEPENDANT_FILES[@]}"; then - sed --sandbox -i -E -e 's/^const BROWSER = "(firefox|chrome)";/const BROWSER = "'"$browser"'";/' "$BUILD_DIR/$file"; + if string_in_array "$(basename "$file")" "${BROWSER_DEPENDANT_FILES[@]}"; then + sed -E -i 's/^const BROWSER(:[\w\d\._]+)? = "(firefox|chrome)";/const BROWSER = "'"$browser"'";/' "$file"; fi done } + +# Start: if [[ "$EUID" -eq 0 ]]; then echo "Please do not run this script as root."; echo "Aborting..." @@ -41,8 +47,12 @@ if [[ "$target" == "package" ]]; then # Most of the process is the same. target="build"; package=0; -else +elif [[ "$target" == "build" ]]; then package=1; +else + echo "Unknown target operation."; + echo "Usage: build.sh "; + exit 1; fi readonly BROWSER="$2"; @@ -55,21 +65,15 @@ readonly CHROME_MIN_VERSION="102"; readonly BACKGROUND_FILE="scripts\/background.js"; # JS files that contain a "const BROWSER = '...';" line. -readonly BROWSER_DEPENDANT_FILES=("scripts/wsh-event-injection.js" "scripts/background.js"); +readonly BROWSER_DEPENDANT_FILES=("wsh-event-injection.js" "background.js"); # Files that contain a "#{{version}}" line. -readonly VERSION_DISPLAY_FILES=("html/popup.html"); +readonly VERSION_DISPLAY_FILES=("popup.html"); # Files to be copied to the build directory. -readonly FILES=("css" "data" "html" "img" "scripts" "LICENSE" "README.md"); +readonly FILES=("css" "data" "html" "img" "LICENSE" "README.md"); readonly BUILD_DIR="build"; -if [[ -z "$target" || -z "$BROWSER" ]]; then - echo "Missing arguments"; - echo "Usage: build.sh "; - exit 1; -fi - if [[ "$BROWSER" == "firefox" ]]; then min_version='"browser_specific_settings": {"gecko": {"strict_min_version": "'$FIREFOX_MIN_VERSION'"}}'; service_worker='"scripts": ["'$BACKGROUND_FILE'"]'; @@ -84,19 +88,10 @@ fi if [[ "$target" == "build" ]]; then manifest_output_path="$BUILD_DIR/manifest.json"; copy_files "$BROWSER"; -elif [[ "$target" == "develop" ]]; then - manifest_output_path="manifest.json"; - # If the file is browser dependant, replace the BROWSER constant. - for file in "${BROWSER_DEPENDANT_FILES[@]}"; do - sed --sandbox -i -E -e 's/^const BROWSER = "(firefox|chrome)";/const BROWSER = "'"$BROWSER"'";/' "$file"; - done -else - echo "Invalid operation argument: $target"; - exit 1; fi # Replace #{{...}} with the actual values in the manifest file. -if ! sed --sandbox -E -e "s/#\{\{name\}\}/$WSH_NAME/" \ +if ! sed -E -e "s/#\{\{name\}\}/$WSH_NAME/" \ -e "s/#\{\{description\}\}/$WSH_DESCRIPTION/" \ -e "s/#\{\{version\}\}/$WSH_VERSION/" \ -e "s/#\{\{min_version\}\}/$min_version/" \ diff --git a/data/default-options.json b/data/default-options.json index 9c30677..10d5a54 100644 --- a/data/default-options.json +++ b/data/default-options.json @@ -1,6 +1,6 @@ { "enabled": true, - "catch_links": false, + "catch-links": false, "justify-box-text": false, "box-font-size": 14, "box-timeout": 15 diff --git a/html/info.html b/html/info.html index 5d4fc1f..be6ae14 100644 --- a/html/info.html +++ b/html/info.html @@ -39,7 +39,7 @@

Here you have the list of the stuff you can link (the "Text" column represents the text you need to select; case insensitive):

- + diff --git a/html/popup.html b/html/popup.html index 1163356..bf95564 100644 --- a/html/popup.html +++ b/html/popup.html @@ -3,6 +3,7 @@ +
diff --git a/metadata/manifest.template.json b/metadata/manifest.template.json index c5a5746..378487f 100644 --- a/metadata/manifest.template.json +++ b/metadata/manifest.template.json @@ -11,15 +11,15 @@ "content_scripts": [ { "matches": ["https://www.worldcubeassociation.org/*"], - "js": ["scripts/purify.min.js", "scripts/content/base.js", "scripts/content/wca-website.js", "scripts/content/main.js"] + "js": ["scripts/purify.min.js", "scripts/content/base.js", "scripts/content/wca-website.js", "scripts/factory.js", "scripts/content/main.js"] }, { "matches": ["https://forum.worldcubeassociation.org/*"], - "js": ["scripts/purify.min.js", "scripts/content/base.js", "scripts/content/wca-forum.js", "scripts/content/main.js"] + "js": ["scripts/purify.min.js", "scripts/content/base.js", "scripts/content/wca-forum.js", "scripts/factory.js", "scripts/content/main.js"] }, { "matches": ["https://mail.google.com/*"], - "js": ["scripts/purify.min.js", "scripts/content/base.js", "scripts/content/gmail.js", "scripts/content/main.js"] + "js": ["scripts/purify.min.js", "scripts/content/base.js", "scripts/content/gmail.js", "scripts/factory.js", "scripts/content/main.js"] } ], "background": { diff --git a/package-lock.json b/package-lock.json index 3946733..bd012e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,14 +5,106 @@ "packages": { "": { "devDependencies": { + "@types/chrome": "^0.0.268", + "@types/codemirror": "^5.60.15", + "@types/dompurify": "^3.0.5", + "@types/firefox-webext-browser": "^120.0.3", + "@types/tern": "^0.23.9", "typescript": "^5.4.5" } }, + "node_modules/@types/chrome": { + "version": "0.0.268", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.268.tgz", + "integrity": "sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/codemirror": { + "version": "5.60.15", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", + "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/firefox-webext-browser": { + "version": "120.0.4", + "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-120.0.4.tgz", + "integrity": "sha512-lBrpf08xhiZBigrtdQfUaqX1UauwZ+skbFiL8u2Tdra/rklkKadYmIzTwkNZSWtuZ7OKpFqbE2HHfDoFqvZf6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.15.tgz", + "integrity": "sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 1afa750..1be5c63 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,10 @@ { "devDependencies": { + "@types/chrome": "^0.0.268", + "@types/codemirror": "^5.60.15", + "@types/dompurify": "^3.0.5", + "@types/firefox-webext-browser": "^120.0.3", + "@types/tern": "^0.23.9", "typescript": "^5.4.5" } } diff --git a/scripts/background.js b/scripts/background.ts similarity index 63% rename from scripts/background.js rename to scripts/background.ts index c0bb905..f2233d9 100644 --- a/scripts/background.js +++ b/scripts/background.ts @@ -1,33 +1,40 @@ -const BROWSER = "chrome"; -const SITES = { - wca_main: "https://www.worldcubeassociation.org/", - wca_forum: "https://forum.worldcubeassociation.org/", - gmail: "https://mail.google.com/" -} -const VALID_URLS = Object.values(SITES).map(url => url + '*'); -const COMMANDS = ["short-replace", "long-replace", "display-regulation", "stop-error", "enable", "disable"]; -const REGULATIONS_JSON = "data/wca-regulations.json"; -const DOCUMENTS_JSON = "data/wca-documents.json"; -const DEFAULT_OPTIONS_JSON = "data/default-options.json"; - -function sendToContentScript(command, all=false) { +// @ts-ignore +const BROWSER: allowed_options.OBrowser = "chrome"; +const VALID_SITES: Array = [ + "https://www.worldcubeassociation.org/", + "https://forum.worldcubeassociation.org/", + "https://mail.google.com/" +] +// @ts-ignore +const VALID_URLS: Array = VALID_SITES.map(url => url + '*'); +const COMMANDS: Array = ["short-replace", "long-replace", "display-regulation", "stop-error", "enable", "disable"]; +const REGULATIONS_JSON: string = "data/wca-regulations.json"; +const DOCUMENTS_JSON: string = "data/wca-documents.json"; +const DEFAULT_OPTIONS_JSON: string = "data/default-options.json"; + +async function sendToContentScript(command: allowed_options.OCommand, all: boolean = false): Promise { if (COMMANDS.includes(command)) { - return (async () => { - let tabs; - if (all) tabs = await chrome.tabs.query({url: VALID_URLS}); - else tabs = await chrome.tabs.query({active: true, lastFocusedWindow: true, url: VALID_URLS}); - for (let tab of tabs) { - await chrome.tabs.sendMessage(tab.id, {command: command, url: tab.url}) - .catch((error) => { - console.error("Could not send message to content script: " + error); - }); - } - })(); + let tabs: chrome.tabs.Tab[]; + let messages: Promise[] = []; + if (all) { + tabs = await chrome.tabs.query({windowType: "normal", status: "complete", url: VALID_URLS}); + } else { + tabs = await chrome.tabs.query({windowType: "normal", status: "complete", url: VALID_URLS, active: true, currentWindow: true}); + } + for (let tab of tabs) { + if (!tab.id) continue; + const promise: Promise = chrome.tabs.sendMessage(tab.id, {command: command, url: tab.url}); + promise.catch((error) => { + console.error("Could not send message to content script: " + error); + }); + messages.push(promise); + } + await Promise.allSettled(messages); } } -async function injectWSHEvent(tab_id) { +async function injectWSHEvent(tab_id: number) { // Inject the WSHReplaceEvent listener into the WCA page. try { if (BROWSER === "chrome") { @@ -49,10 +56,10 @@ async function injectWSHEvent(tab_id) { return true; } -async function fetchDocuments() { - let regulations; - let regulations_version; - let documents; +async function fetchDocumentsFromFiles(): Promise { + let regulations: Record; + let documents: Array; + let regulations_version: string; try { let [regulationsResponse, documentsResponse] = await Promise.all([ @@ -63,7 +70,8 @@ async function fetchDocuments() { let regulationsData = await regulationsResponse.json(); regulations_version = regulationsData.shift().version; regulations = {}; - regulationsData.forEach(element => { + // Here we are generating a dictionary with the regulation id as the key. + regulationsData.forEach((element: wcadocs.TRegulation) => { regulations[element.id.toLowerCase()] = element; }); @@ -86,10 +94,10 @@ async function fetchDocuments() { } } -async function getOptionsFromStorage(options) { +function getOptionsFromStorage(options: allowed_options.OStoredValue[]): {[key: string]: any} | undefined { /* Returns undefined on exception. */ try { - return await chrome.storage.local.get(options); + return chrome.storage.local.get(options); } catch (error) { console.error(`Could not read option/s [${options}] from storage. ${error}`); @@ -108,7 +116,7 @@ chrome.runtime.onInstalled.addListener(async (details) => { const json_option_keys = Object.keys(json_options); chrome.storage.local.get(json_option_keys) .then((stored_options) => { - let undefined_options = {}; + let undefined_options: Record = {}; for (let jok of json_option_keys) { if (stored_options[jok] === undefined) { undefined_options[jok] = json_options[jok]; @@ -124,22 +132,22 @@ chrome.runtime.onInstalled.addListener(async (details) => { sendToContentScript("stop-error", true); }); - await fetchDocuments(); + await fetchDocumentsFromFiles(); }); chrome.runtime.onStartup.addListener(async () => { const result = getOptionsFromStorage(["regulations", "documents"]); if (result === undefined || result.regulations === undefined || result.documents === undefined) { - await fetchDocuments(); + await fetchDocumentsFromFiles(); } }); -chrome.commands.onCommand.addListener(async (command) => { +chrome.commands.onCommand.addListener(async (command: allowed_options.OCommand) => { if (COMMANDS.includes(command)) await sendToContentScript(command); }); chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { - if (sender.id !== chrome.runtime.id) return; + if (sender.id !== chrome.runtime.id || !sender.tab || !sender.tab.id) return; switch (message.command) { case "inject-wsh-event": let status = 1; @@ -149,9 +157,10 @@ chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { if (BROWSER === "chrome") { sendResponse({status: status}); break; + } else if (BROWSER === "firefox") { + return {status: status}; } - // else: - return {status: status}; + break; case "get-internal-url": let url = undefined; if (message.params !== undefined) { diff --git a/scripts/common.d.ts b/scripts/common.d.ts new file mode 100644 index 0000000..d4d5cda --- /dev/null +++ b/scripts/common.d.ts @@ -0,0 +1,61 @@ + +declare namespace wcadocs { + /* + * Types related to WCA Documents. + */ + type TRegulation = { + "class": string, + "id": string, + "content_html": string, + "url": string, + "guideline_label"?: string + }; + type TRegulationsDict = { + [id: string]: TRegulation + } + type TDocument = { + "short_name": string, + "long_name": string, + "url": string + }; + type TDocumentList = TDocument[]; +} + +declare namespace allowed_options { + /* + * Types related to allowed options. + */ + type OReplaceMode = "short-replace" | "long-replace"; + type OCommand = OReplaceMode | "get-internal-url" | "display-regulation" | "inject-wsh-event" | "stop-error" | "enable" | "disable"; + type OBrowser = "chrome" | "firefox"; + type OStoredValue = "regulations" | "documents" | "display-config" | "regulations-version" | "enabled" | "catch-links" | "justify-box-text" | "box-font-size" | "box-timeout"; +} + +declare namespace communication { + /* + * Types related to communication between content scripts and background scripts. + */ + type TSelectionResponse = { + text: string, + rangeStart: CodeMirror.Position, + rangeEnd: CodeMirror.Position, + cmInstanceId: number + }; + type TBasicSelection = { + text: string, + range?: [number, number] | null, + extraFields?: any; + } +} + +// DOMPurify. +declare class DOMPurify { + static sanitize(text: string, options: any): string; +} +// Firefox XPCNativeWrapper. +declare function XPCNativeWrapper(obj: any): any; +declare interface CMElement extends Element { + CodeMirror: CodeMirror.Editor; + // Firefox-specific. + wrappedJSObject: CMElement; +} diff --git a/scripts/content/base.js b/scripts/content/base.js deleted file mode 100644 index f10f37f..0000000 --- a/scripts/content/base.js +++ /dev/null @@ -1,143 +0,0 @@ - -class BaseContentModule { - // -- URLs -- // - static WCA_MAIN_URL = "https://www.worldcubeassociation.org/"; - static WCAREGS_URL = "https://wcaregs.netlify.app/"; - static WCA_SHORT_URL = "https://wca.link/"; - static GOOGLE_SAFE_REDIRECT_URL = "https://www.google.com/url?q="; - static REGULATIONS_RELATIVE_URL = "regulations/"; - static PERSON_RELATIVE_URL = "persons/"; - static INCIDENT_LOG_RELATIVE_URL = "incidents/"; - - // -- Regex -- // - static REGULATION_REGEX = /(([1-9][0-9]?[a-z]([1-9][0-9]?[a-z]?)?)|([a-z][1-9][0-9]?([a-z]([1-9][0-9]?)?)?))\b\+{0,10}/i; - static PERSON_REGEX = /\b[1-9]\d{3}[a-z]{4}\d{2}\b/i - static INCIDENT_LOG_REGEX = /\bil#[1-9]\d{0,5}\b/i - static CATCH_LINKS_REGEX = new RegExp(`(${this.WCA_MAIN_URL}${this.REGULATIONS_RELATIVE_URL}(guidelines.html)?|${this.WCAREGS_URL}|${this.WCA_SHORT_URL})(#|%23)`, "i"); - - constructor(regulations, documents, siteName, siteURL) { - this._regulations = regulations; - this._documents = documents; - this._siteName = siteName; - this._siteURL = siteURL; - this._documentFunctions = [ - this._getWCADocument, - this._getRegulationOrGuideline, - this._getPerson, - this._getIncidentLog - ]; - } - - get regulations() { - return this._regulations; - } - - get documents() { - return this._documents; - } - - get siteName() { - return this._siteName; - } - - get siteURL() { - return this._siteURL; - } - - setUp() { - return true; - } - - getPageSelection() { - /* - returns: - { - text: selected text, - range: (selected range | null) - } - */ - throw new Error("getPageSelection() not implemented."); - } - - replace(link_text, link_url, selection) { - throw new Error("replace() not implemented."); - } - - log(message) { - console.log(`[WCA Staff Helper][${this.siteName}] ${message}`); - } - - _getWCADocument(text, mode) { - "use strict"; - for (let doc of this._documents) { - if (text === doc.short_name.toLowerCase()) { - let link_text; - if (mode === "short-replace") { - link_text = doc.short_name; - } else { - link_text = doc["long_name"]; - } - return [link_text, doc.url]; - } - } - return [null, null]; - } - - _getRegulationOrGuideline(text, mode) { - "use strict"; - let reg_num = text.match(BaseContentModule.REGULATION_REGEX); - if (!reg_num || text.length !== reg_num[0].length || !this._regulations[reg_num[0]]) return [null, null]; - reg_num = reg_num[0]; - const type = text.includes("+") ? "Guideline" : "Regulation"; - const link_text = mode === "short-replace" ? this._regulations[reg_num].id : `${type} ${this._regulations[reg_num].id}`; - const link_url = `${BaseContentModule.WCA_MAIN_URL}${this._regulations[reg_num].url.substring(1)}`; - return [link_text, link_url]; - } - - _getPerson(text) { - "use strict"; - let person = text.match(BaseContentModule.PERSON_REGEX); - if (!person || text.length !== person[0].length) return [null, null]; - const link_text = person[0].toUpperCase(); - const link_url = `${BaseContentModule.WCA_MAIN_URL}${BaseContentModule.PERSON_RELATIVE_URL}${link_text}`; - return [link_text, link_url]; - } - - _getIncidentLog(text, mode) { - "use strict"; - let incident_log = text.match(BaseContentModule.INCIDENT_LOG_REGEX); - if (!incident_log || text.length !== incident_log[0].length) return [null, null]; - incident_log = incident_log[0].split("#")[1]; - const link_text = mode === "short-replace" ? `#${incident_log}` : `Incident Log #${incident_log}`; - const link_url = `${BaseContentModule.WCA_MAIN_URL}${BaseContentModule.INCIDENT_LOG_RELATIVE_URL}${incident_log}`; - return [link_text, link_url]; - } - - getLinkData(text, mode) { - "use strict"; - let link_text, link_url; - const doc_string = text.toLowerCase(); - - for (let func of this._documentFunctions) { - [link_text, link_url] = func.call(this, doc_string, mode); - if (link_text && link_url) return [link_text, link_url]; - } - return [null, null]; - } - - static async getOptionsFromStorage(options) { - /* Returns undefined on exception. */ - try { - return await chrome.storage.local.get(options); - } - catch (error) { - console.error(`Could not read option/s [${options}] from storage. ${error}`); - } - return undefined; - } - - static async sendCommand(command, params={}) { - /* Send command and get response */ - return await chrome.runtime.sendMessage({command: command, params: params}); - } -} diff --git a/scripts/content/base.ts b/scripts/content/base.ts new file mode 100644 index 0000000..5e64547 --- /dev/null +++ b/scripts/content/base.ts @@ -0,0 +1,152 @@ + +interface ContentModule { + setUp(): Promise; + getPageSelection(targetReplacement?: boolean): Promise; + replace(link_text: string, link_url: string, selection: communication.TBasicSelection): void; + getLinkData(text: string, mode: allowed_options.OReplaceMode): [string, string] | [null, null]; + log(message: string): void; + regulations: wcadocs.TRegulationsDict; + documents: wcadocs.TDocumentList; +} + +abstract class BaseContentModule implements ContentModule { + // -- URLs -- // + static WCA_MAIN_URL = "https://www.worldcubeassociation.org/"; + static WCAREGS_URL = "https://wcaregs.netlify.app/"; + static WCA_SHORT_URL = "https://wca.link/"; + static GOOGLE_SAFE_REDIRECT_URL = "https://www.google.com/url?q="; + static REGULATIONS_RELATIVE_URL = "regulations/"; + static PERSON_RELATIVE_URL = "persons/"; + static INCIDENT_LOG_RELATIVE_URL = "incidents/"; + + // -- Regex -- // + static REGULATION_REGEX = /(([1-9][0-9]?[a-z]([1-9][0-9]?[a-z]?)?)|([a-z][1-9][0-9]?([a-z]([1-9][0-9]?)?)?))\b\+{0,10}/i; + static PERSON_REGEX = /\b[1-9]\d{3}[a-z]{4}\d{2}\b/i + static INCIDENT_LOG_REGEX = /\bil#[1-9]\d{0,5}\b/i + static CATCH_LINKS_REGEX = new RegExp(`((${this.WCA_MAIN_URL}|${this.WCA_SHORT_URL})${this.REGULATIONS_RELATIVE_URL}((full|guidelines.html)/?)?|${this.WCAREGS_URL})(#|%23)`, "i"); + + // -- Properties -- // + protected readonly _regulations: wcadocs.TRegulationsDict; + protected readonly _documents: wcadocs.TDocumentList; + protected readonly _siteName: string; + protected readonly _siteURL: string; + protected readonly _documentFunctions: Array; + + protected constructor(regulations: wcadocs.TRegulationsDict, documents: wcadocs.TDocumentList, siteName: string, siteURL: string) { + this._regulations = regulations; + this._documents = documents; + this._siteName = siteName; + this._siteURL = siteURL; + this._documentFunctions = [ + this._getWCADocument, + this._getRegulationOrGuideline, + this._getPerson, + this._getIncidentLog + ]; + } + + abstract setUp(): Promise; + + abstract getPageSelection(targetReplacement?: boolean): Promise; + + /* + * Returns false if the replacement operation was aborted. + * A true return value does not mean that the replacement succeeded. + */ + abstract replace(link_text: string, link_url: string, selection: communication.TBasicSelection): boolean; + + log(message: string) { + console.log(`[WCA Staff Helper][${this._siteName.toString()}] ${message.toString()}`); + } + + get regulations() { + return this._regulations; + } + + get documents() { + return this._documents; + } + + protected _getWCADocument(text: string, mode: allowed_options.OReplaceMode) { + "use strict"; + for (let doc of this._documents) { + if (text === doc.short_name.toLowerCase()) { + let link_text; + if (mode === "short-replace") { + link_text = doc.short_name; + } else { + link_text = doc["long_name"]; + } + return [link_text, doc.url]; + } + } + return [null, null]; + } + + protected _getRegulationOrGuideline(text: string, mode: allowed_options.OReplaceMode) { + "use strict"; + const reg_num = text.match(BaseContentModule.REGULATION_REGEX); + if (!reg_num || text.length !== reg_num[0].length || !this._regulations[reg_num[0]]) return [null, null]; + const reg_num_str = reg_num[0]; + const type = text.includes("+") ? "Guideline" : "Regulation"; + const link_text = mode === "short-replace" ? this._regulations[reg_num_str].id : `${type} ${this._regulations[reg_num_str].id}`; + const link_url = `${BaseContentModule.WCA_MAIN_URL}${BaseContentModule.REGULATIONS_RELATIVE_URL}full/#${this._regulations[reg_num_str].id}`; + return [link_text, link_url]; + } + + protected _getPerson(text: string) { + "use strict"; + const person = text.match(BaseContentModule.PERSON_REGEX); + if (!person || text.length !== person[0].length) return [null, null]; + const link_text = person[0].toUpperCase(); + const link_url = `${BaseContentModule.WCA_MAIN_URL}${BaseContentModule.PERSON_RELATIVE_URL}${link_text}`; + return [link_text, link_url]; + } + + protected _getIncidentLog(text: string, mode: allowed_options.OReplaceMode) { + "use strict"; + const incident_log = text.match(BaseContentModule.INCIDENT_LOG_REGEX); + if (!incident_log || text.length !== incident_log[0].length) return [null, null]; + const incident_log_str = incident_log[0].split("#")[1]; + const link_text = mode === "short-replace" ? `#${incident_log_str}` : `Incident Log #${incident_log_str}`; + const link_url = `${BaseContentModule.WCA_MAIN_URL}${BaseContentModule.INCIDENT_LOG_RELATIVE_URL}${incident_log_str}`; + return [link_text, link_url]; + } + + getLinkData(text: string, mode: allowed_options.OReplaceMode): [string, string] | [null, null] { + /* + * Returns [link_text, link_url] if the text is a valid link. + */ + "use strict"; + let link_text: string; + let link_url: string; + const doc_string = text.toLowerCase(); + + for (let func of this._documentFunctions) { + [link_text, link_url] = func.call(this, doc_string, mode); + if (link_text && link_url) { + // We don't trust the browser's storage. + const text = DOMPurify.sanitize(link_text, {ALLOWED_TAGS: []}); + const url = DOMPurify.sanitize(link_url, {ALLOWED_TAGS: []}); + return [text, url]; + } + } + return [null, null]; + } + + static async getOptionsFromStorage(options: allowed_options.OStoredValue[]) { + /* Returns undefined on exception. */ + try { + return await chrome.storage.local.get(options); + } + catch (error) { + console.error(`Could not read option/s [${options.toString()}] from storage. ${error.toString()}`); + } + return undefined; + } + + static async sendCommand(command: allowed_options.OCommand, params={}) { + /* Send command and get response */ + return await chrome.runtime.sendMessage({command: command, params: params}); + } +} diff --git a/scripts/content/gmail.js b/scripts/content/gmail.ts similarity index 63% rename from scripts/content/gmail.js rename to scripts/content/gmail.ts index 8262f15..dfec451 100644 --- a/scripts/content/gmail.js +++ b/scripts/content/gmail.ts @@ -1,14 +1,27 @@ -class ContentModule extends BaseContentModule { +class GmailContent extends BaseContentModule { - constructor(regulations, documents) { + static #instance: GmailContent; + + private constructor(regulations: wcadocs.TRegulationsDict, documents: wcadocs.TDocumentList) { super(regulations, documents, "Gmail", "https://mail.google.com/"); } - getPageSelection() { - return new Promise((resolve, reject) => { + static getInstance(regulations: wcadocs.TRegulationsDict, documents: wcadocs.TDocumentList) { + if (!GmailContent.#instance) { + GmailContent.#instance = new GmailContent(regulations, documents); + } + return GmailContent.#instance; + } + + async setUp(): Promise { + return true; + } + + getPageSelection(): Promise { + return new Promise((resolve: (value: communication.TBasicSelection) => void): void => { const s = document.getSelection(); - if (s.rangeCount === 0) { + if (!s || s.rangeCount === 0) { resolve({text: ""}); } else { resolve({text: s.toString(), extraFields: {range: s.getRangeAt(0)}}); @@ -16,7 +29,7 @@ class ContentModule extends BaseContentModule { }); } - replace(link_text, link_url, selection) { + replace(link_text: string, link_url: string, selection: any) { // Get Gmail composition windows. const editable_elements = document.getElementsByClassName("editable"); // Check: Is the selected text in a compose element? @@ -29,14 +42,14 @@ class ContentModule extends BaseContentModule { } if (!selection_has_editable_parent) { console.log("Selected text is not in the gmail composition element."); - return; + return false; } // We check that the range start and end are in the same div. if (selection.range.startContainer.parentElement.nodeName !== "DIV" || selection.range.startContainer.parentElement !== selection.range.endContainer.parentElement) { console.log("Cannot replace text in the selection."); - return; + return false; } selection.range.deleteContents(); @@ -45,5 +58,6 @@ class ContentModule extends BaseContentModule { link.textContent = link_text; link.rel = "noreferrer"; selection.range.insertNode(link); + return true; } } diff --git a/scripts/content/main.js b/scripts/content/main.ts similarity index 78% rename from scripts/content/main.js rename to scripts/content/main.ts index 93ab886..1a9cb3d 100644 --- a/scripts/content/main.js +++ b/scripts/content/main.ts @@ -1,55 +1,53 @@ + +type TRegulationBox = { + box_node: HTMLDivElement | null, + p_node: HTMLParagraphElement | null, + pin_img: HTMLImageElement | null, + active: boolean, + timer: number, + pinned: boolean, + timeout: number, + justified: boolean, + font_size: number +} + // --- Global variables --- // const REPLACE_COMMANDS = ["short-replace", "long-replace"]; let stop_error = false; let setup_done = false; -let regulation_box = { +let regulation_box: TRegulationBox = { box_node: null, p_node: null, pin_img: null, active: false, - timer: null, + timer: -1, pinned: false, - timeout: null, + timeout: 0, justified: false, - font_size: null + font_size: 0 }; -let enabled = false; -let link_catching_enabled = false; -let content_class; +let enabled: boolean = false; +let link_catching_enabled: boolean = false; +let content_class: ContentModule; -async function fetchDocuments() { - /* Gets regulations and documents from storage. */ - let regulations = null; - let documents = null; - const result = await BaseContentModule.getOptionsFromStorage(["regulations", "documents"]); - if (result !== undefined && result.regulations !== undefined && result.documents !== undefined) { - regulations = result.regulations; - documents = result.documents; - } else { - alert("Regulations and document data not found. Try restarting your browser."); - stop_error = true; - } - return [regulations, documents]; -} -async function getPageSelection() { +async function getPageSelection(targetReplacement: boolean): Promise { // Get selected text. let response; let selection = ""; try { - response = await content_class.getPageSelection(); + response = await content_class.getPageSelection(targetReplacement); selection = response.text.trim(); } catch (e) { console.log(`Could not get selected text: ${e}`); } - // Try the native way to get the selection. - if (!response || selection === "") { - selection = document.getSelection().toString().trim(); + if (!response) { + selection = ""; } return selection; } -function getRegulationFromString(string) { +function getRegulationFromString(string: string): wcadocs.TRegulation | undefined { const reg_num = string.toLowerCase().match(BaseContentModule.REGULATION_REGEX); if (!reg_num) { return undefined; @@ -57,7 +55,7 @@ function getRegulationFromString(string) { return content_class.regulations[reg_num[0]]; // Returns undefined if the regulation doesn't exist. } -function getRegulationFromUrl(original_url) { +function getRegulationFromUrl(original_url: string): wcadocs.TRegulation | undefined { // We need to deal with Google's safe redirect urls: let url = original_url; if (original_url.startsWith(BaseContentModule.GOOGLE_SAFE_REDIRECT_URL)) { @@ -68,16 +66,16 @@ function getRegulationFromUrl(original_url) { return getRegulationFromString(url); } -async function displayRegulationBox(regulation, original_url="") { +async function displayRegulationBox(regulation: wcadocs.TRegulation | undefined, original_url="") { /* Display the regulation in a box/popup. */ - let div_node = regulation_box.box_node; - let p_node = regulation_box.p_node; - let unsafe_HTML; + let div_node = regulation_box.box_node as unknown as HTMLDivElement; + let p_node = regulation_box.p_node as unknown as HTMLParagraphElement; + let unsafe_HTML: string; - if (regulation !== undefined) { + if (regulation) { const guideline_label = regulation["guideline_label"] === undefined ? "" : `${regulation["guideline_label"]}`; - unsafe_HTML = `${regulation["id"]}) ${guideline_label} ${regulation["content_html"]}`; + unsafe_HTML = `${regulation["id"]}) ${guideline_label} ${regulation["content_html"]}`; } else if (original_url !== "") { // If the regulation is not found, display the original link. unsafe_HTML = `Something went wrong. If you want to open the link, disable the link catching option (safer, requires tab reload) or follow the original link (be careful!): ${original_url}`; @@ -107,7 +105,7 @@ async function displayRegulationBox(regulation, original_url="") { regulation_box.timer = setTimeout(() => { div_node.style.display = "none"; regulation_box.active = false; - regulation_box.timer = null; + regulation_box.timer = -1; }, regulation_box.timeout * 1000); } } @@ -144,14 +142,6 @@ async function setUpLinkCatching() { close_img.src = close_icon.url; close_img.alt = "Close"; btn_div_node.appendChild(close_img); - close_img.addEventListener("click", () => { - box_node.style.display = "none"; - regulation_box.active = false; - clearTimeout(regulation_box.timer); - regulation_box.timer = null; - regulation_box.pinned = false; - regulation_box.pin_img.style.display = "block"; - }); // Create the pin button. let pin_img = document.createElement("img"); @@ -161,9 +151,20 @@ async function setUpLinkCatching() { pin_img.alt = "Pin"; pin_img.style.display = "block"; btn_div_node.appendChild(pin_img); + + // Listener for the close button. + close_img.addEventListener("click", () => { + box_node.style.display = "none"; + regulation_box.active = false; + clearTimeout(regulation_box.timer); + regulation_box.timer = -1; + regulation_box.pinned = false; + regulation_box.pin_img!.style.display = "block"; + }); + // Listener for the pin button. pin_img.addEventListener("click", () => { clearTimeout(regulation_box.timer); - regulation_box.timer = null; + regulation_box.timer = -1; regulation_box.pinned = true; pin_img.style.display = "none"; }); @@ -176,6 +177,7 @@ async function setUpLinkCatching() { // Catch links to regulations. document.addEventListener("click", async (click_event) => { const clicked_element = click_event.target; + if (!clicked_element || !(clicked_element instanceof HTMLAnchorElement) || !clicked_element["href"]) return; const wl = window.location.href; // We want to open the link normally if: @@ -186,9 +188,10 @@ async function setUpLinkCatching() { // - We are already in the regulations/wcaregs page. // - The clicked element is the regulation number in the box. // - The link does not point to the regulations/wcaregs page with a "#" (covered in the next if). - if (!enabled || stop_error || !link_catching_enabled || clicked_element["tagName"] !== "A" || - wl.startsWith(BaseContentModule.WCA_MAIN_URL + BaseContentModule.REGULATIONS_RELATIVE_URL) || wl.startsWith(BaseContentModule.WCAREGS_URL) || - (regulation_box.box_node.contains(clicked_element) && clicked_element["id"] === "reg-num")) { + if (!enabled || stop_error || !link_catching_enabled || + wl.startsWith(BaseContentModule.WCA_MAIN_URL + BaseContentModule.REGULATIONS_RELATIVE_URL) || + wl.startsWith(BaseContentModule.WCAREGS_URL) || + (regulation_box.box_node!.contains(clicked_element) && clicked_element["id"] === "reg-num")) { return; } @@ -201,15 +204,19 @@ async function setUpLinkCatching() { } async function lazySetUp() { - let regulations, documents; - [regulations, documents] = await fetchDocuments(); - content_class = new ContentModule(regulations, documents); - if (content_class.setUp() === false) { + try { + content_class = await Factory.getContentClass(window.location.href); + } catch (e) { + console.error(`Could not create the content class: ${e}`); stop_error = true; return; } - await setUpLinkCatching(); - setup_done = true; + if (await content_class.setUp()) { + await setUpLinkCatching(); + setup_done = true; + } else { + stop_error = true; + } } async function setUp() { @@ -241,10 +248,10 @@ async function setUp() { } } -async function replace(command) { +async function replace(command: allowed_options.OReplaceMode): Promise { /* Execute the text replacement flow using the content class. */ - content_class.getPageSelection() + content_class.getPageSelection(true) .then((response) => { const selected_text = response.text.trim().toLowerCase(); if (selected_text === "") return; @@ -257,7 +264,7 @@ async function replace(command) { content_class.replace(link_text, link_url, response.extraFields); }) .catch((error) => { - console.log(`Could not get the selected text: ${error}`); + console.log(`Could not get a valid text selection from the page: ${error.toString()}`); }); } @@ -269,7 +276,7 @@ chrome.runtime.onMessage.addListener( switch (message.command) { case "display-regulation": if (enabled) { - const selection = await getPageSelection(); + const selection = await getPageSelection(false); if (selection !== "") { await displayRegulationBox(getRegulationFromString(selection)); } diff --git a/scripts/content/wca-forum.js b/scripts/content/wca-forum.js deleted file mode 100644 index 97d8578..0000000 --- a/scripts/content/wca-forum.js +++ /dev/null @@ -1,24 +0,0 @@ - -class ContentModule extends BaseContentModule { - constructor(regulations, documents) { - super(regulations, documents, "WCA Forum", "https://forum.worldcubeassociation.org/"); - } - - getPageSelection() { - return new Promise((resolve) => { - const editor_elem = document.querySelector(".d-editor-input"); - const range = [editor_elem.selectionStart, editor_elem.selectionEnd]; - const text = editor_elem.value.slice(range[0], range[1]); - const detail = { - range: range, - editorElement: editor_elem - }; - resolve({text: text, extraFields: detail}); - }); - } - - replace(link_text, link_url, extraFields) { - const safe_text = DOMPurify.sanitize(`[${link_text}](${link_url})`, {ALLOWED_TAGS: []}) - extraFields.editorElement.setRangeText(safe_text, extraFields.range[0], extraFields.range[1], "end"); - } -} diff --git a/scripts/content/wca-forum.ts b/scripts/content/wca-forum.ts new file mode 100644 index 0000000..e54e8de --- /dev/null +++ b/scripts/content/wca-forum.ts @@ -0,0 +1,49 @@ + +class WCAForumContent extends BaseContentModule { + + static #instance: WCAForumContent; + + private constructor(regulations: wcadocs.TRegulationsDict, documents: wcadocs.TDocumentList) { + super(regulations, documents, "WCA Forum", "https://forum.worldcubeassociation.org/"); + } + + static getInstance(regulations: wcadocs.TRegulationsDict, documents: wcadocs.TDocumentList) { + if (!WCAForumContent.#instance) { + WCAForumContent.#instance = new WCAForumContent(regulations, documents); + } + return WCAForumContent.#instance; + } + + async setUp() { + return true; + } + + getPageSelection(targetReplacement: boolean): Promise { + return new Promise((resolve: (value: communication.TBasicSelection) => void): void => { + if (!targetReplacement) { + const selection = document.getSelection(); + if (selection !== null) + resolve({text: selection.toString()}) + } + const editor_elem = document.querySelector(".d-editor-input") as HTMLInputElement; + if (!editor_elem || editor_elem.selectionStart === null || editor_elem.selectionEnd === null + || editor_elem.selectionStart === editor_elem.selectionEnd) { + resolve({text: ""}); + return; + } + const range = [editor_elem.selectionStart, editor_elem.selectionEnd]; + const text = editor_elem.value.slice(range[0], range[1]); + const detail = { + range: range, + editorElement: editor_elem + }; + resolve({text: text, extraFields: detail}); + }); + } + + replace(link_text: string, link_url: string, extraFields: any) { + const safe_text = DOMPurify.sanitize(`[${link_text}](${link_url})`, {ALLOWED_TAGS: []}) + extraFields.editorElement.setRangeText(safe_text, extraFields.range[0], extraFields.range[1], "end"); + return true; + } +} diff --git a/scripts/content/wca-website.js b/scripts/content/wca-website.js deleted file mode 100644 index 261d6e7..0000000 --- a/scripts/content/wca-website.js +++ /dev/null @@ -1,74 +0,0 @@ -// -- Custom events -- // -const SELECTION_REQUEST_EVENT = "WSHSelectionRequestEvent"; -const SELECTION_RESPONSE_EVENT = "WSHSelectionResponseEvent"; -const REPLACE_EVENT = "WSHReplaceEvent"; - -// -- Constants -- // -const SELECTION_PENDING_FAIL = "A selection is already pending"; -const SELECTION_TIMEOUT_FAIL = "WSHSelectionTimeoutFail"; -const SELECTION_TIMEOUT = 1000; - -class ContentModule extends BaseContentModule { - - // Private fields. - #pendingSelection; - - constructor(regulations, documents) { - super(regulations, documents, "WCA Website", "https://www.worldcubeassociation.org/"); - - } - - setUp() { - if (document.getElementsByClassName("EasyMDEContainer").length === 0) return false; - - this.#pendingSelection = false; - - BaseContentModule.sendCommand("inject-wsh-event") - .then((response) => { - if (response && response.status !== 0) { - console.error("Could not inject WSHReplaceEvent"); - return false; - } - return true; - }) - .catch((error) => { - console.error("Could not inject WSHReplaceEvent: " + error); - return false; - }); - } - - getPageSelection() { - return new Promise((resolve, reject) => { - if (this.#pendingSelection) { - reject(SELECTION_PENDING_FAIL); - return; - } - - this.#pendingSelection = true; - - // If we never get a response, reject the promise. - const timeout = setTimeout(() => { - this.#pendingSelection = false; - reject(SELECTION_TIMEOUT_FAIL); - }, SELECTION_TIMEOUT); - - // Add event listener for selection response. - document.addEventListener(SELECTION_RESPONSE_EVENT, (e) => { - this.#pendingSelection = false; - clearTimeout(timeout); - resolve({text: e.detail.text, extraFields: e.detail}); - }, {once: true}); - - // Request selection from the page to CodeMirror 5. - const request = new CustomEvent(SELECTION_REQUEST_EVENT); - document.dispatchEvent(request); - }); - } - - replace(link_text, link_url, extraFields) { - let detail = extraFields; - detail["text"] = `[${link_text}](${link_url})`; - const e = new CustomEvent(REPLACE_EVENT, {detail: detail}); - document.dispatchEvent(e); - } -} diff --git a/scripts/content/wca-website.ts b/scripts/content/wca-website.ts new file mode 100644 index 0000000..c6c4d77 --- /dev/null +++ b/scripts/content/wca-website.ts @@ -0,0 +1,94 @@ +// -- Custom events -- // +const SELECTION_REQUEST_EVENT = "WSHSelectionRequestEvent"; +const SELECTION_RESPONSE_EVENT = "WSHSelectionResponseEvent"; +const REPLACE_EVENT = "WSHReplaceEvent"; + +// -- Constants -- // +const SELECTION_PENDING_FAIL = "A selection is already pending"; +const SELECTION_TIMEOUT_FAIL = "WSHSelectionTimeoutFail"; +const SELECTION_TIMEOUT = 1000; + +class WCAWebsiteContent extends BaseContentModule { + + // Private fields. + static #instance: WCAWebsiteContent; + #pendingSelection: boolean; + + private constructor(regulations: wcadocs.TRegulationsDict, documents: wcadocs.TDocumentList) { + super(regulations, documents, "WCA Website", "https://www.worldcubeassociation.org/"); + this.#pendingSelection = false; + } + + static getInstance(regulations: wcadocs.TRegulationsDict, documents: wcadocs.TDocumentList) { + if (!WCAWebsiteContent.#instance) { + WCAWebsiteContent.#instance = new WCAWebsiteContent(regulations, documents); + } + return WCAWebsiteContent.#instance; + } + + async setUp(): Promise { + if (document.getElementsByClassName("EasyMDEContainer").length === 0) return false; + + this.#pendingSelection = false; + + try { + const response = await BaseContentModule.sendCommand("inject-wsh-event"); + if (response && response.status !== 0) { + console.error("Could not inject WSHReplaceEvent"); + return false; + } + } catch (error) { + console.error("Could not inject WSHReplaceEvent: " + error); + return false; + } + return true; + } + + getPageSelection(targetReplacement?: boolean): Promise { + /* + * The selection is only editable if it came from CodeMirror. + * If the caller wants to use the selection to replace, we only return selections from CodeMirror. + */ + return new Promise((resolve: (value: communication.TBasicSelection) => void, reject: (reason?: String) => void): void => { + if (this.#pendingSelection) { + reject(SELECTION_PENDING_FAIL); + return; + } + + const documentSelection = document.getSelection(); + if (!targetReplacement && documentSelection && documentSelection.rangeCount > 0) { + const text = documentSelection.toString(); + if (text !== "") { + resolve({text: text}) + return; + } + } + + this.#pendingSelection = true; + // If we never get a response, reject the promise. + const timeout = setTimeout(() => { + this.#pendingSelection = false; + reject(SELECTION_TIMEOUT_FAIL); + }, SELECTION_TIMEOUT); + + // Add event listener for selection response. + document.addEventListener(SELECTION_RESPONSE_EVENT, (custom_event: CustomEvent): void => { + clearTimeout(timeout); + this.#pendingSelection = false; + resolve({text: custom_event.detail.text, extraFields: custom_event.detail}); + }, {once: true}); + + // Request selection from the page to CodeMirror 5. + const request = new CustomEvent(SELECTION_REQUEST_EVENT, {bubbles: false}); + document.dispatchEvent(request); + }); + } + + replace(link_text: string, link_url: string, extraFields: communication.TBasicSelection["extraFields"]) { + let detail = extraFields; + detail["text"] = `[${link_text}](${link_url})`; + const e = new CustomEvent(REPLACE_EVENT, {bubbles: false, detail: detail}); + document.dispatchEvent(e); + return true; + } +} diff --git a/scripts/factory.ts b/scripts/factory.ts new file mode 100644 index 0000000..8bd085f --- /dev/null +++ b/scripts/factory.ts @@ -0,0 +1,47 @@ +/* + * Factory class for creating instances of BaseContentModule subclasses. + */ +class Factory { + private static readonly mappedSites = { + wca_main: "https://www.worldcubeassociation.org/", + wca_forum: "https://forum.worldcubeassociation.org/", + gmail: "https://mail.google.com/" + }; + + private static async fetchDocumentsFromStorage() { + /* Gets regulations and documents from storage. */ + let regulations = null; + let documents = null; + const result = await BaseContentModule.getOptionsFromStorage(["regulations", "documents"]); + if (result && result.regulations && result.documents) { + regulations = result.regulations; + documents = result.documents; + } else { + alert("Regulations and document data not found. Try restarting your browser."); + stop_error = true; + } + return [regulations, documents]; + } + + /* + * Get the class for the content script based on the site. + */ + static async getContentClass(site: string) { + let contentClass; + if (site.startsWith(this.mappedSites.wca_main)) { + contentClass = WCAWebsiteContent; + } else if (site.startsWith(this.mappedSites.wca_forum)) { + contentClass = WCAForumContent; + } else if (site.startsWith(this.mappedSites.gmail)) { + contentClass = GmailContent; + } else { + throw new Error(`No content class found for site: ${site}`); + } + let regulations, documents; + [regulations, documents] = await Factory.fetchDocumentsFromStorage(); + if (!regulations || !documents) { + throw new Error("Could not retrieve WCA documents from storage.") + } + return contentClass.getInstance(regulations, documents); + } +} diff --git a/scripts/info.js b/scripts/info.ts similarity index 77% rename from scripts/info.js rename to scripts/info.ts index f15e0b5..2805079 100644 --- a/scripts/info.js +++ b/scripts/info.ts @@ -5,12 +5,11 @@ const documents = { "Code of Conduct (CoC)": "coc", "Regulation or Guideline": "Only the number of the regulation or guideline (you can use lowercase for this too).", "WCA ID": "Only the WCA ID (it needs to be a valid WCA ID, but it won't check if it exists).", - "Incident Log": "il# (i.e. \"il#51\")" + "Incident Log": `il# (i.e. \"il#51\")` }; -const table = document.getElementById("documents"); +const table = document.getElementById("documents") as HTMLTableElement; -for (let key in documents) { - const value = documents[key]; +for (const [key, value] of Object.entries(documents)) { const row = table.insertRow(-1); const item_column = row.insertCell(0); const text_column = row.insertCell(1); diff --git a/scripts/popup.js b/scripts/popup.ts similarity index 84% rename from scripts/popup.js rename to scripts/popup.ts index b7f9c76..0c12e53 100644 --- a/scripts/popup.js +++ b/scripts/popup.ts @@ -4,23 +4,21 @@ const SITES = { wca_forum: "https://forum.worldcubeassociation.org/", gmail: "https://mail.google.com/" } +// @ts-ignore const VALID_URLS = Object.values(SITES).map(url => url + '*'); -// --- Allowed commands --- // -const COMMANDS = ["display-regulation"]; - // --- DOM elements --- // -const status_text = document.getElementById('status-text'); -const regulations_version_p = document.getElementById('regulations-version'); -const info_btn = document.getElementById('info-btn'); -const config_btn = document.getElementById('config-btn'); -const config_div = document.getElementById('config-container'); +const status_text = document.getElementById('status-text') as HTMLParagraphElement; +const regulations_version_p = document.getElementById('regulations-version') as HTMLParagraphElement; +const info_btn = document.getElementById('info-btn') as HTMLButtonElement; +const config_btn = document.getElementById('config-btn') as HTMLButtonElement; +const config_div = document.getElementById('config-container') as HTMLDivElement; const options = { - "enabled": document.getElementById('conf-enabled'), - "catch-links": document.getElementById('conf-catch-links'), - "justify-box-text": document.getElementById('conf-justify-box-text'), - "box-font-size": document.getElementById('conf-box-font-size'), - "box-timeout": document.getElementById('conf-box-timeout') + "enabled": document.getElementById('conf-enabled') as HTMLInputElement, + "catch-links": document.getElementById('conf-catch-links') as HTMLInputElement, + "justify-box-text": document.getElementById('conf-justify-box-text') as HTMLInputElement, + "box-font-size": document.getElementById('conf-box-font-size') as HTMLInputElement, + "box-timeout": document.getElementById('conf-box-timeout') as HTMLInputElement }; // --- Popup setup --- // @@ -42,7 +40,7 @@ function setPopupInfo() { url: VALID_URLS }) .then((tabs) => { - if (tabs.length === 0) { + if (tabs.length === 0 || tabs[0].url === undefined) { status_text.textContent = "Unknown site"; return; } @@ -120,7 +118,7 @@ function popupSetup() { } else { new_value = v; } - input_elem.value = new_value; + input_elem.value = String(new_value); break; default: console.error("Unknown option type: " + typeof input_elem.value); diff --git a/scripts/purify.min.js b/scripts/purify.min.js index bb5263e..a30c02b 100644 --- a/scripts/purify.min.js +++ b/scripts/purify.min.js @@ -1,3 +1,3 @@ -/*! @license DOMPurify 3.1.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.0/LICENSE */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),A=b(RegExp.prototype.test),_=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=function(){return"undefined"==typeof window?null:window},Z=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var J=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:V();const o=e=>t(e);if(o.version="3.1.0",o.removed=[],!n||!n.document||9!==n.document.nodeType)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=L(j,"cloneNode"),$=L(j,"nextSibling"),J=L(j,"childNodes"),Q=L(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ee,te="";const{implementation:ne,createNodeIterator:oe,createDocumentFragment:re,getElementsByTagName:ie}=r,{importNode:ae}=a;let le={};o.isSupported="function"==typeof e&&"function"==typeof Q&&ne&&void 0!==ne.createHTMLDocument;const{MUSTACHE_EXPR:ce,ERB_EXPR:se,TMPLIT_EXPR:ue,DATA_ATTR:me,ARIA_ATTR:pe,IS_SCRIPT_OR_DATA:fe,ATTR_WHITESPACE:de,CUSTOM_ELEMENT:he}=K;let{IS_ALLOWED_URI:ge}=K,Te=null;const ye=S({},[...D,...C,...O,...v,...M]);let Ee=null;const Ae=S({},[...I,...U,...P,...F]);let _e=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ne=null,be=null,Se=!0,Re=!0,we=!1,Le=!0,De=!1,Ce=!0,Oe=!1,xe=!1,ve=!1,ke=!1,Me=!1,Ie=!1,Ue=!0,Pe=!1;const Fe="user-content-";let He=!0,ze=!1,Be={},We=null;const Ge=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ye=null;const je=S({},["audio","video","img","source","image","track"]);let Xe=null;const qe=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),$e="http://www.w3.org/1998/Math/MathML",Ke="http://www.w3.org/2000/svg",Ve="http://www.w3.org/1999/xhtml";let Ze=Ve,Je=!1,Qe=null;const et=S({},[$e,Ke,Ve],d);let tt=null;const nt=["application/xhtml+xml","text/html"],ot="text/html";let rt=null,it=null;const at=r.createElement("form"),lt=function(e){return e instanceof RegExp||e instanceof Function},ct=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!it||it!==e){if(e&&"object"==typeof e||(e={}),e=w(e),tt=-1===nt.indexOf(e.PARSER_MEDIA_TYPE)?ot:e.PARSER_MEDIA_TYPE,rt="application/xhtml+xml"===tt?d:f,Te=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,rt):ye,Ee=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,rt):Ae,Qe=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):et,Xe=E(e,"ADD_URI_SAFE_ATTR")?S(w(qe),e.ADD_URI_SAFE_ATTR,rt):qe,Ye=E(e,"ADD_DATA_URI_TAGS")?S(w(je),e.ADD_DATA_URI_TAGS,rt):je,We=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,rt):Ge,Ne=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,rt):{},be=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,rt):{},Be=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,Se=!1!==e.ALLOW_ARIA_ATTR,Re=!1!==e.ALLOW_DATA_ATTR,we=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Le=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,De=e.SAFE_FOR_TEMPLATES||!1,Ce=!1!==e.SAFE_FOR_XML,Oe=e.WHOLE_DOCUMENT||!1,ke=e.RETURN_DOM||!1,Me=e.RETURN_DOM_FRAGMENT||!1,Ie=e.RETURN_TRUSTED_TYPE||!1,ve=e.FORCE_BODY||!1,Ue=!1!==e.SANITIZE_DOM,Pe=e.SANITIZE_NAMED_PROPS||!1,He=!1!==e.KEEP_CONTENT,ze=e.IN_PLACE||!1,ge=e.ALLOWED_URI_REGEXP||Y,Ze=e.NAMESPACE||Ve,_e=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(_e.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&<(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(_e.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(_e.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),De&&(Re=!1),Me&&(ke=!0),Be&&(Te=S({},M),Ee=[],!0===Be.html&&(S(Te,D),S(Ee,I)),!0===Be.svg&&(S(Te,C),S(Ee,U),S(Ee,F)),!0===Be.svgFilters&&(S(Te,O),S(Ee,U),S(Ee,F)),!0===Be.mathMl&&(S(Te,v),S(Ee,P),S(Ee,F))),e.ADD_TAGS&&(Te===ye&&(Te=w(Te)),S(Te,e.ADD_TAGS,rt)),e.ADD_ATTR&&(Ee===Ae&&(Ee=w(Ee)),S(Ee,e.ADD_ATTR,rt)),e.ADD_URI_SAFE_ATTR&&S(Xe,e.ADD_URI_SAFE_ATTR,rt),e.FORBID_CONTENTS&&(We===Ge&&(We=w(We)),S(We,e.FORBID_CONTENTS,rt)),He&&(Te["#text"]=!0),Oe&&S(Te,["html","head","body"]),Te.table&&(S(Te,["tbody"]),delete Ne.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw _('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ee=e.TRUSTED_TYPES_POLICY,te=ee.createHTML("")}else void 0===ee&&(ee=Z(G,c)),null!==ee&&"string"==typeof te&&(te=ee.createHTML(""));i&&i(e),it=e}},st=S({},["mi","mo","mn","ms","mtext"]),ut=S({},["foreignobject","desc","title","annotation-xml"]),mt=S({},["title","style","font","a","script"]),pt=S({},[...C,...O,...x]),ft=S({},[...v,...k]),dt=function(e){let t=Q(e);t&&t.tagName||(t={namespaceURI:Ze,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!Qe[e.namespaceURI]&&(e.namespaceURI===Ke?t.namespaceURI===Ve?"svg"===n:t.namespaceURI===$e?"svg"===n&&("annotation-xml"===o||st[o]):Boolean(pt[n]):e.namespaceURI===$e?t.namespaceURI===Ve?"math"===n:t.namespaceURI===Ke?"math"===n&&ut[o]:Boolean(ft[n]):e.namespaceURI===Ve?!(t.namespaceURI===Ke&&!ut[o])&&(!(t.namespaceURI===$e&&!st[o])&&(!ft[n]&&(mt[n]||!pt[n]))):!("application/xhtml+xml"!==tt||!Qe[e.namespaceURI]))},ht=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},gt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Ee[e])if(ke||Me)try{ht(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Tt=function(e){let t=null,n=null;if(ve)e=""+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===tt&&Ze===Ve&&(e=''+e+"");const o=ee?ee.createHTML(e):e;if(Ze===Ve)try{t=(new W).parseFromString(o,tt)}catch(e){}if(!t||!t.documentElement){t=ne.createDocument(Ze,"template",null);try{t.documentElement.innerHTML=Je?te:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),Ze===Ve?ie.call(t,Oe?"html":"body")[0]:Oe?t.documentElement:i},yt=function(e){return oe.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},Et=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},At=function(e){return"function"==typeof b&&e instanceof b},_t=function(e,t,n){le[e]&&u(le[e],(e=>{e.call(o,t,n,it)}))},Nt=function(e){let t=null;if(_t("beforeSanitizeElements",e,null),Et(e))return ht(e),!0;const n=rt(e.nodeName);if(_t("uponSanitizeElement",e,{tagName:n,allowedTags:Te}),e.hasChildNodes()&&!At(e.firstElementChild)&&A(/<[/\w]/g,e.innerHTML)&&A(/<[/\w]/g,e.textContent))return ht(e),!0;if(7===e.nodeType)return ht(e),!0;if(Ce&&8===e.nodeType&&A(/<[/\w]/g,e.data))return ht(e),!0;if(!Te[n]||Ne[n]){if(!Ne[n]&&St(n)){if(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,n))return!1;if(_e.tagNameCheck instanceof Function&&_e.tagNameCheck(n))return!1}if(He&&!We[n]){const t=Q(e)||e.parentNode,n=J(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o)t.insertBefore(X(n[o],!0),$(e))}}return ht(e),!0}return e instanceof R&&!dt(e)?(ht(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!A(/<\/no(script|embed|frames)/i,e.innerHTML)?(De&&3===e.nodeType&&(t=e.textContent,u([ce,se,ue],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),_t("afterSanitizeElements",e,null),!1):(ht(e),!0)},bt=function(e,t,n){if(Ue&&("id"===t||"name"===t)&&(n in r||n in at))return!1;if(Re&&!be[t]&&A(me,t));else if(Se&&A(pe,t));else if(!Ee[t]||be[t]){if(!(St(e)&&(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,e)||_e.tagNameCheck instanceof Function&&_e.tagNameCheck(e))&&(_e.attributeNameCheck instanceof RegExp&&A(_e.attributeNameCheck,t)||_e.attributeNameCheck instanceof Function&&_e.attributeNameCheck(t))||"is"===t&&_e.allowCustomizedBuiltInElements&&(_e.tagNameCheck instanceof RegExp&&A(_e.tagNameCheck,n)||_e.tagNameCheck instanceof Function&&_e.tagNameCheck(n))))return!1}else if(Xe[t]);else if(A(ge,g(n,de,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!Ye[e]){if(we&&!A(fe,g(n,de,"")));else if(n)return!1}else;return!0},St=function(e){return"annotation-xml"!==e&&h(e,he)},Rt=function(e){_t("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ee};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=rt(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,_t("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(gt(a,e),!n.keepAttr)continue;if(!Le&&A(/\/>/i,p)){gt(a,e);continue}De&&u([ce,se,ue],(e=>{p=g(p,e," ")}));const f=rt(e.nodeName);if(bt(f,s,p)){if(!Pe||"id"!==s&&"name"!==s||(gt(a,e),p=Fe+p),ee&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ee.createHTML(p);break;case"TrustedScriptURL":p=ee.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),m(o.removed)}catch(e){}}}_t("afterSanitizeAttributes",e,null)},wt=function e(t){let n=null;const o=yt(t);for(_t("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)_t("uponSanitizeShadowNode",n,null),Nt(n)||(n.content instanceof s&&e(n.content),Rt(n));_t("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(Je=!e,Je&&(e="\x3c!--\x3e"),"string"!=typeof e&&!At(e)){if("function"!=typeof e.toString)throw _("toString is not a function");if("string"!=typeof(e=e.toString()))throw _("dirty is not a string, aborting")}if(!o.isSupported)return e;if(xe||ct(t),o.removed=[],"string"==typeof e&&(ze=!1),ze){if(e.nodeName){const t=rt(e.nodeName);if(!Te[t]||Ne[t])throw _("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Tt("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),1===r.nodeType&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!ke&&!De&&!Oe&&-1===e.indexOf("<"))return ee&&Ie?ee.createHTML(e):e;if(n=Tt(e),!n)return ke?null:Ie?te:""}n&&ve&&ht(n.firstChild);const c=yt(ze?e:n);for(;i=c.nextNode();)Nt(i)||(i.content instanceof s&&wt(i.content),Rt(i));if(ze)return e;if(ke){if(Me)for(l=re.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Ee.shadowroot||Ee.shadowrootmode)&&(l=ae.call(a,l,!0)),l}let m=Oe?n.outerHTML:n.innerHTML;return Oe&&Te["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&A(q,n.ownerDocument.doctype.name)&&(m="\n"+m),De&&u([ce,se,ue],(e=>{m=g(m,e," ")})),ee&&Ie?ee.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ct(e),xe=!0},o.clearConfig=function(){it=null,xe=!1},o.isValidAttribute=function(e,t,n){it||ct({});const o=rt(e),r=rt(t);return bt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(le[e]=le[e]||[],p(le[e],t))},o.removeHook=function(e){if(le[e])return m(le[e])},o.removeHooks=function(e){le[e]&&(le[e]=[])},o.removeAllHooks=function(){le={}},o}();return J})); +/*! @license DOMPurify 3.1.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.6/LICENSE */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=b(Array.prototype.forEach),m=b(Array.prototype.pop),p=b(Array.prototype.push),f=b(String.prototype.toLowerCase),d=b(String.prototype.toString),h=b(String.prototype.match),g=b(String.prototype.replace),T=b(String.prototype.indexOf),y=b(String.prototype.trim),E=b(Object.prototype.hasOwnProperty),_=b(RegExp.prototype.test),A=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=1,Z=3,J=7,Q=8,ee=9,te=function(){return"undefined"==typeof window?null:window};var ne=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:te();const o=e=>t(e);if(o.version="3.1.6",o.removed=[],!n||!n.document||n.document.nodeType!==ee)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=C(j,"cloneNode"),$=C(j,"remove"),ne=C(j,"nextSibling"),oe=C(j,"childNodes"),re=C(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ie,ae="";const{implementation:le,createNodeIterator:ce,createDocumentFragment:se,getElementsByTagName:ue}=r,{importNode:me}=a;let pe={};o.isSupported="function"==typeof e&&"function"==typeof re&&le&&void 0!==le.createHTMLDocument;const{MUSTACHE_EXPR:fe,ERB_EXPR:de,TMPLIT_EXPR:he,DATA_ATTR:ge,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:_e}=K;let{IS_ALLOWED_URI:Ae}=K,Ne=null;const be=S({},[...L,...D,...v,...x,...M]);let Se=null;const Re=S({},[...I,...U,...P,...F]);let we=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ce=null,Le=null,De=!0,ve=!0,Oe=!1,xe=!0,ke=!1,Me=!0,Ie=!1,Ue=!1,Pe=!1,Fe=!1,He=!1,ze=!1,Be=!0,We=!1,Ge=!0,Ye=!1,je={},Xe=null;const qe=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let $e=null;const Ke=S({},["audio","video","img","source","image","track"]);let Ve=null;const Ze=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Je="http://www.w3.org/1998/Math/MathML",Qe="http://www.w3.org/2000/svg",et="http://www.w3.org/1999/xhtml";let tt=et,nt=!1,ot=null;const rt=S({},[Je,Qe,et],d);let it=null;const at=["application/xhtml+xml","text/html"];let lt=null,ct=null;const st=r.createElement("form"),ut=function(e){return e instanceof RegExp||e instanceof Function},mt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ct||ct!==e){if(e&&"object"==typeof e||(e={}),e=w(e),it=-1===at.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,lt="application/xhtml+xml"===it?d:f,Ne=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,lt):be,Se=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,lt):Re,ot=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):rt,Ve=E(e,"ADD_URI_SAFE_ATTR")?S(w(Ze),e.ADD_URI_SAFE_ATTR,lt):Ze,$e=E(e,"ADD_DATA_URI_TAGS")?S(w(Ke),e.ADD_DATA_URI_TAGS,lt):Ke,Xe=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,lt):qe,Ce=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,lt):{},Le=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,lt):{},je=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,De=!1!==e.ALLOW_ARIA_ATTR,ve=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,xe=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ke=e.SAFE_FOR_TEMPLATES||!1,Me=!1!==e.SAFE_FOR_XML,Ie=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,He=e.RETURN_DOM_FRAGMENT||!1,ze=e.RETURN_TRUSTED_TYPE||!1,Pe=e.FORCE_BODY||!1,Be=!1!==e.SANITIZE_DOM,We=e.SANITIZE_NAMED_PROPS||!1,Ge=!1!==e.KEEP_CONTENT,Ye=e.IN_PLACE||!1,Ae=e.ALLOWED_URI_REGEXP||Y,tt=e.NAMESPACE||et,we=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ut(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ut(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ke&&(ve=!1),He&&(Fe=!0),je&&(Ne=S({},M),Se=[],!0===je.html&&(S(Ne,L),S(Se,I)),!0===je.svg&&(S(Ne,D),S(Se,U),S(Se,F)),!0===je.svgFilters&&(S(Ne,v),S(Se,U),S(Se,F)),!0===je.mathMl&&(S(Ne,x),S(Se,P),S(Se,F))),e.ADD_TAGS&&(Ne===be&&(Ne=w(Ne)),S(Ne,e.ADD_TAGS,lt)),e.ADD_ATTR&&(Se===Re&&(Se=w(Se)),S(Se,e.ADD_ATTR,lt)),e.ADD_URI_SAFE_ATTR&&S(Ve,e.ADD_URI_SAFE_ATTR,lt),e.FORBID_CONTENTS&&(Xe===qe&&(Xe=w(Xe)),S(Xe,e.FORBID_CONTENTS,lt)),Ge&&(Ne["#text"]=!0),Ie&&S(Ne,["html","head","body"]),Ne.table&&(S(Ne,["tbody"]),delete Ce.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ie=e.TRUSTED_TYPES_POLICY,ae=ie.createHTML("")}else void 0===ie&&(ie=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(G,c)),null!==ie&&"string"==typeof ae&&(ae=ie.createHTML(""));i&&i(e),ct=e}},pt=S({},["mi","mo","mn","ms","mtext"]),ft=S({},["foreignobject","annotation-xml"]),dt=S({},["title","style","font","a","script"]),ht=S({},[...D,...v,...O]),gt=S({},[...x,...k]),Tt=function(e){p(o.removed,{element:e});try{re(e).removeChild(e)}catch(t){$(e)}},yt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Se[e])if(Fe||He)try{Tt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Et=function(e){let t=null,n=null;if(Pe)e=""+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===it&&tt===et&&(e=''+e+"");const o=ie?ie.createHTML(e):e;if(tt===et)try{t=(new W).parseFromString(o,it)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(tt,"template",null);try{t.documentElement.innerHTML=nt?ae:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),tt===et?ue.call(t,Ie?"html":"body")[0]:Ie?t.documentElement:i},_t=function(e){return ce.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},At=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Nt=function(e){return"function"==typeof b&&e instanceof b},bt=function(e,t,n){pe[e]&&u(pe[e],(e=>{e.call(o,t,n,ct)}))},St=function(e){let t=null;if(bt("beforeSanitizeElements",e,null),At(e))return Tt(e),!0;const n=lt(e.nodeName);if(bt("uponSanitizeElement",e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Nt(e.firstElementChild)&&_(/<[/\w]/g,e.innerHTML)&&_(/<[/\w]/g,e.textContent))return Tt(e),!0;if(e.nodeType===J)return Tt(e),!0;if(Me&&e.nodeType===Q&&_(/<[/\w]/g,e.data))return Tt(e),!0;if(!Ne[n]||Ce[n]){if(!Ce[n]&&wt(n)){if(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n))return!1;if(we.tagNameCheck instanceof Function&&we.tagNameCheck(n))return!1}if(Ge&&!Xe[n]){const t=re(e)||e.parentNode,n=oe(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=X(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,ne(e))}}}return Tt(e),!0}return e instanceof R&&!function(e){let t=re(e);t&&t.tagName||(t={namespaceURI:tt,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!ot[e.namespaceURI]&&(e.namespaceURI===Qe?t.namespaceURI===et?"svg"===n:t.namespaceURI===Je?"svg"===n&&("annotation-xml"===o||pt[o]):Boolean(ht[n]):e.namespaceURI===Je?t.namespaceURI===et?"math"===n:t.namespaceURI===Qe?"math"===n&&ft[o]:Boolean(gt[n]):e.namespaceURI===et?!(t.namespaceURI===Qe&&!ft[o])&&!(t.namespaceURI===Je&&!pt[o])&&!gt[n]&&(dt[n]||!ht[n]):!("application/xhtml+xml"!==it||!ot[e.namespaceURI]))}(e)?(Tt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!_(/<\/no(script|embed|frames)/i,e.innerHTML)?(ke&&e.nodeType===Z&&(t=e.textContent,u([fe,de,he],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),bt("afterSanitizeElements",e,null),!1):(Tt(e),!0)},Rt=function(e,t,n){if(Be&&("id"===t||"name"===t)&&(n in r||n in st))return!1;if(ve&&!Le[t]&&_(ge,t));else if(De&&_(Te,t));else if(!Se[t]||Le[t]){if(!(wt(e)&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,e)||we.tagNameCheck instanceof Function&&we.tagNameCheck(e))&&(we.attributeNameCheck instanceof RegExp&&_(we.attributeNameCheck,t)||we.attributeNameCheck instanceof Function&&we.attributeNameCheck(t))||"is"===t&&we.allowCustomizedBuiltInElements&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n)||we.tagNameCheck instanceof Function&&we.tagNameCheck(n))))return!1}else if(Ve[t]);else if(_(Ae,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!$e[e]){if(Oe&&!_(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},wt=function(e){return"annotation-xml"!==e&&h(e,_e)},Ct=function(e){bt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Se};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=lt(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,bt("uponSanitizeAttribute",e,n),p=n.attrValue,Me&&_(/((--!?|])>)|<\/(style|title)/i,p)){yt(a,e);continue}if(n.forceKeepAttr)continue;if(yt(a,e),!n.keepAttr)continue;if(!xe&&_(/\/>/i,p)){yt(a,e);continue}ke&&u([fe,de,he],(e=>{p=g(p,e," ")}));const f=lt(e.nodeName);if(Rt(f,s,p)){if(!We||"id"!==s&&"name"!==s||(yt(a,e),p="user-content-"+p),ie&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ie.createHTML(p);break;case"TrustedScriptURL":p=ie.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),At(e)?Tt(e):m(o.removed)}catch(e){}}}bt("afterSanitizeAttributes",e,null)},Lt=function e(t){let n=null;const o=_t(t);for(bt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)bt("uponSanitizeShadowNode",n,null),St(n)||(n.content instanceof s&&e(n.content),Ct(n));bt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(nt=!e,nt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Nt(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Ue||mt(t),o.removed=[],"string"==typeof e&&(Ye=!1),Ye){if(e.nodeName){const t=lt(e.nodeName);if(!Ne[t]||Ce[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Et("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===V&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!ke&&!Ie&&-1===e.indexOf("<"))return ie&&ze?ie.createHTML(e):e;if(n=Et(e),!n)return Fe?null:ze?ae:""}n&&Pe&&Tt(n.firstChild);const c=_t(Ye?e:n);for(;i=c.nextNode();)St(i)||(i.content instanceof s&&Lt(i.content),Ct(i));if(Ye)return e;if(Fe){if(He)for(l=se.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Se.shadowroot||Se.shadowrootmode)&&(l=me.call(a,l,!0)),l}let m=Ie?n.outerHTML:n.innerHTML;return Ie&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&_(q,n.ownerDocument.doctype.name)&&(m="\n"+m),ke&&u([fe,de,he],(e=>{m=g(m,e," ")})),ie&&ze?ie.createHTML(m):m},o.setConfig=function(){mt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ue=!0},o.clearConfig=function(){ct=null,Ue=!1},o.isValidAttribute=function(e,t,n){ct||mt({});const o=lt(e),r=lt(t);return Rt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(pe[e]=pe[e]||[],p(pe[e],t))},o.removeHook=function(e){if(pe[e])return m(pe[e])},o.removeHooks=function(e){pe[e]&&(pe[e]=[])},o.removeAllHooks=function(){pe={}},o}();return ne})); //# sourceMappingURL=purify.min.js.map diff --git a/scripts/wsh-event-injection.js b/scripts/wsh-event-injection.ts similarity index 54% rename from scripts/wsh-event-injection.js rename to scripts/wsh-event-injection.ts index e593173..f5237c3 100644 --- a/scripts/wsh-event-injection.js +++ b/scripts/wsh-event-injection.ts @@ -1,39 +1,44 @@ -const BROWSER = "chrome"; -function add_listeners(cm, cmId) { +// @ts-ignore +const BROWSER: allowed_options.OBrowser = "chrome"; + +function add_listeners(cm: CodeMirror.Editor, cmId: number) { document.addEventListener("WSHSelectionRequestEvent", () => { // Do not proceed if the current CodeMirror instance is not the one that the event is intended for. if (!cm.hasFocus()) return; - const detail = { + // TODO: ¿Podría estar haciendo un dispatch con información sensible del usuario? + // TODO: Shadow Root. + const detail: communication.TSelectionResponse = { text: cm.getSelection(), rangeStart: cm.getCursor("from"), rangeEnd: cm.getCursor("to"), cmInstanceId: cmId }; - const event = new CustomEvent("WSHSelectionResponseEvent", {detail: detail}); + const event = new CustomEvent("WSHSelectionResponseEvent", {bubbles: false, detail: detail}); document.dispatchEvent(event); }); - document.addEventListener("WSHReplaceEvent", function(e) { - // Do not proceed if the current CodeMirror instance is not the one that the event is intended for. + document.addEventListener("WSHReplaceEvent", (e: CustomEvent) => { + const detail = e.detail as communication.TSelectionResponse; const rs = cm.getCursor("from"); const re = cm.getCursor("to"); - if (cm.hasFocus() && e.detail.cmInstanceId === cmId && - e.detail.rangeStart.line === rs.line && e.detail.rangeEnd.line === re.line && - e.detail.rangeStart.ch === rs.ch && e.detail.rangeEnd.ch === re.ch) { + if (cm.hasFocus() && detail.cmInstanceId === cmId && + detail.rangeStart.line === rs.line && detail.rangeEnd.line === re.line && + detail.rangeStart.ch === rs.ch && detail.rangeEnd.ch === re.ch) { - cm.replaceSelection(DOMPurify.sanitize(e.detail.text, {ALLOWED_TAGS: []})); + cm.replaceSelection(DOMPurify.sanitize(detail.text, {ALLOWED_TAGS: []})); } }); } -const cm_array = document.querySelectorAll(".CodeMirror"); +const cm_array: NodeListOf = document.querySelectorAll(".CodeMirror"); if (cm_array.length > 0) { let cmId = 0; - for (let cm_elem of cm_array) { - let cm; + for (const cm_elem of Array.from(cm_array)) { + let cm: CodeMirror.Editor | undefined; + // @ts-ignore if (BROWSER === "firefox") { cm = cm_elem.wrappedJSObject.CodeMirror; // Following recommendation from: @@ -42,9 +47,9 @@ if (cm_array.length > 0) { } else { cm = cm_elem.CodeMirror; } + if (!cm) continue; add_listeners(cm, cmId); - // Increment the ID for the next CodeMirror instance. cmId++; } diff --git a/tsconfig.json b/tsconfig.json index 807e822..7b45632 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,14 @@ { "compilerOptions": { - "outDir": "./built", + "outDir": "./build/scripts", "allowJs": true, - "target": "es6", + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", "noEmitOnError": true, "noImplicitAny": true, - "strictNullChecks": true + "strictNullChecks": true, + "strictPropertyInitialization": true }, "include": ["./scripts/**/*"] } \ No newline at end of file
Item