diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 25fa621..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib" -} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..46e279f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright 2019-2020 Tildes Community and Contributors +https://gitlab.com/tildes-community/tildes-reextended + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/License b/License deleted file mode 100644 index 116e31b..0000000 --- a/License +++ /dev/null @@ -1,8 +0,0 @@ -Copyright 2019 Tildes Community and Contributors -https://gitlab.com/tildes-community/tildes-reextended - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..23c80a7 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# Tildes ReExtended + +> An updated and reimagined recreation of the [original Tildes Extended](https://github.com/theCrius/tildes-extended) web extension by Crius. + +## Differences + +### Removed Functionality + +Large parts of the original Tildes Extended have been removed for various reasons: + +* **Link In New Tab**: this functionality now exists natively and can be configured [in your settings](https://tildes.net/settings)! +* **Markdown Preview**: this too exists natively (although not a "live" preview). Another reason this isn't included is due to Tildes using a customized flavor of Markdown that is difficult to replicate accurately enough with what's available and keep up to date. +* **Sticky Header**: with the dedicated "Back To Top" button now I wasn't sure if there was a need for it, so it's left out. +* **Custom Styles**: this feature introduced many issues in Tildes Extended while better and more dedicated extensions exist such as [Stylus](https://add0n.com/stylus.html), that can reliably handle custom styles instead. + +### Extended Functionality + +Some functionality has also been extended more: + +* [x] The **Back To Top** button has been separated out into its own feature. It used to be apart of the "Jump To New Comments" one. +* [ ] The **Random Tildes Logo** feature now picks from theme-appropriate logos instead of a regular tilde character. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/7) +* [x] The **Jump To New Comment** button now uncollapses comments if the new one is collapsed or is deeper inside a collapsed one. + +#### User Labels + +* [x] Multiple labels per person. +* [x] Specify priority of labels. +* [x] A dropdown with theme-appropriate colors for easy access. +* [x] Pick any hex color you want. +* [ ] Dedicated interface to add, edit, and remove labels. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/1) + +### New Functionality + +And various new features have been added such as: + +* [ ] Hide (and unhide) topics from the topic listing. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/3) +* [x] Hide your own and/or other people's vote counts. +* [ ] Anonymize usernames. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/5) +* [ ] Assign unique colors to people's usernames. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/6) +* [x] Export and import your extension settings. + +## License + +Open-sourced under the [MIT License](https://gitlab.com/tildes-community/tildes-reextended/blob/main/LICENSE). diff --git a/ReadMe.md b/ReadMe.md deleted file mode 100644 index 11fc850..0000000 --- a/ReadMe.md +++ /dev/null @@ -1,44 +0,0 @@ -# Tildes ReExtended - -> An updated and reimagined recreation of [Crius' original Tildes Extended](https://github.com/theCrius/tildes-extended) web extension. - -## Differences - -### Removed Functionality - -Large parts of the original Tildes Extended have been removed for various reasons: - -* "Link In New Tab": this functionality now exists natively and can be configured [in your settings](https://tildes.net/settings). -* "Markdown Preview": this too exists natively (although not a "live" preview). Another reason this isn't included is due to Tildes using a customized flavor of Markdown that is difficult to replicate accurately enough with what's available and keep up to date. -* "Sticky Header": with the dedicated "Back To Top" button now I wasn't sure if there was a need for it, so it's left out. -* "Custom Styles": this feature introduced many issues in Tildes Extended that while better and dedicated extensions exist such as [Stylus](https://add0n.com/stylus.html) that can reliably handle custom styles instead. - -### Extended Functionality - -Some functionality has also been extended more: - -* [x] The "Back To Top" button has been separated out into its own feature. It used to be apart of the "Jump To New Comments" one. -* [ ] The "Random Tildes Logo" feature now picks from theme-appropriate logos instead of a regular tilde character. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/7) -* [x] The "Jump To New Comment" button now uncollapses comments if the new one is collapsed or is inside a collapsed one. - -#### User Labels - -* [x] Multiple labels per person. -* [x] Specify priority of labels. -* [x] A dropdown with theme-appropriate colors for easy access. -* [x] Able to pick any color you want. -* [ ] Dedicated interface to add, edit, and remove labels. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/1) - -### New Functionality - -And various new features have been added such as: - -* [ ] Hide (and unhide) topics from the topic listing. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/3) -* [x] Hide all vote counts. Or all but your own. -* [ ] Anonymize usernames while adding a unique color to usernames for easy recognition. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/5) -* [ ] Assign unique colors to people's usernames. [*WIP](https://gitlab.com/tildes-community/tildes-reextended/issues/6) -* [x] Export and import your settings. - -## License - -Licensed under [MIT](License). diff --git a/package.json b/package.json index ac8f75d..51a6a6a 100644 --- a/package.json +++ b/package.json @@ -2,48 +2,42 @@ "name": "tildes-reextended", "description": "An updated and reimagined recreation of Tildes Extended to enhance and improve the experience of Tildes.net.", "license": "MIT", - "version": "0.3.3", "repository": "https://gitlab.com/tildes-community/tildes-reextended", "authors": [ "Bauke " ], "private": true, "scripts": { - "watch": "NODE_ENV=development parcel source/manifest.json -d build/ --no-hmr", + "watch": "NODE_ENV=development parcel 'source/manifest.json' -d 'build/' --no-hmr", "start": "web-ext run --source-dir build/ --bc", "start:chromium": "yarn start --chromium-profile chromium/ --keep-profile-changes --target chromium --start-url \"chrome://extensions\"", "start:firefox": "yarn start --firefox-profile firefox/ --keep-profile-changes --target firefox-desktop --start-url \"about:debugging#/runtime/this-firefox\"", - "build": "yarn clean && parcel build source/manifest.json -d build/ && web-ext build --source-dir build/ && yarn zip-source", - "zip-source": "zip -r web-ext-artifacts/tildes_reextended-source.zip ReadMe.md yarn.lock tsconfig.json package.json .gitignore License source/ scripts/ .vscode/", "clean": "trash .cache build/ web-ext-artifacts/", - "bump": "ts-node scripts/bump-version.ts && yarn build", - "test": "xo && stylelint 'source/scss/**/*.scss'" + "build": "yarn clean && parcel build 'source/manifest.json' -d 'build/' && web-ext build --source-dir 'build/' && yarn zip-source", + "zip-source": "zip -r web-ext-artifacts/tildes_reextended-source.zip README.md yarn.lock tsconfig.json package.json .gitignore LICENSE source/", + "test": "xo && stylelint 'source/scss/**/*.scss' && tsc --noEmit" }, "dependencies": { "caret-pos": "^2.0.0", "debounce": "^1.2.0", + "htm": "^3.0.4", "modern-normalize": "^1.0.0", "platform": "^1.3.6", + "preact": "^10.5.3", "webextension-polyfill-ts": "^0.20.0" }, "devDependencies": { "@types/debounce": "^1.2.0", "@types/platform": "^1.3.3", - "@types/prompts": "^2.0.9", - "@types/semver": "^7.3.4", "eslint-config-xo-typescript": "^0.33.0", + "husky": "^4.3.0", "parcel-bundler": "^1.12.4", "parcel-plugin-web-extension": "^1.6.1", - "prompts": "^2.3.2", "sass": "^1.26.11", - "semver": "^7.3.2", - "simple-git": "^2.20.1", "stylelint": "^13.7.2", "stylelint-config-xo-scss": "^0.13.0", "stylelint-config-xo-space": "^0.14.0", "trash-cli": "^3.1.0", - "ts-node": "^9.0.0", - "type-fest": "^0.17.0", "typescript": "^4.0.3", "web-ext": "^5.1.0", "web-ext-types": "^3.2.1", @@ -55,42 +49,36 @@ "stylelint-config-xo-space" ], "ignoreFiles": [ + "source/**/*.ts", "build/**" ], "rules": { "scss/at-rule-no-unknown": null, + "at-rule-empty-line-before": null, "at-rule-no-unknown": null, "block-no-empty": null, "no-descending-specificity": null } }, "xo": { - "global": [ - "Blob", - "confirm", + "globals": [ "document", - "FileReader", - "getComputedStyle", - "performance", "window" ], - "ignores": [ - "build/", - "public/" - ], "prettier": true, "rules": { "@typescript-eslint/no-implicit-any-catch": "off", - "@typescript-eslint/member-ordering": "off", - "@typescript-eslint/no-loop-func": "off", - "unicorn/prefer-dataset": "off", - "unicorn/prefer-modern-dom-apis": "off", - "capitalized-comments": "off", - "no-await-in-loop": "off" + "@typescript-eslint/no-loop-func": "off" }, "space": true }, "browserslist": [ "last 2 Chrome versions" - ] + ], + "husky": { + "hooks": { + "pre-commit": "yarn test", + "pre-push": "yarn test" + } + } } diff --git a/scripts/bump-version.ts b/scripts/bump-version.ts deleted file mode 100644 index 5481e34..0000000 --- a/scripts/bump-version.ts +++ /dev/null @@ -1,116 +0,0 @@ -import {promises as fs} from 'fs'; -import {join} from 'path'; -import prompts from 'prompts'; -import semver from 'semver'; -import git from 'simple-git/promise'; - -(async (): Promise => { - const manifestJSONPath: string = join(__dirname, '../source/manifest.json'); - const packageJSONPath: string = join(__dirname, '../package.json'); - - const manifestJSON: any = JSON.parse( - await fs.readFile(manifestJSONPath, 'utf8') - ); - - const packageJSON: any = JSON.parse( - await fs.readFile(packageJSONPath, 'utf8') - ); - - if (manifestJSON.version !== packageJSON.version) { - console.log( - `manifest.json and package.json versions are not the same:\n` + - `${String(manifestJSON.version)} | ${String(packageJSON.version)}` - ); - return; - } - - const currentVersion: string = manifestJSON.version; - const input = await prompts({ - message: 'Bump major, minor or patch?', - name: 'type', - type: 'select', - choices: [ - { - title: 'Major', - description: `${currentVersion} -> ${semver.inc( - currentVersion, - 'major' - )!}`, - value: 'major' - }, - { - title: 'Minor', - description: `${currentVersion} -> ${semver.inc( - currentVersion, - 'minor' - )!}`, - value: 'minor' - }, - { - title: 'Patch', - description: `${currentVersion} -> ${semver.inc( - currentVersion, - 'patch' - )!}`, - value: 'patch' - } - ] as Array | undefined, - initial: 1 - }); - - switch (input.type) { - case 'major': - break; - case 'minor': - break; - case 'patch': - break; - default: - console.log(`Unknown input: ${String(input.type)}`); - return; - } - - const newVersion: string | null = semver.inc(currentVersion, input.type); - if (newVersion === null) { - console.log( - `Something went wrong with semver incrementing ${currentVersion}` - ); - return; - } - - if (process.env.NODE_ENV === 'development') { - console.log( - 'Running in development, not writing JSONs to file or committing the changes.' - ); - return; - } - - const repository: git.SimpleGit = git(join(__dirname, '../')); - const status: git.StatusResult = await repository.status(); - if (status.staged.length > 0 || status.created.length > 0) { - console.log( - `Git repository has ${status.staged.length}/${status.created.length} staged/created files, commit these or unstage them then run this script again.` - ); - return; - } - - console.log( - `Bumping ${currentVersion} to ${newVersion}, writing JSONs to file.` - ); - manifestJSON.version = newVersion; - await fs.writeFile( - manifestJSONPath, - JSON.stringify(manifestJSON, null, 2) + '\n' - ); - - packageJSON.version = newVersion; - await fs.writeFile( - packageJSONPath, - JSON.stringify(packageJSON, null, 2) + '\n' - ); - - console.log('Committing changed files and tagging version.'); - await repository.add([manifestJSONPath, packageJSONPath]); - await repository.commit(`Version: ${newVersion}`); - await repository.addAnnotatedTag(`${newVersion}`, `Version ${newVersion}`); -})(); diff --git a/source/ts/background.ts b/source/background.ts similarity index 67% rename from source/ts/background.ts rename to source/background.ts index 7bf41d4..76188f5 100644 --- a/source/ts/background.ts +++ b/source/background.ts @@ -1,11 +1,11 @@ import {browser} from 'webextension-polyfill-ts'; // Add listeners to open the options page when: -// * The extension icon gets clicked. -// * The extension first gets installed. +// * The extension icon is clicked. +// * The extension is installed. browser.browserAction.onClicked.addListener(openOptionsPage); browser.runtime.onInstalled.addListener(openOptionsPage); -async function openOptionsPage(): Promise { +async function openOptionsPage() { await browser.runtime.openOptionsPage(); } diff --git a/source/content-scripts.ts b/source/content-scripts.ts new file mode 100644 index 0000000..23da1f5 --- /dev/null +++ b/source/content-scripts.ts @@ -0,0 +1,81 @@ +import {html} from 'htm/preact'; +import {render} from 'preact'; +import { + AutocompleteFeature, + BackToTopFeature, + extractAndSaveGroups, + getSettings, + initialize, + JumpToNewCommentFeature, + log, + runHideVotesFeature, + runMarkdownToolbarFeature, + TRXComponent, + UserLabelsFeature +} from '.'; + +window.addEventListener('load', async () => { + const start = window.performance.now(); + initialize(); + const settings = await getSettings(); + + // Any features that will use `settings.data.knownGroups` should be added to + // this array so that when groups are changed on Tildes, TRX can still update + // them without having to change the hardcoded values. + const usesKnownGroups = [settings.features.autocomplete]; + // Only when any of the features that uses this data try to save the groups. + if (usesKnownGroups.some((value) => value)) { + settings.data.knownGroups = await extractAndSaveGroups(settings); + } + + // Object to hold the active components we are going to render. + const components: Record = {}; + + if (settings.features.autocomplete) { + components.autocomplete = html` + <${AutocompleteFeature} settings=${settings} /> + `; + } + + if (settings.features.backToTop) { + components.backToTop = html`<${BackToTopFeature} />`; + } + + if (settings.features.hideVotes) { + runHideVotesFeature(settings); + } + + if (settings.features.jumpToNewComment) { + components.jumpToNewComment = html`<${JumpToNewCommentFeature} />`; + } + + if (settings.features.markdownToolbar) { + runMarkdownToolbarFeature(); + } + + if (settings.features.userLabels) { + components.userLabels = html` + <${UserLabelsFeature} settings=${settings} /> + `; + } + + // Insert a placeholder at the end of the body first, then render the rest + // and use that as the replacement element. Otherwise render() would put it + // at the beginning of the body which causes a bunch of different issues. + const replacement = document.createElement('div'); + document.body.append(replacement); + + // The jump to new comment button must come right before + // the back to top button. The CSS depends on them being in this order. + render( + html`
+ ${components.jumpToNewComment} ${components.backToTop} + ${components.autocomplete} ${components.userLabels} +
`, + document.body, + replacement + ); + + const initializedIn = window.performance.now() - start; + log(`Initialized in approximately ${initializedIn} milliseconds.`); +}); diff --git a/source/html/options.html b/source/html/options.html deleted file mode 100644 index a761583..0000000 --- a/source/html/options.html +++ /dev/null @@ -1,265 +0,0 @@ - - - - - - - - Tildes ReExtended Options - - - - - -
- -
- -
-
-
-

Autocomplete

- -
-

- Adds autocompletion for user mentions (starting with @) - and groups (starting with ~) in textareas. -

-
-
-
-

Back To Top

- -
-
-

- Adds a hovering button to the bottom-right of all pages (once - you've scrolled past a certain point) that when clicked will - scroll you back to the top. -

-
-
-
-
-

Hide Votes

- -
-
-

- Select which vote counts you don't want to see, "Comments" and - "Topics" will hide the votes on other people's comments/topics - while "Own Comments/Topics" will hide the votes on your own - comments/topics. -

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
-
-
-

Jump To New Comment

- -
-
-

- Adds a hovering button to the bottom-right of comment sections to - automatically scroll to the next new comment. -

-
-
-
-
-

Markdown Toolbar

- -
-
-

- Adds a toolbar with a selection of Markdown snippets that when - used will insert the according Markdown where your cursor is. - Particularly useful for the - expandable section/spoilerbox - syntax. If you have text selected, the Markdown will be inserted - around your text. -

-

- For a list of all snippets, - see this issue. -

-
-
-
-
-

User Labels

- -
-
-

- Adds the ability to add customizable labels to users. When in a - comments section or in the topic listing a username is visible, a - [+] will be next to it. Clicking on that will bring - up a dialog to add a label. The values you can customize are: -

-
- -
    -
  • - Username: - specifies who the label will be applied to. -
  • -
  • - Priority: - determines the order of labels. If multiple labels have the - same priority they will be sorted alphabetically. In the topic - listing only the highest priority label will be shown. -
  • -
  • - Color: - will set the background color of the label. The foreground - color - is calculated to be black or white depending on the brightness - of the background color.
    - Valid color values are 3, 4, 6 or 8 character hex colors and a - special "transparent" value for a transparent background.
    - Next to the color input there is also a dropdown menu with - various names, these are colors taken from the Tildes theme - you're using. Note that if you add a label with one of these - colors they won't change when you switch themes. -
  • -
  • - Text: - the text that will go in your label. If left empty, the label - will be a 12 by 12 pixel square instead. -
  • -
-
-

- To edit or remove labels, click on the labels wherever you see - them or - use the menu below.*WIP -

-
-
-
-
-

About & Info

- -
-
-

- When this feature is enabled, debug information will be logged to - the console. -

-

- Tildes ReExtended is a recreation of - Crius' Tildes Extended web - extension, completely remade from scratch. Open-sourced under the MIT license - and maintained as a - - Tildes Community project. -

-

- You can report bugs or request features in the GitLab issue tracker - through the link at the bottom of this page or by - private - messaging Bauke - on Tildes. If you're reporting a bug through, please use the links - available in the footer. They will prefill a bug report - template with some system information and instructions that can help - tremendously with determining the problem. -

-
- - - -
-

- When importing settings, note that your current settings will be - deleted and overwritten with the new ones. -

-
-
- - - -
-
-
-
- -
- - - - diff --git a/source/index.html b/source/index.html new file mode 100644 index 0000000..3355076 --- /dev/null +++ b/source/index.html @@ -0,0 +1,22 @@ + + + + + + + Tildes ReExtended + + + + + + + + + + + diff --git a/source/index.ts b/source/index.ts new file mode 100644 index 0000000..943e6fd --- /dev/null +++ b/source/index.ts @@ -0,0 +1,19 @@ +import {html} from 'htm/preact'; +import {createContext} from 'preact'; +import {Settings} from './settings'; + +export type TRXComponent = ReturnType; + +type AppContextValues = { + settings: Settings; + setActiveFeature: (feature: string) => void; + toggleFeature: (feature: string) => void; +}; + +// We create this context with null as we'll create the state and the other +// functions inside App itself. See `settings-page.ts` for that. +export const AppContext = createContext(null!); + +export * from './scripts'; +export * from './settings'; +export * from './utilities'; diff --git a/source/manifest.json b/source/manifest.json index fd7431f..7ecf404 100644 --- a/source/manifest.json +++ b/source/manifest.json @@ -3,7 +3,7 @@ "manifest_version": 2, "name": "Tildes ReExtended", "description": "An updated and reimagined recreation of Tildes Extended to enhance and improve the experience of Tildes.net.", - "version": "0.3.3", + "version": "1.0.0", "permissions": [ "downloads", "storage", @@ -18,7 +18,7 @@ }, "background": { "scripts": [ - "./ts/background.ts" + "./background.ts" ] }, "browser_action": { @@ -27,7 +27,7 @@ } }, "options_ui": { - "page": "./html/options.html", + "page": "./index.html", "open_in_tab": true }, "content_scripts": [ @@ -37,19 +37,10 @@ ], "run_at": "document_end", "css": [ - "./scss/scripts/jump-to-new-comment.scss", - "./scss/scripts/back-to-top.scss", - "./scss/scripts/user-labels.scss", - "./scss/scripts/markdown-toolbar.scss", - "./scss/scripts/autocomplete.scss" + "./scss/scripts.scss" ], "js": [ - "./ts/scripts/jump-to-new-comment.ts", - "./ts/scripts/back-to-top.ts", - "./ts/scripts/user-labels.ts", - "./ts/scripts/markdown-toolbar.ts", - "./ts/scripts/hide-votes.ts", - "./ts/scripts/autocomplete.ts" + "./content-scripts.ts" ] } ], diff --git a/source/scripts/autocomplete.ts b/source/scripts/autocomplete.ts new file mode 100644 index 0000000..002aa09 --- /dev/null +++ b/source/scripts/autocomplete.ts @@ -0,0 +1,213 @@ +import {offset, Offset} from 'caret-pos'; +import {html} from 'htm/preact'; +import {Component} from 'preact'; +import {log, querySelectorAll, Settings} from '..'; + +type Props = { + settings: Settings; +}; + +type State = { + groups: Set; + groupsHidden: boolean; + groupsMatches: Set; + groupsPosition: Offset | null; + usernames: Set; + usernamesHidden: boolean; + usernamesMatches: Set; + usernamesPosition: Offset | null; +}; + +export class AutocompleteFeature extends Component { + constructor(props: Props) { + super(props); + + // Get all the groups without their leading tildes. + const groups = props.settings.data.knownGroups.map((value) => + value.startsWith('~') ? value.slice(1) : value + ); + + // Get all the usernames on the page without their leading @s, and get + // all the username from the saved user labels. + const usernames = [ + ...querySelectorAll('.link-user').map((value) => + value.textContent!.replace(/^@/, '').toLowerCase() + ), + ...props.settings.data.userLabels.map((value) => value.username) + ].sort((a, b) => a.localeCompare(b)); + + this.state = { + groups: new Set(groups), + groupsHidden: true, + groupsMatches: new Set(groups), + groupsPosition: null, + usernames: new Set(usernames), + usernamesHidden: true, + usernamesMatches: new Set(usernames), + usernamesPosition: null + }; + + // Add a keydown listener for the entire page. + document.addEventListener('keydown', this.globalInputHandler); + + log( + `Autocomplete: Initialized with ${this.state.groups.size} groups and ` + + `${this.state.usernames.size} usernames.` + ); + } + + globalInputHandler = (event: KeyboardEvent) => { + const activeElement = document.activeElement as HTMLElement; + // Only add the autocompletes to textareas. + if (activeElement.tagName !== 'TEXTAREA') { + return; + } + + // Helper function to create autocompletes with. + const createHandler = ( + prefix: string, + target: string, + values: Set + ) => { + const dataAttribute = `data-trx-autocomplete-${target}`; + + if (event.key === prefix && !activeElement.getAttribute(dataAttribute)) { + activeElement.setAttribute(dataAttribute, 'true'); + activeElement.addEventListener('keyup', (event) => + this.textareaInputHandler(event, prefix, target, values) + ); + + this.textareaInputHandler(event, prefix, target, values); + } + }; + + createHandler('~', 'groups', this.state.groups); + createHandler('@', 'usernames', this.state.usernames); + }; + + textareaInputHandler = ( + event: KeyboardEvent, + prefix: string, + target: string, + values: Set + ) => { + const textarea = event.target as HTMLTextAreaElement; + const text = textarea.value; + + // If the prefix isn't in the textarea, return early. + if (!text.includes(prefix)) { + this.hide(target); + return; + } + + // Grab the starting position of the caret (text cursor). + const position = textarea.selectionStart; + + // Grab the last index of the prefix inside the beginning of the textarea + // and the starting position of the caret. + const prefixIndex = text.slice(0, position).lastIndexOf(prefix); + + // Grab the input between the prefix and the caret position, which will be + // what the user is currently typing. + const input = text.slice(prefixIndex + prefix.length, position); + + // If there is any whitespace in the input or there is no input at all, + // return early. Usernames cannot have whitespace in them. + if (/\s/.exec(input) || input === '') { + this.hide(target); + return; + } + + // Find all the values that match the input using `includes`. + const matches = new Set( + [...values].filter((value) => value.includes(input.toLowerCase())) + ); + + // If there are no matches, return early. + if (matches.size === 0) { + this.hide(target); + return; + } + + // Otherwise make sure the list is shown in the correct place and also + // has all the new matches. + this.show(target, offset(textarea)); + this.update(target, matches); + }; + + update = (target: string, matches: Set) => { + if (target === 'groups') { + this.setState({ + groupsMatches: matches + }); + } else if (target === 'usernames') { + this.setState({ + usernamesMatches: matches + }); + } + }; + + show = (target: string, position: Offset) => { + if (target === 'groups') { + this.setState({ + groupsHidden: false, + groupsPosition: position + }); + } else if (target === 'usernames') { + this.setState({ + usernamesHidden: false, + usernamesPosition: position + }); + } + }; + + hide = (target: string) => { + if (target === 'groups') { + this.setState({groupsHidden: true}); + } else if (target === 'usernames') { + this.setState({usernamesHidden: true}); + } + }; + + render() { + // Create the list of groups and usernames. + const groups = [...this.state.groupsMatches].map( + (value) => html`
  • ~${value}
  • ` + ); + const usernames = [...this.state.usernamesMatches].map( + (value) => html`
  • @${value}
  • ` + ); + + // Create the CSS class whether or not to hide the autocomplete. + const groupsHidden = this.state.groupsHidden ? 'trx-hidden' : ''; + const usernamesHidden = this.state.usernamesHidden ? 'trx-hidden' : ''; + + // Create the position for the group and usernames autocomplete. + const groupsLeft = this.state.groupsPosition?.left ?? 0; + const groupsTop = + (this.state.groupsPosition?.top ?? 0) + + (this.state.groupsPosition?.height ?? 0); + + const usernamesLeft = this.state.usernamesPosition?.left ?? 0; + const usernamesTop = + (this.state.usernamesPosition?.top ?? 0) + + (this.state.usernamesPosition?.height ?? 0); + + return html` +
      + ${usernames} +
    +
      + ${groups} +
    + `; + } +} diff --git a/source/scripts/back-to-top.ts b/source/scripts/back-to-top.ts new file mode 100644 index 0000000..9cf3307 --- /dev/null +++ b/source/scripts/back-to-top.ts @@ -0,0 +1,48 @@ +import debounce from 'debounce'; +import {html} from 'htm/preact'; +import {Component} from 'preact'; +import {log} from '..'; + +type Props = Record; + +type State = { + hidden: boolean; +}; + +export class BackToTopFeature extends Component { + constructor() { + super(); + this.state = { + hidden: true + }; + + // Add a "debounced" handler to the scroll listener, this will make it so + // the handler will only run after scrolling has ended for 150ms. + window.addEventListener('scroll', debounce(this.scrollHandler, 150)); + + // Run the handler once in case the page was already scroll down. + this.scrollHandler(); + + log(`Back To Top: Initialized.`); + } + + scrollHandler = () => { + this.setState({hidden: window.scrollY < 500}); + }; + + scrollToTop = () => { + window.scrollTo({behavior: 'smooth', top: 0}); + }; + + render() { + const hidden = this.state.hidden ? 'trx-hidden' : ''; + + return html` + Back To Top + `; + } +} diff --git a/source/scripts/hide-votes.ts b/source/scripts/hide-votes.ts new file mode 100644 index 0000000..e67ea60 --- /dev/null +++ b/source/scripts/hide-votes.ts @@ -0,0 +1,63 @@ +import {log, querySelectorAll, Settings} from '..'; + +export function runHideVotesFeature(settings: Settings) { + const observer = new window.MutationObserver(() => { + observer.disconnect(); + hideVotes(settings); + startObserver(); + }); + + function startObserver() { + observer.observe(document, { + childList: true, + subtree: true + }); + } + + hideVotes(settings); + startObserver(); + + log('Hide Votes: Initialized.'); +} + +function hideVotes(settings: Settings) { + if (settings.data.hideVotes.comments) { + const commentVotes = querySelectorAll( + '.btn-post-action[data-ic-put-to*="/vote"]:not(.trx-votes-hidden)', + '.btn-post-action[data-ic-delete-from*="/vote"]:not(.trx-votes-hidden)' + ); + + for (const vote of commentVotes) { + vote.classList.add('trx-votes-hidden'); + vote.textContent = vote.textContent!.slice( + 0, + vote.textContent!.indexOf(' ') + ); + } + } + + if (settings.data.hideVotes.ownComments) { + for (const vote of querySelectorAll('.comment-votes')) { + vote.classList.add('trx-hidden'); + } + } + + if (settings.data.hideVotes.topics || settings.data.hideVotes.ownTopics) { + const selectors: string[] = []; + + // Topics by other people will be encapsulated with a ` + `; +} + +function snippetDropdown(props: Props): TRXComponent { + const options = snippets.map( + (snippet) => html`` + ); + + const change = (event: Event) => { + event.preventDefault(); + + const snippet = snippets.find( + (value) => value.name === (event.target as HTMLSelectElement).value + )!; + + insertSnippet({ + ...props, + snippet + }); + + (event.target as HTMLSelectElement).selectedIndex = 0; + }; + + return html``; +} + +function insertSnippet(props: Required) { + const {textarea, snippet} = props; + const {selectionStart, selectionEnd} = textarea; + + // Since you have to press a button or go into a dropdown to click on a + // snippet, the textarea won't be focused anymore. So focus it again. + textarea.focus(); + + let {index, markdown} = snippet; + + // If any text has been selected, include it. + if (selectionStart !== selectionEnd) { + markdown = + markdown.slice(0, index) + + textarea.value.slice(selectionStart, selectionEnd) + + markdown.slice(index); + + // Change the index when the Link snippet is used so the cursor ends up + // in the URL part of the Markdown: "[existing text](cursor here)". + if (snippet.name === 'Link') { + index += 2; + } + } + + textarea.value = + textarea.value.slice(0, selectionStart) + + markdown + + textarea.value.slice(selectionEnd); + + textarea.selectionEnd = selectionEnd + index; +} diff --git a/source/scripts/user-labels.ts b/source/scripts/user-labels.ts new file mode 100644 index 0000000..e455cea --- /dev/null +++ b/source/scripts/user-labels.ts @@ -0,0 +1,429 @@ +import debounce from 'debounce'; +import {Component, render} from 'preact'; +import {html} from 'htm/preact'; +import { + createElementFromString, + isColorBright, + isValidHexColor, + log, + querySelectorAll, + setSettings, + Settings, + themeColors +} from '..'; + +type Props = { + settings: Settings; +}; + +type State = { + color: string; + selectedColor: string; + hidden: boolean; + id: number | null; + priority: number; + target: HTMLElement | null; + text: string; + username: string; +}; + +const colorPattern: string = [ + '^(?:#(?:', // (?:) are non-capturing groups. + '[a-f\\d]{8}|', // The order of 8 -> 6 -> 4 -> 3 character hex colors matters. + '[a-f\\d]{6}|', + '[a-f\\d]{4}|', + '[a-f\\d]{3})', + '|transparent)$' // "Transparent" is also allowed in the input. +].join(''); + +export class UserLabelsFeature extends Component { + constructor(props: Props) { + super(props); + + const selectedColor = window + .getComputedStyle(document.body) + .getPropertyValue(themeColors[1].value) + .trim(); + + this.state = { + color: selectedColor, + hidden: true, + id: null, + text: '', + priority: 0, + selectedColor, + target: null, + username: '' + }; + + const count = this.addLabelsToUsernames(querySelectorAll('.link-user')); + log(`User Labels: Initialized for ${count} user links.`); + } + + hide = () => { + this.setState({hidden: true}); + }; + + addLabelsToUsernames = (elements: Element[], onlyID?: number): number => { + const settings = this.props.settings; + const inTopicListing = document.querySelector('.topic-listing') !== null; + + // Sort the labels by priority or alphabetically, so 2 labels with the same + // priority will be sorted alphabetically. + const sortedLabels = settings.data.userLabels.sort((a, b): number => { + if (inTopicListing) { + // If we're in the topic listing sort with highest priority first. + if (a.priority !== b.priority) { + return b.priority - a.priority; + } + } else if (a.priority !== b.priority) { + // If we're not in the topic listing, sort with lowest priority first. + // We will add elements backwards, so the first label will be + // behind all the other labels. + return a.priority - b.priority; + } + + return b.text.localeCompare(a.text); + }); + + for (const element of elements) { + const username: string = element + .textContent!.replace(/@/g, '') + .toLowerCase(); + + const userLabels = sortedLabels.filter( + (value) => + value.username === username && + (onlyID === undefined ? true : value.id === onlyID) + ); + + const addLabel = html` + + this.addLabelHandler(event, username)} + > + [+] + + `; + + if (!inTopicListing && onlyID === undefined) { + const addLabelPlaceholder = document.createElement('span'); + element.after(addLabelPlaceholder); + render(addLabel, element.parentElement!, addLabelPlaceholder); + } + + if (userLabels.length === 0 && onlyID === undefined) { + if ( + inTopicListing && + (element.nextElementSibling === null || + !element.nextElementSibling.className.includes('trx-user-label')) + ) { + const addLabelPlaceholder = document.createElement('span'); + element.after(addLabelPlaceholder); + render(addLabel, element.parentElement!, addLabelPlaceholder); + } + + continue; + } + + for (const userLabel of userLabels) { + const bright = isColorBright(userLabel.color.trim()) + ? 'trx-bright' + : ''; + + const label = createElementFromString(` + ${userLabel.text} + `); + + label.addEventListener('click', (event: MouseEvent) => + this.editLabelHandler(event, userLabel.id) + ); + + element.after(label); + label.setAttribute('style', `background-color: ${userLabel.color};`); + + // If we're in the topic listing, stop after adding 1 label. + if (inTopicListing) { + break; + } + } + } + + return elements.length; + }; + + addLabelHandler = (event: MouseEvent, username: string) => { + event.preventDefault(); + const target = event.target as HTMLElement; + + if (this.state.target === target && !this.state.hidden) { + this.hide(); + } else { + const selectedColor = window + .getComputedStyle(document.body) + .getPropertyValue(themeColors[1].value) + .trim(); + + this.setState({ + hidden: false, + target, + username, + color: selectedColor, + id: null, + text: '', + priority: 0, + selectedColor + }); + } + }; + + editLabelHandler = (event: MouseEvent, id: number) => { + event.preventDefault(); + const target = event.target as HTMLElement; + + if (this.state.target === target && !this.state.hidden) { + this.hide(); + } else { + const label = this.props.settings.data.userLabels.find( + (value) => value.id === id + ); + if (label === undefined) { + log( + 'User Labels: Tried to edit label with ID that could not be found.', + true + ); + return; + } + + this.setState({ + hidden: false, + target, + ...label + }); + } + }; + + colorChange = (event: Event) => { + let color: string = (event.target as HTMLInputElement).value.toLowerCase(); + if (!color.startsWith('#') && !color.startsWith('t') && color.length > 0) { + color = `#${color}`; + } + + if (color !== 'transparent' && !isValidHexColor(color)) { + log('User Labels: Color must be a valid hex color or "transparent".'); + } + + // If the color was changed through the preset values, also change the + // selected color state. + if ((event.target as HTMLElement).tagName === 'SELECT') { + this.setState({color, selectedColor: color}); + } else { + this.setState({color}); + } + }; + + labelChange = (event: Event) => { + this.setState({text: (event.target as HTMLInputElement).value}); + }; + + priorityChange = (event: Event) => { + this.setState({ + priority: Number((event.target as HTMLInputElement).value) + }); + }; + + save = (event: MouseEvent) => { + event.preventDefault(); + const {color, id, text, priority, username} = this.state; + if (color === '' || username === '') { + log('Cannot save user label without all values present.'); + return; + } + + const {settings} = this.props; + // If no ID is present then save a new label otherwise edit the existing one. + if (id === null) { + let newID = 1; + if (settings.data.userLabels.length > 0) { + newID = settings.data.userLabels.sort((a, b) => b.id - a.id)[0].id + 1; + } + + settings.data.userLabels.push({ + color, + id: newID, + priority, + text, + username + }); + + this.addLabelsToUsernames(querySelectorAll('.link-user'), newID); + } else { + const index = settings.data.userLabels.findIndex( + (value) => value.id === id + ); + settings.data.userLabels.splice(index, 1); + settings.data.userLabels.push({ + id, + color, + priority, + text, + username + }); + + const elements = querySelectorAll(`[data-trx-label-id="${id}"]`); + const bright = isColorBright(color); + for (const element of elements) { + element.textContent = text; + element.setAttribute('style', `background-color: ${color};`); + if (bright) { + element.classList.add('trx-bright'); + } else { + element.classList.remove('trx-bright'); + } + } + } + + void setSettings(settings); + this.props.settings = settings; + this.hide(); + }; + + remove = (event: MouseEvent) => { + event.preventDefault(); + const {id} = this.state; + if (id === null) { + log('User Labels: Tried remove label when ID was null.'); + return; + } + + const {settings} = this.props; + const index = settings.data.userLabels.findIndex( + (value) => value.id === id + ); + if (index === undefined) { + log( + `User Labels: Tried to remove label with ID ${id} that could not be found.`, + true + ); + return; + } + + querySelectorAll(`[data-trx-label-id="${id}"]`).map((value) => + value.remove() + ); + + settings.data.userLabels.splice(index, 1); + void setSettings(settings); + this.props.settings = settings; + this.hide(); + }; + + render() { + const bodyStyle = window.getComputedStyle(document.body); + const themeSelectOptions = themeColors.map( + ({name, value}) => + html` + + ` + ); + + const bright = isColorBright(this.state.color) ? 'trx-bright' : ''; + const hidden = this.state.hidden ? 'trx-hidden' : ''; + const {color, text: label, priority, selectedColor, username} = this.state; + + let top = 0; + let left = 0; + + const target = this.state.target; + if (target !== null) { + const bounds = target.getBoundingClientRect(); + top = bounds.y + bounds.height + 4 + window.scrollY; + left = bounds.x + window.scrollX; + } + + const position = `left: ${left}px; top: ${top}px;`; + const previewStyle = `background-color: ${color}`; + + return html`
    +
    + + + +
    + +
    + + +
    + + + +
    +
    + +
    + + +
    + + +
    +

    ${label}

    +
    +
    +
    + +
    + Save + Close + Remove +
    +
    `; + } +} diff --git a/source/scss/_colors.scss b/source/scss/_colors.scss new file mode 100644 index 0000000..69fbe15 --- /dev/null +++ b/source/scss/_colors.scss @@ -0,0 +1,23 @@ +body { + $accents: ( + 'red' #dc322f, + 'orange' #cb4b16, + 'yellow' #b58900, + 'green' #859900, + 'cyan' #2aa198, + 'blue' #268bd2, + 'violet' #6c71c4, + 'magenta' #d33682, + ); + + --background-primary: #{adjust-color(#002b36, $lightness: -5%)}; + --background-secondary: #002b36; + --background-tertiary: #000; + --foreground: #fdf6e3; + + @each $name, $color in $accents { + --#{$name}: #{$color}; + --light-#{$name}: #{adjust-color($color, $lightness: 10%)}; + --dark-#{$name}: #{adjust-color($color, $lightness: -10%)}; + } +} diff --git a/source/scss/_options.scss b/source/scss/_options.scss deleted file mode 100644 index 66af032..0000000 --- a/source/scss/_options.scss +++ /dev/null @@ -1,278 +0,0 @@ -// stylelint-disable-next-line scss/partial-no-import -@import 'utilities'; - -html { - font-size: 62.5%; -} - -body { - background-color: adjust-color($background, $lightness: -5%); - color: $foreground; - font-family: sans-serif; - font-size: 2rem; -} - -a, -a:visited { - color: adjust-color($blue, $lightness: 10%); - - &:hover { - color: $magenta; - } -} - -h1, -h2, -p, -ul { - margin: 0; -} - -ul { - padding-left: 2rem; -} - -button { - &:hover { - cursor: pointer; - } -} - -code { - background-color: adjust-color($background, $lightness: -5%); -} - -details { - border: 0.25rem solid $blue; - margin-bottom: 1rem; - padding: 1rem; - - > summary { - cursor: pointer; - - &::after { - content: 'Click to expand.'; - } - } - - &[open] > summary { - margin-bottom: 0.25rem; - - &::after { - content: 'Click to collapse.'; - } - } -} - -p { - margin-bottom: 1rem; -} - -*:last-child > p:last-child { - margin-bottom: 0; -} - -.red-re { - color: $red; -} - -p > .red-re { - font-weight: bolder; -} - -.asterisk-link { - font-family: monospace; - text-decoration: none; -} - -#wrapper { - padding: 1rem; -} - -#header { - align-items: center; - display: flex; - padding-bottom: 1rem; - - > img { - height: 4rem; - margin: 0 1rem 0 0; - } - - > h1 { - margin-right: auto; - } - - > #version { - align-self: flex-end; - } -} - -#main { - background-color: $background; - border-top: 0.25rem solid $blue; - border-bottom: 0.25rem solid $cyan; - display: grid; - grid-template-columns: 35% 65%; - padding: 1rem; -} - -#settings-list { - display: flex; - flex-direction: column; - margin-right: 1rem; - - > a { - background-color: adjust-color($background, $lightness: 5%); - border-bottom: 0.25rem solid adjust-color($background, $lightness: -5%); - color: $foreground; - padding: 2rem; - - &:last-child { - border-bottom: none; - } - - &:hover:not(.active) { - background-color: adjust-color($blue, $lightness: -20%); - cursor: pointer; - } - - &.active { - background-color: adjust-color($blue, $lightness: -10%); - } - } -} - -.setting { - > header { - border-bottom: 0.25rem solid $red; - margin-bottom: 1rem; - display: flex; - padding-bottom: 0.5rem; - - > h2 { - margin-right: auto; - } - - > button { - align-self: flex-start; - background-color: $red; - border: none; - color: $foreground; - padding: 0.5rem; - min-width: 10rem; - - &:hover { - background-color: adjust-color($red, $lightness: -10%); - } - } - } - - &.enabled > header { - border-color: $green; - - > button { - background-color: $green; - - &:hover { - background-color: adjust-color($green, $lightness: -10%); - } - } - } - - &:not(.active) { - display: none; - } -} - -#debug { - display: flex; - flex-direction: column; - height: 100%; - - &:not(.active) { - display: none; - } - - &-buttons { - align-self: flex-end; - margin-top: auto; - } -} - -#hide-votes { - form { - margin-left: 1rem; - - > div { - margin-bottom: 0.5rem; - } - } -} - -#import-export { - align-items: center; - display: flex; - justify-content: center; - margin-bottom: 1rem; - - > button { - border: none; - color: $foreground; - background-color: $cyan; - padding: 1rem; - - &:hover { - background-color: adjust-color($cyan, $lightness: -10%); - } - - &:last-child { - margin-left: 1rem; - } - } -} - -#footer { - align-items: center; - display: flex; - justify-content: center; - padding: 2rem 0; - - p { - margin: 0; - } -} - -#copy-bug-template-button, -#log-stored-data-button, -#remove-all-data-button { - align-self: flex-end; - border: none; - color: $foreground; - padding: 0.5rem 1rem; - width: auto; -} - -#copy-bug-template-button, -#log-stored-data-button { - background-color: $blue; - - &:hover { - background-color: adjust-color($blue, $lightness: -10%); - } -} - -#remove-all-data-button { - background-color: $red; - - &:hover { - background-color: adjust-color($red, $lightness: -10%); - } -} - -@media (min-width: $large-breakpoint) { - #wrapper { - margin: 0 auto; - width: $large-breakpoint; - } -} diff --git a/source/scss/_reset.scss b/source/scss/_reset.scss new file mode 100644 index 0000000..3163795 --- /dev/null +++ b/source/scss/_reset.scss @@ -0,0 +1,12 @@ +h1, +h2, +h3, +h4, +h5, +li, +ol, +p, +ul { + margin: 0; + padding: 0; +} diff --git a/source/scss/_settings.scss b/source/scss/_settings.scss new file mode 100644 index 0000000..75f5dd7 --- /dev/null +++ b/source/scss/_settings.scss @@ -0,0 +1,126 @@ +.setting { + &.enabled { + --setting-color: var(--green); + --setting-color-alt: var(--dark-green); + } + + &.disabled { + --setting-color: var(--red); + --setting-color-alt: var(--dark-red); + } + + header { + border-bottom: 2px solid var(--setting-color); + display: flex; + margin-bottom: 8px; + padding-bottom: 8px; + + h2 { + margin-right: auto; + } + + button { + background-color: var(--setting-color); + color: var(--foreground); + border: none; + font-weight: bold; + min-width: 10rem; + padding: 4px 0; + + &:hover { + background-color: var(--setting-color-alt); + cursor: pointer; + } + } + } + + .content { + br { + margin-bottom: 8px; + } + + code { + background-color: var(--background-primary); + } + + p { + line-height: 125%; + margin-bottom: 8px; + } + } + + .info { + border: 1px solid var(--blue); + padding: 8px; + } + + .divider { + border-top: 1px solid var(--blue); + margin: 16px 0; + } + + .button { + --button-color: var(--blue); + --button-color-alt: var(--dark-blue); + background-color: var(--button-color); + border: none; + color: var(--foreground); + font-weight: bold; + min-width: 15rem; + padding: 8px 0; + + &:hover { + background-color: var(--button-color-alt); + cursor: pointer; + } + + &.destructive { + --button-color: var(--red); + --button-color-alt: var(--dark-red); + } + } + + .import-export { + display: grid; + gap: 8px; + grid-template-columns: repeat(2, 1fr); + margin-bottom: 8px; + + p { + grid-column: 1 / 3; + margin: 0; + } + } + + .misc-utilities .inner { + display: grid; + gap: 8px; + grid-template-columns: repeat(2, 1fr); + } + + .checkbox-list { + display: flex; + flex-direction: column; + gap: 8px; + list-style: none; + + label { + cursor: pointer; + display: inline-flex; + user-select: none; + } + + input { + cursor: pointer; + margin-right: 8px; + } + } + + .user-label-values { + display: flex; + flex-direction: column; + gap: 8px; + list-style: square; + padding: 8px 8px 8px 24px; + } +} diff --git a/source/scss/_shared.scss b/source/scss/_shared.scss new file mode 100644 index 0000000..f393133 --- /dev/null +++ b/source/scss/_shared.scss @@ -0,0 +1,3 @@ +.trx-hidden { + display: none; +} diff --git a/source/scss/_utilities.scss b/source/scss/_utilities.scss deleted file mode 100644 index 38455cd..0000000 --- a/source/scss/_utilities.scss +++ /dev/null @@ -1,95 +0,0 @@ -$background: #002b36; -$foreground: #fdf6e3; - -$red: #dc322f; -$orange: #cb4b16; -$yellow: #b58900; -$green: #859900; -$cyan: #2aa198; -$blue: #268bd2; -$violet: #6c71c4; -$magenta: #d33682; - -$small-breakpoint: 600px; -$medium-breakpoint: 900px; -$large-breakpoint: 1200px; -$extra-large-breakpoint: 1800px; - -.trx-flash-message { - align-items: center; - background-color: $background; - color: $foreground; - border: 0.25rem solid $blue; - bottom: 20px; - display: flex; - justify-content: center; - left: 20px; - overflow: hidden; - opacity: 0%; - padding: 1rem; - position: fixed; - transition: opacity 0.5s; - width: 350px; - - &:hover { - cursor: pointer; - } - - &.trx-opaque { - opacity: 100%; - } - - &.trx-flash-error { - background-color: adjust-color($red, $lightness: -25%); - border-color: $red; - } -} - -.trx-hidden { - // stylelint-disable-next-line declaration-no-important - display: none !important; -} - -.trx-offscreen { - left: -250vw; - position: absolute; -} - -@mixin invalid($color) { - .trx-invalid { - border-color: $color; - } -} - -body { - @include invalid(#d91e18); - - // stylelint-disable-next-line selector-class-pattern - &.theme- { - &atom-one-dark { - @include invalid(#E06C75); - } - - &black { - @include invalid(#f00); - } - - &dracula { - @include invalid(#ff5555); - } - - &gruvbox-dark, - &gruvbox-light { - @include invalid(#fb4934); - } - - &solarized-dark, - &solarized-light { - @include invalid(#dc322f); - } - - &zenburn { - @include invalid(#dc8c6c); - } - } -} diff --git a/source/scss/_variables.scss b/source/scss/_variables.scss new file mode 100644 index 0000000..2b9ef21 --- /dev/null +++ b/source/scss/_variables.scss @@ -0,0 +1,4 @@ +$small-breakpoint: 600px; +$medium-breakpoint: 900px; +$large-breakpoint: 1200px; +$extra-large-breakpoint: 1800px; diff --git a/source/scss/index.scss b/source/scss/index.scss new file mode 100644 index 0000000..fbbc124 --- /dev/null +++ b/source/scss/index.scss @@ -0,0 +1,146 @@ +@import 'reset'; +@import 'variables'; +@import 'colors'; + +html { + font-size: 62.5%; +} + +body { + background-color: var(--background-primary); + color: var(--foreground); + display: flex; + flex-direction: column; + font-size: 1.5rem; + gap: 16px; + margin: 16px; +} + +a { + color: var(--light-blue); + + &:hover { + color: var(--magenta); + } +} + +details { + border: 1px solid var(--blue); + + &[open] summary { + border-bottom: 1px solid var(--blue); + } + + summary { + padding: 8px; + + &:hover { + background-color: var(--blue); + cursor: pointer; + } + } + + .inner { + padding: 8px; + } +} + +.main-wrapper, +.page-header, +.page-footer { + margin-left: auto; + margin-right: auto; + width: $large-breakpoint; + + @media (max-width: $large-breakpoint) { + width: 100%; + } +} + +.page-header { + align-items: center; + display: flex; + + img { + height: 4rem; + } + + h1 { + align-items: center; + display: flex; + gap: 16px; + margin-right: auto; + } + + .version { + align-self: flex-end; + border-bottom-width: 2px; + font-weight: bold; + } +} + +.page-footer { + background-color: var(--background-secondary); + padding: 16px; + + a { + font-weight: bold; + } + + p:not(:last-child) { + margin-bottom: 8px; + } +} + +.main-wrapper { + display: grid; + gap: 16px; + grid-template-columns: $large-breakpoint / 4 auto; +} + +.page-aside { + background-color: var(--background-secondary); + padding: 8px; + + ul { + list-style: none; + } + + li { + border: 1px solid transparent; + font-weight: bold; + padding: 8px; + + &:hover { + background-color: var(--light-blue); + border-color: var(--light-blue); + cursor: pointer; + } + + &:not(:last-child) { + margin-bottom: 8px; + } + + &.active { + border-color: var(--light-blue); + } + + &.enabled { + display: flex; + + &::after { + color: var(--light-green); + content: 'ā—'; + margin-left: auto; + } + } + } +} + +.page-main { + background-color: var(--background-secondary); + padding: 16px; +} + +@import 'shared'; +@import 'settings'; diff --git a/source/scss/options.scss b/source/scss/options.scss deleted file mode 100644 index bdb6a33..0000000 --- a/source/scss/options.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import '~modern-normalize'; -// stylelint-disable-next-line scss/at-import-no-partial-leading-underscore -@import '_options'; diff --git a/source/scss/scripts.scss b/source/scss/scripts.scss new file mode 100644 index 0000000..d648ff0 --- /dev/null +++ b/source/scss/scripts.scss @@ -0,0 +1,7 @@ +@import 'scripts/autocomplete'; +@import 'scripts/back-to-top'; +@import 'scripts/jump-to-new-comment'; +@import 'scripts/markdown-toolbar'; +@import 'scripts/user-labels'; + +@import 'shared'; diff --git a/source/scss/scripts/autocomplete.scss b/source/scss/scripts/_autocomplete.scss similarity index 90% rename from source/scss/scripts/autocomplete.scss rename to source/scss/scripts/_autocomplete.scss index e12d5bb..9e4c0af 100644 --- a/source/scss/scripts/autocomplete.scss +++ b/source/scss/scripts/_autocomplete.scss @@ -1,4 +1,4 @@ -.trx-autocomplete-form { +.trx-autocomplete { background-color: var(--background-secondary-color); border: 1px solid var(--border-color); font-size: 80%; diff --git a/source/scss/scripts/back-to-top.scss b/source/scss/scripts/_back-to-top.scss similarity index 85% rename from source/scss/scripts/back-to-top.scss rename to source/scss/scripts/_back-to-top.scss index c477648..20451fe 100644 --- a/source/scss/scripts/back-to-top.scss +++ b/source/scss/scripts/_back-to-top.scss @@ -1,5 +1,3 @@ -@import '../utilities'; - #trx-back-to-top { bottom: 2vh; position: fixed; diff --git a/source/scss/scripts/jump-to-new-comment.scss b/source/scss/scripts/_jump-to-new-comment.scss similarity index 75% rename from source/scss/scripts/jump-to-new-comment.scss rename to source/scss/scripts/_jump-to-new-comment.scss index 090130e..0f84cbf 100644 --- a/source/scss/scripts/jump-to-new-comment.scss +++ b/source/scss/scripts/_jump-to-new-comment.scss @@ -1,5 +1,3 @@ -@import '../utilities'; - #trx-jump-to-new-comment { bottom: 2vh; position: fixed; diff --git a/source/scss/scripts/markdown-toolbar.scss b/source/scss/scripts/_markdown-toolbar.scss similarity index 100% rename from source/scss/scripts/markdown-toolbar.scss rename to source/scss/scripts/_markdown-toolbar.scss diff --git a/source/scss/scripts/_user-labels.scss b/source/scss/scripts/_user-labels.scss new file mode 100644 index 0000000..12b3c9b --- /dev/null +++ b/source/scss/scripts/_user-labels.scss @@ -0,0 +1,81 @@ +.trx-user-label { + &, + &-add { + color: #fff; + cursor: pointer; + display: inline-block; + font-style: initial; + margin: 0 2px; + min-height: 12px; + min-width: 12px; + padding: 0 2px; + } + + &-add { + background-color: var(--background-primary-color); + color: var(--foreground-primary-color); + } +} + +.trx-bright { + color: #000; +} + +.trx-user-label-form { + background-color: var(--background-primary-color); + border: 1px solid; + color: var(--foreground-primary-color); + display: flex; + flex-direction: column; + gap: 4px; + padding: 4px 8px; + position: absolute; + width: 350px; + + select { + text-overflow: ellipsis; + } +} + +.trx-label-username { + display: flex; + flex-direction: column; + margin-right: 4px; +} + +.trx-label-priority { + display: flex; + flex-direction: column; +} + +.trx-label-username-priority { + display: grid; + grid-template-columns: 75% 25%; +} + +.trx-label-grid { + display: grid; + gap: 4px; + grid-template-columns: repeat(2, 1fr); +} + +.trx-label-preview { + align-items: center; + border: 1px solid var(--border-color); + color: #fff; + display: flex; + height: 100%; + justify-content: center; + overflow: hidden; + width: 100%; + + &.trx-bright { + color: #000; + } +} + +.trx-label-actions { + align-items: center; + display: flex; + justify-content: space-evenly; +} diff --git a/source/scss/scripts/user-labels.scss b/source/scss/scripts/user-labels.scss deleted file mode 100644 index 94668a5..0000000 --- a/source/scss/scripts/user-labels.scss +++ /dev/null @@ -1,101 +0,0 @@ -@import '../utilities'; - -.trx-user-label { - color: #fff; - display: inline-block; - min-height: 12px; - min-width: 12px; - - &, - &-add { - font-style: initial; - margin: 0 2px; - padding: 0 2px; - - &:hover { - cursor: pointer; - } - } - - &-add { - background-color: #0002; - } -} - -.trx-bright { - color: #000; -} - -#trx-user-label-form { - border: 1px solid; - display: flex; - flex-direction: column; - padding: 4px; - position: absolute; - width: 325px; - - > div:nth-child(1), - > div:nth-child(2) { - display: flex; - - > label, - > input { - &:nth-of-type(1) { - margin-right: 4px; - width: 75%; - } - - &:nth-of-type(2) { - width: calc(25% - 4px); - } - } - } -} - -#trx-user-label-form-color { - display: flex; - - > input { - margin-right: 4px; - width: 50%; - } - - > select { - width: 50%; - } -} - -#trx-user-label-input { - display: flex; - - > input { - margin-right: 4px; - width: 50%; - } - - > #trx-user-label-preview { - align-items: center; - border: 1px solid; - color: #fff; - display: flex; - justify-content: center; - overflow: hidden; - width: 50%; - - &.trx-bright { - color: #000; - } - } -} - -#trx-user-label-actions { - align-items: center; - display: flex; - justify-content: space-evenly; -} - -body.theme-black { - .trx-user-label-add { - background-color: #fff2; - } -} diff --git a/source/settings-page.ts b/source/settings-page.ts new file mode 100644 index 0000000..c3f27d1 --- /dev/null +++ b/source/settings-page.ts @@ -0,0 +1,143 @@ +import {html} from 'htm/preact'; +import {render} from 'preact'; +import {useState} from 'preact/hooks'; + +import { + AppContext, + createReportTemplate, + features, + getManifest, + getSettings, + initialize, + Link, + setSettings, + Settings, + TRXManifest +} from '.'; + +window.addEventListener('load', async () => { + initialize(); + + render( + html`<${App} manifest=${getManifest()} settings=${await getSettings()} />`, + document.body + ); +}); + +type Props = { + manifest: TRXManifest; + settings: Settings; +}; + +function App(props: Props) { + const {manifest, settings} = props; + + // Create some state to set the active feature tab. + const [activeFeature, _setActiveFeature] = useState( + settings.data.latestActiveFeatureTab + ); + function setActiveFeature(feature: string) { + // Update the state and save the settings. + _setActiveFeature(feature); + settings.data.latestActiveFeatureTab = feature; + void setSettings(settings); + } + + // Create some state to set the enabled features. + const [enabledFeatures, _setFeature] = useState( + new Set( + Object.entries(settings.features) + .filter(([_, value]) => value) + .map(([key, _]) => key) + ) + ); + function toggleFeature(feature: string) { + settings.features[feature] = !settings.features[feature]; + const features = new Set( + Object.entries(settings.features) + .filter(([_, value]) => value) + .map(([key, _]) => key) + ); + _setFeature(features); + void setSettings(settings); + } + + // Create the version link for the header. + const version = manifest.version; + const versionURL = encodeURI( + `https://gitlab.com/tildes-community/tildes-reextended/-/tags/${version}` + ); + const versionLink = html`<${Link} + class="version" + text="v${version}" + url="${versionURL}" + />`; + + // Create the GitLab report a bug link for the footer. + const gitlabTemplate = createReportTemplate('gitlab', version); + const gitlabURL = encodeURI( + `https://gitlab.com/tildes-community/tildes-reextended/issues/new?issue[description]=${gitlabTemplate}` + ); + const gitlabLink = html`<${Link} text="GitLab" url="${gitlabURL}" />`; + + // Create the Tildes report a bug link for the footer. + const tildesReportTemplate = createReportTemplate('tildes', version); + const tildesURL = encodeURI( + `https://tildes.net/user/Community/new_message?subject=Tildes ReExtended Bug&message=${tildesReportTemplate}` + ); + const tildesLink = html`<${Link} text="Tildes" url="${tildesURL}" />`; + + const asideElements = features.map( + ({key, value}) => + html`
  • + ${value} +
  • ` + ); + + const mainElements = features.map( + ({key, value, component}) => + html`<${component()} + class="${activeFeature === key ? '' : 'trx-hidden'}" + enabled="${enabledFeatures.has(key)}" + feature=${key} + key=${key} + title="${value}" + />` + ); + + return html` + <${AppContext.Provider} value=${{ + settings, + setActiveFeature, + toggleFeature + }}> + + +
    + +
    ${mainElements}
    +
    + +
    +

    Report a bug via ${gitlabLink} or ${tildesLink}.

    +

    Ā© Tildes Community and Contributors

    +
    + + `; +} + +// Do not export anything from this file, otherwise if a content script +// somehow imports anything that is also connected to this file, it will try +// to run on Tildes as well. This file is solely for the extension options page! diff --git a/source/settings/components/about.ts b/source/settings/components/about.ts new file mode 100644 index 0000000..ed9a067 --- /dev/null +++ b/source/settings/components/about.ts @@ -0,0 +1,112 @@ +import {html} from 'htm/preact'; +import { + getSettings, + exportSettings, + importFileHandler, + Link, + log, + removeAllData, + Setting, + SettingProps, + TRXComponent +} from '../..'; + +async function logSettings() { + log(await getSettings(), true); +} + +export function AboutSetting(props: SettingProps): TRXComponent { + const importSettings = () => { + (document.querySelector('#import-settings') as HTMLInputElement).click(); + }; + + const communityLink = html` + <${Link} + url="https://gitlab.com/tildes-community" + text="Tildes Community project" + /> + `; + + const criusLink = html` + <${Link} url="https://tildes.net/user/crius" text="Crius" /> + `; + + const gitlabIssuesLink = html` + <${Link} + url="https://gitlab.com/tildes-community/tildes-reextended/-/issues" + text="GitLab issue tracker" + /> + `; + + const gitlabLicenseLink = html` + <${Link} + url="https://gitlab.com/tildes-community/tildes-reextended/blob/main/LICENSE" + text="MIT License" + /> + `; + + const messageCommunityLink = html` + <${Link} + url="https://tildes.net/user/Community/new_message" + text="message Community" + /> + `; + + const tildesExtendedLink = html` + <${Link} + url="https://github.com/theCrius/tildes-extended" + text="Tildes Extended" + /> + `; + + return html`<${Setting} ...${props}> +

    + This feature will make debugging logs output to the console when enabled. +

    + +

    + Tildes ReExtended is a from-scratch recreation of the original${' '} + ${tildesExtendedLink} web extension by ${criusLink}. Open-sourced${' '} + with the ${gitlabLicenseLink} and maintained as a ${communityLink}. +

    + +

    + To report bugs or request new features use the links at the bottom of this + page, check out the ${gitlabIssuesLink} or ${messageCommunityLink}${' '} + on Tildes. +

    + +
    + +
    +

    + Note that importing settings will delete and overwrite your existing + ones. +

    + + + + +
    + +
    + +
    + Danger Zone + +
    + + + +
    +
    + `; +} diff --git a/source/settings/components/autocomplete.ts b/source/settings/components/autocomplete.ts new file mode 100644 index 0000000..ba6cc2d --- /dev/null +++ b/source/settings/components/autocomplete.ts @@ -0,0 +1,11 @@ +import {html} from 'htm/preact'; +import {Setting, SettingProps, TRXComponent} from '../..'; + +export function AutocompleteSetting(props: SettingProps): TRXComponent { + return html`<${Setting} ...${props}> +

    + Adds autocompletion in textareas for user mentions (starting with${' '} + @) and groups (starting with ~). +

    + `; +} diff --git a/source/settings/components/back-to-top.ts b/source/settings/components/back-to-top.ts new file mode 100644 index 0000000..4431554 --- /dev/null +++ b/source/settings/components/back-to-top.ts @@ -0,0 +1,12 @@ +import {html} from 'htm/preact'; +import {Setting, SettingProps, TRXComponent} from '../..'; + +export function BackToTopSetting(props: SettingProps): TRXComponent { + return html`<${Setting} ...${props}> +

    + Adds a hovering button to the bottom-right of all pages once you've + scrolled down far enough that, when clicked, will scroll you back to the + top of the page. +

    + `; +} diff --git a/source/settings/components/hide-votes.ts b/source/settings/components/hide-votes.ts new file mode 100644 index 0000000..706bad0 --- /dev/null +++ b/source/settings/components/hide-votes.ts @@ -0,0 +1,55 @@ +import {html} from 'htm/preact'; +import {useContext, useState} from 'preact/hooks'; +import { + AppContext, + setSettings, + Setting, + SettingProps, + TRXComponent +} from '../..'; + +export function HideVotesSetting(props: SettingProps): TRXComponent { + const {settings} = useContext(AppContext); + + const [checked, setChecked] = useState(settings.data.hideVotes); + function toggle(target: string) { + checked[target] = !checked[target]; + setChecked(checked); + + settings.data.hideVotes = checked; + void setSettings(settings); + } + + // Checkbox labels and "targets". The targets should match the keys as defined + // in the user extension settings. + const checkboxes = [ + {label: 'Your comments', target: 'ownComments'}, + {label: 'Your topics', target: 'ownTopics'}, + {label: "Other's comments", target: 'comments'}, + {label: "Other's topics", target: 'topics'} + ].map( + ({label, target}) => + html` +
  • + +
  • + ` + ); + + return html`<${Setting} ...${props}> +

    + Hides vote counts from topics and comments of yourself or other people. +

    + +
      + ${checkboxes} +
    + `; +} diff --git a/source/settings/components/index.ts b/source/settings/components/index.ts new file mode 100644 index 0000000..5ac3935 --- /dev/null +++ b/source/settings/components/index.ts @@ -0,0 +1,52 @@ +import {html} from 'htm/preact'; +import {useContext} from 'preact/hooks'; +import {AppContext, TRXComponent} from '../..'; + +export type SettingProps = { + children: TRXComponent | undefined; + class: string; + enabled: boolean; + feature: string; + title: string; +}; + +function toggleFeature(feature: string) { + const {toggleFeature: toggle} = useContext(AppContext); + toggle(feature); +} + +function Header(props: SettingProps): TRXComponent { + const enabled = props.enabled ? 'Enabled' : 'Disabled'; + + return html`
    +

    ${props.title}

    + +
    `; +} + +// A base component for all the settings, this adds the header and the +// enable/disable buttons. This can also be used as a placeholder for new +// settings when you're still developing them. +export function Setting(props: SettingProps): TRXComponent { + const children = + props.children === undefined + ? html`

    This setting still needs a component!

    ` + : props.children; + + const enabled = (props.enabled ? 'Enabled' : 'Disabled').toLowerCase(); + + return html` +
    + <${Header} ...${props} /> +
    ${children}
    +
    + `; +} + +export * from './about'; +export * from './autocomplete'; +export * from './back-to-top'; +export * from './hide-votes'; +export * from './jump-to-new-comment'; +export * from './markdown-toolbar'; +export * from './user-labels'; diff --git a/source/settings/components/jump-to-new-comment.ts b/source/settings/components/jump-to-new-comment.ts new file mode 100644 index 0000000..52864b8 --- /dev/null +++ b/source/settings/components/jump-to-new-comment.ts @@ -0,0 +1,11 @@ +import {html} from 'htm/preact'; +import {Setting, SettingProps, TRXComponent} from '../..'; + +export function JumpToNewCommentSetting(props: SettingProps): TRXComponent { + return html`<${Setting} ...${props}> +

    + Adds a hovering button to the bottom-right of pages with new comments + that, when clicked, will scroll you to the next new comment. +

    + `; +} diff --git a/source/settings/components/markdown-toolbar.ts b/source/settings/components/markdown-toolbar.ts new file mode 100644 index 0000000..ae6ba31 --- /dev/null +++ b/source/settings/components/markdown-toolbar.ts @@ -0,0 +1,27 @@ +import {html} from 'htm/preact'; +import {Link, Setting, SettingProps, TRXComponent} from '../..'; + +export function MarkdownToolbarSetting(props: SettingProps): TRXComponent { + return html`<${Setting} ...${props}> +

    + Adds a toolbar with a selection of Markdown snippets that when used will + insert the according Markdown where your cursor is. Particularly useful + for the${' '} + <${Link} + url="https://docs.tildes.net/instructions/text-formatting#expandable-sections" + text="expandable section" + /> + /spoilerbox syntax. If you have text selected, the Markdown will be + inserted around your text. + +
    + + A full list of the snippets is available${' '} + <${Link} + url="https://gitlab.com/tildes-community/tildes-reextended/-/issues/12" + text="on GitLab" + /> + . +

    + `; +} diff --git a/source/settings/components/user-labels.ts b/source/settings/components/user-labels.ts new file mode 100644 index 0000000..c323fea --- /dev/null +++ b/source/settings/components/user-labels.ts @@ -0,0 +1,39 @@ +import {html} from 'htm/preact'; +import {Setting, SettingProps, TRXComponent} from '../..'; + +export function UserLabelsSetting(props: SettingProps): TRXComponent { + return html`<${Setting} ...${props}> +

    + Adds a way to create customizable labels to users. Wherever a link to a + person's profile is available, a [+] will be put next to it. + Clicking on that will bring up a dialog to add a new label and clicking on + existing labels will bring up the same dialog to edit them. +

    + +
    + View Customizable Values +
      +
    • Username: who to apply the label to.
    • +
    • + Priority: determines the order of labels. If multiple labels + have the same priority they will be sorted alphabetically. In the + topic listing only the highest priority label will be shown. +
    • +
    • + Color: will set the background color of the label. The + foreground color is calculated to be black or white depending on the + brightness of the background color. +
      + Valid values are hex colors or transparent. +
      + Colors based on your current Tildes theme are also available in the + dropdown menu. +
    • +
    • + Text: the text to go in the label. If left empty the label will + show as a 12 by 12 pixel square instead. +
    • +
    +
    + `; +} diff --git a/source/settings/defaults.ts b/source/settings/defaults.ts new file mode 100644 index 0000000..4685faf --- /dev/null +++ b/source/settings/defaults.ts @@ -0,0 +1,124 @@ +import { + AboutSetting, + AutocompleteSetting, + BackToTopSetting, + HideVotesSetting, + JumpToNewCommentSetting, + MarkdownToolbarSetting, + Settings, + UserLabelsSetting +} from '..'; + +export const defaultSettings: Settings = { + data: { + hideVotes: { + comments: true, + topics: true, + ownComments: true, + ownTopics: true + }, + // If groups are added or removed from Tildes this does not necessarily need + // to be updated. There is a helper function available to update it whenever + // the user goes to "/groups", where all the groups are easily available. + // Features that use this data should be added to the `usesKnownGroups` + // array that is near the top of `content-scripts.ts`. + knownGroups: [ + '~anime', + '~arts', + '~books', + '~comp', + '~creative', + '~design', + '~enviro', + '~finance', + '~food', + '~games', + '~games.game_design', + '~games.tabletop', + '~health', + '~health.coronavirus', + '~hobbies', + '~humanities', + '~lgbt', + '~life', + '~misc', + '~movies', + '~music', + '~news', + '~science', + '~space', + '~sports', + '~talk', + '~tech', + '~test', + '~tildes', + '~tildes.official', + '~tv' + ], + latestActiveFeatureTab: 'debug', + userLabels: [] + }, + features: { + autocomplete: true, + backToTop: true, + debug: false, + hideVotes: false, + jumpToNewComment: true, + markdownToolbar: true, + userLabels: true + } +}; + +export const defaultActiveFeature = defaultSettings.data.latestActiveFeatureTab; + +/** + * The array of features available in TRX. + * * The index exists to sort the sidebar listing. + * * The key should match the corresponding key from `Settings.features`. + * * The value should be the header title for display. + * * The component function should return the corresponding settings components. + */ +export const features = [ + { + index: 0, + key: 'autocomplete', + value: 'Autocomplete', + component: () => AutocompleteSetting + }, + { + index: 0, + key: 'backToTop', + value: 'Back To Top', + component: () => BackToTopSetting + }, + { + index: 0, + key: 'hideVotes', + value: 'Hide Votes', + component: () => HideVotesSetting + }, + { + index: 0, + key: 'jumpToNewComment', + value: 'Jump To New Comment', + component: () => JumpToNewCommentSetting + }, + { + index: 0, + key: 'markdownToolbar', + value: 'Markdown Toolbar', + component: () => MarkdownToolbarSetting + }, + { + index: 0, + key: 'userLabels', + value: 'User Labels', + component: () => UserLabelsSetting + }, + { + index: 1, + key: 'debug', + value: 'About & Info', + component: () => AboutSetting + } +].sort((a, b) => a.index - b.index); diff --git a/source/settings/export.ts b/source/settings/export.ts new file mode 100644 index 0000000..429c22b --- /dev/null +++ b/source/settings/export.ts @@ -0,0 +1,28 @@ +import {browser} from 'webextension-polyfill-ts'; +import {getSettings, log} from '..'; + +export async function exportSettings(event: MouseEvent): Promise { + event.preventDefault(); + + const settings = await getSettings(); + const settingsBlob = new window.Blob([JSON.stringify(settings, null, 2)], { + type: 'text/json' + }); + + const objectURL = URL.createObjectURL(settingsBlob); + + try { + await browser.downloads.download({ + filename: 'tildes-reextended-settings.json', + url: objectURL, + saveAs: true + }); + } catch (error) { + log(error); + } finally { + // According to MDN, when creating an object URL we should also revoke it + // when "it's safe to do so" to prevent excessive memory/storage use. + // 60 seconds should probably be enough time to download the settings. + setTimeout(() => URL.revokeObjectURL(objectURL), 60 * 1000); + } +} diff --git a/source/ts/import-export.ts b/source/settings/import.ts similarity index 59% rename from source/ts/import-export.ts rename to source/settings/import.ts index e41fd53..4ded018 100644 --- a/source/ts/import-export.ts +++ b/source/settings/import.ts @@ -1,47 +1,43 @@ -import {browser} from 'webextension-polyfill-ts'; import { - querySelector, - flashMessage, - Settings, - log, getSettings, - isValidTildesUsername, + log, isValidHexColor, - setSettings -} from './utilities'; - -export async function importSettingsHandler(event: MouseEvent): Promise { - event.preventDefault(); - const fileInput: HTMLInputElement = querySelector('#import-file'); - fileInput.click(); -} + isValidTildesUsername, + setSettings, + Settings +} from '..'; export async function importFileHandler(event: Event): Promise { + // Grab the imported files (if any). const fileList: FileList | null = (event.target as HTMLInputElement).files; + if (fileList === null) { - flashMessage('No file imported.'); + log('No file imported.'); return; } - const reader: FileReader = new FileReader(); + const reader = new window.FileReader(); + reader.addEventListener( 'load', async (): Promise => { let data: Partial; + try { // eslint-disable-next-line @typescript-eslint/no-base-to-string data = JSON.parse(reader.result!.toString()); } catch (error) { log(error, true); - flashMessage(error, true); return; } const settings: Settings = await getSettings(); const newSettings: Settings = {...settings}; + if (typeof data.data !== 'undefined') { if (typeof data.data.userLabels !== 'undefined') { newSettings.data.userLabels = []; + for (const label of data.data.userLabels) { if ( typeof label.username === 'undefined' || @@ -71,40 +67,17 @@ export async function importFileHandler(event: Event): Promise { } await setSettings(newSettings); - flashMessage( - 'Successfully imported your settings, reloading the page to apply.' - ); + log('Successfully imported your settings, reloading the page to apply.'); setTimeout(() => { window.location.reload(); - }, 2500); + }, 1000); } ); + reader.addEventListener('error', (): void => { log(reader.error, true); reader.abort(); }); + reader.readAsText(fileList[0]); } - -export async function exportSettingsHandler(event: MouseEvent): Promise { - event.preventDefault(); - const settings: Settings = await getSettings(); - const settingsBlob: Blob = new Blob([JSON.stringify(settings, null, 2)], { - type: 'text/json' - }); - const blobObjectURL: string = URL.createObjectURL(settingsBlob); - try { - await browser.downloads.download({ - filename: 'tildes_reextended-settings.json', - url: blobObjectURL, - saveAs: true - }); - } catch (error) { - log(error); - } finally { - // According to MDN, when creating an object URL we should also revoke it - // when "it's safe to do so" to prevent excess memory/storage use. 60 - // seconds should be enough time to download the settings. - setTimeout(() => URL.revokeObjectURL(blobObjectURL), 60000); - } -} diff --git a/source/settings/index.ts b/source/settings/index.ts new file mode 100644 index 0000000..2d0ae5f --- /dev/null +++ b/source/settings/index.ts @@ -0,0 +1,113 @@ +import {browser, Manifest} from 'webextension-polyfill-ts'; +import {defaultSettings, log} from '..'; + +/** + * UserLabel type definition. + */ +export type UserLabel = { + color: string; + id: number; + priority: number; + text: string; + username: string; +}; + +/** + * User extension settings. + */ +export type Settings = { + data: { + hideVotes: { + [index: string]: boolean; + comments: boolean; + topics: boolean; + ownComments: boolean; + ownTopics: boolean; + }; + knownGroups: string[]; + latestActiveFeatureTab: string; + userLabels: UserLabel[]; + version?: string; + }; + features: { + [index: string]: boolean; + autocomplete: boolean; + backToTop: boolean; + debug: boolean; + hideVotes: boolean; + jumpToNewComment: boolean; + markdownToolbar: boolean; + userLabels: boolean; + }; +}; + +/** + * Fetches and returns the user extension settings. + */ +export async function getSettings(): Promise { + const syncSettings: any = await browser.storage.sync.get(defaultSettings); + const settings: Settings = { + data: {...defaultSettings.data, ...syncSettings.data}, + features: {...defaultSettings.features, ...syncSettings.features} + }; + + window.TildesReExtended.debug = settings.features.debug; + // If we're in development, force debug output. + if (getManifest().nodeEnv === 'development') { + window.TildesReExtended.debug = true; + } + + return settings; +} + +/** + * Saves the user extension settings. + * @param newSettings The new settings to save. + */ +export async function setSettings(newSettings: Settings): Promise { + return browser.storage.sync.set(newSettings); +} + +/** + * Tildes ReExtended WebExtension manifest type definition. + */ +export type TRXManifest = {nodeEnv?: string} & Manifest.ManifestBase; + +/** + * Fetch the WebExtension manifest. + */ +export function getManifest(): TRXManifest { + const manifest: Manifest.ManifestBase = browser.runtime.getManifest(); + return {...manifest}; +} + +/** + * Removes all user extension settings and reloads the page. + * @param event The mouse click event. + */ +export async function removeAllData(event: MouseEvent): Promise { + event.preventDefault(); + if ( + // eslint-disable-next-line no-alert + !window.confirm( + 'Are you sure you want to delete your data? There is no way to ' + + 'recover it once it has been deleted.' + ) + ) { + return; + } + + await browser.storage.sync.clear(); + log( + 'Data removed, reloading this page to reinitialize default settings.', + true + ); + setTimeout(() => { + window.location.reload(); + }, 1000); +} + +export * from './components'; +export * from './defaults'; +export * from './export'; +export * from './import'; diff --git a/source/ts/options.ts b/source/ts/options.ts deleted file mode 100644 index 323350c..0000000 --- a/source/ts/options.ts +++ /dev/null @@ -1,324 +0,0 @@ -import platform from 'platform'; -import {browser} from 'webextension-polyfill-ts'; -import { - importSettingsHandler, - importFileHandler, - exportSettingsHandler -} from './import-export'; -import { - getSettings, - Settings, - camelToKebab, - log, - kebabToCamel, - querySelector, - setSettings, - createElementFromString, - flashMessage, - querySelectorAll -} from './utilities'; - -window.addEventListener( - 'load', - async (): Promise => { - const settings: Settings = await getSettings(); - // Grab the version anchor from the header and add the tag link to it. - const versionSpan: HTMLAnchorElement = querySelector('#version'); - const {version} = browser.runtime.getManifest(); - versionSpan.setAttribute( - 'href', - `https://gitlab.com/tildes-community/tildes-reextended/-/tags/${version}` - ); - versionSpan.textContent = `v${version}`; - - const gitlabReportTemplate: string = createReportTemplate('gitlab'); - const tildesReportTemplate: string = createReportTemplate('tildes'); - // Grab the "Report A Bug" anchors and add a prefilled template for them. - const gitlabReportAnchors: HTMLAnchorElement[] = querySelectorAll( - '.report-a-bug-gitlab' - ); - for (const element of gitlabReportAnchors) { - element.setAttribute( - 'href', - encodeURI( - `https://gitlab.com/tildes-community/tildes-reextended/issues/new?issue[description]=${gitlabReportTemplate}` - ) - ); - } - - const tildesReportAnchors: HTMLAnchorElement[] = querySelectorAll( - '.report-a-bug-tildes' - ); - for (const element of tildesReportAnchors) { - element.setAttribute( - 'href', - encodeURI( - `https://tildes.net/user/Bauke/new_message?subject=Tildes ReExtended Bug&message=${tildesReportTemplate}` - ) - ); - } - - ['comments', 'topics', 'own-comments', 'own-topics'].forEach( - (value: string): void => { - const hideCheckbox: HTMLInputElement = querySelector( - `#hide-votes-${value}` - ); - const settingsKey: string = kebabToCamel(value); - hideCheckbox.checked = settings.data.hideVotes[settingsKey]; - hideCheckbox.addEventListener( - 'change', - async (): Promise => { - settings.data.hideVotes[settingsKey] = hideCheckbox.checked; - await setSettings(settings); - } - ); - } - ); - - const importSettingsButton: HTMLButtonElement = querySelector( - '#import-button' - ); - importSettingsButton.addEventListener('click', importSettingsHandler); - - const importFileInput: HTMLInputElement = querySelector('#import-file'); - importFileInput.addEventListener('change', importFileHandler); - - const exportSettingsButton: HTMLButtonElement = querySelector( - '#export-button' - ); - exportSettingsButton.addEventListener('click', exportSettingsHandler); - - const copyBugTemplateButton: HTMLButtonElement = querySelector( - '#copy-bug-template-button' - ); - copyBugTemplateButton.addEventListener('click', copyBugTemplateHandler); - - const logStoredDataButton: HTMLButtonElement = querySelector( - '#log-stored-data-button' - ); - logStoredDataButton.addEventListener('click', logStoredDataHandler); - - const removeAllDataButton: HTMLButtonElement = querySelector( - '#remove-all-data-button' - ); - removeAllDataButton.addEventListener('click', removeAllDataHandler); - - // Set the latest feature to active. - const latestActiveListItem: HTMLAnchorElement = querySelector( - `#${settings.data.latestActiveFeatureTab}-list` - ); - latestActiveListItem.classList.add('active'); - const latestActiveContent: HTMLDivElement = querySelector( - `#${settings.data.latestActiveFeatureTab}` - ); - latestActiveContent.classList.add('active'); - - for (const key in settings.features) { - if (Object.hasOwnProperty.call(settings.features, key)) { - const value: boolean = settings.features[key]; - // Convert the camelCase key to a kebab-case string. - const id: string = camelToKebab(key); - - const settingContent: HTMLDivElement | null = document.querySelector( - `#${id}` - ); - if (settingContent === null) { - log( - `New setting key:${key} id:${id} does not have an entry in the settings content!`, - true - ); - continue; - } - - if (value) { - settingContent.classList.add('enabled'); - } - - // Set the button text to enable/disable based on the current setting. - const toggleButton: HTMLButtonElement = querySelector(`#${id}-button`); - toggleButton.addEventListener('click', toggleButtonClickHandler); - toggleButton.textContent = value ? 'Enabled' : 'Disabled'; - - // Add a checkmark to the list item if the feature is enabled. - const listItem: HTMLAnchorElement = querySelector(`#${id}-list`); - listItem.addEventListener('click', listItemClickHandler); - if (value) { - listItem.textContent += ' āœ”'; - } - } - } - - if (typeof settings.data.version !== 'undefined') { - if (settings.data.version !== version) { - flashMessage(`Updated to ${version}!`); - } - } - - settings.data.version = version; - await setSettings(settings); - } -); - -async function toggleButtonClickHandler(event: MouseEvent): Promise { - event.preventDefault(); - const target: HTMLButtonElement = event.target as HTMLButtonElement; - const settings: Settings = await getSettings(); - - // Convert the kebab-case ID to camelCase and remove the `-button` suffix. - const wantedSettingKey: string = kebabToCamel( - target.id.slice(0, target.id.lastIndexOf('-')) - ); - - // Toggle the value and update it in the settings. - const wantedSettingValue = !settings.features[wantedSettingKey]; - settings.features[wantedSettingKey] = wantedSettingValue; - await setSettings(settings); - - // Update the button text. - target.textContent = wantedSettingValue ? 'Enabled' : 'Disabled'; - - // Grab the equivalent list item and update the checkmark. - const listItem: HTMLAnchorElement = querySelector( - `#${camelToKebab(wantedSettingKey)}-list` - ); - if (wantedSettingValue) { - listItem.textContent += ' āœ”'; - } else { - listItem.textContent = listItem.textContent!.slice( - 0, - listItem.textContent!.lastIndexOf(' ') - ); - } - - const settingContent: HTMLDivElement = querySelector( - `#${camelToKebab(wantedSettingKey)}` - ); - if (wantedSettingValue) { - settingContent.classList.add('enabled'); - } else { - settingContent.classList.remove('enabled'); - } -} - -async function listItemClickHandler(event: MouseEvent): Promise { - const target: HTMLAnchorElement = event.target as HTMLAnchorElement; - const id: string = target.id.slice(0, target.id.lastIndexOf('-')); - const currentActiveListItem: HTMLAnchorElement = querySelector( - '#settings-list > .active' - ); - // If the currently selected item is the same as the new one, do nothing. - if (target.id === currentActiveListItem.id) { - return; - } - - // Hide the currently active settings content. - const currentActiveContent: HTMLDivElement = querySelector( - '#settings-content > .active' - ); - currentActiveContent.classList.remove('active'); - - // And show the newly selected active settings content. - const newActiveContent: HTMLDivElement = querySelector(`#${id}`); - newActiveContent.classList.add('active'); - - // Remove the active style from the currently active settings list item. - currentActiveListItem.classList.remove('active'); - - // And add it to the newly selected settings list item. - target.classList.add('active'); - - // Update the latest active feature data. - const settings: Settings = await getSettings(); - settings.data.latestActiveFeatureTab = id; - await setSettings(settings); -} - -function copyBugTemplateHandler(event: MouseEvent): void { - event.preventDefault(); - const temporaryElement: HTMLTextAreaElement = createElementFromString( - `` - ); - temporaryElement.classList.add('trx-offscreen'); - document.body.append(temporaryElement); - temporaryElement.select(); - try { - document.execCommand('copy'); - flashMessage('Copied bug report template to clipboard.'); - } catch (error) { - flashMessage( - 'Failed to copy bug report template to clipboard. Check the console for an error.', - true - ); - log(error, true); - } finally { - temporaryElement.remove(); - log('Removed temporary textarea from DOM.'); - } -} - -async function logStoredDataHandler(event: MouseEvent): Promise { - event.preventDefault(); - log(JSON.stringify(await getSettings(), null, 2), true); -} - -async function removeAllDataHandler(event: MouseEvent): Promise { - event.preventDefault(); - if ( - // eslint-disable-next-line no-alert - !confirm( - 'Are you sure you want to delete your data? There is no way to recover your data once it has been deleted.' - ) - ) { - return; - } - - await browser.storage.sync.clear(); - flashMessage( - 'Data removed, reloading this page to reinitialize default settings.' - ); - setTimeout(() => { - window.location.reload(); - }, 2500); -} - -function createReportTemplate(location: 'gitlab' | 'tildes'): string { - let introText = - "Thank you for taking the time to report a bug! Don't forget to fill in an\n appropriate title above, and make sure the information below is correct."; - - if (location === 'tildes') { - introText = - 'Thank you for taking the time to report a bug! Please make sure the\n information below is correct.'; - } - - // Set the headers using HTML tags, these can't be with #-style Markdown - // headers as they'll be interpreted as an ID instead of Markdown content - // and so GitLab won't add it to the description. - let reportTemplate = `

    Bug Report

    - -

    Info

    \n -| Type | Value | -|------|-------| -| Operating System | ${platform.os!.toString()} | -| Browser | ${platform.name!} ${platform.version!} (${platform.layout!}) |\n`; - // The platform manufacturer and product can be null in certain cases (such as - // desktops) so only when they're both not null include them. - if (platform.manufacturer !== null && platform.product !== null) { - reportTemplate += `| Device | ${platform.manufacturer!} ${platform.product!} |\n`; - } - - reportTemplate += `\n

    The Problem

    -\n\n -

    A Solution

    -\n\n\n`; - - return reportTemplate; -} diff --git a/source/ts/scripts/autocomplete.ts b/source/ts/scripts/autocomplete.ts deleted file mode 100644 index 55346dd..0000000 --- a/source/ts/scripts/autocomplete.ts +++ /dev/null @@ -1,222 +0,0 @@ -import {offset, Offset} from 'caret-pos'; -import { - Settings, - getSettings, - createElementFromString, - extractAndSaveGroups -} from '../utilities'; - -const knownGroups: Set = new Set(); -const knownUsers: Set = new Set(); - -(async (): Promise => { - let settings: Settings = await getSettings(); - if (!settings.features.autocomplete) { - return; - } - - try { - settings = await extractAndSaveGroups(settings); - } catch { - // This will intentionally error when we're not in "/groups". - } - - for (const group of settings.data.knownGroups) { - if (group.startsWith('~')) { - knownGroups.add(group.slice(1)); - } else { - knownGroups.add(group); - } - } - - // Add usernames from all linked users on the page. - const userLinks = document.querySelectorAll('.link-user'); - for (const link of userLinks) { - const username: string = link.textContent!.replace(/@/g, '').toLowerCase(); - knownUsers.add(username); - } - - // Add usernames we have saved in the user labels. - for (const label of settings.data.userLabels) { - knownUsers.add(label.username); - } - - document.addEventListener('keydown', globalInputHandler); -})(); - -function globalInputHandler(event: KeyboardEvent) { - const activeElement: HTMLElement = document.activeElement as HTMLElement; - // Only add the autocompletes to textareas. - if (activeElement.tagName !== 'TEXTAREA') { - return; - } - - // If a ~ is entered in a textarea and that textarea doesn't already have - // the group input handler running, add it. - if ( - event.key === '~' && - !activeElement.getAttribute('data-trx-autocomplete-group') - ) { - activeElement.setAttribute('data-trx-autocomplete-group', 'true'); - - // Sort the groups alphabetically. - const groups: string[] = [...knownGroups]; - groups.sort((a, b) => a.localeCompare(b)); - - if (!document.querySelector('#trx-autocomplete-group-form')) { - const form: HTMLFormElement = createOrGetAutocompleteForm( - 'group', - groups - ); - document.body.append(form); - } - - textareaInputHandler(event, '~', 'group', groups); - activeElement.addEventListener('keyup', (event) => - textareaInputHandler(event, '~', 'group', groups) - ); - } - - // If an @ is entered in a textarea and that textarea doesn't already have - // the user input handler running, add it. - if ( - event.key === '@' && - !activeElement.getAttribute('data-trx-autocomplete-user') - ) { - activeElement.setAttribute('data-trx-autocomplete-user', 'true'); - - // Sort the usernames alphabetically. - const users: string[] = [...knownUsers]; - users.sort((a, b) => a.localeCompare(b)); - - if (!document.querySelector('#trx-autocomplete-user-form')) { - const form: HTMLFormElement = createOrGetAutocompleteForm('user', users); - document.body.append(form); - } - - textareaInputHandler(event, '@', 'user', users); - activeElement.addEventListener('keyup', (event) => - textareaInputHandler(event, '@', 'user', users) - ); - } -} - -function textareaInputHandler( - event: KeyboardEvent, - prefix: string, - id: string, - values: string[] -) { - const textarea: HTMLTextAreaElement = event.target as HTMLTextAreaElement; - const text: string = textarea.value; - - // If the prefix isn't in the textarea, return early. - if (!text.includes(prefix)) { - hideAutocompleteForm(id); - return; - } - - // Grab the starting position of the caret (text cursor). - const position: number = textarea.selectionStart; - - // Grab the last index of the prefix inside the beginning of the textarea and - // the starting position of the caret. Basically doing a reversed index of. - const prefixIndex: number = text.slice(0, position).lastIndexOf(prefix); - - // Grab the input between the prefix and the caret position, which will be - // what the user is currently typing. - const input = text.slice(prefixIndex + prefix.length, position); - - // If there is any whitespace in the input or there is no input at all, return - // early. - if (/\s/.exec(input) || input === '') { - hideAutocompleteForm(id); - return; - } - - // Find all the values that match the input. - const matches: string[] = []; - for (const value of values) { - if (value.includes(input.toLocaleLowerCase())) { - matches.push(value); - } - } - - // If there are no matches, return early. - if (matches.length === 0) { - hideAutocompleteForm(id); - return; - } - - // If the autocomplete form is hidden, unhide it. - if (document.querySelector(`#trx-autocomplete-${id}-form.trx-hidden`)) { - showAutocompleteForm(id, offset(textarea)); - } - - // And finally update the values in the autocomplete form. - updateAutocompleteFormValues(id, matches); -} - -function createOrGetAutocompleteForm( - id: string, - values: string[] -): HTMLFormElement { - const existing: Element | null = document.querySelector( - `#trx-autocomplete-${id}-form` - ); - if (existing !== null) { - return existing as HTMLFormElement; - } - - const options: string[] = []; - for (const value of values) { - options.push(`
  • ${value}
  • `); - } - - const autocompleteFormTemplate = `
    -
      ${options.join('\n')}
    -
    `; - const form: HTMLFormElement = createElementFromString( - autocompleteFormTemplate - ); - - return form; -} - -function hideAutocompleteForm(id: string): HTMLFormElement { - const form: HTMLFormElement = createOrGetAutocompleteForm(id, []); - form.classList.add('trx-hidden'); - return form; -} - -function showAutocompleteForm(id: string, offset: Offset): HTMLFormElement { - const form: HTMLFormElement = createOrGetAutocompleteForm(id, []); - form.classList.remove('trx-hidden'); - - form.setAttribute( - 'style', - `left: ${offset.left}px; top: ${offset.top + offset.height}px;` - ); - return form; -} - -function updateAutocompleteFormValues( - id: string, - values: string[] -): HTMLFormElement { - const form: HTMLFormElement = createOrGetAutocompleteForm(id, []); - form.firstElementChild!.remove(); - - const options: string[] = []; - for (const value of values) { - options.push(`
  • ${value}
  • `); - } - - const list: HTMLUListElement = createElementFromString( - `
      ${options.join('\n')}
    ` - ); - - form.append(list); - return form; -} diff --git a/source/ts/scripts/back-to-top.ts b/source/ts/scripts/back-to-top.ts deleted file mode 100644 index 7d4bc7b..0000000 --- a/source/ts/scripts/back-to-top.ts +++ /dev/null @@ -1,44 +0,0 @@ -import debounce from 'debounce'; -import { - getSettings, - Settings, - createElementFromString, - querySelector -} from '../utilities'; - -(async (): Promise => { - const settings: Settings = await getSettings(); - if (!settings.features.backToTop) { - return; - } - - // Create the Back To Top button. - const backToTopButton: HTMLAnchorElement = createElementFromString( - 'Back To Top' - ); - backToTopButton.addEventListener('click', clickHandler); - document.body.append(backToTopButton); - - // Add a "debounced" handler to the scroll listener, this will make it so - // the handler will only run after scrolling has ended for 150ms. - window.addEventListener('scroll', debounce(scrollHandler, 150)); - // And finally run the handler once, in case the page was already scrolled - // down when it got loaded. - scrollHandler(); -})(); - -function scrollHandler(): void { - const backToTopButton: HTMLAnchorElement = querySelector('#trx-back-to-top'); - const yPosition: number = window.scrollY; - // TODO: See if 500 is a good position. Tildes Extended was originally at 250 - // but I think that might be a bit too little. - if (yPosition > 500) { - backToTopButton.classList.remove('trx-hidden'); - } else { - backToTopButton.classList.add('trx-hidden'); - } -} - -function clickHandler(): void { - window.scrollTo({behavior: 'smooth', top: 0}); -} diff --git a/source/ts/scripts/hide-votes.ts b/source/ts/scripts/hide-votes.ts deleted file mode 100644 index 3adf3cc..0000000 --- a/source/ts/scripts/hide-votes.ts +++ /dev/null @@ -1,82 +0,0 @@ -import {Settings, getSettings} from '../utilities'; - -(async (): Promise => { - const settings: Settings = await getSettings(); - if (!settings.features.hideVotes) { - return; - } - - const observer: MutationObserver = new window.MutationObserver( - async (): Promise => { - observer.disconnect(); - await hideVotes(); - startObserver(); - } - ); - - function startObserver(): void { - observer.observe(document, { - childList: true, - subtree: true - }); - } - - await hideVotes(); - startObserver(); -})(); - -async function hideVotes(): Promise { - const settings: Settings = await getSettings(); - if (settings.data.hideVotes.comments) { - const commentVotes: HTMLButtonElement[] = [ - ...document.querySelectorAll( - '.btn-post-action[name="vote"]:not(.trx-votes-hidden)' - ), - ...document.querySelectorAll( - '.btn-post-action[name="unvote"]:not(.trx-votes-hidden)' - ) - ] as HTMLButtonElement[]; - for (const vote of commentVotes) { - vote.classList.add('trx-votes-hidden'); - vote.textContent = vote.textContent!.slice( - 0, - vote.textContent!.indexOf(' ') - ); - } - } - - if (settings.data.hideVotes.ownComments) { - const commentVotes: NodeListOf = document.querySelectorAll( - '.comment-votes' - ); - for (const vote of commentVotes) { - vote.classList.add('trx-hidden'); - } - } - - if (settings.data.hideVotes.topics || settings.data.hideVotes.ownTopics) { - // Topics by other people will be encapsulated with a `` - ); - tabItem.addEventListener('click', (event: MouseEvent): void => - insertSnippet(snippet, textarea, event) - ); - tabMenu.insertAdjacentElement('beforeend', tabItem); - } - - // When the dropdown value changes, add the snippet. - markdownSelect.addEventListener('change', (): void => { - const snippet: MarkdownSnippet | undefined = markdownSnippets.find( - (value) => value.name === markdownSelect.value - ); - if (typeof snippet === 'undefined') { - return; - } - - insertSnippet(snippet, textarea); - // Reset the dropdown index so it always displays "More..." and so it's - // possible to select the same snippet multiple times. - markdownSelect.selectedIndex = 0; - }); - - // Insert the dropdown after the tab menu. - tabMenu.insertAdjacentElement('afterend', markdownSelect); - } -} - -function insertSnippet( - snippet: MarkdownSnippet, - textarea: HTMLTextAreaElement, - event?: MouseEvent -): void { - // If insertSnippet is called from a button it will pass through event. - // So preventDefault() that when it's defined. - if (typeof event !== 'undefined') { - event.preventDefault(); - } - - // Since you have to press a button or go into a dropdown to click on a - // snippet, the textarea won't be focused anymore. So focus it again. - textarea.focus(); - const currentSelectionStart: number = textarea.selectionStart; - const currentSelectionEnd: number = textarea.selectionEnd; - let {markdown} = snippet; - let snippetIndex: number = snippet.index; - // If text has been selected, change the markdown so it includes what's - // been selected. - if (currentSelectionStart !== currentSelectionEnd) { - markdown = - snippet.markdown.slice(0, snippetIndex) + - textarea.value.slice(currentSelectionStart, currentSelectionEnd) + - snippet.markdown.slice(snippetIndex); - - // A special behavior for the Link snippet so it places the cursor in the - // URL part of a Markdown link instead of the text part: "[](here)". - if (snippet.name === 'Link') { - snippetIndex += 2; - } - } - - textarea.value = - textarea.value.slice(0, currentSelectionStart) + - markdown + - textarea.value.slice(currentSelectionEnd); - textarea.selectionEnd = currentSelectionEnd + snippetIndex; -} - -// This function gets called at the beginning of the script to set the snippet -// indexes where the dollar sign is and to remove the dollar sign from it. -// This could be manually done but I figure it's easier to write snippets and -// have a placeholder for where the cursor is intended to go than to count -// where the index is manually and figure it out yourself. -function calculateSnippetIndexes(): void { - for (const snippet of markdownSnippets) { - const insertIndex: number = snippet.markdown.indexOf('$'); - const newMarkdown: string = snippet.markdown.replace('$', ''); - snippet.index = insertIndex; - snippet.markdown = newMarkdown; - } -} diff --git a/source/ts/scripts/user-labels.ts b/source/ts/scripts/user-labels.ts deleted file mode 100644 index bcd996a..0000000 --- a/source/ts/scripts/user-labels.ts +++ /dev/null @@ -1,369 +0,0 @@ -import {Except} from 'type-fest'; -import debounce from 'debounce'; -import {ColorKey, themeColors} from '../theme-colors'; -import { - getSettings, - Settings, - log, - createElementFromString, - UserLabel, - isInTopicListing, - getCSSCustomPropertyValue, - isColorBright, - setSettings, - appendStyleAttribute, - querySelector -} from '../utilities'; -import { - getLabelForm, - getLabelFormValues, - hideLabelForm, - getLabelFormID -} from './user-labels/label-form'; -import { - editLabelHandler, - addLabelHandler, - labelTextInputHandler, - presetColorSelectHandler, - labelColorInputHandler -} from './user-labels/handlers'; - -(async (): Promise => { - const settings: Settings = await getSettings(); - if (!settings.features.userLabels) { - return; - } - - addLabelsToUsernames(settings); - const existingLabelForm: HTMLElement | null = document.querySelector( - '#trx-user-label-form' - ); - if (existingLabelForm !== null) { - existingLabelForm.remove(); - } - - const themeSelectOptions: string[] = []; - for (const color in themeColors) { - if (Object.hasOwnProperty.call(themeColors, color)) { - const colorValue = getCSSCustomPropertyValue( - themeColors[color as ColorKey] - ); - themeSelectOptions.push( - `` - ); - } - } - - const labelFormTemplate = `
    -
    - - -
    -
    - - -
    - -
    - - -
    - -
    - -
    -

    -
    -
    -
    - Save - Close - Remove -
    -
    `; - const labelForm: HTMLFormElement = createElementFromString(labelFormTemplate); - document.body.append(labelForm); - labelForm.setAttribute( - 'style', - `background-color: var(${themeColors.backgroundPrimary});` + - `border-color: var(${themeColors.foregroundSecondary});` - ); - - const labelColorInput: HTMLInputElement = querySelector( - '#trx-user-label-form-color > input' - ); - labelColorInput.addEventListener( - 'keyup', - debounce(labelColorInputHandler, 250) - ); - - const presetColorSelect: HTMLSelectElement = querySelector( - '#trx-user-label-form-color > select' - ); - presetColorSelect.addEventListener('change', presetColorSelectHandler); - presetColorSelect.value = getCSSCustomPropertyValue( - themeColors.backgroundSecondary - ); - - const labelTextInput: HTMLInputElement = querySelector( - '#trx-user-label-input > input' - ); - labelTextInput.addEventListener('keyup', labelTextInputHandler); - - const labelPreview: HTMLDivElement = querySelector('#trx-user-label-preview'); - labelPreview.setAttribute( - 'style', - `background-color: var(${themeColors.backgroundPrimary});` + - `border-color: var(${themeColors.foregroundSecondary});` - ); - - const formSaveButton: HTMLAnchorElement = querySelector( - '#trx-user-label-save' - ); - formSaveButton.addEventListener('click', saveUserLabel); - - const formCloseButton: HTMLAnchorElement = querySelector( - '#trx-user-label-close' - ); - formCloseButton.addEventListener('click', hideLabelForm); - - const formRemoveButton: HTMLAnchorElement = querySelector( - '#trx-user-label-remove' - ); - formRemoveButton.addEventListener('click', removeUserLabel); - - const commentObserver = new window.MutationObserver( - async (mutations: MutationRecord[]): Promise => { - const commentElements: HTMLElement[] = mutations - .map((value) => value.target as HTMLElement) - .filter( - (value) => - value.classList.contains('comment-itself') || - value.classList.contains('comment') - ); - if (commentElements.length === 0) { - return; - } - - commentObserver.disconnect(); - addLabelsToUsernames(await getSettings()); - startObserver(); - } - ); - - function startObserver(): void { - const topicComments: HTMLElement | null = document.querySelector( - '.topic-comments' - ); - if (topicComments !== null) { - commentObserver.observe(topicComments, { - childList: true, - subtree: true - }); - return; - } - - const postListing: HTMLElement | null = document.querySelector( - '.post-listing' - ); - if (postListing !== null) { - commentObserver.observe(postListing, { - childList: true, - subtree: true - }); - } - } - - startObserver(); -})(); - -// TODO: Refactor this function to be able to only add labels to specific -// elements. At the moment it goes through all `.link-user` elements which is -// inefficient. -function addLabelsToUsernames(settings: Settings): void { - for (const element of [ - ...document.querySelectorAll('.trx-user-label'), - ...document.querySelectorAll('.trx-user-label-add') - ]) { - element.remove(); - } - - for (const element of document.querySelectorAll('.link-user')) { - const username: string = element - .textContent!.replace(/@/g, '') - .toLowerCase(); - - const addLabelSpan: HTMLSpanElement = createElementFromString( - `[+]` - ); - addLabelSpan.addEventListener('click', (event: MouseEvent): void => - addLabelHandler(event, addLabelSpan) - ); - if (!isInTopicListing()) { - element.insertAdjacentElement('afterend', addLabelSpan); - appendStyleAttribute( - addLabelSpan, - `color: var(${themeColors.foregroundPrimary});` - ); - } - - const userLabels: UserLabel[] = settings.data.userLabels.filter( - (value) => value.username === username - ); - if (userLabels.length === 0) { - if ( - isInTopicListing() && - (element.nextElementSibling === null || - !element.nextElementSibling.className.includes('trx-user-label')) - ) { - element.insertAdjacentElement('afterend', addLabelSpan); - appendStyleAttribute( - addLabelSpan, - `color: var(${themeColors.foregroundPrimary});` - ); - } - - continue; - } - - if (isInTopicListing()) { - userLabels.sort((a, b) => b.priority - a.priority); - } else { - userLabels.sort((a, b): number => { - if (a.priority !== b.priority) { - return a.priority - b.priority; - } - - return b.text.localeCompare(a.text); - }); - } - - for (const userLabel of userLabels) { - const userLabelSpan: HTMLSpanElement = createElementFromString( - `${userLabel.text}` - ); - userLabelSpan.addEventListener( - 'click', - async (event: MouseEvent): Promise => - editLabelHandler(event, userLabelSpan) - ); - element.insertAdjacentElement('afterend', userLabelSpan); - // Set the inline-style after the element gets added to the DOM, this - // will prevent a CSP error saying inline-styles aren't permitted. - userLabelSpan.setAttribute( - 'style', - `background-color: ${userLabel.color};` - ); - if (isColorBright(userLabel.color.trim())) { - userLabelSpan.classList.add('trx-bright'); - } else { - userLabelSpan.classList.remove('trx-bright'); - } - - if (isInTopicListing()) { - break; - } - } - } -} - -async function saveUserLabel(): Promise { - const settings: Settings = await getSettings(); - const labelForm: HTMLFormElement = getLabelForm(); - const labelNoID: Except | undefined = getLabelFormValues(); - if (typeof labelNoID === 'undefined') { - return; - } - - const existingIDString: string | null = labelForm.getAttribute( - 'data-trx-user-label-id' - ); - if (existingIDString === null) { - settings.data.userLabels.push({ - id: (await getHighestLabelID(settings)) + 1, - ...labelNoID - }); - await setSettings(settings); - hideLabelForm(); - addLabelsToUsernames(settings); - return; - } - - const existingID = Number(existingIDString); - const existingLabel: UserLabel | undefined = settings.data.userLabels.find( - (value) => value.id === existingID - ); - if (typeof existingLabel === 'undefined') { - log(`Tried to find label with ID that doesn't exist: ${existingID}`, true); - return; - } - - const existingLabelIndex: number = settings.data.userLabels.findIndex( - (value) => value.id === existingID - ); - settings.data.userLabels.splice(existingLabelIndex, 1); - settings.data.userLabels.push({ - id: existingID, - ...labelNoID - }); - await setSettings(settings); - hideLabelForm(); - addLabelsToUsernames(settings); -} - -async function removeUserLabel(): Promise { - const labelNoID: Except | undefined = getLabelFormValues(); - if (typeof labelNoID === 'undefined') { - return; - } - - const id: number | undefined = getLabelFormID(); - if (typeof id === 'undefined') { - log('Attempted to remove user label without an ID.'); - hideLabelForm(); - return; - } - - const settings: Settings = await getSettings(); - const labelIndex: number = settings.data.userLabels.findIndex( - (value) => value.id === id - ); - if (typeof findLabelByID(id) === 'undefined') { - log( - `Attempted to remove user label with an ID that doesn't exist ${id}.`, - true - ); - hideLabelForm(); - return; - } - - settings.data.userLabels.splice(labelIndex, 1); - await setSettings(settings); - hideLabelForm(); - addLabelsToUsernames(settings); -} - -export async function findLabelByID( - id: number, - settings?: Settings -): Promise { - if (typeof settings === 'undefined') { - settings = await getSettings(); - } - - return settings.data.userLabels.find((value) => value.id === id); -} - -async function getHighestLabelID(settings?: Settings): Promise { - if (typeof settings === 'undefined') { - settings = await getSettings(); - } - - if (settings.data.userLabels.length === 0) { - return 0; - } - - return Math.max(...settings.data.userLabels.map((value) => value.id)); -} diff --git a/source/ts/scripts/user-labels/handlers.ts b/source/ts/scripts/user-labels/handlers.ts deleted file mode 100644 index e3a1068..0000000 --- a/source/ts/scripts/user-labels/handlers.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - log, - isValidHexColor, - UserLabel, - querySelector, - getCSSCustomPropertyValue -} from '../../utilities'; -import {findLabelByID} from '../user-labels'; -import {themeColors} from '../../theme-colors'; -import { - getLabelForm, - setLabelFormColor, - setLabelFormPriority, - setLabelFormUserID, - setLabelFormUsername, - setLabelFormText, - setLabelFormTitle, - showLabelForm, - updatePreview -} from './label-form'; - -export function addLabelHandler( - event: MouseEvent, - element: HTMLSpanElement -): void { - const labelForm = getLabelForm(); - labelForm.removeAttribute('data-trx-user-label-id'); - setLabelFormTitle('Add New Label'); - setLabelFormUsername(element.getAttribute('data-trx-username')!); - setLabelFormPriority(0); - setLabelFormColor(getCSSCustomPropertyValue(themeColors.backgroundSecondary)); - setLabelFormText(''); - showLabelForm(element); - updatePreview(); -} - -export async function editLabelHandler( - event: MouseEvent, - element: HTMLSpanElement -): Promise { - setLabelFormTitle('Edit Existing Label'); - const labelID = Number(element.getAttribute('data-trx-user-label-id')!); - const label: UserLabel | undefined = await findLabelByID(labelID); - if (typeof label === 'undefined') { - log(`Tried to find label with ID that doesn't exist: ${labelID}`, true); - return; - } - - setLabelFormUserID(label.id); - setLabelFormUsername(label.username.toLowerCase()); - setLabelFormPriority(label.priority); - setLabelFormColor(label.color); - setLabelFormText(label.text); - showLabelForm(element); - updatePreview(); -} - -export function labelColorInputHandler(): void { - const labelColorInput: HTMLInputElement = querySelector( - '#trx-user-label-form-color > input' - ); - let color: string = labelColorInput.value.toLowerCase(); - if (!color.startsWith('#') && !color.startsWith('t') && color.length > 0) { - color = `#${color}`; - labelColorInput.value = color; - } - - if (color !== 'transparent' && !isValidHexColor(color)) { - log('Invalid color input, must be a valid 3/4/6/8-character hex color.'); - labelColorInput.classList.add('trx-invalid'); - return; - } - - labelColorInput.classList.remove('trx-invalid'); - updatePreview(color); -} - -export function labelTextInputHandler(): void { - const labelTextInput: HTMLInputElement = querySelector( - '#trx-user-label-input > input' - ); - updatePreview(undefined, labelTextInput.value); -} - -export function presetColorSelectHandler(): void { - const labelColorInput: HTMLInputElement = querySelector( - '#trx-user-label-form-color > input' - ); - const presetColorSelect: HTMLSelectElement = querySelector( - '#trx-user-label-form-color > select' - ); - labelColorInput.value = presetColorSelect.value; - updatePreview(presetColorSelect.value); -} diff --git a/source/ts/scripts/user-labels/label-form.ts b/source/ts/scripts/user-labels/label-form.ts deleted file mode 100644 index 36d3c58..0000000 --- a/source/ts/scripts/user-labels/label-form.ts +++ /dev/null @@ -1,144 +0,0 @@ -import {Except} from 'type-fest'; -import { - appendStyleAttribute, - UserLabel, - log, - isColorBright, - querySelector, - isValidTildesUsername -} from '../../utilities'; -import {themeColors} from '../../theme-colors'; - -export function getLabelForm(): HTMLFormElement { - return querySelector('#trx-user-label-form'); -} - -export function showLabelForm(label: HTMLSpanElement): HTMLFormElement { - const labelBounds: DOMRect = label.getBoundingClientRect(); - const labelForm = getLabelForm(); - const horizontalOffset: number = labelBounds.x + window.scrollX; - const verticalOffset: number = - labelBounds.y + labelBounds.height + 4 + window.scrollY; - appendStyleAttribute( - labelForm, - `left: ${horizontalOffset}px; top: ${verticalOffset}px;` - ); - labelForm.classList.remove('trx-hidden'); - return labelForm; -} - -export function hideLabelForm(): HTMLFormElement { - const labelForm = getLabelForm(); - labelForm.classList.add('trx-hidden'); - return labelForm; -} - -export function setLabelFormTitle(title: string): void { - const labelTitle: HTMLLabelElement = querySelector( - '#trx-user-label-form > div:first-child > label:first-child' - ); - labelTitle.textContent = title; -} - -export function setLabelFormUserID(id: number): void { - getLabelForm().dataset.trxUserLabelId = String(id); -} - -export function setLabelFormUsername(username: string): void { - const usernameInput: HTMLInputElement = querySelector( - '#trx-user-label-form-username' - ); - usernameInput.value = username.toLowerCase(); -} - -export function setLabelFormPriority(priority: number): void { - const priorityInput: HTMLInputElement = querySelector( - '#trx-user-label-priority' - ); - priorityInput.value = String(priority); -} - -export function setLabelFormColor(color: string): void { - const colorInput: HTMLInputElement = querySelector( - '#trx-user-label-form-color > input' - ); - colorInput.value = color; -} - -export function setLabelFormText(text: string): void { - const textInput: HTMLInputElement = querySelector( - '#trx-user-label-input > input' - ); - textInput.value = text; -} - -export function getLabelFormValues(): Except | undefined { - const usernameInput: HTMLInputElement = querySelector( - '#trx-user-label-form-username' - ); - const priorityInput: HTMLInputElement = querySelector( - '#trx-user-label-priority' - ); - const colorInput: HTMLInputElement = querySelector( - '#trx-user-label-form-color > input' - ); - const textInput: HTMLInputElement = querySelector( - '#trx-user-label-input > input' - ); - - const data: Except = { - color: colorInput.value.toLowerCase(), - priority: Number(priorityInput.value), - text: textInput.value, - username: usernameInput.value.toLowerCase() - }; - - if (!isValidTildesUsername(data.username)) { - log(`Invalid Tildes username detected: ${data.username}`); - usernameInput.classList.add('trx-invalid'); - return undefined; - } - - usernameInput.classList.remove('trx-invalid'); - return data; -} - -export function getLabelFormID(): number | undefined { - const labelForm: HTMLFormElement = getLabelForm(); - const id: string | null = labelForm.getAttribute('data-trx-user-label-id'); - if (id === null) { - return undefined; - } - - return Number(id); -} - -export function updatePreview(color?: string, text?: string): void { - if (typeof color === 'undefined') { - const labelColorInput: HTMLInputElement = querySelector( - '#trx-user-label-form-color > input' - ); - color = labelColorInput.value; - } - - if (typeof text === 'undefined') { - const labelTextInput: HTMLInputElement = querySelector( - '#trx-user-label-input > input' - ); - text = labelTextInput.value; - } - - const labelPreview: HTMLDivElement = querySelector('#trx-user-label-preview'); - labelPreview.setAttribute( - 'style', - `background-color: ${color}; ` + - `border-color: var(${themeColors.foregroundSecondary});` - ); - labelPreview.firstElementChild!.textContent = text; - - if (isColorBright(color.trim())) { - labelPreview.classList.add('trx-bright'); - } else { - labelPreview.classList.remove('trx-bright'); - } -} diff --git a/source/ts/theme-colors.ts b/source/ts/theme-colors.ts deleted file mode 100644 index 1c8449e..0000000 --- a/source/ts/theme-colors.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type ColorKey = keyof typeof themeColors; - -export const themeColors = { - backgroundPrimary: '--background-primary-color', - backgroundSecondary: '--background-secondary-color', - foregroundPrimary: '--foreground-primary-color', - foregroundSecondary: '--foreground-secondary-color', - exemplary: '--comment-label-exemplary-color', - offtopic: '--comment-label-offtopic-color', - joke: '--comment-label-joke-color', - noise: '--comment-label-noise-color', - malice: '--comment-label-malice-color', - mine: '--stripe-mine-color', - official: '--alert-color' -}; diff --git a/source/ts/utilities.ts b/source/ts/utilities.ts deleted file mode 100644 index cc8d357..0000000 --- a/source/ts/utilities.ts +++ /dev/null @@ -1,330 +0,0 @@ -import {browser, Manifest} from 'webextension-polyfill-ts'; - -export interface UserLabel { - color: string; - id: number; - text: string; - priority: number; - username: string; -} - -export interface Settings { - data: { - hideVotes: { - comments: boolean; - topics: boolean; - ownComments: boolean; - ownTopics: boolean; - [index: string]: boolean; - }; - knownGroups: string[]; - latestActiveFeatureTab: string; - userLabels: UserLabel[]; - version?: string; - }; - features: { - autocomplete: boolean; - backToTop: boolean; - debug: boolean; - hideVotes: boolean; - jumpToNewComment: boolean; - markdownToolbar: boolean; - userLabels: boolean; - [index: string]: boolean; - }; -} - -export const defaultSettings: Settings = { - data: { - hideVotes: { - comments: true, - topics: true, - ownComments: true, - ownTopics: true - }, - // If groups are added or removed from Tildes this does not necessarily need - // to be updated. There is a helper function available to update it whenever - // the user goes to "/groups", where all the groups are easily available. So - // scripts that use this should call that function when they are run. - knownGroups: [ - '~anime', - '~arts', - '~books', - '~comp', - '~creative', - '~design', - '~enviro', - '~finance', - '~food', - '~games', - '~games.game_design', - '~games.tabletop', - '~health', - '~health.coronavirus', - '~hobbies', - '~humanities', - '~lgbt', - '~life', - '~misc', - '~movies', - '~music', - '~news', - '~science', - '~space', - '~sports', - '~talk', - '~tech', - '~test', - '~tildes', - '~tildes.official', - '~tv' - ], - latestActiveFeatureTab: 'debug', - userLabels: [] - }, - features: { - autocomplete: true, - backToTop: true, - debug: false, - hideVotes: false, - jumpToNewComment: true, - markdownToolbar: true, - userLabels: true - } -}; - -// Keep a local variable here for the debug logging, this way we don't have to -// call `getSettings()` each time we potentially want to log something. It gets -// set each time `getSettings()` is called so it's always accurate. -let debug = true; - -export async function getSettings(): Promise { - const syncSettings: any = await browser.storage.sync.get(defaultSettings); - const settings: Settings = { - data: {...defaultSettings.data, ...syncSettings.data}, - features: {...defaultSettings.features, ...syncSettings.features} - }; - debug = settings.features.debug; - // If we're in development, force debug output. - if (getManifest().nodeEnv === 'development') { - debug = true; - } - - return settings; -} - -export async function setSettings( - newSettings: Partial -): Promise { - return browser.storage.sync.set(newSettings); -} - -export function log(message: any, override = false): void { - let overrideStyle = ''; - let prefix = '[TRX]'; - if (override) { - prefix = '%c' + prefix; - overrideStyle = 'background-color: #dc322f; margin-right: 9px;'; - } - - if (debug || override) { - if (overrideStyle.length > 0) { - console.debug(prefix, overrideStyle, message); - } else { - console.debug(prefix, message); - } - } -} - -// Helper function to convert kebab-case strings to camelCase ones. -// Primarily for converting element IDs to Object keys. -export function kebabToCamel(input: string): string { - let output = ''; - for (const part of input.split('-')) { - output += part[0].toUpperCase(); - output += part.slice(1); - } - - return output[0].toLowerCase() + output.slice(1); -} - -// The opposite of `kebabToCamel()`. -export function camelToKebab(input: string): string { - const uppercaseMatches: RegExpMatchArray | null = input.match(/[A-Z]/g); - // If there are no uppercase letters in the input, just return it. - if (uppercaseMatches === null) { - return input; - } - - // Find all the indexes of where uppercase letters are. - const uppercaseIndexes: number[] = []; - for (const match of uppercaseMatches) { - const latestIndex: number = - typeof uppercaseIndexes[uppercaseIndexes.length - 1] === 'undefined' - ? 0 - : uppercaseIndexes[uppercaseIndexes.length - 1]; - uppercaseIndexes.push(input.indexOf(match, latestIndex + 1)); - } - - // Convert each section up to the next uppercase letter to lowercase with - // a dash between each section. - let output = ''; - let previousIndex = 0; - for (const index of uppercaseIndexes) { - output += input.slice(previousIndex, index).toLowerCase(); - output += '-'; - previousIndex = index; - } - - output += input.slice(previousIndex).toLowerCase(); - return output; -} - -// This utility function should only be used in cases where we know for certain -// that the wanted element is going to exist. -export function querySelector(selector: string): T { - return document.querySelector(selector)!; -} - -export function querySelectorAll(selector: string): T[] { - const elements: T[] = []; - for (const element of document.querySelectorAll(selector)) { - elements.push(element); - } - - return elements; -} - -export function createElementFromString(input: string): T { - const template: HTMLTemplateElement = document.createElement('template'); - template.innerHTML = input.trim(); - return template.content.firstElementChild! as T; -} - -export function isInTopicListing(): boolean { - return document.querySelector('.topic-listing') !== null; -} - -export function getManifest(): {nodeEnv?: string} & Manifest.ManifestBase { - const manifest: Manifest.ManifestBase = browser.runtime.getManifest(); - return {...manifest}; -} - -export function getCSSCustomPropertyValue(property: string): string { - return getComputedStyle(document.body).getPropertyValue(property); -} - -// Adapted from https://stackoverflow.com/a/12043228/12251171. -export function isColorBright(color: string): boolean { - if (color.startsWith('#')) { - color = color.slice(1); - } - - if (color.length === 4) { - color = color.slice(0, 3); - } - - if (color.length === 8) { - color = color.slice(0, 6); - } - - if (color.length === 3) { - color = color - .split('') - .map((value) => value.repeat(2)) - .join(''); - } - - const red: number = Number.parseInt(color.slice(0, 2), 16); - const green: number = Number.parseInt(color.slice(2, 4), 16); - const blue: number = Number.parseInt(color.slice(4, 6), 16); - const brightness: number = 0.2126 * red + 0.7152 * green + 0.0722 * blue; - return brightness > 128; -} - -export function appendStyleAttribute(element: Element, styles: string): void { - const existingStyles: string | null = element.getAttribute('style'); - if (existingStyles === null) { - element.setAttribute('style', styles); - } else { - element.setAttribute('style', `${existingStyles} ${styles}`); - } -} - -export function isValidHexColor(color: string): boolean { - // Overly verbose validation for 3/4/6/8-character hex colors. - if ( - /^#[a-f\d]{6}$/i.exec(color) === null && - /^#[a-f\d]{3}$/i.exec(color) === null && - /^#[a-f\d]{8}$/i.exec(color) === null && - /^#[a-f\d]{4}$/i.exec(color) === null - ) { - return false; - } - - return true; -} - -export function flashMessage(message: string, error = false): void { - if (document.querySelector('.trx-flash-message') !== null) { - log( - `A flash message already exists, skipping requested one with message:\n${message}` - ); - return; - } - - const messageElement: HTMLDivElement = createElementFromString( - `
    ${message}
    ` - ); - if (error) { - messageElement.classList.add('trx-flash-error'); - } - - let isRemoved = false; - messageElement.addEventListener('click', (): void => { - messageElement.remove(); - isRemoved = true; - }); - document.body.append(messageElement); - setTimeout(() => { - messageElement.classList.add('trx-opaque'); - }, 50); - setTimeout(() => { - if (!isRemoved) { - messageElement.classList.remove('trx-opaque'); - setTimeout(() => { - messageElement.remove(); - }, 500); - } - }, 5000); -} - -// Validation copied from Tildes source code: -// https://gitlab.com/tildes/tildes/blob/master/tildes/tildes/schemas/user.py -export function isValidTildesUsername(username: string): boolean { - return ( - username.length >= 3 && - username.length <= 20 && - /^[a-z\d]([a-z\d]|[_-](?![_-]))*[a-z\d]$/i.exec(username) !== null - ); -} - -// This function will update the saved known groups when we're in the Tildes -// group listing. Any script that uses the known groups should call this before -// running. -export async function extractAndSaveGroups( - settings: Settings -): Promise { - if (window.location.pathname !== '/groups') { - return Promise.reject(new Error('Not in /groups.')); - } - - const groups: string[] = [...document.querySelectorAll('.link-group')].map( - (value) => value.textContent! - ); - - settings.data.knownGroups = groups; - await setSettings(settings); - log('Updated saved groups.', true); - return settings; -} diff --git a/source/types.d.ts b/source/types.d.ts new file mode 100644 index 0000000..f154efb --- /dev/null +++ b/source/types.d.ts @@ -0,0 +1,15 @@ +// A fix for TypeScript so it sees this file as a module and thus allows to +// modify the global scope. +export {}; + +// See the initialize function located in `utilities/index.ts` for actual code. + +type TildesReExtended = { + debug: boolean; +}; + +declare global { + interface Window { + TildesReExtended: TildesReExtended; + } +} diff --git a/source/utilities/color.ts b/source/utilities/color.ts new file mode 100644 index 0000000..ed4aa47 --- /dev/null +++ b/source/utilities/color.ts @@ -0,0 +1,89 @@ +/** + * Returns whether a hex color is "bright". + * @param color The hex color. + */ +export function isColorBright(color: string): boolean { + if (color.startsWith('#')) { + color = color.slice(1); + } + + // 4 character hex colors have an alpha value, we only need RGB here so remove + // the alpha character. + if (color.length === 4) { + color = color.slice(0, 3); + } + + // 8 character hex colors also have an alpha value, so remove the last 2. + if (color.length === 8) { + color = color.slice(0, 6); + } + + // 3 character hex colors can be represented as 6 character ones too, so + // transform it. For example "#123" is the same as "#112233". + if (color.length === 3) { + color = color + .split('') + .map((value) => value.repeat(2)) + .join(''); + } + + // Split the color up into 3 segments of 2 characters and convert them from + // hexadecimal to decimal. + const [red, green, blue] = color + .split(/(.{2})/) + .filter((value) => value !== '') + .map((value) => Number.parseInt(value, 16)); + + // Magical numbers taken from https://stackoverflow.com/a/12043228/12251171. + // "Per ITU-R BT.709" + const brightness = 0.2126 * red + 0.7152 * green + 0.0722 * blue; + return brightness > 128; +} + +// CSS custom properties from the Tildes themes. +export const themeColors = [ + { + name: 'Background Primary', + value: '--background-primary-color' + }, + { + name: 'Background Secondary', + value: '--background-secondary-color' + }, + { + name: 'Foreground Primary', + value: '--foreground-primary-color' + }, + { + name: 'Foreground Secondary', + value: '--foreground-secondary-color' + }, + { + name: 'Exemplary', + value: '--comment-label-exemplary-color' + }, + { + name: 'Off-topic', + value: '--comment-label-offtopic-color' + }, + { + name: 'Joke', + value: '--comment-label-joke-color' + }, + { + name: 'Noise', + value: '--comment-label-noise-color' + }, + { + name: 'Malice', + value: '--comment-label-malice-color' + }, + { + name: 'Mine', + value: '--stripe-mine-color' + }, + { + name: 'Official', + value: '--alert-color' + } +]; diff --git a/source/utilities/components.ts b/source/utilities/components.ts new file mode 100644 index 0000000..21c64d4 --- /dev/null +++ b/source/utilities/components.ts @@ -0,0 +1,25 @@ +import {html} from 'htm/preact'; +import {TRXComponent} from '..'; + +type LinkProps = { + class: string; + text: string; + url: string; +}; + +/** + * A `` helper component with `target="_blank"` and `rel="noopener"`. + * @param props Link properties. + */ +export function Link(props: LinkProps): TRXComponent { + return html` + + ${props.text} + + `; +} diff --git a/source/utilities/groups.ts b/source/utilities/groups.ts new file mode 100644 index 0000000..7fa6681 --- /dev/null +++ b/source/utilities/groups.ts @@ -0,0 +1,24 @@ +import {log, querySelectorAll, setSettings, Settings} from '..'; + +/** + * Tries to extract and save the groups. Returns the current saved groups when + * the user is not in `/groups` and the new ones when they are in `/groups`. + * @param settings The user's extension settings. + */ +export async function extractAndSaveGroups( + settings: Settings +): Promise { + if (window.location.pathname !== '/groups') { + log('Not in "/groups", returning early.'); + return settings.data.knownGroups; + } + + const groups: string[] = querySelectorAll('.link-group').map( + (value) => value.textContent! + ); + + settings.data.knownGroups = groups; + await setSettings(settings); + log('Updated saved groups.', true); + return groups; +} diff --git a/source/utilities/index.ts b/source/utilities/index.ts new file mode 100644 index 0000000..7abd515 --- /dev/null +++ b/source/utilities/index.ts @@ -0,0 +1,50 @@ +/** + * Creates an HTMLElement from a given string. Only use this when using + * `htm/preact` isn't practical. + * @param input The HTML. + */ +export function createElementFromString(input: string): T { + const template: HTMLTemplateElement = document.createElement('template'); + template.innerHTML = input.trim(); + return template.content.firstElementChild! as T; +} + +/** + * Initializes the global window with Tildes ReExtended-specific settings. + */ +export function initialize(): void { + if (window.TildesReExtended === undefined) { + window.TildesReExtended = { + debug: false + }; + } +} + +/** + * Logs something to the console under the debug level. + * @param thing The thing to log. + * @param force If true, ignores whether or not debug logging is enabled. + */ +export function log(thing: any, force = false): void { + let overrideStyle = ''; + let prefix = '[TRX]'; + if (force) { + prefix = '%c' + prefix; + overrideStyle = 'background-color: #dc322f; margin-right: 9px;'; + } + + if (window.TildesReExtended.debug || force) { + if (overrideStyle.length > 0) { + console.debug(prefix, overrideStyle, thing); + } else { + console.debug(prefix, thing); + } + } +} + +export * from './color'; +export * from './components'; +export * from './groups'; +export * from './query-selectors'; +export * from './report-a-bug'; +export * from './validators'; diff --git a/source/utilities/query-selectors.ts b/source/utilities/query-selectors.ts new file mode 100644 index 0000000..61bf950 --- /dev/null +++ b/source/utilities/query-selectors.ts @@ -0,0 +1,25 @@ +// These utility functions mainly exist so it's easier to work with TypeScript's +// typing and so we don't have to write `document.query...` all the time. + +// The first function should only ever be used when we know for certain that +// the target element is going to exist. + +/** + * Returns the first element found that matches the selector. + * @param selector The selector. + */ +export function querySelector(selector: string): T { + return document.querySelector(selector)!; +} + +/** + * Returns all elements found from all the selectors. + * @param selectors The selectors. + */ +export function querySelectorAll( + ...selectors: string[] +): T[] { + return selectors.flatMap((selector) => + Array.from(document.querySelectorAll(selector)) + ); +} diff --git a/source/utilities/report-a-bug.ts b/source/utilities/report-a-bug.ts new file mode 100644 index 0000000..8abf055 --- /dev/null +++ b/source/utilities/report-a-bug.ts @@ -0,0 +1,59 @@ +import platform from 'platform'; + +/** + * Creates a bug report template in Markdown. + * @param location The location this template will apply to. + * @param trxVersion The Tildes ReExtended version to include in the template. + */ +export function createReportTemplate( + location: 'gitlab' | 'tildes', + trxVersion: string +): string { + let introText = + "Thank you for taking the time to report a bug! Don't forget to fill in an\n appropriate title above, and make sure the information below is correct."; + + if (location === 'tildes') { + introText = + 'Thank you for taking the time to report a bug! Please make sure the\n information below is correct.'; + } + + const layout: string = platform.layout!; + const name: string = platform.name!; + const os: string = platform.os?.toString()!; + const version: string = platform.version!; + + // Set the headers using HTML tags, these can't be with #-style Markdown + // headers as they'll be interpreted as an ID instead of Markdown content. + let reportTemplate = `

    Bug Report

    + +

    Info

    \n +| Type | Value | +|------|-------| +| Extension Version | ${trxVersion} | +| Operating System | ${os} | +| Browser | ${name} ${version} (${layout}) |\n`; + + // The platform manufacturer and product can be null in certain cases (such as + // desktops) so only when they're both not null include them. + if (platform.manufacturer !== null && platform.product !== null) { + const manufacturer: string = platform.manufacturer!; + const product: string = platform.product!; + reportTemplate += `| Device | ${manufacturer} ${product} |\n`; + } + + reportTemplate += `\n

    The Problem

    +\n\n\n +

    A Solution

    +\n\n\n`; + + return reportTemplate; +} diff --git a/source/utilities/validators.ts b/source/utilities/validators.ts new file mode 100644 index 0000000..b20d83e --- /dev/null +++ b/source/utilities/validators.ts @@ -0,0 +1,23 @@ +/** + * Return whether the input is a valid hex color with a starting `#`. + * @param color The potential hex color. + */ +export function isValidHexColor(color: string): boolean { + return ( + /^#(?:[a-f\d]{8}|[a-f\d]{6}|[a-f\d]{4}|[a-f\d]{3})$/i.exec(color) !== null + ); +} + +/** + * Return whether the input is a valid Tildes username. + * @param username The potential username. + */ +export function isValidTildesUsername(username: string): boolean { + // Validation copied from Tildes source code: + // https://gitlab.com/tildes/tildes/blob/master/tildes/tildes/schemas/user.py + return ( + username.length >= 3 && + username.length <= 20 && + /^[a-z\d]([a-z\d]|[_-](?![_-]))*[a-z\d]$/i.exec(username) !== null + ); +} diff --git a/tsconfig.json b/tsconfig.json index 91bcaec..301852d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,9 @@ { "compilerOptions": { "esModuleInterop": true, + "lib": [ + "ES2019" + ], "module": "commonjs", "outDir": "build/", "strict": true, @@ -11,8 +14,7 @@ ], }, "include": [ - "source/ts/**/*.ts", - "scripts/*.ts" + "source/**/*.ts", ], "exclude": [ "node_modules/" diff --git a/yarn.lock b/yarn.lock index d1f4f60..92a9fcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -901,6 +901,31 @@ "@cliqz-oss/firefox-client" "0.3.1" es6-promise "^2.0.1" +"@devicefarmer/adbkit-logcat@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@devicefarmer/adbkit-logcat/-/adbkit-logcat-1.1.0.tgz#866d3406dc9f3791446adfe3ae622ffc48607db4" + integrity sha512-K90P5gUXM/w+yzLvJIRQ+tJooNU6ipUPPQkljtPJ0laR66TGtpt4Gqsjm0n9dPHK1W5KGgU1R5wnCd6RTSlPNA== + +"@devicefarmer/adbkit-monkey@~1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@devicefarmer/adbkit-monkey/-/adbkit-monkey-1.0.1.tgz#7d225e5fdbdad8d6772453390ecab3f1b0eb72b1" + integrity sha512-HilPrVrCosYWqSyjfpDtaaN1kJwdlBpS+IAflP3z+e7nsEgk3JGJf1Vg0NgHJooTf5HDfXSyZqMVg+5jvXCK0g== + dependencies: + async "~0.2.9" + +"@devicefarmer/adbkit@2.11.3": + version "2.11.3" + resolved "https://registry.yarnpkg.com/@devicefarmer/adbkit/-/adbkit-2.11.3.tgz#0ad981a20aada3e4eff4871218f633c85cf7f2db" + integrity sha512-rsgWREAvSRQjdP9/3GoAV6Tq+o97haywgbTfCgt5yUqiDpaaq3hlH9FTo9XsdG8x+Jd0VQ9nTC2IXsDu8JGRSA== + dependencies: + "@devicefarmer/adbkit-logcat" "^1.1.0" + "@devicefarmer/adbkit-monkey" "~1.0.1" + bluebird "~2.9.24" + commander "^2.3.0" + debug "~2.6.3" + node-forge "^0.10.0" + split "~0.3.3" + "@eslint/eslintrc@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" @@ -922,18 +947,6 @@ resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c" integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg== -"@kwsites/file-exists@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@kwsites/file-exists/-/file-exists-1.1.1.tgz#ad1efcac13e1987d8dbaf235ef3be5b0d96faa99" - integrity sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw== - dependencies: - debug "^4.1.1" - -"@kwsites/promise-deferred@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" - integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== - "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -1053,11 +1066,6 @@ dependencies: defer-to-connect "^1.0.1" -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - "@types/debounce@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.0.tgz#9ee99259f41018c640b3929e1bb32c3dcecdb192" @@ -1097,9 +1105,9 @@ integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= "@types/node@*": - version "14.11.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.2.tgz#2de1ed6670439387da1c9f549a2ade2b0a799256" - integrity sha512-jiE3QIxJ8JLNcb1Ps6rDbysDhN4xa8DJJvuC9prr6w+1tIh+QAbYyNF3tyiZNLDBIuBCf4KEcV2UvQm/V60xfA== + version "14.11.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.8.tgz#fe2012f2355e4ce08bca44aeb3abbb21cf88d33f" + integrity sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -1116,23 +1124,11 @@ resolved "https://registry.yarnpkg.com/@types/platform/-/platform-1.3.3.tgz#a3d23941770d320e2c4312516a442ab35210d019" integrity sha512-1fuOulBHWIxAPLBtLms+UtbeRDt6rL7gP5R+Yugfzdg+poCLxXqXTE8i+FpYeiytGRLUEtnFkjsY/j+usbQBqw== -"@types/prompts@^2.0.9": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.0.9.tgz#19f419310eaa224a520476b19d4183f6a2b3bd8f" - integrity sha512-TORZP+FSjTYMWwKadftmqEn6bziN5RnfygehByGsjxoK5ydnClddtv6GikGWPvCm24oI+YBwck5WDxIIyNxUrA== - dependencies: - "@types/node" "*" - "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== -"@types/semver@^7.3.4": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" - integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== - "@types/unist@^2.0.0", "@types/unist@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" @@ -1227,44 +1223,19 @@ acorn-walk@^6.0.1: integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== acorn@^6.0.1, acorn@^6.0.4: - version "6.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" - integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== acorn@^7.1.1, acorn@^7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" - integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -adbkit-logcat@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/adbkit-logcat/-/adbkit-logcat-1.1.0.tgz#01d7f9b0cef9093a30bcb3b007efff301508962f" - integrity sha1-Adf5sM75CTowvLOwB+//MBUIli8= - -adbkit-monkey@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/adbkit-monkey/-/adbkit-monkey-1.0.1.tgz#f291be701a2efc567a63fc7aa6afcded31430be1" - integrity sha1-8pG+cBou/FZ6Y/x6pq/N7TFDC+E= - dependencies: - async "~0.2.9" - -adbkit@2.11.1: - version "2.11.1" - resolved "https://registry.yarnpkg.com/adbkit/-/adbkit-2.11.1.tgz#7da847fe561254f3121088947bc1907ef053e894" - integrity sha512-hDTiRg9NX3HQt7WoDAPCplUpvzr4ZzQa2lq7BdTTJ/iOZ6O7YNAs6UYD8sFAiBEcYHDRIyq3cm9sZP6uZnhvXw== - dependencies: - adbkit-logcat "^1.1.0" - adbkit-monkey "~1.0.1" - bluebird "~2.9.24" - commander "^2.3.0" - debug "~2.6.3" - node-forge "^0.7.1" - split "~0.3.3" - -addons-linter@2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/addons-linter/-/addons-linter-2.5.0.tgz#4149b62e72889afad04bd1872f3b5e6b03d43aa2" - integrity sha512-d3GGf27ibN9ioxmjEiAFkGQRdyw5W+Gb2/9G55AZ6YygtBjtJDotTnSsE6Tz+mEFY4QKo/OaVs1XKjcZEl2fJA== +addons-linter@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/addons-linter/-/addons-linter-2.7.0.tgz#86e4fddd7dce6231a14d50396716e0c565891446" + integrity sha512-kH+0fAKSc461PnCyYQ0/SeKcxEQ2zxCZwG2GB6xjvfkMiMAwwic87VP62Cffc8H/zHEfYuT8uFmy42ayH5mqEQ== dependencies: "@babel/runtime" "7.11.2" ajv "6.12.5" @@ -1274,7 +1245,7 @@ addons-linter@2.5.0: columnify "1.5.4" common-tags "1.8.0" deepmerge "4.2.2" - dispensary "0.55.0" + dispensary "0.57.0" es6-promisify "6.1.1" eslint "7.9.0" eslint-plugin-no-unsanitized "3.1.2" @@ -1286,10 +1257,10 @@ addons-linter@2.5.0: glob "7.1.6" is-mergeable-object "1.1.1" jed "1.1.1" - mdn-browser-compat-data "1.0.35" + mdn-browser-compat-data "1.0.39" os-locale "5.0.0" pino "6.6.1" - postcss "7.0.32" + postcss "7.0.35" probe-image-size "5.0.0" relaxed-json "1.0.3" semver "7.3.2" @@ -1324,7 +1295,7 @@ ajv-merge-patch@4.1.0: fast-json-patch "^2.0.6" json-merge-patch "^0.2.3" -ajv@6.12.5, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: +ajv@6.12.5: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== @@ -1334,6 +1305,16 @@ ajv@6.12.5, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -1391,11 +1372,10 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: color-convert "^1.9.0" ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: - "@types/color-name" "^1.1.1" color-convert "^2.0.1" ansi-to-html@^0.6.4: @@ -1426,36 +1406,34 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -archiver-utils@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-1.3.0.tgz#e50b4c09c70bf3d680e32ff1b7994e9f9d895174" - integrity sha1-5QtMCccL89aA4y/xt5lOn52JUXQ= +archiver-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" + integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== dependencies: - glob "^7.0.0" - graceful-fs "^4.1.0" + glob "^7.1.4" + graceful-fs "^4.2.0" lazystream "^1.0.0" - lodash "^4.8.0" - normalize-path "^2.0.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.union "^4.6.0" + normalize-path "^3.0.0" readable-stream "^2.0.0" -archiver@~2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-2.1.1.tgz#ff662b4a78201494a3ee544d3a33fe7496509ebc" - integrity sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw= +archiver@~5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.0.2.tgz#b2c435823499b1f46eb07aa18e7bcb332f6ca3fc" + integrity sha512-Tq3yV/T4wxBsD2Wign8W9VQKhaUxzzRmjEiSoOK0SLqPgDP/N1TKdYyBeIEu56T4I9iO4fKTTR0mN9NWkBA0sg== dependencies: - archiver-utils "^1.3.0" - async "^2.0.0" + archiver-utils "^2.1.0" + async "^3.2.0" buffer-crc32 "^0.2.1" - glob "^7.0.0" - lodash "^4.8.0" - readable-stream "^2.0.0" - tar-stream "^1.5.0" - zip-stream "^1.2.0" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + readable-stream "^3.6.0" + readdir-glob "^1.0.0" + tar-stream "^2.1.4" + zip-stream "^4.0.0" argparse@^1.0.7: version "1.0.10" @@ -1623,12 +1601,10 @@ async@^1.5.2: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.0.0: - version "2.6.3" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" - integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== - dependencies: - lodash "^4.17.14" +async@^3.2.0, async@~3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" + integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== async@~0.2.9: version "0.2.10" @@ -1642,11 +1618,6 @@ async@~2.5.0: dependencies: lodash "^4.14.0" -async@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" - integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -1788,13 +1759,14 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^1.0.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.3.tgz#1e8dd80142eac80d7158c9dccc047fb620e035e7" - integrity sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww== +bl@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" + integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" bluebird@~2.9.24: version "2.9.34" @@ -1957,20 +1929,7 @@ buf-compare@^1.0.0: resolved "https://registry.yarnpkg.com/buf-compare/-/buf-compare-1.0.1.tgz#fef28da8b8113a0a0db4430b0b6467b69730b34a" integrity sha1-/vKNqLgROgoNtEMLC2Rntpcws0o= -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - -buffer-crc32@^0.2.1, buffer-crc32@~0.2.3: +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= @@ -1985,11 +1944,6 @@ buffer-equal@0.0.1: resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -2009,7 +1963,7 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.1.0: +buffer@^5.1.0, buffer@^5.5.0: version "5.6.0" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== @@ -2132,9 +2086,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001135: - version "1.0.30001142" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001142.tgz#a8518fdb5fee03ad95ac9f32a9a1e5999469c250" - integrity sha512-pDPpn9ankEpBFZXyCv2I4lh1v/ju+bqb78QfKf+w9XgDAFWBwSYPswXqprRdrgQWK0wQnpIbfwRjNHO1HWqvoQ== + version "1.0.30001146" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz#c61fcb1474520c1462913689201fb292ba6f447c" + integrity sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug== caret-pos@^2.0.0: version "2.0.0" @@ -2396,21 +2350,21 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2: - version "1.5.3" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.3.tgz#c9bbc5f01b58b5492f3d6857459cb6590ce204cc" - integrity sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw== +color-string@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" + integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== dependencies: color-name "^1.0.0" simple-swizzle "^0.2.2" color@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.2.tgz#68148e7f85d41ad7649c5fa8c8106f098d229e10" - integrity sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" + integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== dependencies: color-convert "^1.9.1" - color-string "^1.5.2" + color-string "^1.5.4" colorette@^1.2.1: version "1.2.1" @@ -2469,20 +2423,25 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= +compare-versions@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" + integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== + component-emitter@^1.2.1: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== -compress-commons@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.2.tgz#524a9f10903f3a813389b0225d27c48bb751890f" - integrity sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8= +compress-commons@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.0.1.tgz#c5fa908a791a0c71329fba211d73cd2a32005ea8" + integrity sha512-xZm9o6iikekkI0GnXCmAl3LQGZj5TBDj0zLowsqi7tJtEa3FMGSEcHcqrSJIrOAk1UG/NBbDn/F1q+MG/p/EsA== dependencies: - buffer-crc32 "^0.2.1" - crc32-stream "^2.0.0" - normalize-path "^2.0.0" - readable-stream "^2.0.0" + buffer-crc32 "^0.2.13" + crc32-stream "^4.0.0" + normalize-path "^3.0.0" + readable-stream "^3.6.0" concat-map@0.0.1: version "0.0.1" @@ -2606,13 +2565,13 @@ cp-file@^6.1.0: pify "^4.0.1" safe-buffer "^5.0.1" -crc32-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-2.0.0.tgz#e3cdd3b4df3168dd74e3de3fbbcb7b297fe908f4" - integrity sha1-483TtN8xaN10494/u8t7KX/pCPQ= +crc32-stream@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.0.tgz#05b7ca047d831e98c215538666f372b756d91893" + integrity sha512-tyMw2IeUX6t9jhgXI6um0eKfWq4EIDpfv5m7GX4Jzp7eVelQ360xd8EPXJhp2mHwLQIkqlnMLjzqSZI3a+0wRw== dependencies: crc "^3.4.4" - readable-stream "^2.0.0" + readable-stream "^3.4.0" crc@^3.4.4: version "3.8.0" @@ -2782,9 +2741,9 @@ css-what@2.1: integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== css-what@^3.2.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.1.tgz#81cb70b609e4b1351b1e54cbc90fd9c54af86e2e" - integrity sha512-wHOppVDKl4vTAOWzJt5Ek37Sgd9qq1Bmj/T1OjvicWbU5W7ru7Pqbn0Jdqii3Drx/h+dixHKXNhZYx7blthL7g== + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== cssesc@^3.0.0: version "3.0.0" @@ -3063,11 +3022,6 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3091,10 +3045,10 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dispensary@0.55.0: - version "0.55.0" - resolved "https://registry.yarnpkg.com/dispensary/-/dispensary-0.55.0.tgz#fc9ac1b90d0921a04cb6509bc4c728535f7d518f" - integrity sha512-5+6E0kQNVWIZCGwTw34B48bJQyUuvwJD6hsI/b7ScKbjfrzUIgod/ROsTX6t9d3O031A9O5RPVHIqkX4ZzcAfw== +dispensary@0.57.0: + version "0.57.0" + resolved "https://registry.yarnpkg.com/dispensary/-/dispensary-0.57.0.tgz#aa02ac07e177aaf18f41acbf65b9b4a71f623ba7" + integrity sha512-vgRaZa9Ok8QdrAVtx+s6heBgI1RGT+Y6VA336oPWYADZZz83K+5NOTpLamEKRyJdRY5pYLaWhV2Js7bau1JyKg== dependencies: async "~3.2.0" natural-compare-lite "~1.4.0" @@ -3237,9 +3191,9 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.571: - version "1.3.576" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.576.tgz#2e70234484e03d7c7e90310d7d79fd3775379c34" - integrity sha512-uSEI0XZ//5ic+0NdOqlxp0liCD44ck20OAGyLMSymIWTEAtHKVJi6JM18acOnRgUgX7Q65QqnI+sNncNvIy8ew== + version "1.3.578" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0" + integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q== elliptic@^6.5.3: version "6.5.3" @@ -3269,7 +3223,7 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@~1.4.1: +end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@~1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -3614,9 +3568,9 @@ eslint-plugin-unicorn@^21.0.0: semver "^7.3.2" eslint-rule-docs@^1.1.5: - version "1.1.209" - resolved "https://registry.yarnpkg.com/eslint-rule-docs/-/eslint-rule-docs-1.1.209.tgz#cc027888aad7c7c311447a8e0951eaef56c429ce" - integrity sha512-a0mg7IWKvV47HEuMLdk91Qq+cMl7BPUQ1WHtAk6XfSNqdWsm9Zfx4DptHWWZ2XcaGowIflO6tIv9nM8fLFZRcQ== + version "1.1.210" + resolved "https://registry.yarnpkg.com/eslint-rule-docs/-/eslint-rule-docs-1.1.210.tgz#2619817224052364dc8bd68b0b0c7b8389e85e18" + integrity sha512-6OMWgPnH4naGSj3/VVHz1ESuZo8+pgnblFvNN7FwqQrFB1UU3xO2Z4gYj5zT9LbhmJWIG8axAYr/5rzdxsgGMQ== eslint-scope@^5.0.0, eslint-scope@^5.1.0, eslint-scope@^5.1.1: version "5.1.1" @@ -3643,7 +3597,7 @@ eslint-utils@^2.0.0, eslint-utils@^2.1.0: dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@2.0.0: +eslint-visitor-keys@2.0.0, eslint-visitor-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== @@ -3697,9 +3651,9 @@ eslint@7.9.0: v8-compile-cache "^2.0.3" eslint@^7.6.0: - version "7.10.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.10.0.tgz#494edb3e4750fb791133ca379e786a8f648c72b9" - integrity sha512-BDVffmqWl7JJXqCjAK6lWtcQThZB/aP1HXSH1JKwGwv0LQEdvpR7qzNrUT487RM39B5goWuboFad5ovMBmD8yA== + version "7.11.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.11.0.tgz#aaf2d23a0b5f1d652a08edacea0c19f7fadc0b3b" + integrity sha512-G9+qtYVCHaDi1ZuWzBsOWo2wSwd70TXnU6UHA3cTYHp7gCTXZcpggWFoUVAMRarg68qtPoNfFbzPh+VdOgmwmw== dependencies: "@babel/code-frame" "^7.0.0" "@eslint/eslintrc" "^0.1.3" @@ -3711,7 +3665,7 @@ eslint@^7.6.0: enquirer "^2.3.5" eslint-scope "^5.1.1" eslint-utils "^2.1.0" - eslint-visitor-keys "^1.3.0" + eslint-visitor-keys "^2.0.0" espree "^7.3.0" esquery "^1.2.0" esutils "^2.0.2" @@ -3972,9 +3926,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fast-redact@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-2.0.0.tgz#17bb8f5e1f56ecf4a38c8455985e5eab4c478431" - integrity sha512-zxpkULI9W9MNTK2sJ3BpPQrTEXFNESd2X6O1tXMFpK/XM0G5c5Rll2EVYZH2TqI3xRGK/VaJ+eEOt7pnENJpeA== + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-2.1.0.tgz#dfe3c1ca69367fb226f110aa4ec10ec85462ffdf" + integrity sha512-0LkHpTLyadJavq9sRzzyqIoMZemWli77K2/MGOkafrR64B9ItrvZ9aT+jluvNDsv0YEHjSNhlMBtbokuoqii4A== fast-safe-stringify@^2.0.7: version "2.0.7" @@ -4076,13 +4030,20 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -firefox-profile@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/firefox-profile/-/firefox-profile-2.0.0.tgz#9de0f1918e15f89b20827a7604384f1d99fb2300" - integrity sha512-BPfcUISOV6+UwF6uqo5QS8iuFL6XZvHCm+1iuynIJ7fe1zea69Is77/n/098fp0a9sZ94lvT8rpYB15S/riSaA== +find-versions@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" + integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== + dependencies: + semver-regex "^2.0.0" + +firefox-profile@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/firefox-profile/-/firefox-profile-4.0.0.tgz#554839b19588826839e201c35fdc56362a77e29e" + integrity sha512-Vw31AsjfLDbcApMDwwnhZcz3tWjV6lxB9BNf84FaV44rZXtU87cVbFMBzPEtrJdUDbwPYiuYzprp6yksYGwjSw== dependencies: adm-zip "~0.4.x" - archiver "~2.1.0" + archiver "~5.0.2" async "~2.5.0" fs-extra "~4.0.2" ini "~1.3.3" @@ -4433,7 +4394,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -4590,6 +4551,11 @@ hsla-regex@^1.0.0: resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= +htm@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/htm/-/htm-3.0.4.tgz#c90c891645d2d792bdb9f8c867964b18e3503718" + integrity sha512-VRdvxX3tmrXuT/Ovt59NMp/ORMFi4bceFMDjos1PV4E0mV+5votuID8R60egR9A4U8nLt238R/snlJGz3UYiTQ== + html-comment-regex@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.2.tgz#97d4688aeb5c81886a364faa0cad1dda14d433a7" @@ -4673,6 +4639,22 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +husky@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.0.tgz#0b2ec1d66424e9219d359e26a51c58ec5278f0de" + integrity sha512-tTMeLCLqSBqnflBZnlVDhpaIMucSGaYyX6855jM4AguGeWCeSzNdb1mfyWduTZ3pe3SJVvVWGL0jO1iKZVPfTA== + dependencies: + chalk "^4.0.0" + ci-info "^2.0.0" + compare-versions "^3.6.0" + cosmiconfig "^7.0.0" + find-versions "^3.2.0" + opencollective-postinstall "^2.0.2" + pkg-dir "^4.2.0" + please-upgrade-node "^3.2.0" + slash "^3.0.0" + which-pm-runs "^1.0.0" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -5086,6 +5068,11 @@ is-npm@^4.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== +is-npm@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" + integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== + is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -5526,11 +5513,6 @@ kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - known-css-properties@^0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.19.0.tgz#5d92b7fa16c72d971bda9b7fe295bdf61836ee5b" @@ -5646,6 +5628,21 @@ lodash.clone@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -5696,6 +5693,11 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= +lodash.union@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" + integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -5706,7 +5708,7 @@ lodash.zip@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020" integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA= -lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.8.0, lodash@~4.17.2: +lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@~4.17.2: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -5785,11 +5787,6 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - map-age-cleaner@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" @@ -5857,10 +5854,10 @@ mdast-util-compact@^2.0.0: dependencies: unist-util-visit "^2.0.0" -mdn-browser-compat-data@1.0.35: - version "1.0.35" - resolved "https://registry.yarnpkg.com/mdn-browser-compat-data/-/mdn-browser-compat-data-1.0.35.tgz#f0e97bd84acb044e8e99905c97ddad9edbf528fa" - integrity sha512-7SMAEZgBaElDNcqFhmInBnSo+c+MOzprt7hrGNcEo9hMhDiPQ7L4dwEt6gunudjI0jXenPJaW0S8U4ckeP2uhw== +mdn-browser-compat-data@1.0.39: + version "1.0.39" + resolved "https://registry.yarnpkg.com/mdn-browser-compat-data/-/mdn-browser-compat-data-1.0.39.tgz#d06353cb60f210f9c3a7506727e1943c77b96a2c" + integrity sha512-1U5Lt+pjYxJ1mosBIdK5fr3guzV4v81f8yy0rLAj/cu7ki3ciCe85LVJJ0RLK0lP6VwFtjpXSOESfwAEpz0FyQ== dependencies: extend "3.0.2" @@ -6072,9 +6069,9 @@ modern-normalize@^1.0.0: integrity sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw== moment@^2.19.3: - version "2.29.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425" - integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA== + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== mount-point@^3.0.0: version "3.0.0" @@ -6205,6 +6202,11 @@ node-addon-api@^1.7.1: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== + node-forge@^0.7.1: version "0.7.6" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" @@ -6271,7 +6273,7 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^2.0.0, normalize-path@^2.1.1: +normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= @@ -6460,10 +6462,10 @@ open-editor@^2.0.1: line-column-path "^2.0.0" open "^6.2.0" -open@7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/open/-/open-7.1.0.tgz#68865f7d3cb238520fa1225a63cf28bcf8368a1c" - integrity sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA== +open@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/open/-/open-7.3.0.tgz#45461fdee46444f3645b6e14eb3ca94b82e1be69" + integrity sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw== dependencies: is-docker "^2.0.0" is-wsl "^2.1.1" @@ -6475,6 +6477,11 @@ open@^6.2.0: dependencies: is-wsl "^1.1.0" +opencollective-postinstall@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" + integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== + opn@^5.1.0: version "5.5.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -6963,6 +6970,13 @@ platform@^1.3.6: resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== +please-upgrade-node@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942" + integrity sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg== + dependencies: + semver-compare "^1.0.0" + plur@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/plur/-/plur-4.0.0.tgz#729aedb08f452645fe8c58ef115bf16b0a73ef84" @@ -6986,9 +7000,9 @@ posix-character-classes@^0.1.0: integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= postcss-calc@^7.0.1: - version "7.0.4" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.4.tgz#5e177ddb417341e6d4a193c5d9fd8ada79094f8b" - integrity sha512-0I79VRAd1UTkaHzY9w83P39YGO/M3bG7/tNLrHGEunBolfoGM0hSjrGvjoeaj0JE/zIw5GsI2KZ0UwDJqv5hjw== + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== dependencies: postcss "^7.0.27" postcss-selector-parser "^6.0.2" @@ -7378,6 +7392,15 @@ postcss@7.0.32: source-map "^0.6.1" supports-color "^6.1.0" +postcss@7.0.35, postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.31, postcss@^7.0.32, postcss@^7.0.6: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + postcss@^6.0.1: version "6.0.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" @@ -7387,15 +7410,6 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.11, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.21, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.31, postcss@^7.0.32, postcss@^7.0.6: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== - dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" - posthtml-parser@^0.4.0, posthtml-parser@^0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.4.2.tgz#a132bbdf0cd4bc199d34f322f5c1599385d7c6c1" @@ -7431,6 +7445,11 @@ posthtml@^0.13.1: posthtml-parser "^0.5.0" posthtml-render "^1.2.3" +preact@^10.5.3: + version "10.5.4" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.4.tgz#1e4d148f949fa54656df6c9bc9218bd4e12016e3" + integrity sha512-u0LnVtL9WWF61RLzIbEsVFOdsahoTQkQqeRwyf4eWuLMFrxTH/C47tqcnizbUH54E4KG8UzuuZaMc9KarHmpqQ== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -7484,14 +7503,6 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prompts@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.2.tgz#480572d89ecf39566d2bd3fe2c9fccb7c4c0b068" - integrity sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.4" - proto-props@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/proto-props/-/proto-props-2.0.0.tgz#8ac6e6dec658545815c623a3bc81580deda9a181" @@ -7681,7 +7692,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -7694,7 +7705,7 @@ readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -7703,6 +7714,13 @@ readable-stream@^3.1.1, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readdir-glob@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" + integrity sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA== + dependencies: + minimatch "^3.0.4" + readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -8116,9 +8134,9 @@ safe-regex@^2.1.1: integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sass@^1.26.11: - version "1.26.11" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.11.tgz#0f22cc4ab2ba27dad1d4ca30837beb350b709847" - integrity sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw== + version "1.27.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.27.0.tgz#0657ff674206b95ec20dc638a93e179c78f6ada2" + integrity sha512-0gcrER56OkzotK/GGwgg4fPrKuiFlPNitO7eUJ18Bs+/NBlofJfMxmxqpqJxjae9vu0Wq8TZzrSyxZal00WDig== dependencies: chokidar ">=2.0.0 <4.0.0" @@ -8134,6 +8152,11 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + semver-diff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" @@ -8141,6 +8164,11 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" +semver-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" + integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== + "semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -8293,15 +8321,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -simple-git@^2.20.1: - version "2.20.1" - resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-2.20.1.tgz#bab3f4d083ed6e1655a7c62ab8e8920eecae86f6" - integrity sha512-aa9s2ZLjXlHCVGbDXQLInMLvLkxKEclqMU9X5HMXi3tLWLxbWObz1UgtyZha6ocHarQtFp0OjQW9KHVR1g6wbA== - dependencies: - "@kwsites/file-exists" "^1.1.1" - "@kwsites/promise-deferred" "^1.1.1" - debug "^4.1.1" - simple-swizzle@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" @@ -8309,11 +8328,6 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -sisteransi@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -8396,7 +8410,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@~0.5.10, source-map-support@~0.5.12, source-map-support@~0.5.4: +source-map-support@0.5.19, source-map-support@~0.5.10, source-map-support@~0.5.12, source-map-support@~0.5.4: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -8979,18 +8993,16 @@ tapable@^0.1.8: resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" integrity sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q= -tar-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" - integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== +tar-stream@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" + integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== dependencies: - bl "^1.0.0" - buffer-alloc "^1.2.0" - end-of-stream "^1.0.0" + bl "^4.0.3" + end-of-stream "^1.4.1" fs-constants "^1.0.0" - readable-stream "^2.3.0" - to-buffer "^1.1.1" - xtend "^4.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" term-size@^2.1.0: version "2.2.0" @@ -9084,11 +9096,6 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= -to-buffer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" - integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== - to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" @@ -9221,17 +9228,6 @@ trough@^1.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== -ts-node@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3" - integrity sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg== - dependencies: - arg "^4.1.0" - diff "^4.0.1" - make-error "^1.1.1" - source-map-support "^0.5.17" - yn "3.1.1" - tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -9243,9 +9239,9 @@ tsconfig-paths@^3.9.0: strip-bom "^3.0.0" tslib@^1.8.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" - integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tsutils@^3.17.1: version "3.17.1" @@ -9300,11 +9296,6 @@ type-fest@^0.13.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.17.0: - version "0.17.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.17.0.tgz#268bb55d38701ce3915f60a4367a1e9f28672deb" - integrity sha512-EFi9HE4hHj85XnVV80uAUMgICQmhxYgiEvtmfpcD6jqn6zYr36HxAU6k+i/DSY28TK7/lYL0s4v/kWmiKdqaoA== - type-fest@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" @@ -9511,22 +9502,23 @@ upath@1.2.0, upath@^1.1.1, upath@^1.1.2: resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== -update-notifier@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746" - integrity sha512-9y+Kds0+LoLG6yN802wVXoIfxYEwh3FlZwzMwpCZp62S2i1/Jzeqb9Eeeju3NSHccGGasfGlK5/vEHbAifYRDg== +update-notifier@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.0.0.tgz#308e0ae772f71d66df0303159a945bc1e1fb819a" + integrity sha512-8tqsiVrMv7aZsKNSjqA6DdBLKJpZG1hRpkj1RbOJu1PgyP69OX+EInAnP1EK/ShX5YdPFgwWdk19oquZ0HTM8g== dependencies: boxen "^4.2.0" - chalk "^3.0.0" + chalk "^4.1.0" configstore "^5.0.1" has-yarn "^2.1.0" import-lazy "^2.1.0" is-ci "^2.0.0" is-installed-globally "^0.3.1" - is-npm "^4.0.0" + is-npm "^5.0.0" is-yarn-global "^0.3.0" latest-version "^5.0.0" pupa "^2.0.1" + semver "^7.3.2" semver-diff "^3.1.1" xdg-basedir "^4.0.0" @@ -9623,9 +9615,9 @@ uuid@^3.0.0, uuid@^3.3.2: integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== uuid@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" - integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + version "8.3.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" + integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== v8-compile-cache@^2.0.0, v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1: version "2.1.1" @@ -9735,16 +9727,16 @@ web-ext-types@^3.2.1: integrity sha512-oQZYDU3W8X867h8Jmt3129kRVKklz70db40Y6OzoTTuzOJpF/dB2KULJUf0txVPyUUXuyzV8GmT3nVvRHoG+Ew== web-ext@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/web-ext/-/web-ext-5.1.0.tgz#b1e0ff2ebb349f09cd02d49c54acec255d5e20b7" - integrity sha512-Eupjwvif/9P4uGdZIddJziLLLD/RuzW8r8HEANGCW8e3dlPV4GJu5z815k9DLVshG0v+q/stUPR968Q2p7hhMQ== + version "5.2.0" + resolved "https://registry.yarnpkg.com/web-ext/-/web-ext-5.2.0.tgz#71e09884dfba8370e0e87cf265959ca108d15cff" + integrity sha512-o/s206JW2U/vXHTe/XeBnsUQhIcuphsSVNVrJU+MoMFq8JlU9vI1VdS2RCW+u5NuqAsMvTyV+pA+4hLGB9CGCw== dependencies: "@babel/polyfill" "7.11.5" "@babel/runtime" "7.11.2" "@cliqz-oss/firefox-client" "0.3.1" "@cliqz-oss/node-firefox-connect" "1.2.1" - adbkit "2.11.1" - addons-linter "2.5.0" + "@devicefarmer/adbkit" "2.11.3" + addons-linter "2.7.0" bunyan "1.8.14" camelcase "6.0.0" chrome-launcher "0.13.4" @@ -9752,7 +9744,7 @@ web-ext@^5.1.0: decamelize "4.0.0" es6-error "4.1.1" event-to-promise "0.8.0" - firefox-profile "2.0.0" + firefox-profile "4.0.0" fs-extra "9.0.1" fx-runner "1.0.13" import-fresh "3.2.1" @@ -9760,14 +9752,14 @@ web-ext@^5.1.0: multimatch "4.0.0" mz "2.7.0" node-notifier "8.0.0" - open "7.1.0" + open "7.3.0" parse-json "5.0.1" sign-addon "3.1.0" source-map-support "0.5.19" strip-bom "4.0.0" strip-json-comments "3.1.1" tmp "0.2.1" - update-notifier "4.1.1" + update-notifier "5.0.0" watchpack "1.7.4" ws "7.3.1" yargs "15.4.1" @@ -9835,6 +9827,11 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= + which@1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/which/-/which-1.2.4.tgz#1557f96080604e5b11b3599eb9f45b50a9efd722" @@ -10072,11 +10069,6 @@ yauzl@2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - zip-dir@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/zip-dir/-/zip-dir-1.0.2.tgz#253f907aead62a21acd8721d8b88032b2411c051" @@ -10085,12 +10077,11 @@ zip-dir@1.0.2: async "^1.5.2" jszip "^2.4.0" -zip-stream@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04" - integrity sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ= +zip-stream@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.0.2.tgz#3a20f1bd7729c2b59fd4efa04df5eb7a5a217d2e" + integrity sha512-TGxB2g+1ur6MHkvM644DuZr8Uzyz0k0OYWtS3YlpfWBEmK4woaC2t3+pozEL3dBfIPmpgmClR5B2QRcMgGt22g== dependencies: - archiver-utils "^1.3.0" - compress-commons "^1.2.0" - lodash "^4.8.0" - readable-stream "^2.0.0" + archiver-utils "^2.1.0" + compress-commons "^4.0.0" + readable-stream "^3.6.0"