diff --git a/.editorconfig b/.editorconfig index 459f3d62..60bfc63b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,10 +1,11 @@ +# https://editorconfig.org/ +# https://prettier.io/docs/en/configuration#editorconfig + root = true [*] charset = utf-8 end_of_line = lf -indent_size = 4 -indent_style = space +indent_style = tab insert_final_newline = true max_line_length = 80 -trim_trailing_whitespace = true diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 5f801e54..00000000 --- a/.eslintrc.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "env": { - "browser": true, - "node": true, - "es2021": true, - "es2022": true, - "webextensions": true - }, - "extends": [ - "eslint:recommended", - "airbnb-base", - "plugin:svelte/recommended" - ], - "globals": { - "_browser": "writable", - "browser": "readonly" - }, - "ignorePatterns": [ - "/etc", - "build", - "dist", - "node_modules", - "public" - ], - "overrides": [ - { - "files": ["*.svelte"], - "parser": "svelte-eslint-parser" - } - ], - "settings": { // https://github.com/sveltejs/svelte-eslint-parser/issues/377#issuecomment-1630941193 - "import/parsers": { - "espree": [".js"] - } - }, - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "rules": { - "array-callback-return": "off", - "arrow-body-style": "off", - "arrow-parens": ["error", "as-needed"], - "comma-dangle": ["error", "never"], - "consistent-return": "off", - "default-case": "off", - "eqeqeq": ["error", "smart"], - "global-require": "off", - "guard-for-in": "warn", - "import/extensions": "off", - "import/first": "off", - "import/no-mutable-exports": "off", - "import/no-extraneous-dependencies": ["error", { - "devDependencies": true - }], - "import/prefer-default-export": "off", - "import/no-unresolved": "off", - "indent": ["error", 4, {"SwitchCase": 1}], - "max-len": "off", - "no-alert":"off", - "no-await-in-loop":"warn", - "no-confusing-arrow": ["warn", { - "allowParens": true, - "onlyOneSimpleParam": true - }], - "no-console": ["warn", {"allow": ["info", "warn", "error"]}], - "no-continue": "off", - "no-else-return": ["error", {"allowElseIf": true}], - "no-nested-ternary": "off", - "no-param-reassign": ["warn", { "props": false }], - "no-plusplus": "off", - "no-restricted-syntax": "off", - "no-return-assign": "off", - "no-underscore-dangle":"off", - "no-unused-expressions": "off", - "no-use-before-define": ["error", { - "functions": false, - "classes": true, - "variables": true, - "allowNamedExports": false - }], - "object-curly-newline": ["error", {"consistent": true}], - "object-curly-spacing": ["error", "never"], - "one-var": "off", - "one-var-declaration-per-line": "off", - "prefer-destructuring": "off", - "prefer-object-spread": "warn", - "quotes": ["error", "double"], - "import/no-namespace": "warn", - "svelte/no-at-html-tags": "warn" - } -} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..cc156834 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,9 @@ +# https://git-scm.com/docs/git-blame +# https://github.blog/changelog/2022-03-24-ignore-commits-in-the-blame-view-beta/ +# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view + +# Prettier 3.0.3 +acc35fbec38d72968b735826c6807793a2054aed + +# Xcode swift indent replace spaces to tabs +962c4d81879e85362a5f878889d3aa869f4e6961 diff --git a/.gitattributes b/.gitattributes index d85906ec..b4a7b25c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,7 @@ -pnpm-lock.yaml linguist-generated -public linguist-generated -xcode/iOS-App/Base.lproj/Main.html linguist-generated \ No newline at end of file +# https://git-scm.com/docs/gitattributes +# https://git-scm.com/book/en/Customizing-Git-Git-Attributes +* text=auto + +# https://docs.github.com/en/repositories/working-with-files/managing-files/customizing-how-changed-files-appear-on-github +# https://github.com/github-linguist/linguist/blob/master/docs/overrides.md +public linguist-generated \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 41bd0485..45e462ed 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,10 +5,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Install modules - run: npm install - - name: Run ESLint - run: npm run lint-js - - name: Run Stylelint - run: npm run lint-css + - uses: actions/checkout@v2 + - name: Install modules + run: npm install + - name: Run ESLint + run: npm run lint:js + - name: Run Stylelint + run: npm run lint:css diff --git a/.gitignore b/.gitignore index 73b92ed1..6e9c9304 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,19 @@ +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temp/Dist/Built +temp +dist +dist-ssr +build +built + # Logs logs *.log @@ -7,11 +23,11 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +# npm node_modules -temp -dist -dist-ssr -*.local +#package-lock.json +yarn.lock +pnpm-lock.yaml # Editor directories and files .vscode/* @@ -25,20 +41,18 @@ dist-ssr *.sln *.sw? -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - # xcode xcuserdata/ *.xcuserstate *.dev.xcconfig XCBuildData -xcode/**/build/ -xcode/**/dist/ -# xcode/Userscripts-iOS/Base.lproj/Main.html \ No newline at end of file +xcode/Ext-Safari/Resources/ + +# directories and files +local +*.local +/scripts/*.local.js + +# fastlane +/fastlane/report.xml +/fastlane/**/*.local.rb \ No newline at end of file diff --git a/.postcssrc.json b/.postcssrc.json index cc24b151..030421bc 100644 --- a/.postcssrc.json +++ b/.postcssrc.json @@ -1,5 +1,5 @@ { - "plugins": { - "autoprefixer": {"overrideBrowserslist": ["safari >= 13"]} - } -} \ No newline at end of file + "plugins": { + "autoprefixer": { "overrideBrowserslist": ["safari >= 13"] } + } +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..5c1a0da1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,26 @@ +# env +.env +.env.* +!.env.example +.DS_Store + +# root +/package +/public/**/vendor +/etc + +# dir +node_modules +xcode +build +built +dist +temp + +# files +*.svg + +# Ignore files for PNPM, NPM and YARN +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..a651d67a --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "plugins": ["prettier-plugin-svelte"], + "useTabs": true +} diff --git a/.stylelintrc.json b/.stylelintrc.json index ea60b077..a9464667 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,29 +1,31 @@ { - "extends": [ - "stylelint-config-recommended", - "stylelint-config-standard", - "stylelint-config-html/svelte" - ], - "ignoreFiles": [ - "**/build/**", - "**/dist/**", - "**/etc/**", - "**/node_modules/**", - "**/reset.css" - ], - "rules": { - "alpha-value-notation": "number", - "comment-empty-line-before": ["always",{ - "ignore": ["after-comment", "stylelint-commands"] - }], - "custom-property-empty-line-before": null, - "indentation": 4, - "max-empty-lines": 1, - "no-descending-specificity": null, - "property-no-vendor-prefix": null, - "selector-class-pattern": null, - "selector-pseudo-class-no-unknown": [true, { - "ignorePseudoClasses": ["global"] - }] - } + "extends": ["stylelint-config-standard", "stylelint-config-html"], + "ignoreFiles": [ + "**/etc/**", + "**/dist/**", + "**/build/**", + "**/node_modules/**", + "**/reset.css", + "public/**", + "xcode/**" + ], + "rules": { + "alpha-value-notation": "number", + "comment-empty-line-before": [ + "always", + { + "ignore": ["after-comment", "stylelint-commands"] + } + ], + "custom-property-empty-line-before": null, + "no-descending-specificity": null, + "property-no-vendor-prefix": null, + "selector-class-pattern": null, + "selector-pseudo-class-no-unknown": [ + true, + { + "ignorePseudoClasses": ["global"] + } + ] + } } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 728e8859..02ec4445 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,9 +1,9 @@ +// https://code.visualstudio.com/docs/editor/extension-gallery#_workspace-recommended-extensions { - // See https://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": [ - "svelte.svelte-vscode", - "dbaeumer.vscode-eslint", - "stylelint.vscode-stylelint" - ] -} \ No newline at end of file + "recommendations": [ + "svelte.svelte-vscode", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "stylelint.vscode-stylelint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index ffec34dc..f86b8368 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,24 +1,13 @@ +// https://code.visualstudio.com/docs/getstarted/settings#_workspace-settings { - "editor.formatOnSave": false, - "files.associations": { - ".htmlhintrc": "json" - }, - "eslint.validate": [ - "svelte" - ], - "stylelint.validate": [ - "css", - "postcss", - "svelte" - ], - "svelte.plugin.svelte.compilerWarnings": { - "a11y-missing-attribute": "error" - }, - "svelte.plugin.svelte.rename.enable": false, - "[svelte]": { - "editor.defaultFormatter": "svelte.svelte-vscode" - }, - "[css]": { - "editor.defaultFormatter": "stylelint.vscode-stylelint" - } + "files.associations": { + ".git-blame-ignore-revs": "ignore" + }, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + // https://github.com/microsoft/vscode-eslint#settings-options + "eslint.validate": ["javascript", "svelte"], + "eslint.experimental.useFlatConfig": true, + // https://github.com/stylelint/vscode-stylelint#stylelintvalidate + "stylelint.validate": ["css", "postcss", "svelte"] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d53a574f..c967bcab 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,63 +1,142 @@ +// https://code.visualstudio.com/docs/editor/tasks { - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "pnpm: install", - "detail": "Install dev dependencies", - "type": "shell", - "command": "pnpm install", - "group": "build", - "options": { - "cwd": "./" - } - }, - { - "label": "pnpm: dev", - "detail": "Run dev server", - "type": "shell", - "command": "pnpm dev", - "group": "build", - "options": { - "cwd": "./" - } - }, - { - "label": "pnpm: build", - "detail": "Build to xcode dist path", - "type": "shell", - "command": "pnpm build", - "group": "build", - "options": { - "cwd": "./" - } - }, - { - "label": "xcode: build", - "detail": "xcodebuild", - "type": "shell", - "command": "xcodebuild -scheme Userscripts build", - "group": "build", - "options": { - "cwd": "./xcode/" - } - }, - { - "label": "xcode: rebuild", - "detail": "pnpm build + xcodebuild", - "presentation": { - "reveal": "silent" - }, - "dependsOrder": "sequence", - "dependsOn": [ - "pnpm: build", - "xcode: build" - ], - "group": { - "kind": "build", - "isDefault": true - } - } - ] -} \ No newline at end of file + "version": "2.0.0", + "tasks": [ + { + "label": "npm: install", + "detail": "Install dev dependencies", + "type": "shell", + "command": "npm install", + "group": "build", + "options": { + "cwd": "./" + } + }, + { + "label": "npm: dev", + "detail": "Run dev server", + "type": "shell", + "command": "npm run dev", + "group": "build", + "options": { + "cwd": "./" + } + }, + { + "label": "npm: dev ext-safari-mac", + "detail": "Run dev server and build dev resource to xcode dist", + "type": "shell", + "command": "npm run dev:ext-safari-mac", + "group": "build", + "options": { + "cwd": "./" + } + }, + { + "label": "npm: dev ext-safari-ios", + "detail": "Run dev server and build dev resource to xcode dist", + "type": "shell", + "command": "npm run dev:ext-safari-ios", + "group": "build", + "options": { + "cwd": "./" + } + }, + { + "label": "npm: build mac", + "detail": "Build to xcode dist path", + "type": "shell", + "command": "npm run build:mac", + "group": "build", + "options": { + "cwd": "./" + } + }, + { + "label": "npm: build ios", + "detail": "Build to xcode dist path", + "type": "shell", + "command": "npm run build:ios", + "group": "build", + "options": { + "cwd": "./" + } + }, + { + "label": "xcodebuild: mac-debug", + "detail": "xcodebuild mac debug", + "type": "shell", + "command": "xcodebuild -scheme Mac -configuration Debug", + "group": "build", + "options": { + "cwd": "./xcode/" + } + }, + { + "label": "xcodebuild: mac-vite", + "detail": "xcodebuild mac vite", + "type": "shell", + "command": "xcodebuild -scheme Mac-Vite-Dev -configuration Vite", + "group": "build", + "options": { + "cwd": "./xcode/" + } + }, + { + "label": "dev: ext-safari-mac", + "detail": "npm: dev ext-safari-mac + xcodebuild: mac-vite", + "presentation": { + "reveal": "silent" + }, + "dependsOrder": "sequence", + "dependsOn": ["npm: dev ext-safari-mac", "xcodebuild: mac-vite"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "build: mac debug", + "detail": "npm: build mac + xcodebuild: mac-debug", + "presentation": { + "reveal": "silent" + }, + "dependsOrder": "sequence", + "dependsOn": ["npm: build mac", "xcodebuild: mac-debug"], + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "lint: js", + "detail": "lint js with eslint", + "type": "shell", + "command": "npm run lint:js", + "group": "test", + "options": { + "cwd": "./" + } + }, + { + "label": "lint: css", + "detail": "lint css with stylelint", + "type": "shell", + "command": "npm run lint:css", + "group": "test", + "options": { + "cwd": "./" + } + }, + { + "label": "prettier", + "detail": "applying prettier formatting for all supported files", + "type": "shell", + "command": "npm run prettier", + "options": { + "cwd": "./" + }, + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 1c9e2470..5864d9b8 100644 --- a/README.md +++ b/README.md @@ -5,21 +5,22 @@ An open-source userscript editor for Safari ![Userscripts Safari](/etc/screenshot.png) ## Table of Contents -* [Installation](#installation) -* [Usage](#usage) -* [UI Overview](#ui-overview) - * [Browser Page](#browser-page) - * [Settings Modal](#settings-modal) - * [Popover](#popover) -* [Metadata](#metadata) -* [API](#api) -* [Scripts Directory / Save Location](#scripts-directory) -* [Getting Help](#getting-help) -* [FAQs](#faqs) -* [Contributing](#contributing) -* [Support](#support) -* [Privacy Policy](#privacy-policy) -* [License](#license) + +- [Installation](#installation) +- [Usage](#usage) +- [UI Overview](#ui-overview) + - [Browser Page](#browser-page) + - [Settings Modal](#settings-modal) + - [Popover](#popover) +- [Metadata](#metadata) +- [API](#api) +- [Scripts Directory / Save Location](#scripts-directory) +- [Getting Help](#getting-help) +- [FAQs](#faqs) +- [Contributing](#contributing) +- [Support](#support) +- [Privacy Policy](#privacy-policy) +- [License](#license) ## Installation @@ -41,13 +42,13 @@ Once the app is downloaded and installed the following steps should be taken: **iOS** -- Go to `Settings > Extensions > Userscripts` +- Go to `Settings > Safari > Extensions > Userscripts` - Turn Userscripts `on` - For optimal experience it's recommended that you allow Userscripts for `All Websites` - Once the above is complete **open the containing app** -- Click the "Set Userscripts Directory" button and select the directory, *within the Files.app*, where your userscripts are located and where you wish newly installed userscripts to be placed - - **Tip:** for optimal cross platform experience it's a good idea to use an iCloud folder for syncing between macOS and iOS - - **Note:** syncing between macOS and iOS is not immediate, it is sometimes necessary to open Files.app in order to applying changes made in macOS to be reflected in iOS - that includes userscript deletions, additions and edits +- Click the "Set Userscripts Directory" button and select the directory, _within the Files.app_, where your userscripts are located and where you wish newly installed userscripts to be placed + - **Tip:** for optimal cross platform experience it's a good idea to use an iCloud folder for syncing between macOS and iOS + - **Note:** syncing between macOS and iOS is not immediate, it is sometimes necessary to open Files.app in order to applying changes made in macOS to be reflected in iOS - that includes userscript deletions, additions and edits - Once the directory is set you can close the containing app and open Safari.app - It **may be necessary** to apply further permissions and it's to `Always Allow` Userscripts for `All Websites` @@ -64,34 +65,34 @@ After installing Userscripts on macOS, you **do not** need to select a userscrip ![Userscripts Safari Main Application Window](/etc/ui01.png) 1. **Extension button** - click this button to open the extension interface -1. **Filter bar** - use this input to filter items in the sidebar, by *name* +1. **Filter bar** - use this input to filter items in the sidebar, by _name_ 1. **Sort button** - changes the order of the items in the sidebar by name or modified time -1. **Sidebar buttons** - *described left to right* - - The `settings` button (represented by a [cog](https://wikipedia.org/wiki/Gear)) displays the settings modal (discussed below) - - The `plus` button allows users to add new items - - `New CSS` is a "userscript" that expects [CSS](https://www.w3schools.com/css/) code - - `New Javascript` is a prototypical userscript that expects [Javascript](https://www.w3schools.com/js/DEFAULT.asp) code - - `New Remote` allows the user to add a remote hosted userscript (or style) by inputting the web address (*ex:* `https://www.k21p.com/example.user.js`) +1. **Sidebar buttons** - _described left to right_ + - The `settings` button (represented by a [cog](https://wikipedia.org/wiki/Gear)) displays the settings modal (discussed below) + - The `plus` button allows users to add new items + - `New CSS` is a "userscript" that expects [CSS](https://www.w3schools.com/css/) code + - `New Javascript` is a prototypical userscript that expects [Javascript](https://www.w3schools.com/js/DEFAULT.asp) code + - `New Remote` allows the user to add a remote hosted userscript (or style) by inputting the web address (_ex:_ `https://www.k21p.com/example.user.js`) 1. **Item toggle** - this toggle enables or disables an item -1. **Item** - this is the userscript (or style), clicking on it will load it's contents into the editor - *you can hide descriptions in the settings area!* -1. **Editor buttons (top)** - *described left to right* - - **Update button** - this button allows you to update userscripts that meet the following conditions - - metadata contains `@version` tag - - metadata contains `@updateURL` tag - - **Download button** - click this button to download a copy of your userscript - - *Note:* every userscript that is displayed in the interface is already present on your local machine, at your save location - the download button offers a quick way to retrieve a copy of that file, without needing to click the settings button, and then the save location link within the settings modal - - **Trash button** - moves the currently loaded userscript to the trash bin - it will subsequently be removed from the interface and save location +1. **Item** - this is the userscript (or style), clicking on it will load it's contents into the editor - _you can hide descriptions in the settings area!_ +1. **Editor buttons (top)** - _described left to right_ + - **Update button** - this button allows you to update userscripts that meet the following conditions + - metadata contains `@version` tag + - metadata contains `@updateURL` tag + - **Download button** - click this button to download a copy of your userscript + - _Note:_ every userscript that is displayed in the interface is already present on your local machine, at your save location - the download button offers a quick way to retrieve a copy of that file, without needing to click the settings button, and then the save location link within the settings modal + - **Trash button** - moves the currently loaded userscript to the trash bin - it will subsequently be removed from the interface and save location 1. **Editor buttons (bottom)** - - `Discard` - while editing, reverts any unsaved changes you've made to a userscript - - `Save` - while editing, saves all changes you've made to a userscript - - `Command + S` is the keyboard shortcut for the action + - `Discard` - while editing, reverts any unsaved changes you've made to a userscript + - `Save` - while editing, saves all changes you've made to a userscript + - `Command + S` is the keyboard shortcut for the action ### Settings Modal: ![Userscripts Safari Settings Window](/etc/settings.png) - **Auto Close Brackets** - toggles on/off auto closing of brackets in the editor - - this affects the following characters: `() [] {} "" ''` + - this affects the following characters: `() [] {} "" ''` - **Auto Hint** - automatically shows completion hints while editing - **Hide Descriptions** - hides the item descriptions in the sidebar - **Show Invisibles** - toggles the display of invisible characters in the editor @@ -109,7 +110,7 @@ After installing Userscripts on macOS, you **do not** need to select a userscrip -10. **Open Page Link** - *macOS only*, opens the extension browser page +10. **Open Page Link** - _macOS only_, opens the extension browser page 11. **Enable Injection toggle** - turns on/off page script injection (on/off switch) 12. **Refresh View** - refreshes the popup view 13. **Available Updates View** - the extension periodically checks all userscripts in your save location for updates and when an update is found, it is shown in this view @@ -121,53 +122,53 @@ After installing Userscripts on macOS, you **do not** need to select a userscrip Userscripts Safari currently supports the following userscript metadata: -- `@name` - This will be the name that displays in the sidebar and be used as the filename - you can *not* use the same name for multiple files of the same type +- `@name` - This will be the name that displays in the sidebar and be used as the filename - you can _not_ use the same name for multiple files of the same type - `@description`- Use this to describe what your userscript does - this will be displayed in the sidebar - there is a setting to hide descriptions - `@icon` - This doesn't have a function with this userscript manager, but the **first value** provided in the metadata will be accessible in the `GM_/GM.info` object - `@match` - Domain match patterns - you can use several instances of this field if you'd like multiple domain matches - view [this article for more information on constructing patterns](https://developer.chrome.com/extensions/match_patterns) - - **Note:** this extension only supports `http/s` -- `@exclude-match` - Domain patterns where you do *not* want the script to run + - **Note:** this extension only supports `http/s` +- `@exclude-match` - Domain patterns where you do _not_ want the script to run - `@include` - Used to match against urls for injection, globs and regular expressions are allowed, [read more here](https://wiki.greasespot.net/Include_and_exclude_rules) - `@exclude` - Functions in a similar way as `@include` but rather than injecting, a match against this key's value will prevent injection - `@inject-into` - allows the user to choose which context to inject the script into - - allows the user to choose which context to inject the script into - - values: `auto` (default), `content`, `page` - - `GM` apis are only available when using `content` - - works like [violentmonkey](https://violentmonkey.github.io/api/metadata-block/#inject-into) + - allows the user to choose which context to inject the script into + - values: `auto` (default), `content`, `page` + - `GM` apis are only available when using `content` + - works like [violentmonkey](https://violentmonkey.github.io/api/metadata-block/#inject-into) - `@run-at` - - allows the user to choose the injection timing - - document-start, document-end (default), document-idle - - works like [violentmonkey](https://violentmonkey.github.io/api/metadata-block/#run-at) - - **JS Only** + - allows the user to choose the injection timing + - document-start, document-end (default), document-idle + - works like [violentmonkey](https://violentmonkey.github.io/api/metadata-block/#run-at) + - **JS Only** - `@weight` - - allows the user to further adjust script injection timing - - can be used to ensure one script injects before another - - ONLY accepts integers (floats, strings and everything else will be ignored) - - min value = 1, max value = 999, higher numbers (“heavier”) execute earlier + - allows the user to further adjust script injection timing + - can be used to ensure one script injects before another + - ONLY accepts integers (floats, strings and everything else will be ignored) + - min value = 1, max value = 999, higher numbers (“heavier”) execute earlier - `@require` - - allows users to include remote resources in their scripts - - the value must be a valid url, currently no local file support - - must require a resource of the same file type (JS for JS, CSS for CSS) - - when a resource is required, it is downloaded and saved locally - - the resources is downloaded once at save and never checked for updates or parsed in anyway - - if you want to update the require resources, and the url does not change, you must remove the resources, save, then re-input it - - **require remote resources at your own risk, the extension never validates remote resource code in any way and be aware that using remote resources from untrusted sources can jeopardize your personal security** + - allows users to include remote resources in their scripts + - the value must be a valid url, currently no local file support + - must require a resource of the same file type (JS for JS, CSS for CSS) + - when a resource is required, it is downloaded and saved locally + - the resources is downloaded once at save and never checked for updates or parsed in anyway + - if you want to update the require resources, and the url does not change, you must remove the resources, save, then re-input it + - **require remote resources at your own risk, the extension never validates remote resource code in any way and be aware that using remote resources from untrusted sources can jeopardize your personal security** - `@version` - - used to determine the current version of a userscript - - when paired with `@updateURL`, this will allow the user to update a userscript from a remote source, if the version on their machine is `<` version at the update URL - - `@version` does nothing by itself, it needs to be paired with` @updateURL` for remote updating to function properly + - used to determine the current version of a userscript + - when paired with `@updateURL`, this will allow the user to update a userscript from a remote source, if the version on their machine is `<` version at the update URL + - `@version` does nothing by itself, it needs to be paired with` @updateURL` for remote updating to function properly - `@updateURL` - - the remote url to check version against - - if the version of the file located at the update URL is `>` the version on the local machine, the file will be updated - - `@updateURL` does nothing by itself, it needs to be paired with `@version` for remote updating to function properly + - the remote url to check version against + - if the version of the file located at the update URL is `>` the version on the local machine, the file will be updated + - `@updateURL` does nothing by itself, it needs to be paired with `@version` for remote updating to function properly - `@downloadURL` - - optional download location for a remotely updateable file (*i.e. a file that has both `@version` and `@updateURL`) - - when paired with `@version` and `@updateURL`, if the local version is `<` the version of the file that `@updateURL` points to, the extension will attempt to update the file's code with the contents of the file located at the `@downloadURL` - - `@downloadURL` does nothing by itself, it needs `@version` and `@updateURL` to present in order to function properly + - optional download location for a remotely updateable file (\*i.e. a file that has both `@version` and `@updateURL`) + - when paired with `@version` and `@updateURL`, if the local version is `<` the version of the file that `@updateURL` points to, the extension will attempt to update the file's code with the contents of the file located at the `@downloadURL` + - `@downloadURL` does nothing by itself, it needs `@version` and `@updateURL` to present in order to function properly - `@noframes` - - this key takes no value - - prevents code from being injected into nested frames - + - this key takes no value + - prevents code from being injected into nested frames + **All userscripts need at least 1 `@match` or `@include` to run!** ## API @@ -175,95 +176,97 @@ Userscripts Safari currently supports the following userscript metadata: Userscripts currently supports the following api methods. All methods are asynchronous unless otherwise noted. Users must `@grant` these methods in order to use them in a userscript. When using API methods, it's only possible to inject into the content script scope due to security concerns. - `GM.addStyle(css)` - - `css: String` - - on success returns a promise resolved with the css string argument provided + - `css: String` + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved if succeeds, rejected with error message if fails - `GM.setValue(key, value)` - - `key: String`, `value: Any` - - on success returns a promise resolved with an object indicating success + - `key: String` + - `value: Any` - any can be JSON-serialized + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved if succeeds, rejected with error message if fails - `GM.getValue(key, defaultValue)` - - `key: String`, `defaultValue: Any` - - on success returns a promise resolved with the value that was set or default value provided + - `key: String` + - `defaultValue: Any` - optional + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved with the `value` that was set or `defaultValue` provided or `undefined` if succeeds, rejected with error message if fails - `GM.deleteValue(key)` - - `key: String` - - on success returns a promise resolved with an object indicating success + - `key: String` + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved if succeeds, rejected with error message if fails - `GM.listValues()` - - on success returns a promise resolved with an array of the key names of **presently set** values + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved with an array of the key names of **presently set** values if succeeds, rejected with error message if fails - `GM.getTab()` - - on success returns a promise resolved with `Any` data that is persistent as long as this tab is open + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved with `Any` data that is persistent as long as this tab is open if succeeds, rejected with error message if fails - `GM.saveTab(tabObj)` - - `tabObj: Any` - - on success returns a promise resolved with an object indicating success + - `tabObj: Any` - any can be JSON-serialized + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved if succeeds, rejected with error message if fails - `GM.openInTab(url, openInBackground)` - - `url: String`, `openInBackground: Bool` - - on success returns a promise resolved with the [tab data](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab) for the tab just opened + - `url: String` + - `openInBackground: Bool` - optional, `false` by default + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved with [tab data](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab) for the tab just opened if succeeds, rejected with error message if fails - `GM.closeTab(tabId)` - - `tabId: Int` - - `tabId` is **optional** and if omitted the tab that called `US.closeTab` will be closed - - on success returns a promise resolved with an object indicating success + - `tabId: Int` - optional, the `caller tab` by default + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved if succeeds, rejected with error message if fails - `GM.setClipboard(data, type)` - - `data: String` - **required** - - `type: String` - **optional** and defaults to `text/plain` - - [read more here](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent/clipboardData) - - on success returns a promise resolved with a `Bool` indicating success + - `data: String` + - `type: String` - optional, `text/plain` by default + - [read more here](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent/clipboardData) + - returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise), resolved with a `Bool` value indicating succeeds or fails, rejected with error message if fails - `GM.info` && `GM_info` - - is available without needing to add it to `@grant` - - an object containing information about the running userscript - - `scriptHandler: String` - returns `Userscripts` - - `version: String` - the version of Userscripts app - - `scriptMetaStr: String` - the metablock for the currently running script - - `script: Object` - contains data about the currently running script - - `description: String` - - `exclude-match: [String]` - - `excludes: [String]` - - `grant: [String]` - - `includes: [String]` - - `inject-into: String` - - `matches: [String]` - - `name: String` - - `namespace: String` - - `noframes: Bool` - - `require: [String]` - - `resources: [String]` - *currently not implemented* - - `run-at: String` - - `version: String` - *the userscript version value* + - is available without needing to add it to `@grant` + - an object containing information about the running userscript + - `scriptHandler: String` - returns `Userscripts` + - `version: String` - the version of Userscripts app + - `scriptMetaStr: String` - the metablock for the currently running script + - `script: Object` - contains data about the currently running script + - `description: String` + - `exclude-match: [String]` + - `excludes: [String]` + - `grant: [String]` + - `includes: [String]` + - `inject-into: String` + - `matches: [String]` + - `name: String` + - `namespace: String` + - `noframes: Bool` + - `require: [String]` + - `resources: [String]` - _currently not implemented_ + - `run-at: String` + - `version: String` - _the userscript version value_ - `GM.xmlHttpRequest(details)` - - `details: Object` - - the `details` object accepts the following properties - - `url` - `String` - **required** - - `method` - `String` - defaults to `GET` - - `user` - `String` - - `password` - `String` - - `headers` - `Object` - - `overrideMimeType` - - `timeout` - `Int` - - `binary` - `Bool` - - `data` - `String` - - `responseType` - `String` - - read more about [XMLHttpRequests here](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) - - event handlers: - - `onabort` - `function` - - `onerror` - `function` - - `onload` - `function` - - `onloadend` - `function` - - `onloadstart` - `function` - - `onprogress` - `function` - - `onreadystatechange` - `function` - - `ontimeout` - `function` - - the response object passed to the event handlers has the following properties: - - `readyState` - - `response` - - `responseHeaders` - - `responseType` - - `responseURL` - - `status` - - `statusText` - - `timeout` - - `withCredentials` - - `responseText` (when `responseType` is `text`) - - returns an object with a single property, `abort`, which is a `function` - - usage: `const foo = GM.xmlHttpRequest({...});` ... `foo.abort()` to abort the request + - `details: Object` + - the `details` object accepts the following properties + - `url: String` - required + - `method: String` - optional, `GET` by default + - `user: String` - optional + - `password: String` - optional + - `headers: Object` - optional + - `overrideMimeTyp: String` - optional + - `timeout: Int` - optional + - `binary: Bool` - optional + - `data: String` - optional + - `responseType: String` - optional + - refer to [`XMLHttpRequests`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) + - event handlers: + - `onabort: Function` - optional + - `onerror: Function` - optional + - `onload: Function` - optional + - `onloadend: Function` - optional + - `onloadstart: Function` - optional + - `onprogress: Function` - optional + - `onreadystatechange: Function` - optional + - `ontimeout: Function` - optional + - the response object passed to the event handlers has the following properties: + - `readyState` + - `response` + - `responseHeaders` + - `responseType` + - `responseURL` + - `status` + - `statusText` + - `timeout` + - `withCredentials` + - `responseText` (when `responseType` is `text`) + - returns an object with a single property, `abort`, which is a `Function` + - usage: `const foo = GM.xmlHttpRequest({...});` ... `foo.abort();` to abort the request - `GM_xmlhttpRequest(details)` - - an alias for `GM.xmlHttpRequest`, works exactly the same + - an alias for `GM.xmlHttpRequest`, works exactly the same ## Scripts Directory @@ -277,7 +280,7 @@ This is the directory where the app/extension will read from and write to. This ## Getting Help -If you encounter a problem while using this app/extension or are in need of some assistance, please open an issue here in the repository. When doing so, please provide as much detail as possible. This includes listing system specs and what website and script you are trying to execute. *Please follow the issue template!* +If you encounter a problem while using this app/extension or are in need of some assistance, please open an issue here in the repository. When doing so, please provide as much detail as possible. This includes listing system specs and what website and script you are trying to execute. _Please follow the issue template!_ ## FAQs @@ -299,22 +302,25 @@ If you encounter a problem while using this app/extension or are in need of some **When I use `@require`, where are the required files stored?** -> All required files are saved *as Javascript files* in the extension container folder in macOS 11.x. That folder is located in the default save location, at: `~/Library/Containers/Userscripts/Data/Documents/require/`. +> All required files are saved _as Javascript files_ in the extension container folder in macOS 11.x. That folder is located in the default save location, at: `~/Library/Containers/Userscripts/Data/Documents/require/`. > > If you move files from the require folder or manually edit the `manifest.json` file, you will likely break app/extension functionality. ## Contributing -Code level contributions are welcome. *I prefer to collaborate directly with contributors rather than receiving spontaneous pull requests*. If you feel you can improve the project in some way, please reach out to me by email or by opening an issue with your improvement/feature request. + +Code level contributions are welcome. _I prefer to collaborate directly with contributors rather than receiving spontaneous pull requests_. If you feel you can improve the project in some way, please reach out to me by email or by opening an issue with your improvement/feature request. Further, any issue marked "help wanted" is actively seeking assistance. Please respond to those issues with feedback, guidance or offers of coding assistance. Notes: + - use [semantic commit messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) - under most circumstances, you should fork the most current version of the `develop` branch for your contributions **Please ensure your contributions align with the project's license before committing anything.** ## Support + The quickest and easiest way to support the project is by [leaving a positive review on the App Store](https://apps.apple.com/us/app/userscripts/id1463298887) if you enjoy the extension and want to see future improvements. Seeing these reviews let me know I am doing something right, or wrong, and motivates me to continue working on the project. The second best way to help out is to sign up to beta test new versions of the app. Since this extension values your privacy, and **does not collect any data from users**, it is difficult to gauge how the extension is being used. By signing up to be a beta tester it not only allows you to test upcoming features, but also gives me the opportunity to elicit direct feedback from real users. @@ -324,10 +330,11 @@ The second best way to help out is to sign up to beta test new versions of the a **[macOS Beta Sign Up Form](https://forms.gle/cUDtKg1ip4Vc9Xhc7)** ## Privacy Policy + Userscripts does not collect any data from its users nor monitor activities or actions you perform within the application and extension. This means everything that you do with the application and extension is private to you and is never shared with the developers or third parties. Since there is no data collection, there is no data retention of any kind. ## License -Copyright (c) 2022 Justin Wasack +Copyright (c) 2018-2023 Justin Wasack Licensed under the [GNU General Public License v3.0](/LICENSE) license for all open source applications. A commercial license is required for all other applications. diff --git a/docs/contributing.md b/docs/contributing.md index b039ead6..5b2c616b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -6,11 +6,16 @@ We hope this guide will helps you understand how to contribute to this project. For development and build environments please refer to [development guide](dev.md). -**Note: Portions of this guide are outdated. Some processes are still being tweaked and optimized, we will update them in due course.** +> [!NOTE] +> Portions of this guide are outdated. Some processes are still being tweaked and optimized, we will update them in due course. + +> [!IMPORTANT] +> Any pull requests must first go through code formatting and linters, otherwise you will fail the CI check. Refer to the `scripts` in [`package.json`](../package.json) # Branches ### `main`: default branch + - corresponding to development channel - all development work around this branch - protected branches not accept commits @@ -18,6 +23,7 @@ For development and build environments please refer to [development guide](dev.m - all sub-branches will be merged via PRs ### `beta`: latest test version + - generated from the master branch - corresponding to TestFlight channel - only accept version number commits @@ -25,6 +31,7 @@ For development and build environments please refer to [development guide](dev.m - never merge back into master branch ### `release`: latest public version + - generated from the beta branch - generate GitHub Releases and Tags - corresponding to App Store channel @@ -32,6 +39,7 @@ For development and build environments please refer to [development guide](dev.m - always merge back into master branch # Commits + ### Please use semantic commit messages Format: `(): ` @@ -72,6 +80,7 @@ We will improve the guidelines for creating PRs in the future, like create [`pul If you are not a member of the project, first you need to fork the master branch of the repository. ### General development + - create a new branch from the master branch - suggested branch naming format like: - `issue/333` @@ -95,4 +104,5 @@ If you are not a member of the project, first you need to fork the master branch - Despite various tests, we may still find bugs through user feedback in the public version, and we will fix those minor bugs through hotfixes. # About + [Userscripts](https://github.com/quoid/userscripts) @ 2018-2023 diff --git a/docs/dev.md b/docs/dev.md index 8a8a331b..85633998 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -1,26 +1,47 @@ +# Environment + The `root` directory is a typical multi-page app of Vite with Svelte JavaScript project. The `xcode` directory is the root of the Xcode project where the Safari extension app is built. For other directory structure instructions, please refer to [structure.md](structure.md). -# Environment -- [`Node.js`](https://nodejs.dev/en/learn/how-to-install-nodejs/) -- [`pnpm`](https://pnpm.io/installation) (optional) -- [`Xcode`](https://geo.itunes.apple.com/app/xcode/id497799835) +### Recommended code editor + +[Visual Studio Code](https://github.com/Microsoft/vscode) with extensions: [/.vscode/extensions.json](../.vscode/extensions.json) + +# Template + +The project is currently based on [`template-svelte`](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-svelte) from [`create-vite`](https://github.com/vitejs/vite/tree/main/packages/create-vite). + +Additional reference templates are from [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). + +Reviewing the template will help you understand the composition of the project. + +# Requirement + +- [`Node.js`](https://nodejs.org/) +- [`Xcode`](https://geo.itunes.apple.com/app/id497799835) # Dev -- `pnpm install` -- `pnpm dev` + +- `npm install` [^1] +- `npm run dev` [^1] # Build -- `pnpm build` + +- `npm run build:mac` [^1][^2] - `cd ./xcode` -- `xcodebuild` or build with Xcode App +- `xcodebuild -scheme Mac` [^1][^2][^3] or build with `Xcode` App + +[^1]: These commands can also be executed directly through the vscode tasks. Please refer to: [/.vscode/tasks.json](../.vscode/tasks.json) +[^2]: Select the corresponding target and platform to build. Please refer to: [/package.json](../package.json) and [xcode-schemes](../xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/) +[^3]: Local setup may be required. Please refer to: [Building from the Command Line with Xcode FAQ](https://developer.apple.com/library/archive/technotes/tn2339/_index.html) # Xcode ### Configurations + The Xcode project contains two configurations, which have independent `xcconfig` configuration files, and can run on your local at the same time without conflicts. - `Debug`: No developer account is required, that is `Sign to Run Locally` by default, which will speed up the build during development. This is convenient for developers without an account, and contributors who only need to do simple local debugging. @@ -28,18 +49,21 @@ The Xcode project contains two configurations, which have independent `xcconfig` - `Release`: A developer account is required, which means it can be used for distribution as well as running on real iOS/iPadOS devices. You will need to override your developer account information in `xcconfig` to complete the build. ### xcconfig -All `xcconfig` files are in the `./xcode/xcconfig/` directory. Each `.xcconfig` file can be overridden by `.dev.xcconfig` in the same path, they will be ignored by `git`, so you can override any build settings locally. -For example, you can create an `Userscripts-Release.dev.xcconfig` file to override `Userscripts-Release.xcconfig` and fill in your own developer account information there: +All `xcconfig` files are in the [`/xcode/xcconfig/`](../xcode/xcconfig) directory. Each `.xcconfig` file can be overridden by `.dev.xcconfig` in the same path, they will be ignored by `git`, so you can override any build settings locally. + +For example, you can create an `Userscripts-Release.dev.xcconfig` file to override [`Userscripts-Release.xcconfig`](../xcode/xcconfig/Userscripts-Release.xcconfig) and fill in your own developer account information there: `Userscripts-Release.dev.xcconfig` + ``` DEVELOPMENT_TEAM = XXXXXXXXXX ``` -Another example, if you want `Debug` builds to be also signed, so instead of enabling `Allow Unsigned Extensions` every time in Safari, you can create: +Another example, if you want `Debug` builds to be also signed, so instead of enabling `Allow Unsigned Extensions` every time in Safari, you can create: `Userscripts-Debug.dev.xcconfig` + ``` CODE_SIGN_IDENTITY = Apple Development DEVELOPMENT_TEAM = XXXXXXXXXX @@ -48,7 +72,9 @@ DEVELOPMENT_TEAM = XXXXXXXXXX Note that all existing `.xcconfig` files already include `.dev.xcconfig` files of the same name, so they will be applied automatically once you create them. You only need to fill in the setting items that need to be overridden or added. # Contribute + [Contributing guide](contributing.md) # About -[Userscripts](https://github.com/quoid/userscripts) @ 2018-2023 \ No newline at end of file + +[Userscripts](https://github.com/quoid/userscripts) @ 2018-2023 diff --git a/docs/structure.md b/docs/structure.md index 5535ea2a..d493c3b7 100644 --- a/docs/structure.md +++ b/docs/structure.md @@ -1,28 +1,41 @@ # Project directory structure + For development and build environments please refer to [development guide](dev.md). ## `docs` + Store documents and project description files, etc. ## `etc` + Readme references and project history assets ## `public` + Project Static Asset https://vitejs.dev/guide/assets.html#the-public-directory -### `./public/page/jshint.min.js` +### `./public/extension-page/jshint.min.js` + CodeMirror 5 `codemirror/addon/lint/javascript-lint.js` depends on `jshint.js` https://codemirror.net/5/doc/manual.html#addon_lint https://github.com/jshint/jshint/blob/main/dist/jshint.js +## `scripts` + +Custom build scripts ref in `package.json` +Used to build the source code in `src` below + ## `src` -Project Web App Source Code -Build the project from here to extension resources + +Project Web App/Ext Source Code +Build the project from here to xcode resources ## `xcode` -Project Native App Source Code -Build the project from here to extension bundle app + +Project Bundled App Source Code +Build the project from here to extension bundled app # About + [Userscripts](https://github.com/quoid/userscripts) @ 2018-2023 diff --git a/docs/template.md b/docs/template.md deleted file mode 100644 index 69c2ac55..00000000 --- a/docs/template.md +++ /dev/null @@ -1,47 +0,0 @@ -# Svelte + Vite - -This template should help get you started developing with Svelte in Vite. - -## Recommended IDE Setup - -[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). - -## Need an official Svelte framework? - -Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. - -## Technical considerations - -**Why use this over SvelteKit?** - -- It brings its own routing solution which might not be preferable for some users. -- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. - -This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. - -Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. - -**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** - -Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. - -**Why include `.vscode/extensions.json`?** - -Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. - -**Why enable `checkJs` in the JS template?** - -It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration. - -**Why is HMR not preserving my local component state?** - -HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). - -If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. - -```js -// store.js -// An extremely simple external store -import { writable } from 'svelte/store' -export default writable(0) -``` diff --git a/entry-app-webview.html b/entry-app-webview.html new file mode 100644 index 00000000..7e4280cc --- /dev/null +++ b/entry-app-webview.html @@ -0,0 +1,17 @@ + + + + + + + + Userscripts App + + +
+ + + diff --git a/entry-ext-action-popup.html b/entry-ext-action-popup.html new file mode 100644 index 00000000..c329fa52 --- /dev/null +++ b/entry-ext-action-popup.html @@ -0,0 +1,14 @@ + + + + + + + Userscripts popup + + + +
+ + + diff --git a/entry-ext-background.html b/entry-ext-background.html new file mode 100644 index 00000000..e3ee5d4c --- /dev/null +++ b/entry-ext-background.html @@ -0,0 +1,9 @@ + + + + Userscripts background + + + + + diff --git a/entry-ext-extension-page.html b/entry-ext-extension-page.html new file mode 100644 index 00000000..b112106c --- /dev/null +++ b/entry-ext-extension-page.html @@ -0,0 +1,15 @@ + + + + + + + Userscripts page + + + +
+ + + + diff --git a/entry-page.html b/entry-page.html deleted file mode 100644 index 82a9df6b..00000000 --- a/entry-page.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - Userscripts page - - - -
- - - - diff --git a/entry-popup.html b/entry-popup.html deleted file mode 100644 index 2694df66..00000000 --- a/entry-popup.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Userscripts popup - - - -
- - - diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..8bf9c246 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,82 @@ +/** + * @file ESLint project configuration (New flat config) + * @see {@link https://eslint.org/docs/latest/use/configure/configuration-files-new} + * @see {@link https://eslint.org/docs/latest/use/configure/migration-guide} + * @see {@link https://eslint.org/docs/latest/extend/plugin-migration-flat-config} + * @see {@link https://eslint.org/blog/2022/08/new-config-system-part-1/} + * @see {@link https://eslint.org/blog/2022/08/new-config-system-part-2/} + */ + +import js from "@eslint/js"; +import sveltePlugin from "eslint-plugin-svelte"; +import svelteParser from "svelte-eslint-parser"; +import prettier from "eslint-config-prettier"; +import globals from "globals"; + +export default [ + /** + * @see {@link https://eslint.org/docs/latest/use/configure/configuration-files-new#using-predefined-configurations} + * @see {@link https://github.com/eslint/eslint/tree/main/packages/js} + */ + js.configs.recommended, + + /** + * @see {@link https://github.com/sveltejs/eslint-plugin-svelte} + * currently no official svelte/recommended flat config + * flatten and call it through a custom config object + */ + { + files: ["**/*.svelte"], + languageOptions: { + parser: svelteParser, + }, + processor: "svelte/svelte", + plugins: { + svelte: sveltePlugin, + }, + rules: { + ...sveltePlugin.configs.base.overrides[0].rules, + ...sveltePlugin.configs.recommended.rules, + }, + }, + + /** + * @see {@link https://github.com/prettier/eslint-config-prettier} + * turns off the conflict rules, put it last + */ + prettier, + + /** custom config objects */ + + /** @see {@link https://eslint.org/docs/latest/use/configure/configuration-files-new#globally-ignoring-files-with-ignores} */ + { + ignores: ["**/dist/", "**/build/", "etc/", "xcode/", "public/"], + }, + + /** @see {@link https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options} */ + { + files: ["scripts/**/*.js"], + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, + { + files: ["src/{app,dev}/**/*.{js,svelte}"], + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, + { + files: ["src/ext/**/*.{js,svelte}"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.webextensions, + }, + }, + }, +]; diff --git a/etc/App-iOS-WebView.zip b/etc/App-iOS-WebView.zip deleted file mode 100644 index 49863444..00000000 Binary files a/etc/App-iOS-WebView.zip and /dev/null differ diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 00000000..86b5e78c --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,4 @@ +# For more information about the Appfile, see: +# https://docs.fastlane.tools/advanced/#appfile + +app_identifier("com.userscripts.macos") diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 00000000..f5551d35 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,75 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools + +LOCAL_FASTFILE = "local/Fastfile.local.rb" +if File.file?(LOCAL_FASTFILE) + import(LOCAL_FASTFILE) +end + +PROJECT_PATH = "xcode/Userscripts.xcodeproj" + +# https://docs.fastlane.tools/actions/is_ci/ +if is_ci + setup_ci + IS_CI = true +else + IS_CI = false +end + +# https://docs.fastlane.tools/actions/app_store_connect_api_key/ +app_store_connect_api_key( + key_id: ENV['APP_STORE_CONNECT_API_KEY_KEY_ID'], + issuer_id: ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID'], + key_content: ENV['APP_STORE_CONNECT_API_KEY_KEY'], + key_filepath: ENV['APP_STORE_CONNECT_API_KEY_KEY_FILEPATH'], +) + +default_platform(:ios) + +platform :ios do + lane :beta do + match + BUILD_NUMBER = latest_testflight_build_number(platform: "ios") + 1 + build_app( + project: PROJECT_PATH, + scheme: "iOS", + xcargs: { + CURRENT_PROJECT_VERSION: BUILD_NUMBER.to_s, + # MARKETING_VERSION: ENV['MARKETING_VERSION'], + }, + output_directory: "build", + output_name: "Userscripts-iOS-Beta", + silent: IS_CI, + ) + upload_to_testflight(skip_submission: true) + end + + lane :release do + puts "ios release #{ENV['MARKETING_VERSION']}" + beta + end +end + +platform :mac do + lane :beta do + match + BUILD_NUMBER = latest_testflight_build_number(platform: "osx") + 1 + build_mac_app( + project: PROJECT_PATH, + scheme: "Mac", + xcargs: { + CURRENT_PROJECT_VERSION: BUILD_NUMBER.to_s, + # MARKETING_VERSION: ENV['MARKETING_VERSION'], + }, + output_directory: "build", + output_name: "Userscripts-Mac-Beta", + silent: IS_CI, + ) + upload_to_testflight(skip_submission: true) + end + + lane :release do + puts "mac release #{ENV['MARKETING_VERSION']}" + beta + end +end \ No newline at end of file diff --git a/fastlane/Gemfile b/fastlane/Gemfile new file mode 100644 index 00000000..a4353219 --- /dev/null +++ b/fastlane/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "fastlane" +# gem "rubocop" diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock new file mode 100644 index 00000000..9a4c9f50 --- /dev/null +++ b/fastlane/Gemfile.lock @@ -0,0 +1,220 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.950.0) + aws-sdk-core (3.201.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.156.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.110.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.3.1) + fastlane (2.221.1) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.6) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.7.2) + jwt (2.8.2) + base64 + mini_magick (4.13.1) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.1) + nanaimo (0.3.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.5.0) + os (1.1.4) + plist (3.7.1) + public_suffix (6.0.0) + rake (13.2.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.9) + strscan + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + strscan (3.1.0) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.5.0) + word_wrap (1.0.0) + xcodeproj (1.24.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + arm64-darwin-23 + ruby + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.5.11 diff --git a/fastlane/Matchfile b/fastlane/Matchfile new file mode 100644 index 00000000..3cbce194 --- /dev/null +++ b/fastlane/Matchfile @@ -0,0 +1,12 @@ +# https://docs.fastlane.tools/actions/match + +readonly(true) +type("appstore") +app_identifier([ + "com.userscripts.macos", + "com.userscripts.macos.Userscripts-Extension", +]) + +for_platform :mac do + additional_cert_types(["mac_installer_distribution"]) +end \ No newline at end of file diff --git a/index.html b/index.html index b97bad98..215948a7 100644 --- a/index.html +++ b/index.html @@ -1,26 +1,28 @@ - + - - - - Userscripts dev - - - -
- - - + + + + Userscripts dev + + + +
+ + + diff --git a/jsconfig.json b/jsconfig.json index a8284bdf..2fae38f7 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,33 +1,26 @@ +// https://code.visualstudio.com/docs/languages/jsconfig +// https://www.typescriptlang.org/docs/handbook/tsconfig-json.html +// https://www.typescriptlang.org/tsconfig + +// https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html +// https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html + { - "compilerOptions": { - "moduleResolution": "Node", - "target": "ESNext", - "module": "ESNext", - /** - * svelte-preprocess cannot figure out whether you have - * a value or a type, so tell TypeScript to enforce using - * `import type` instead of `import` for Types. - */ - "importsNotUsedAsValues": "error", - "isolatedModules": true, - "resolveJsonModule": true, - /** - * To have warnings / errors of the Svelte compiler at the - * correct position, enable source maps by default. - */ - "sourceMap": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable this if you'd like to use dynamic types. - */ - // "checkJs": true - }, - /** - * Use global.d.ts instead of compilerOptions.types - * to avoid limiting type declarations. - */ - "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..34e70d75 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3887 @@ +{ + "name": "userscripts", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "userscripts", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.4.5", + "@types/webextension-polyfill": "^0.10.5", + "autoprefixer": "^10.4.16", + "cm-show-invisibles": "^3.1.0", + "codemirror": "^5.65.15", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-svelte": "^2.34.0", + "prettier": "3.0.3", + "prettier-plugin-svelte": "3.0.3", + "stylelint": "^15.11.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-standard": "^34.0.0", + "svelte": "4.2.0", + "vite": "^4.5.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.2.tgz", + "integrity": "sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.2.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.1.tgz", + "integrity": "sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.5.tgz", + "integrity": "sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.3.2", + "@csstools/css-tokenizer": "^2.2.1" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz", + "integrity": "sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.13" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", + "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.6.tgz", + "integrity": "sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.3", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", + "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.2.0", + "svelte": "^3.54.0 || ^4.0.0", + "vite": "^4.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz", + "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.4.tgz", + "integrity": "sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==", + "dev": true + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz", + "integrity": "sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==", + "dev": true + }, + "node_modules/@types/webextension-polyfill": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@types/webextension-polyfill/-/webextension-polyfill-0.10.5.tgz", + "integrity": "sha512-LlmbFLUB7+BDrb9nMuM0wlqtx9LZbBV2x3W98o02cD7Y8i10+sBenTlhG56vr47dzC7WIVXbURii+5jMJsyjLw==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", + "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "dev": true, + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001561", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", + "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cm-show-invisibles": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cm-show-invisibles/-/cm-show-invisibles-3.1.0.tgz", + "integrity": "sha512-dfWd4QmuSMoSgONkCv4+REfPrxp3R3OE8jnxR3u2IjkLPolHArGOhMetBLo6gTeiL8tA4ff5v+VBZ1u1AZx+4A==", + "dev": true + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/codemirror": { + "version": "5.65.15", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.15.tgz", + "integrity": "sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-functions-list": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.1.tgz", + "integrity": "sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==", + "dev": true, + "engines": { + "node": ">=12 || >=16" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "peer": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.576", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", + "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.53.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", + "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.53.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.34.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.34.1.tgz", + "integrity": "sha512-HnLzYevh9bLL0Rj2d4dmZY9EutN0BL5JsJRHqtJFIyaEmdxxd3ZuY5zNoSjIFhctFMSntsClbd6TwYjgaOY0Xw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "debug": "^4.3.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.29.0", + "postcss": "^8.4.5", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.0.11", + "semver": "^7.5.3", + "svelte-eslint-parser": ">=0.33.0 <1.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0", + "svelte": "^3.37.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.2.tgz", + "integrity": "sha512-Olnt+V7xYdvGze9YTbGFZIfQXuGV4R3nQwwl8BrtgaPE/wq8UFpUHWuTNc05saowhSr1ZO6tx+V6RjE9D5YQog==", + "dev": true, + "peer": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", + "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.5", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", + "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/meow": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/meow/-/meow-10.1.5.tgz", + "integrity": "sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.2", + "camelcase-keys": "^7.0.0", + "decamelize": "^5.0.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.2", + "read-pkg-up": "^8.0.0", + "redent": "^4.0.0", + "trim-newlines": "^4.0.2", + "type-fest": "^1.2.2", + "yargs-parser": "^20.2.9" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-html": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.5.0.tgz", + "integrity": "sha512-kCMRWJRHKicpA166kc2lAVUGxDZL324bkj/pVOb6RhjB0Z5Krl7mN0AsVkBhVIRZZirY0lyQXG38HCVaoKVNoA==", + "dev": true, + "peer": true, + "dependencies": { + "htmlparser2": "^8.0.0", + "js-tokens": "^8.0.0", + "postcss": "^8.4.0", + "postcss-safe-parser": "^6.0.0" + }, + "engines": { + "node": "^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", + "integrity": "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==", + "dev": true + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz", + "integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.0.3.tgz", + "integrity": "sha512-dLhieh4obJEK1hnZ6koxF+tMUrZbV5YGvRpf2+OADyanjya5j0z1Llo8iGwiHmFWZVG/hLEw/AJD5chXd9r3XA==", + "dev": true, + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-6.0.0.tgz", + "integrity": "sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^3.0.2", + "parse-json": "^5.2.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-8.0.0.tgz", + "integrity": "sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==", + "dev": true, + "dependencies": { + "find-up": "^5.0.0", + "read-pkg": "^6.0.0", + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/redent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-4.0.0.tgz", + "integrity": "sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==", + "dev": true, + "dependencies": { + "indent-string": "^5.0.0", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true + }, + "node_modules/stylelint": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.11.0.tgz", + "integrity": "sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw==", + "dev": true, + "dependencies": { + "@csstools/css-parser-algorithms": "^2.3.1", + "@csstools/css-tokenizer": "^2.2.0", + "@csstools/media-query-list-parser": "^2.1.4", + "@csstools/selector-specificity": "^3.0.0", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^8.2.0", + "css-functions-list": "^3.2.1", + "css-tree": "^2.3.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.1", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^7.0.0", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^5.2.4", + "import-lazy": "^4.0.0", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.29.0", + "mathml-tag-names": "^2.1.3", + "meow": "^10.1.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.28", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.0.13", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "style-search": "^0.1.0", + "supports-hyperlinks": "^3.0.0", + "svg-tags": "^1.0.0", + "table": "^6.8.1", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + } + }, + "node_modules/stylelint-config-html": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-html/-/stylelint-config-html-1.1.0.tgz", + "integrity": "sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==", + "dev": true, + "engines": { + "node": "^12 || >=14" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "postcss-html": "^1.0.0", + "stylelint": ">=14.0.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-13.0.0.tgz", + "integrity": "sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==", + "dev": true, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "stylelint": "^15.10.0" + } + }, + "node_modules/stylelint-config-standard": { + "version": "34.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-34.0.0.tgz", + "integrity": "sha512-u0VSZnVyW9VSryBG2LSO+OQTjN7zF9XJaAJRX/4EwkmU0R2jYwmBSN10acqZisDitS0CLiEiGjX7+Hrq8TAhfQ==", + "dev": true, + "dependencies": { + "stylelint-config-recommended": "^13.0.0" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "stylelint": "^15.10.0" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-7.0.1.tgz", + "integrity": "sha512-uLfFktPmRetVCbHe5UPuekWrQ6hENufnA46qEGbfACkK5drjTTdQYUragRgMjHldcbYG+nslUerqMPjbBSHXjQ==", + "dev": true, + "dependencies": { + "flat-cache": "^3.1.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", + "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/svelte": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.0.tgz", + "integrity": "sha512-kVsdPjDbLrv74SmLSUzAsBGquMs4MPgWGkGLpH+PjOYnFOziAvENVzgJmyOCV2gntxE32aNm8/sqNKD6LbIpeQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^3.2.1", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.0", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz", + "integrity": "sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==", + "dev": true, + "dependencies": { + "eslint-scope": "^7.0.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "postcss": "^8.4.29", + "postcss-scss": "^4.0.8" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/table": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-newlines": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", + "integrity": "sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vite": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", + "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index bd837525..36940a00 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,38 @@ { - "name": "userscripts", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "lint-js": "eslint ./", - "lint-css": "stylelint '**/*.{css,svelte}'" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.3", - "autoprefixer": "^10.4.14", - "cm-show-invisibles": "^3.1.0", - "codemirror": "^5.65.14", - "eslint": "^8.46.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-plugin-import": "^2.28.0", - "eslint-plugin-svelte": "^2.32.4", - "postcss-html": "^1.5.0", - "stylelint": "^15.10.2", - "stylelint-config-html": "^1.1.0", - "stylelint-config-recommended": "^13.0.0", - "stylelint-config-standard": "^34.0.0", - "svelte": "^4.1.2", - "vite": "^4.4.8" - } -} \ No newline at end of file + "name": "userscripts", + "private": true, + "type": "module", + "sideEffects": false, + "scripts": { + "dev": "vite", + "dev:ext-safari-mac": "SAFARI_PLATFORM=mac node scripts/dev-ext-safari.js", + "dev:ext-safari-ios": "SAFARI_PLATFORM=ios node scripts/dev-ext-safari.js", + "build:mac": "npm run build:app && npm run build:ext-safari-mac", + "build:ios": "npm run build:app && npm run build:ext-safari-ios", + "build:app": "node scripts/build-app.js", + "build:ext-safari-mac": "SAFARI_PLATFORM=mac node scripts/build-ext-safari-15.js", + "build:ext-safari-ios": "SAFARI_PLATFORM=ios node scripts/build-ext-safari-15.js", + "build:ext-safari-mac-16.4": "SAFARI_PLATFORM=mac node scripts/build-ext-safari-16.4.js", + "build:ext-safari-ios-16.4": "SAFARI_PLATFORM=ios node scripts/build-ext-safari-16.4.js", + "lint:js": "eslint . && prettier --check . --plugin prettier-plugin-svelte", + "lint:css": "stylelint '**/*.{css,html,svelte}'", + "prettier": "prettier --write . --plugin prettier-plugin-svelte" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.4.5", + "@types/webextension-polyfill": "^0.10.5", + "autoprefixer": "^10.4.16", + "cm-show-invisibles": "^3.1.0", + "codemirror": "^5.65.15", + "eslint": "^8.52.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-svelte": "^2.34.0", + "prettier": "3.0.3", + "prettier-plugin-svelte": "3.0.3", + "stylelint": "^15.11.0", + "stylelint-config-html": "^1.1.0", + "stylelint-config-standard": "^34.0.0", + "svelte": "4.2.0", + "vite": "^4.5.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 26d2041a..00000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,2848 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -devDependencies: - '@sveltejs/vite-plugin-svelte': - specifier: ^2.4.3 - version: 2.4.3(svelte@4.1.2)(vite@4.4.8) - autoprefixer: - specifier: ^10.4.14 - version: 10.4.14(postcss@8.4.27) - cm-show-invisibles: - specifier: ^3.1.0 - version: 3.1.0 - codemirror: - specifier: ^5.65.14 - version: 5.65.14 - eslint: - specifier: ^8.46.0 - version: 8.46.0 - eslint-config-airbnb-base: - specifier: ^15.0.0 - version: 15.0.0(eslint-plugin-import@2.28.0)(eslint@8.46.0) - eslint-plugin-import: - specifier: ^2.28.0 - version: 2.28.0(eslint@8.46.0) - eslint-plugin-svelte: - specifier: ^2.32.4 - version: 2.32.4(eslint@8.46.0)(svelte@4.1.2) - postcss-html: - specifier: ^1.5.0 - version: 1.5.0 - stylelint: - specifier: ^15.10.2 - version: 15.10.2 - stylelint-config-html: - specifier: ^1.1.0 - version: 1.1.0(postcss-html@1.5.0)(stylelint@15.10.2) - stylelint-config-recommended: - specifier: ^13.0.0 - version: 13.0.0(stylelint@15.10.2) - stylelint-config-standard: - specifier: ^34.0.0 - version: 34.0.0(stylelint@15.10.2) - svelte: - specifier: ^4.1.2 - version: 4.1.2 - vite: - specifier: ^4.4.8 - version: 4.4.8 - -packages: - - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true - - /@ampproject/remapping@2.2.1: - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.18 - dev: true - - /@babel/code-frame@7.22.5: - resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/highlight': 7.22.5 - dev: true - - /@babel/helper-validator-identifier@7.22.5: - resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/highlight@7.22.5: - resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-validator-identifier': 7.22.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - dev: true - - /@csstools/css-parser-algorithms@2.3.1(@csstools/css-tokenizer@2.2.0): - resolution: {integrity: sha512-xrvsmVUtefWMWQsGgFffqWSK03pZ1vfDki4IVIIUxxDKnGBzqNgv0A7SB1oXtVNEkcVO8xi1ZrTL29HhSu5kGA==} - engines: {node: ^14 || ^16 || >=18} - peerDependencies: - '@csstools/css-tokenizer': ^2.2.0 - dependencies: - '@csstools/css-tokenizer': 2.2.0 - dev: true - - /@csstools/css-tokenizer@2.2.0: - resolution: {integrity: sha512-wErmsWCbsmig8sQKkM6pFhr/oPha1bHfvxsUY5CYSQxwyhA9Ulrs8EqCgClhg4Tgg2XapVstGqSVcz0xOYizZA==} - engines: {node: ^14 || ^16 || >=18} - dev: true - - /@csstools/media-query-list-parser@2.1.4(@csstools/css-parser-algorithms@2.3.1)(@csstools/css-tokenizer@2.2.0): - resolution: {integrity: sha512-V/OUXYX91tAC1CDsiY+HotIcJR+vPtzrX8pCplCpT++i8ThZZsq5F5dzZh/bDM3WUOjrvC1ljed1oSJxMfjqhw==} - engines: {node: ^14 || ^16 || >=18} - peerDependencies: - '@csstools/css-parser-algorithms': ^2.3.1 - '@csstools/css-tokenizer': ^2.2.0 - dependencies: - '@csstools/css-parser-algorithms': 2.3.1(@csstools/css-tokenizer@2.2.0) - '@csstools/css-tokenizer': 2.2.0 - dev: true - - /@csstools/selector-specificity@3.0.0(postcss-selector-parser@6.0.13): - resolution: {integrity: sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g==} - engines: {node: ^14 || ^16 || >=18} - peerDependencies: - postcss-selector-parser: ^6.0.13 - dependencies: - postcss-selector-parser: 6.0.13 - dev: true - - /@esbuild/android-arm64@0.18.17: - resolution: {integrity: sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.18.17: - resolution: {integrity: sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.18.17: - resolution: {integrity: sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.18.17: - resolution: {integrity: sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.18.17: - resolution: {integrity: sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.18.17: - resolution: {integrity: sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.18.17: - resolution: {integrity: sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.18.17: - resolution: {integrity: sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.18.17: - resolution: {integrity: sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.18.17: - resolution: {integrity: sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.18.17: - resolution: {integrity: sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.18.17: - resolution: {integrity: sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.18.17: - resolution: {integrity: sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.18.17: - resolution: {integrity: sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.18.17: - resolution: {integrity: sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.18.17: - resolution: {integrity: sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.18.17: - resolution: {integrity: sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.18.17: - resolution: {integrity: sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.18.17: - resolution: {integrity: sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.18.17: - resolution: {integrity: sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.18.17: - resolution: {integrity: sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.18.17: - resolution: {integrity: sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@eslint-community/eslint-utils@4.4.0(eslint@8.46.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 8.46.0 - eslint-visitor-keys: 3.4.2 - dev: true - - /@eslint-community/regexpp@4.6.2: - resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - - /@eslint/eslintrc@2.1.1: - resolution: {integrity: sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.6.1 - globals: 13.20.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/js@8.46.0: - resolution: {integrity: sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /@humanwhocodes/config-array@0.11.10: - resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/object-schema@1.2.1: - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - dev: true - - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 - dev: true - - /@jridgewell/resolve-uri@3.1.0: - resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/sourcemap-codec@1.4.14: - resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} - dev: true - - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true - - /@jridgewell/trace-mapping@0.3.18: - resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} - dependencies: - '@jridgewell/resolve-uri': 3.1.0 - '@jridgewell/sourcemap-codec': 1.4.14 - dev: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 - dev: true - - /@sveltejs/vite-plugin-svelte-inspector@1.0.3(@sveltejs/vite-plugin-svelte@2.4.3)(svelte@4.1.2)(vite@4.4.8): - resolution: {integrity: sha512-Khdl5jmmPN6SUsVuqSXatKpQTMIifoQPDanaxC84m9JxIibWvSABJyHpyys0Z+1yYrxY5TTEQm+6elh0XCMaOA==} - engines: {node: ^14.18.0 || >= 16} - peerDependencies: - '@sveltejs/vite-plugin-svelte': ^2.2.0 - svelte: ^3.54.0 || ^4.0.0 - vite: ^4.0.0 - dependencies: - '@sveltejs/vite-plugin-svelte': 2.4.3(svelte@4.1.2)(vite@4.4.8) - debug: 4.3.4 - svelte: 4.1.2 - vite: 4.4.8 - transitivePeerDependencies: - - supports-color - dev: true - - /@sveltejs/vite-plugin-svelte@2.4.3(svelte@4.1.2)(vite@4.4.8): - resolution: {integrity: sha512-NY2h+B54KHZO3kDURTdARqthn6D4YSIebtfW75NvZ/fwyk4G+AJw3V/i0OBjyN4406Ht9yZcnNWMuRUFnDNNiA==} - engines: {node: ^14.18.0 || >= 16} - peerDependencies: - svelte: ^3.54.0 || ^4.0.0 - vite: ^4.0.0 - dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 1.0.3(@sveltejs/vite-plugin-svelte@2.4.3)(svelte@4.1.2)(vite@4.4.8) - debug: 4.3.4 - deepmerge: 4.3.1 - kleur: 4.1.5 - magic-string: 0.30.2 - svelte: 4.1.2 - svelte-hmr: 0.15.3(svelte@4.1.2) - vite: 4.4.8 - vitefu: 0.2.4(vite@4.4.8) - transitivePeerDependencies: - - supports-color - dev: true - - /@types/estree@1.0.1: - resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} - dev: true - - /@types/json5@0.0.29: - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - dev: true - - /@types/minimist@1.2.2: - resolution: {integrity: sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==} - dev: true - - /@types/normalize-package-data@2.4.1: - resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} - dev: true - - /acorn-jsx@5.3.2(acorn@8.10.0): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.10.0 - dev: true - - /acorn@8.10.0: - resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - - /ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - dev: true - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - dependencies: - dequal: 2.0.3 - dev: true - - /array-buffer-byte-length@1.0.0: - resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} - dependencies: - call-bind: 1.0.2 - is-array-buffer: 3.0.2 - dev: true - - /array-includes@3.1.6: - resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - get-intrinsic: 1.2.1 - is-string: 1.0.7 - dev: true - - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true - - /array.prototype.findlastindex@1.2.2: - resolution: {integrity: sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - es-shim-unscopables: 1.0.0 - get-intrinsic: 1.2.1 - dev: true - - /array.prototype.flat@1.3.1: - resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - es-shim-unscopables: 1.0.0 - dev: true - - /array.prototype.flatmap@1.3.1: - resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - es-shim-unscopables: 1.0.0 - dev: true - - /arraybuffer.prototype.slice@1.0.1: - resolution: {integrity: sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.0 - call-bind: 1.0.2 - define-properties: 1.2.0 - get-intrinsic: 1.2.1 - is-array-buffer: 3.0.2 - is-shared-array-buffer: 1.0.2 - dev: true - - /arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - dev: true - - /astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - dev: true - - /autoprefixer@10.4.14(postcss@8.4.27): - resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 - dependencies: - browserslist: 4.21.10 - caniuse-lite: 1.0.30001519 - fraction.js: 4.2.0 - normalize-range: 0.1.2 - picocolors: 1.0.0 - postcss: 8.4.27 - postcss-value-parser: 4.2.0 - dev: true - - /available-typed-arrays@1.0.5: - resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} - engines: {node: '>= 0.4'} - dev: true - - /axobject-query@3.2.1: - resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} - dependencies: - dequal: 2.0.3 - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /balanced-match@2.0.0: - resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==} - dev: true - - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /browserslist@4.21.10: - resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001519 - electron-to-chromium: 1.4.485 - node-releases: 2.0.13 - update-browserslist-db: 1.0.11(browserslist@4.21.10) - dev: true - - /call-bind@1.0.2: - resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} - dependencies: - function-bind: 1.1.1 - get-intrinsic: 1.2.1 - dev: true - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /camelcase-keys@7.0.2: - resolution: {integrity: sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==} - engines: {node: '>=12'} - dependencies: - camelcase: 6.3.0 - map-obj: 4.3.0 - quick-lru: 5.1.1 - type-fest: 1.4.0 - dev: true - - /camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - dev: true - - /caniuse-lite@1.0.30001519: - resolution: {integrity: sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==} - dev: true - - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /cm-show-invisibles@3.1.0: - resolution: {integrity: sha512-dfWd4QmuSMoSgONkCv4+REfPrxp3R3OE8jnxR3u2IjkLPolHArGOhMetBLo6gTeiL8tA4ff5v+VBZ1u1AZx+4A==} - dev: true - - /code-red@1.0.3: - resolution: {integrity: sha512-kVwJELqiILQyG5aeuyKFbdsI1fmQy1Cmf7dQ8eGmVuJoaRVdwey7WaMknr2ZFeVSYSKT0rExsa8EGw0aoI/1QQ==} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - '@types/estree': 1.0.1 - acorn: 8.10.0 - estree-walker: 3.0.3 - periscopic: 3.1.0 - dev: true - - /codemirror@5.65.14: - resolution: {integrity: sha512-VSNugIBDGt0OU9gDjeVr6fNkoFQznrWEUdAApMlXQNbfE8gGO19776D6MwSqF/V/w/sDwonsQ0z7KmmI9guScg==} - dev: true - - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - dev: true - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /colord@2.9.3: - resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - dev: true - - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - - /confusing-browser-globals@1.0.11: - resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} - dev: true - - /cosmiconfig@8.2.0: - resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} - engines: {node: '>=14'} - dependencies: - import-fresh: 3.3.0 - js-yaml: 4.1.0 - parse-json: 5.2.0 - path-type: 4.0.0 - dev: true - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: true - - /css-functions-list@3.2.0: - resolution: {integrity: sha512-d/jBMPyYybkkLVypgtGv12R+pIFw4/f/IHtCTxWpZc8ofTYOPigIgmA6vu5rMHartZC+WuXhBUHfnyNUIQSYrg==} - engines: {node: '>=12.22'} - dev: true - - /css-tree@2.3.1: - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - dependencies: - mdn-data: 2.0.30 - source-map-js: 1.0.2 - dev: true - - /cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - dev: true - - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - - /decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} - dependencies: - decamelize: 1.2.0 - map-obj: 1.0.1 - dev: true - - /decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - dev: true - - /decamelize@5.0.1: - resolution: {integrity: sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==} - engines: {node: '>=10'} - dev: true - - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - - /deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - dev: true - - /define-properties@1.2.0: - resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} - engines: {node: '>= 0.4'} - dependencies: - has-property-descriptors: 1.0.0 - object-keys: 1.1.1 - dev: true - - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: true - - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true - - /doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - dev: true - - /domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: true - - /domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - dependencies: - domelementtype: 2.3.0 - dev: true - - /domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dev: true - - /electron-to-chromium@1.4.485: - resolution: {integrity: sha512-1ndQ5IBNEnFirPwvyud69GHL+31FkE09gH/CJ6m3KCbkx3i0EVOrjwz4UNxRmN9H8OVHbC6vMRZGN1yCvjSs9w==} - dev: true - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true - - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true - - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - dependencies: - is-arrayish: 0.2.1 - dev: true - - /es-abstract@1.22.1: - resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} - engines: {node: '>= 0.4'} - dependencies: - array-buffer-byte-length: 1.0.0 - arraybuffer.prototype.slice: 1.0.1 - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - es-set-tostringtag: 2.0.1 - es-to-primitive: 1.2.1 - function.prototype.name: 1.1.5 - get-intrinsic: 1.2.1 - get-symbol-description: 1.0.0 - globalthis: 1.0.3 - gopd: 1.0.1 - has: 1.0.3 - has-property-descriptors: 1.0.0 - has-proto: 1.0.1 - has-symbols: 1.0.3 - internal-slot: 1.0.5 - is-array-buffer: 3.0.2 - is-callable: 1.2.7 - is-negative-zero: 2.0.2 - is-regex: 1.1.4 - is-shared-array-buffer: 1.0.2 - is-string: 1.0.7 - is-typed-array: 1.1.12 - is-weakref: 1.0.2 - object-inspect: 1.12.3 - object-keys: 1.1.1 - object.assign: 4.1.4 - regexp.prototype.flags: 1.5.0 - safe-array-concat: 1.0.0 - safe-regex-test: 1.0.0 - string.prototype.trim: 1.2.7 - string.prototype.trimend: 1.0.6 - string.prototype.trimstart: 1.0.6 - typed-array-buffer: 1.0.0 - typed-array-byte-length: 1.0.0 - typed-array-byte-offset: 1.0.0 - typed-array-length: 1.0.4 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.11 - dev: true - - /es-set-tostringtag@2.0.1: - resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - has-tostringtag: 1.0.0 - dev: true - - /es-shim-unscopables@1.0.0: - resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} - dependencies: - has: 1.0.3 - dev: true - - /es-to-primitive@1.2.1: - resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} - engines: {node: '>= 0.4'} - dependencies: - is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.0.4 - dev: true - - /esbuild@0.18.17: - resolution: {integrity: sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.18.17 - '@esbuild/android-arm64': 0.18.17 - '@esbuild/android-x64': 0.18.17 - '@esbuild/darwin-arm64': 0.18.17 - '@esbuild/darwin-x64': 0.18.17 - '@esbuild/freebsd-arm64': 0.18.17 - '@esbuild/freebsd-x64': 0.18.17 - '@esbuild/linux-arm': 0.18.17 - '@esbuild/linux-arm64': 0.18.17 - '@esbuild/linux-ia32': 0.18.17 - '@esbuild/linux-loong64': 0.18.17 - '@esbuild/linux-mips64el': 0.18.17 - '@esbuild/linux-ppc64': 0.18.17 - '@esbuild/linux-riscv64': 0.18.17 - '@esbuild/linux-s390x': 0.18.17 - '@esbuild/linux-x64': 0.18.17 - '@esbuild/netbsd-x64': 0.18.17 - '@esbuild/openbsd-x64': 0.18.17 - '@esbuild/sunos-x64': 0.18.17 - '@esbuild/win32-arm64': 0.18.17 - '@esbuild/win32-ia32': 0.18.17 - '@esbuild/win32-x64': 0.18.17 - dev: true - - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true - - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: true - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.28.0)(eslint@8.46.0): - resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} - engines: {node: ^10.12.0 || >=12.0.0} - peerDependencies: - eslint: ^7.32.0 || ^8.2.0 - eslint-plugin-import: ^2.25.2 - dependencies: - confusing-browser-globals: 1.0.11 - eslint: 8.46.0 - eslint-plugin-import: 2.28.0(eslint@8.46.0) - object.assign: 4.1.4 - object.entries: 1.1.6 - semver: 6.3.1 - dev: true - - /eslint-import-resolver-node@0.3.8: - resolution: {integrity: sha512-tEe+Pok22qIGaK3KoMP+N96GVDS66B/zreoVVmiavLvRUEmGRtvb4B8wO9jwnb8d2lvHtrkhZ7UD73dWBVnf/Q==} - dependencies: - debug: 3.2.7 - is-core-module: 2.13.0 - resolve: 1.22.4 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-module-utils@2.8.0(eslint-import-resolver-node@0.3.8)(eslint@8.46.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - debug: 3.2.7 - eslint: 8.46.0 - eslint-import-resolver-node: 0.3.8 - transitivePeerDependencies: - - supports-color - dev: true - - /eslint-plugin-import@2.28.0(eslint@8.46.0): - resolution: {integrity: sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - dependencies: - array-includes: 3.1.6 - array.prototype.findlastindex: 1.2.2 - array.prototype.flat: 1.3.1 - array.prototype.flatmap: 1.3.1 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.46.0 - eslint-import-resolver-node: 0.3.8 - eslint-module-utils: 2.8.0(eslint-import-resolver-node@0.3.8)(eslint@8.46.0) - has: 1.0.3 - is-core-module: 2.13.0 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.6 - object.groupby: 1.0.0 - object.values: 1.1.6 - resolve: 1.22.4 - semver: 6.3.1 - tsconfig-paths: 3.14.2 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - dev: true - - /eslint-plugin-svelte@2.32.4(eslint@8.46.0)(svelte@4.1.2): - resolution: {integrity: sha512-VJ12i2Iogug1jvhwxSlognnfGj76P5gks/V4pUD4SCSVQOp14u47MNP0zAG8AQR3LT0Fi1iUvIFnY4l9z5Rwbg==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0-0 - svelte: ^3.37.0 || ^4.0.0 - peerDependenciesMeta: - svelte: - optional: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) - '@jridgewell/sourcemap-codec': 1.4.15 - debug: 4.3.4 - eslint: 8.46.0 - esutils: 2.0.3 - known-css-properties: 0.28.0 - postcss: 8.4.27 - postcss-load-config: 3.1.4(postcss@8.4.27) - postcss-safe-parser: 6.0.0(postcss@8.4.27) - postcss-selector-parser: 6.0.13 - semver: 7.5.4 - svelte: 4.1.2 - svelte-eslint-parser: 0.32.2(svelte@4.1.2) - transitivePeerDependencies: - - supports-color - - ts-node - dev: true - - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - - /eslint-visitor-keys@3.4.2: - resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /eslint@8.46.0: - resolution: {integrity: sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.46.0) - '@eslint-community/regexpp': 4.6.2 - '@eslint/eslintrc': 2.1.1 - '@eslint/js': 8.46.0 - '@humanwhocodes/config-array': 0.11.10 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.2 - espree: 9.6.1 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.20.0 - graphemer: 1.4.0 - ignore: 5.2.4 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.3 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.10.0 - acorn-jsx: 5.3.2(acorn@8.10.0) - eslint-visitor-keys: 3.4.2 - dev: true - - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true - - /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - dependencies: - '@types/estree': 1.0.1 - dev: true - - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true - - /fast-glob@3.3.1: - resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: true - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - - /fastest-levenshtein@1.0.16: - resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} - engines: {node: '>= 4.9.1'} - dev: true - - /fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} - dependencies: - reusify: 1.0.4 - dev: true - - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flat-cache: 3.0.4 - dev: true - - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - dev: true - - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - dev: true - - /for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - dependencies: - is-callable: 1.2.7 - dev: true - - /fraction.js@4.2.0: - resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} - dev: true - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /function-bind@1.1.1: - resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} - dev: true - - /function.prototype.name@1.1.5: - resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - functions-have-names: 1.2.3 - dev: true - - /functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - dev: true - - /get-intrinsic@1.2.1: - resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} - dependencies: - function-bind: 1.1.1 - has: 1.0.3 - has-proto: 1.0.1 - has-symbols: 1.0.3 - dev: true - - /get-symbol-description@1.0.0: - resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - - /global-modules@2.0.0: - resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} - engines: {node: '>=6'} - dependencies: - global-prefix: 3.0.0 - dev: true - - /global-prefix@3.0.0: - resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} - engines: {node: '>=6'} - dependencies: - ini: 1.3.8 - kind-of: 6.0.3 - which: 1.3.1 - dev: true - - /globals@13.20.0: - resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.0 - dev: true - - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.1 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - dev: true - - /globjoin@0.1.4: - resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} - dev: true - - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} - dependencies: - get-intrinsic: 1.2.1 - dev: true - - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true - - /hard-rejection@2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - dev: true - - /has-bigints@1.0.2: - resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - dev: true - - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /has-property-descriptors@1.0.0: - resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} - dependencies: - get-intrinsic: 1.2.1 - dev: true - - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} - dev: true - - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} - dev: true - - /has-tostringtag@1.0.0: - resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - - /has@1.0.3: - resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} - engines: {node: '>= 0.4.0'} - dependencies: - function-bind: 1.1.1 - dev: true - - /hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} - dependencies: - lru-cache: 6.0.0 - dev: true - - /html-tags@3.3.1: - resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} - engines: {node: '>=8'} - dev: true - - /htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - dev: true - - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - dev: true - - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true - - /import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /indent-string@5.0.0: - resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} - engines: {node: '>=12'} - dev: true - - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true - - /ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: true - - /internal-slot@1.0.5: - resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} - engines: {node: '>= 0.4'} - dependencies: - get-intrinsic: 1.2.1 - has: 1.0.3 - side-channel: 1.0.4 - dev: true - - /is-array-buffer@3.0.2: - resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-typed-array: 1.1.12 - dev: true - - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true - - /is-bigint@1.0.4: - resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} - dependencies: - has-bigints: 1.0.2 - dev: true - - /is-boolean-object@1.1.2: - resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: true - - /is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - dev: true - - /is-core-module@2.13.0: - resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} - dependencies: - has: 1.0.3 - dev: true - - /is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-negative-zero@2.0.2: - resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} - engines: {node: '>= 0.4'} - dev: true - - /is-number-object@1.0.7: - resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true - - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true - - /is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - dev: true - - /is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - dev: true - - /is-reference@3.0.1: - resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} - dependencies: - '@types/estree': 1.0.1 - dev: true - - /is-regex@1.1.4: - resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - has-tostringtag: 1.0.0 - dev: true - - /is-shared-array-buffer@1.0.2: - resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} - dependencies: - call-bind: 1.0.2 - dev: true - - /is-string@1.0.7: - resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} - engines: {node: '>= 0.4'} - dependencies: - has-tostringtag: 1.0.0 - dev: true - - /is-symbol@1.0.4: - resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} - engines: {node: '>= 0.4'} - dependencies: - has-symbols: 1.0.3 - dev: true - - /is-typed-array@1.1.12: - resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} - engines: {node: '>= 0.4'} - dependencies: - which-typed-array: 1.1.11 - dev: true - - /is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - dependencies: - call-bind: 1.0.2 - dev: true - - /isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - dev: true - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true - - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true - - /js-tokens@8.0.1: - resolution: {integrity: sha512-3AGrZT6tuMm1ZWWn9mLXh7XMfi2YtiLNPALCVxBCiUVq0LD1OQMxV/AdS/s7rLJU5o9i/jBZw/N4vXXL5dm29A==} - dev: true - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true - - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: true - - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - - /json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - dependencies: - minimist: 1.2.8 - dev: true - - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: true - - /kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - dev: true - - /known-css-properties@0.27.0: - resolution: {integrity: sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==} - dev: true - - /known-css-properties@0.28.0: - resolution: {integrity: sha512-9pSL5XB4J+ifHP0e0jmmC98OGC1nL8/JjS+fi6mnTlIf//yt/MfVLtKg7S6nCtj/8KTcWX7nRlY0XywoYY1ISQ==} - dev: true - - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - dev: true - - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true - - /locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true - - /lodash.truncate@4.4.2: - resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - dev: true - - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - - /magic-string@0.30.2: - resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - - /map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - dev: true - - /map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - dev: true - - /mathml-tag-names@2.1.3: - resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} - dev: true - - /mdn-data@2.0.30: - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - dev: true - - /meow@10.1.5: - resolution: {integrity: sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - '@types/minimist': 1.2.2 - camelcase-keys: 7.0.2 - decamelize: 5.0.1 - decamelize-keys: 1.1.1 - hard-rejection: 2.1.0 - minimist-options: 4.1.0 - normalize-package-data: 3.0.3 - read-pkg-up: 8.0.0 - redent: 4.0.0 - trim-newlines: 4.1.1 - type-fest: 1.4.0 - yargs-parser: 20.2.9 - dev: true - - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true - - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: true - - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - dev: true - - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /minimist-options@4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} - dependencies: - arrify: 1.0.1 - is-plain-obj: 1.1.0 - kind-of: 6.0.3 - dev: true - - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true - - /nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - - /node-releases@2.0.13: - resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} - dev: true - - /normalize-package-data@3.0.3: - resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} - engines: {node: '>=10'} - dependencies: - hosted-git-info: 4.1.0 - is-core-module: 2.13.0 - semver: 7.5.4 - validate-npm-package-license: 3.0.4 - dev: true - - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true - - /normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - dev: true - - /object-inspect@1.12.3: - resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} - dev: true - - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - dev: true - - /object.assign@4.1.4: - resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - has-symbols: 1.0.3 - object-keys: 1.1.1 - dev: true - - /object.entries@1.1.6: - resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - dev: true - - /object.fromentries@2.0.6: - resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - dev: true - - /object.groupby@1.0.0: - resolution: {integrity: sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - get-intrinsic: 1.2.1 - dev: true - - /object.values@1.1.6: - resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - dev: true - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: true - - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} - dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: true - - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - dependencies: - '@babel/code-frame': 7.22.5 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - dev: true - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true - - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true - - /periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - dependencies: - '@types/estree': 1.0.1 - estree-walker: 3.0.3 - is-reference: 3.0.1 - dev: true - - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /postcss-html@1.5.0: - resolution: {integrity: sha512-kCMRWJRHKicpA166kc2lAVUGxDZL324bkj/pVOb6RhjB0Z5Krl7mN0AsVkBhVIRZZirY0lyQXG38HCVaoKVNoA==} - engines: {node: ^12 || >=14} - dependencies: - htmlparser2: 8.0.2 - js-tokens: 8.0.1 - postcss: 8.4.27 - postcss-safe-parser: 6.0.0(postcss@8.4.27) - dev: true - - /postcss-load-config@3.1.4(postcss@8.4.27): - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - dependencies: - lilconfig: 2.1.0 - postcss: 8.4.27 - yaml: 1.10.2 - dev: true - - /postcss-resolve-nested-selector@0.1.1: - resolution: {integrity: sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==} - dev: true - - /postcss-safe-parser@6.0.0(postcss@8.4.27): - resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.3.3 - dependencies: - postcss: 8.4.27 - dev: true - - /postcss-scss@4.0.6(postcss@8.4.27): - resolution: {integrity: sha512-rLDPhJY4z/i4nVFZ27j9GqLxj1pwxE80eAzUNRMXtcpipFYIeowerzBgG3yJhMtObGEXidtIgbUpQ3eLDsf5OQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.4.19 - dependencies: - postcss: 8.4.27 - dev: true - - /postcss-selector-parser@6.0.13: - resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} - engines: {node: '>=4'} - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - dev: true - - /postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: true - - /postcss@8.4.27: - resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.6 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: true - - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - dev: true - - /read-pkg-up@8.0.0: - resolution: {integrity: sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ==} - engines: {node: '>=12'} - dependencies: - find-up: 5.0.0 - read-pkg: 6.0.0 - type-fest: 1.4.0 - dev: true - - /read-pkg@6.0.0: - resolution: {integrity: sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q==} - engines: {node: '>=12'} - dependencies: - '@types/normalize-package-data': 2.4.1 - normalize-package-data: 3.0.3 - parse-json: 5.2.0 - type-fest: 1.4.0 - dev: true - - /redent@4.0.0: - resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} - engines: {node: '>=12'} - dependencies: - indent-string: 5.0.0 - strip-indent: 4.0.0 - dev: true - - /regexp.prototype.flags@1.5.0: - resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - functions-have-names: 1.2.3 - dev: true - - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - dev: true - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true - - /resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - dev: true - - /resolve@1.22.4: - resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} - hasBin: true - dependencies: - is-core-module: 2.13.0 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: true - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - - /rollup@3.27.2: - resolution: {integrity: sha512-YGwmHf7h2oUHkVBT248x0yt6vZkYQ3/rvE5iQuVBh3WO8GcJ6BNeOkpoX1yMHIiBm18EMLjBPIoUDkhgnyxGOQ==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /safe-array-concat@1.0.0: - resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} - engines: {node: '>=0.4'} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - has-symbols: 1.0.3 - isarray: 2.0.5 - dev: true - - /safe-regex-test@1.0.0: - resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-regex: 1.1.4 - dev: true - - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true - - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true - - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - object-inspect: 1.12.3 - dev: true - - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true - - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true - - /slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - dev: true - - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - dev: true - - /spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} - dependencies: - spdx-expression-parse: 3.0.1 - spdx-license-ids: 3.0.13 - dev: true - - /spdx-exceptions@2.3.0: - resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - dev: true - - /spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} - dependencies: - spdx-exceptions: 2.3.0 - spdx-license-ids: 3.0.13 - dev: true - - /spdx-license-ids@3.0.13: - resolution: {integrity: sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==} - dev: true - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: true - - /string.prototype.trim@1.2.7: - resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - dev: true - - /string.prototype.trimend@1.0.6: - resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - dev: true - - /string.prototype.trimstart@1.0.6: - resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} - dependencies: - call-bind: 1.0.2 - define-properties: 1.2.0 - es-abstract: 1.22.1 - dev: true - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - dev: true - - /strip-indent@4.0.0: - resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} - engines: {node: '>=12'} - dependencies: - min-indent: 1.0.1 - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - - /style-search@0.1.0: - resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} - dev: true - - /stylelint-config-html@1.1.0(postcss-html@1.5.0)(stylelint@15.10.2): - resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==} - engines: {node: ^12 || >=14} - peerDependencies: - postcss-html: ^1.0.0 - stylelint: '>=14.0.0' - dependencies: - postcss-html: 1.5.0 - stylelint: 15.10.2 - dev: true - - /stylelint-config-recommended@13.0.0(stylelint@15.10.2): - resolution: {integrity: sha512-EH+yRj6h3GAe/fRiyaoO2F9l9Tgg50AOFhaszyfov9v6ayXJ1IkSHwTxd7lB48FmOeSGDPLjatjO11fJpmarkQ==} - engines: {node: ^14.13.1 || >=16.0.0} - peerDependencies: - stylelint: ^15.10.0 - dependencies: - stylelint: 15.10.2 - dev: true - - /stylelint-config-standard@34.0.0(stylelint@15.10.2): - resolution: {integrity: sha512-u0VSZnVyW9VSryBG2LSO+OQTjN7zF9XJaAJRX/4EwkmU0R2jYwmBSN10acqZisDitS0CLiEiGjX7+Hrq8TAhfQ==} - engines: {node: ^14.13.1 || >=16.0.0} - peerDependencies: - stylelint: ^15.10.0 - dependencies: - stylelint: 15.10.2 - stylelint-config-recommended: 13.0.0(stylelint@15.10.2) - dev: true - - /stylelint@15.10.2: - resolution: {integrity: sha512-UxqSb3hB74g4DTO45QhUHkJMjKKU//lNUAOWyvPBVPZbCknJ5HjOWWZo+UDuhHa9FLeVdHBZXxu43eXkjyIPWg==} - engines: {node: ^14.13.1 || >=16.0.0} - hasBin: true - dependencies: - '@csstools/css-parser-algorithms': 2.3.1(@csstools/css-tokenizer@2.2.0) - '@csstools/css-tokenizer': 2.2.0 - '@csstools/media-query-list-parser': 2.1.4(@csstools/css-parser-algorithms@2.3.1)(@csstools/css-tokenizer@2.2.0) - '@csstools/selector-specificity': 3.0.0(postcss-selector-parser@6.0.13) - balanced-match: 2.0.0 - colord: 2.9.3 - cosmiconfig: 8.2.0 - css-functions-list: 3.2.0 - css-tree: 2.3.1 - debug: 4.3.4 - fast-glob: 3.3.1 - fastest-levenshtein: 1.0.16 - file-entry-cache: 6.0.1 - global-modules: 2.0.0 - globby: 11.1.0 - globjoin: 0.1.4 - html-tags: 3.3.1 - ignore: 5.2.4 - import-lazy: 4.0.0 - imurmurhash: 0.1.4 - is-plain-object: 5.0.0 - known-css-properties: 0.27.0 - mathml-tag-names: 2.1.3 - meow: 10.1.5 - micromatch: 4.0.5 - normalize-path: 3.0.0 - picocolors: 1.0.0 - postcss: 8.4.27 - postcss-resolve-nested-selector: 0.1.1 - postcss-safe-parser: 6.0.0(postcss@8.4.27) - postcss-selector-parser: 6.0.13 - postcss-value-parser: 4.2.0 - resolve-from: 5.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - style-search: 0.1.0 - supports-hyperlinks: 3.0.0 - svg-tags: 1.0.0 - table: 6.8.1 - write-file-atomic: 5.0.1 - transitivePeerDependencies: - - supports-color - dev: true - - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-hyperlinks@3.0.0: - resolution: {integrity: sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==} - engines: {node: '>=14.18'} - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - dev: true - - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true - - /svelte-eslint-parser@0.32.2(svelte@4.1.2): - resolution: {integrity: sha512-Ok9D3A4b23iLQsONrjqtXtYDu5ZZ/826Blaw2LeFZVTg1pwofKDG4mz3/GYTax8fQ0plRGHI6j+d9VQYy5Lo/A==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 - peerDependenciesMeta: - svelte: - optional: true - dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.2 - espree: 9.6.1 - postcss: 8.4.27 - postcss-scss: 4.0.6(postcss@8.4.27) - svelte: 4.1.2 - dev: true - - /svelte-hmr@0.15.3(svelte@4.1.2): - resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} - engines: {node: ^12.20 || ^14.13.1 || >= 16} - peerDependencies: - svelte: ^3.19.0 || ^4.0.0 - dependencies: - svelte: 4.1.2 - dev: true - - /svelte@4.1.2: - resolution: {integrity: sha512-/evA8U6CgOHe5ZD1C1W3va9iJG7mWflcCdghBORJaAhD2JzrVERJty/2gl0pIPrJYBGZwZycH6onYf+64XXF9g==} - engines: {node: '>=16'} - dependencies: - '@ampproject/remapping': 2.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.18 - acorn: 8.10.0 - aria-query: 5.3.0 - axobject-query: 3.2.1 - code-red: 1.0.3 - css-tree: 2.3.1 - estree-walker: 3.0.3 - is-reference: 3.0.1 - locate-character: 3.0.0 - magic-string: 0.30.2 - periscopic: 3.1.0 - dev: true - - /svg-tags@1.0.0: - resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} - dev: true - - /table@6.8.1: - resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} - engines: {node: '>=10.0.0'} - dependencies: - ajv: 8.12.0 - lodash.truncate: 4.4.2 - slice-ansi: 4.0.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: true - - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /trim-newlines@4.1.1: - resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} - engines: {node: '>=12'} - dev: true - - /tsconfig-paths@3.14.2: - resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - dev: true - - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - - /type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - dev: true - - /typed-array-buffer@1.0.0: - resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - get-intrinsic: 1.2.1 - is-typed-array: 1.1.12 - dev: true - - /typed-array-byte-length@1.0.0: - resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} - engines: {node: '>= 0.4'} - dependencies: - call-bind: 1.0.2 - for-each: 0.3.3 - has-proto: 1.0.1 - is-typed-array: 1.1.12 - dev: true - - /typed-array-byte-offset@1.0.0: - resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - has-proto: 1.0.1 - is-typed-array: 1.1.12 - dev: true - - /typed-array-length@1.0.4: - resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} - dependencies: - call-bind: 1.0.2 - for-each: 0.3.3 - is-typed-array: 1.1.12 - dev: true - - /unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - dependencies: - call-bind: 1.0.2 - has-bigints: 1.0.2 - has-symbols: 1.0.3 - which-boxed-primitive: 1.0.2 - dev: true - - /update-browserslist-db@1.0.11(browserslist@4.21.10): - resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.21.10 - escalade: 3.1.1 - picocolors: 1.0.0 - dev: true - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.0 - dev: true - - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: true - - /validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - dependencies: - spdx-correct: 3.2.0 - spdx-expression-parse: 3.0.1 - dev: true - - /vite@4.4.8: - resolution: {integrity: sha512-LONawOUUjxQridNWGQlNizfKH89qPigK36XhMI7COMGztz8KNY0JHim7/xDd71CZwGT4HtSRgI7Hy+RlhG0Gvg==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - esbuild: 0.18.17 - postcss: 8.4.27 - rollup: 3.27.2 - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /vitefu@0.2.4(vite@4.4.8): - resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 - peerDependenciesMeta: - vite: - optional: true - dependencies: - vite: 4.4.8 - dev: true - - /which-boxed-primitive@1.0.2: - resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} - dependencies: - is-bigint: 1.0.4 - is-boolean-object: 1.1.2 - is-number-object: 1.0.7 - is-string: 1.0.7 - is-symbol: 1.0.4 - dev: true - - /which-typed-array@1.1.11: - resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} - engines: {node: '>= 0.4'} - dependencies: - available-typed-arrays: 1.0.5 - call-bind: 1.0.2 - for-each: 0.3.3 - gopd: 1.0.1 - has-tostringtag: 1.0.0 - dev: true - - /which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - - /write-file-atomic@5.0.1: - resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - imurmurhash: 0.1.4 - signal-exit: 4.1.0 - dev: true - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - - /yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - dev: true - - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: true - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true diff --git a/xcode/Safari-Extension/Resources/images/icon-128.png b/public/ext/safari-15/images/icon-128.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-128.png rename to public/ext/safari-15/images/icon-128.png diff --git a/xcode/Safari-Extension/Resources/images/icon-256.png b/public/ext/safari-15/images/icon-256.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-256.png rename to public/ext/safari-15/images/icon-256.png diff --git a/xcode/Safari-Extension/Resources/images/icon-48.png b/public/ext/safari-15/images/icon-48.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-48.png rename to public/ext/safari-15/images/icon-48.png diff --git a/xcode/Safari-Extension/Resources/images/icon-512.png b/public/ext/safari-15/images/icon-512.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-512.png rename to public/ext/safari-15/images/icon-512.png diff --git a/xcode/Safari-Extension/Resources/images/icon-96.png b/public/ext/safari-15/images/icon-96.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/icon-96.png rename to public/ext/safari-15/images/icon-96.png diff --git a/xcode/Safari-Extension/Resources/images/toolbar-icon-16.png b/public/ext/safari-15/images/toolbar-icon-16.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/toolbar-icon-16.png rename to public/ext/safari-15/images/toolbar-icon-16.png diff --git a/xcode/Safari-Extension/Resources/images/toolbar-icon-32.png b/public/ext/safari-15/images/toolbar-icon-32.png similarity index 100% rename from xcode/Safari-Extension/Resources/images/toolbar-icon-32.png rename to public/ext/safari-15/images/toolbar-icon-32.png diff --git a/public/ext/safari-15/manifest.json b/public/ext/safari-15/manifest.json new file mode 100644 index 00000000..99c503ee --- /dev/null +++ b/public/ext/safari-15/manifest.json @@ -0,0 +1,63 @@ +{ + "manifest_version": 2, + "default_locale": "en", + "name": "__MSG_extension_name__", + "description": "__MSG_extension_description__", + "version": "_VERSION_", + "icons": { + "48": "images/icon-48.png", + "96": "images/icon-96.png", + "128": "images/icon-128.png", + "256": "images/icon-256.png", + "512": "images/icon-512.png" + }, + "background": { + "scripts": ["dist/background.js"], + "persistent": false + }, + "browser_action": { + "default_popup": "dist/entry-ext-action-popup.html", + "default_icon": { + "16": "images/toolbar-icon-16.png", + "32": "images/toolbar-icon-32.png" + } + }, + "content_scripts": [ + { + "js": ["dist/content-scripts/userscripts.js"], + "matches": [""], + "run_at": "document_start", + "all_frames": true + }, + { + "js": ["dist/content-scripts/dot-user-js.js"], + "matches": [ + "*://*/*.user.js", + "*://*/*.user.js?*", + "*://*/*.user.css", + "*://*/*.user.css?*" + ], + "run_at": "document_start", + "all_frames": false + }, + { + "js": ["dist/content-scripts/greasyfork.js"], + "matches": ["*://*.greasyfork.org/*"], + "run_at": "document_start", + "all_frames": false + } + ], + "permissions": [ + "", + "clipboardWrite", + "contextMenus", + "declarativeNetRequest", + "declarativeNetRequestWithHostAccess", + "menus", + "nativeMessaging", + "storage", + "tabs", + "unlimitedStorage", + "webNavigation" + ] +} diff --git a/public/ext/safari-16.4/images/128.svg b/public/ext/safari-16.4/images/128.svg new file mode 100644 index 00000000..c6ec62ad --- /dev/null +++ b/public/ext/safari-16.4/images/128.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ext/safari-16.4/images/action.svg b/public/ext/safari-16.4/images/action.svg new file mode 100644 index 00000000..ad16f09d --- /dev/null +++ b/public/ext/safari-16.4/images/action.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ext/safari-16.4/manifest.json b/public/ext/safari-16.4/manifest.json new file mode 100644 index 00000000..7d2c00da --- /dev/null +++ b/public/ext/safari-16.4/manifest.json @@ -0,0 +1,48 @@ +{ + "manifest_version": 2, + "default_locale": "en", + "name": "__MSG_extension_name__", + "description": "__MSG_extension_description__", + "version": "_VERSION_", + "icons": { + "48": "images/128.svg", + "96": "images/128.svg", + "128": "images/128.svg", + "256": "images/128.svg", + "512": "images/128.svg" + }, + "background": { + "scripts": ["dist/background.js"], + "persistent": false + }, + "browser_action": { + "default_popup": "dist/entry-ext-action-popup.html", + "default_icon": "images/action.svg" + }, + "content_scripts": [ + { + "js": ["dist/content-scripts/userscripts.js"], + "matches": [""], + "run_at": "document_start", + "all_frames": true + } + ], + "permissions": [ + "", + "clipboardWrite", + "contextMenus", + "declarativeNetRequestWithHostAccess", + "menus", + "nativeMessaging", + "scripting", + "storage", + "tabs", + "unlimitedStorage", + "webNavigation" + ], + "browser_specific_settings": { + "safari": { + "strict_min_version": "16.4" + } + } +} diff --git a/public/ext/safari-dev/manifest-ios.json b/public/ext/safari-dev/manifest-ios.json new file mode 100644 index 00000000..2cb0308a --- /dev/null +++ b/public/ext/safari-dev/manifest-ios.json @@ -0,0 +1,52 @@ +{ + "manifest_version": 2, + "default_locale": "en", + "name": "__MSG_extension_name__", + "description": "__MSG_extension_description__", + "version": "_VERSION_", + "icons": { + "48": "images/128.svg", + "96": "images/128.svg", + "128": "images/128.svg", + "256": "images/128.svg", + "512": "images/128.svg" + }, + "background": { + "page": "dist/entry-ext-background.html", + "persistent": false + }, + "browser_action": { + "default_popup": "dist/entry-ext-action-popup.html", + "default_icon": "images/action.svg" + }, + "options_ui": { + "page": "dist/entry-ext-extension-page.html#settings" + }, + "content_scripts": [ + { + "js": ["dist/content-scripts/userscripts.js"], + "matches": [""], + "run_at": "document_start", + "all_frames": true + } + ], + "permissions": [ + "", + "clipboardWrite", + "contextMenus", + "declarativeNetRequestWithHostAccess", + "menus", + "nativeMessaging", + "scripting", + "storage", + "tabs", + "unlimitedStorage", + "webNavigation" + ], + "browser_specific_settings": { + "safari": { + "strict_min_version": "16.4" + } + }, + "content_security_policy": "script-src 'self' https://localhost:55173 https://userscripts.test:55173" +} diff --git a/public/ext/safari-dev/manifest-mac.json b/public/ext/safari-dev/manifest-mac.json new file mode 100644 index 00000000..046f8239 --- /dev/null +++ b/public/ext/safari-dev/manifest-mac.json @@ -0,0 +1,52 @@ +{ + "manifest_version": 2, + "default_locale": "en", + "name": "__MSG_extension_name__", + "description": "__MSG_extension_description__", + "version": "_VERSION_", + "icons": { + "48": "images/128.svg", + "96": "images/128.svg", + "128": "images/128.svg", + "256": "images/128.svg", + "512": "images/128.svg" + }, + "background": { + "page": "dist/entry-ext-background.html", + "persistent": true + }, + "browser_action": { + "default_popup": "dist/entry-ext-action-popup.html", + "default_icon": "images/action.svg" + }, + "options_ui": { + "page": "dist/entry-ext-extension-page.html#settings" + }, + "content_scripts": [ + { + "js": ["dist/content-scripts/userscripts.js"], + "matches": [""], + "run_at": "document_start", + "all_frames": true + } + ], + "permissions": [ + "", + "clipboardWrite", + "contextMenus", + "declarativeNetRequestWithHostAccess", + "menus", + "nativeMessaging", + "scripting", + "storage", + "tabs", + "unlimitedStorage", + "webNavigation" + ], + "browser_specific_settings": { + "safari": { + "strict_min_version": "16.4" + } + }, + "content_security_policy": "script-src 'self' https://localhost:55173 https://userscripts.test:55173" +} diff --git a/public/ext/shared-dev/images/128.svg b/public/ext/shared-dev/images/128.svg new file mode 100644 index 00000000..76189438 --- /dev/null +++ b/public/ext/shared-dev/images/128.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ext/shared-dev/images/action.svg b/public/ext/shared-dev/images/action.svg new file mode 100644 index 00000000..3789dd89 --- /dev/null +++ b/public/ext/shared-dev/images/action.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/ext/shared/_locales/en/messages.json b/public/ext/shared/_locales/en/messages.json new file mode 100644 index 00000000..e840a3a2 --- /dev/null +++ b/public/ext/shared/_locales/en/messages.json @@ -0,0 +1,10 @@ +{ + "extension_name": { + "message": "_NAME_", + "description": "The display name for the extension." + }, + "extension_description": { + "message": "Save and run javascript for the web pages you visit", + "description": "Description of what the extension does." + } +} diff --git a/public/page/jshint.min.js b/public/ext/vendor/jshint.min.js similarity index 100% rename from public/page/jshint.min.js rename to public/ext/vendor/jshint.min.js diff --git a/scripts/build-app.js b/scripts/build-app.js new file mode 100644 index 00000000..f4713f70 --- /dev/null +++ b/scripts/build-app.js @@ -0,0 +1,37 @@ +/** + * @file Build App WebView resources using the Vite JavaScript API + * @see {@link https://vitejs.dev/guide/api-javascript.html JavaScript API} + * + * All build processes start at the same time due to asynchronous calls + * The assets name is irrelevant, just need to determine the entry path + */ + +import { build } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +/** + * Define default vite config options + * Disable auto resolving {@link vite.config.js} + * @see {@link https://vitejs.dev/config/ Config} + * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig configFile} + * @type {import("vite").InlineConfig} + */ +const defineConfig = { + base: "./", + configFile: false, +}; + +/** + * Build App-Shared WebView resources to xcode dist + */ +build({ + ...defineConfig, + plugins: [svelte()], + build: { + outDir: "xcode/App-Shared/Resources/dist/", + copyPublicDir: false, + rollupOptions: { + input: "entry-app-webview.html", + }, + }, +}); diff --git a/scripts/build-ext-safari-15.js b/scripts/build-ext-safari-15.js new file mode 100644 index 00000000..3242f700 --- /dev/null +++ b/scripts/build-ext-safari-15.js @@ -0,0 +1,98 @@ +/** + * @file Build Safari extension resources using the Vite JavaScript API + * @see {@link https://vitejs.dev/guide/api-javascript.html JavaScript API} + * + * Safari supports for modules in background since 16.4 + * @see {@link https://developer.apple.com/documentation/safari-release-notes/safari-16_4-release-notes#Safari-Extensions} + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background#browser_compatibility} + * To ensure forward compatibility, background script use independent builds in v4 + * + * Content scripts not support import modules, and due to their privileges and the + * speed of injecting user scripts, use a independent build currently + * + * All build processes start at the same time due to asynchronous calls + * The assets name is irrelevant, just need to determine the entry path + */ + +import { build } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { cp, emptyBuildDir, rootDir, SAFARI_EXT_RESOURCES } from "./utils.js"; + +/** + * Define default vite config options + * Disable auto resolving {@link vite.config.js} + * @see {@link https://vitejs.dev/config/ Config} + * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig InlineConfig} + * @type {import("vite").InlineConfig} + */ +const defineConfig = { + configFile: false, + envFile: false, + root: await rootDir(), + base: "./", + define: { + "import.meta.env.BROWSER": JSON.stringify("safari"), + "import.meta.env.NATIVE_APP": JSON.stringify("app"), + }, +}; + +/** + * Empty resources directory + * Copy public static assets + */ +await emptyBuildDir(SAFARI_EXT_RESOURCES); +cp("public/ext/shared", SAFARI_EXT_RESOURCES); +cp("public/ext/safari-15", SAFARI_EXT_RESOURCES); + +/** Build content scripts */ +[ + { userscripts: "src/ext/content-scripts/entry-userscripts.js" }, + { "dot-user-js": "src/ext/content-scripts/entry-dot-user-js.js" }, + { greasyfork: "src/ext/content-scripts/entry-greasyfork.js" }, +].forEach((input) => { + build({ + ...defineConfig, + build: { + outDir: `${SAFARI_EXT_RESOURCES}/dist/content-scripts/`, + emptyOutDir: false, + copyPublicDir: false, + rollupOptions: { + input, + output: { entryFileNames: "[name].js" }, + }, + }, + }); +}); + +/** Build background scripts */ +build({ + ...defineConfig, + build: { + outDir: `${SAFARI_EXT_RESOURCES}/dist/`, + emptyOutDir: false, + copyPublicDir: false, + rollupOptions: { + input: { background: "src/ext/background/main.js" }, + output: { entryFileNames: "[name].js" }, + }, + }, +}); + +/** Build shared modules */ +/** @type {import("rollup").InputOption} */ +let input = ["entry-ext-action-popup.html", "entry-ext-extension-page.html"]; +if (process.env.SAFARI_PLATFORM === "ios") { + input = ["entry-ext-action-popup.html"]; +} +build({ + ...defineConfig, + plugins: [svelte()], + publicDir: "public/ext/vendor/", + build: { + outDir: `${SAFARI_EXT_RESOURCES}/dist/`, + emptyOutDir: false, + rollupOptions: { + input, + }, + }, +}); diff --git a/scripts/build-ext-safari-16.4.js b/scripts/build-ext-safari-16.4.js new file mode 100644 index 00000000..12a67481 --- /dev/null +++ b/scripts/build-ext-safari-16.4.js @@ -0,0 +1,106 @@ +/** + * @file Build Safari extension resources using the Vite JavaScript API + * @see {@link https://vitejs.dev/guide/api-javascript.html JavaScript API} + * + * Safari supports for modules in background since 16.4 + * @see {@link https://developer.apple.com/documentation/safari-release-notes/safari-16_4-release-notes#Safari-Extensions} + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background#browser_compatibility} + * + * Content scripts not support import modules, and due to their privileges and the + * speed of injecting user scripts, use a independent build currently + * + * All build processes start at the same time due to asynchronous calls + * The assets name is irrelevant, just need to determine the entry path + */ + +import { build } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { cp, emptyBuildDir, rootDir, SAFARI_EXT_RESOURCES } from "./utils.js"; + +/** + * Define default vite config options + * Disable auto resolving {@link vite.config.js} + * @see {@link https://vitejs.dev/config/ Config} + * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig configFile} + * @type {import("vite").InlineConfig} + */ +const defineConfig = { + configFile: false, + envFile: false, + root: await rootDir(), + base: "./", + define: { + "import.meta.env.BROWSER": JSON.stringify("safari"), + "import.meta.env.NATIVE_APP": JSON.stringify("app"), + }, +}; + +/** + * Empty resources directory + * Copy public static assets + */ +await emptyBuildDir(SAFARI_EXT_RESOURCES); +cp("public/ext/shared", SAFARI_EXT_RESOURCES); +cp("public/ext/safari-16.4", SAFARI_EXT_RESOURCES); + +/** Build content scripts */ +[ + { userscripts: "src/ext/content-scripts/entry-userscripts.js" }, + { "dot-user-js": "src/ext/content-scripts/entry-dot-user-js.js" }, + { greasyfork: "src/ext/content-scripts/entry-greasyfork.js" }, +].forEach((input) => { + build({ + ...defineConfig, + build: { + outDir: `${SAFARI_EXT_RESOURCES}/dist/content-scripts/`, + emptyOutDir: false, + copyPublicDir: false, + rollupOptions: { + input, + output: { entryFileNames: "[name].js" }, + }, + }, + }); +}); + +/** + * Build background scripts + * Modular background may not load correctly on Safari startup + * Currently build classic script separately to avoid this error + */ +build({ + ...defineConfig, + build: { + outDir: `${SAFARI_EXT_RESOURCES}/dist/`, + emptyOutDir: false, + copyPublicDir: false, + rollupOptions: { + input: { background: "src/ext/background/main.js" }, + output: { entryFileNames: "[name].js" }, + }, + }, +}); + +/** Build shared modules */ +/** @type {import("rollup").InputOption} */ +let input = { + // background: "src/ext/background/main.js", + "action-popup": "entry-ext-action-popup.html", + "extension-page": "entry-ext-extension-page.html", +}; +if (process.env.SAFARI_PLATFORM === "ios") { + delete input["extension-page"]; +} +build({ + ...defineConfig, + plugins: [svelte()], + publicDir: "public/ext/vendor/", + build: { + outDir: `${SAFARI_EXT_RESOURCES}/dist/`, + emptyOutDir: false, + rollupOptions: { + input, + output: { entryFileNames: "[name].js" }, + }, + }, +}); diff --git a/scripts/dev-ext-safari.js b/scripts/dev-ext-safari.js new file mode 100644 index 00000000..2b4e7c25 --- /dev/null +++ b/scripts/dev-ext-safari.js @@ -0,0 +1,220 @@ +/** + * @file Develop Safari extension resources using the Vite JavaScript API + * @see {@link https://vitejs.dev/guide/api-javascript.html JavaScript API} + * This development build and server requires a valid https certificate to support remote real-time development + * Typically using a self-signed certificate, it needs to be installed and trusted by the device or simulator + * @see {@link https://developer.apple.com/library/archive/qa/qa1948/} + * @see {@link https://developer.apple.com/library/archive/technotes/tn2326/} + */ + +import { build, createServer } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { + cp, + emptyBuildDir, + sharedServerOptions, + rootDir, + SAFARI_EXT_RESOURCES, +} from "./utils.js"; +import https from "node:https"; + +/** + * Define default vite config options + * Disable auto resolving {@link vite.config.js} + * @see {@link https://vitejs.dev/config/ Config} + * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig configFile} + * @type {import("vite").InlineConfig} + */ +const defineConfig = { + configFile: false, + envFile: false, + root: await rootDir(), + base: "./", + define: { + "import.meta.env.BROWSER": JSON.stringify("safari"), + "import.meta.env.NATIVE_APP": JSON.stringify("app"), + }, +}; + +/** + * Build resources for remote real-time development + * @param {import("vite").ViteDevServer} server + * @param {string} origin + */ +async function buildResources(server, origin) { + /** + * empty resources directory + * copy public static assets + */ + await emptyBuildDir("dist"); + await emptyBuildDir(SAFARI_EXT_RESOURCES); + cp("public/ext/shared", SAFARI_EXT_RESOURCES); + cp("public/ext/shared-dev", SAFARI_EXT_RESOURCES); + if (process.env.SAFARI_PLATFORM === "ios") { + cp( + "public/ext/safari-dev/manifest-ios.json", + `${SAFARI_EXT_RESOURCES}/manifest.json`, + ); + } else { + cp( + "public/ext/safari-dev/manifest-mac.json", + `${SAFARI_EXT_RESOURCES}/manifest.json`, + ); + } + + /** build content scripts */ + [ + { userscripts: "src/ext/content-scripts/entry-userscripts.js" }, + { "dot-user-js": "src/ext/content-scripts/entry-dot-user-js.js" }, + { greasyfork: "src/ext/content-scripts/entry-greasyfork.js" }, + ].forEach((input) => { + /** build proxy content scripts replace actual code */ + build({ + ...defineConfig, + plugins: [ + { + name: "generate-content-proxy", + load(id) { + const name = id.replace(/.+entry-/, ""); + const url = `${origin}/dist/content-scripts/${name}`; + return `// proxy content + const xhr = new XMLHttpRequest(); + xhr.open("GET", "${url}", false); + xhr.send(); + const code = xhr.responseText; + try { + Function(code + "//# sourceURL=proxy-${name}")(); + } catch (error) { + console.error(error); + }`; + }, + }, + ], + build: { + outDir: `${SAFARI_EXT_RESOURCES}/dist/content-scripts/`, + emptyOutDir: false, + copyPublicDir: false, + rollupOptions: { + input, + output: { entryFileNames: "[name].js" }, + }, + }, + }); + /** build content scripts for dev server and watch changes */ + build({ + ...defineConfig, + build: { + outDir: `dist/content-scripts/`, + emptyOutDir: false, + copyPublicDir: false, + rollupOptions: { + input, + output: { entryFileNames: "[name].js" }, + }, + watch: {}, + minify: false, + }, + }); + }); + + /** generate entrance dist */ + build({ + ...defineConfig, + publicDir: "public/ext/vendor/", + plugins: [ + /** + * @see {@link https://github.com/vitejs/vite/issues/14263} + * Dev only requires entrances, so order `pre` without transform modules + */ + { + name: "generate-dev-entrance", + transformIndexHtml: { + order: "pre", + async handler(html, ctx) { + const str = await server.transformIndexHtml(ctx.path, html); + return str.replaceAll(`src="/`, `src="${origin}/`); + }, + }, + }, + ], + build: { + outDir: `${SAFARI_EXT_RESOURCES}/dist/`, + emptyOutDir: false, + rollupOptions: { + input: { + background: "entry-ext-background.html", + "action-popup": "entry-ext-action-popup.html", + "extension-page": "entry-ext-extension-page.html", + }, + }, + }, + }); +} + +/** + * Define shared constants + * Developing in native machine and simulator can just use `https://localhost:port/` as the origin + * But using a fixed domain name on non-native devices such as real iOS devices is a better choice + * The local domain name needs to be resolved to the LAN IP address on LAN DNS or the target device + * When the origin specified here is unreachable, localhost will be automatically used as the alternate + * The origin requires as an exemption to be added to the `manifest.json` due to default CSP limitations + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_Security_Policy} + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_security_policy} + */ +const serverOptions = await sharedServerOptions(); +const SERVER_PORT = 55173; +const SERVER_ORIGIN = `https://userscripts.test:${SERVER_PORT}`; + +/** + * Check if the server is reachable with self-signed certificate + * @param {string | URL} url + * @returns {Promise} + */ +async function serverCheck(url) { + const options = { + method: "HEAD", + ca: serverOptions.ca, + headers: { Accept: "*/*" }, + }; + return new Promise((resolve) => { + const req = https.request(url, options, (res) => { + if (res.headers.server === SERVER_ORIGIN) { + resolve(true); + } + resolve(false); + }); + req.on("error", (e) => { + console.error(`${req.host}: ${e.message}`); + resolve(false); + }); + req.end(); + }); +} + +/** main process */ +(async () => { + /** run development server */ + const server = await createServer({ + ...defineConfig, + plugins: [svelte()], + server: { + host: true, + port: SERVER_PORT, + strictPort: true, + https: { key: serverOptions.key, cert: serverOptions.cert }, + headers: { server: SERVER_ORIGIN }, + }, + }); + await server.listen(); + server.printUrls(); + + // Check available origins and build resources + for (let url of [SERVER_ORIGIN, ...server.resolvedUrls.local]) { + if (await serverCheck(url)) { + const origin = url.at(-1) === "/" ? url.slice(0, -1) : url; + console.info(`build with origin: ${origin}`); + buildResources(server, origin); + break; + } + } +})(); diff --git a/scripts/jsconfig.json b/scripts/jsconfig.json new file mode 100644 index 00000000..f0633242 --- /dev/null +++ b/scripts/jsconfig.json @@ -0,0 +1,26 @@ +// https://code.visualstudio.com/docs/languages/jsconfig +// https://www.typescriptlang.org/docs/handbook/tsconfig-json.html +// https://www.typescriptlang.org/tsconfig + +// https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html +// https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html + +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "nodenext", + + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["**/*.d.ts", "**/*.js"] +} diff --git a/scripts/preview-app.js b/scripts/preview-app.js new file mode 100644 index 00000000..d9060d8d --- /dev/null +++ b/scripts/preview-app.js @@ -0,0 +1,35 @@ +/** + * @file Preview App WebView resources using the Vite JavaScript API + * @see {@link https://vitejs.dev/guide/api-javascript.html JavaScript API} + */ + +import { preview } from "vite"; + +/** + * Define default vite config options + * Disable auto resolving {@link vite.config.js} + * @see {@link https://vitejs.dev/config/ Config} + * @see {@link https://vitejs.dev/guide/api-javascript.html#inlineconfig configFile} + * @type {import("vite").InlineConfig} + */ +const defineConfig = { + base: "./", + configFile: false, +}; + +/** + * Preview App-Shared WebView resources from xcode dist + */ +(async () => { + const previewServer = await preview({ + ...defineConfig, + preview: { + // port: 4173, + open: "entry-app-webview.html", + }, + build: { + outDir: "xcode/App-Shared/Resources/dist/", + }, + }); + previewServer.printUrls(); +})(); diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 00000000..c80fb51b --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,92 @@ +import { chdir, cwd } from "node:process"; +import { fileURLToPath } from "node:url"; +import { + copyFile, + mkdir, + readdir, + readFile, + realpath, + rm, + stat, +} from "node:fs/promises"; + +/** Define shared constants */ +export const SAFARI_APP_RESOURCES = "xcode/App-Shared/Resources"; +export const SAFARI_EXT_RESOURCES = "xcode/Ext-Safari/Resources"; + +/** + * If not then cd to root dir and returns the path + * @returns {Promise} project root directory + */ +export async function rootDir() { + const root = fileURLToPath(new URL("..", import.meta.url)); + if (cwd() !== (await realpath(root))) { + chdir(root); + console.info("cd:", root); + } + return root; +} + +/** + * Empty the build directory safely + * @param {string} dir + * @returns {Promise} + */ +export async function emptyBuildDir(dir) { + const buildPaths = ["dist", SAFARI_APP_RESOURCES, SAFARI_EXT_RESOURCES]; + if (!buildPaths.includes(dir)) { + console.error("Non-build path, cannot be empty."); + return false; + } + const root = await rootDir(); + try { + for (const sub of await readdir(dir)) { + const path = `${root}/${dir}/${sub}`; + // console.log("rm:", path); // DEBUG + await rm(path, { force: true, recursive: true }); + } + return true; + } catch (error) { + console.error("emptyBuildDir:", error); + return false; + } +} + +/** + * Copy a file or folder recursively + * @param {string} src Source file or directory + * @param {string} dest Destination file or directory + */ +export async function cp(src, dest) { + // console.log("cp:", src, "->", dest); // DEBUG + await rootDir(); + const srcStat = await stat(src); + if (srcStat.isFile()) return copyFile(src, dest); + if (!srcStat.isDirectory()) return; + await mkdir(dest, { recursive: true }); + for (const sub of await readdir(src)) { + if ([".DS_Store"].includes(sub)) continue; + await cp(`${src}/${sub}`, `${dest}/${sub}`); + } +} + +/** + * Read file and return text + * @param {string} file file path + * @returns {Promise} file content + */ +export async function read(file) { + const path = fileURLToPath(new URL(file, import.meta.url)); + return readFile(path, "utf8"); +} + +/** + * @see {@link https://vitejs.dev/config/server-options.html#server-https} + * @see {@link https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener} + * @returns {Promise} https.ServerOptions + */ +export const sharedServerOptions = async () => ({ + ca: await read("./local/ca.pem"), + key: await read("./local/key.pem"), + cert: await read("./local/cert.pem"), +}); diff --git a/src/App.svelte b/src/App.svelte deleted file mode 100644 index ac5d3e65..00000000 --- a/src/App.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - -
-
- - - -
-

Userscripts dev

- -

- page, - popup -

- -

- This is the development preview environment -

-
- - diff --git a/src/app.css b/src/app.css deleted file mode 100644 index 1fedad38..00000000 --- a/src/app.css +++ /dev/null @@ -1,82 +0,0 @@ -/* stylelint-disable */ -:root { - font-family: Inter, Avenir, Helvetica, Arial, sans-serif; - font-size: 16px; - line-height: 24px; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -webkit-text-size-adjust: 100%; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -.card { - padding: 2em; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/src/app/App.svelte b/src/app/App.svelte new file mode 100644 index 00000000..51818dcd --- /dev/null +++ b/src/app/App.svelte @@ -0,0 +1,130 @@ + + +
+ Userscripts App Icon + +

+ You can turn on the Userscripts iOS Safari extension in Settings. + + Read the docs. + +

+ +
CURRENT DIRECTORY:
+
+ +
+
+ + diff --git a/src/app/app.css b/src/app/app.css new file mode 100644 index 00000000..2eaa3de0 --- /dev/null +++ b/src/app/app.css @@ -0,0 +1,51 @@ +html { + font-size: 100%; + height: 100vh; + overflow: hidden; +} + +body { + background-color: var(--color-bg-secondary); + color: var(--text-color-primary); + font: var(--text-medium); + height: 100%; + letter-spacing: var(--letter-spacing-medium); + position: relative; + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; +} + +@media screen and (height <= 400px) { + html { + font-size: 80%; + } + + p { + font: var(--text-large); + letter-spacing: var(--letter-spacing-large); + } +} + +@media screen and (height >= 700px) and (width >= 600px) { + html { + font-size: 150%; + } +} + +#app { + height: 100%; +} + +button { + border: none; + cursor: pointer; + padding: 0; + user-select: none; +} + +.link { + background: none; + color: var(--color-blue); + cursor: pointer; + text-decoration: underline; +} diff --git a/src/app/global.d.ts b/src/app/global.d.ts new file mode 100644 index 00000000..0b01b0d8 --- /dev/null +++ b/src/app/global.d.ts @@ -0,0 +1,27 @@ +// https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html + +/// +/// + +declare global { + interface Window { + APP: { + show: ( + platform: "ios" | "mac", + enabled: boolean, + useSettingsInsteadOfPreferences: boolean, + ) => void; + printVersion: (v: string, b: string) => void; + printDirectory: (d: string) => void; + }; + webkit: { + messageHandlers: { + controller: { + postMessage: function; + }; + }; + }; + } +} + +export {}; diff --git a/src/assets/logo.png b/src/app/img/icon.png similarity index 100% rename from src/assets/logo.png rename to src/app/img/icon.png diff --git a/src/shared/img/logo.svg b/src/app/img/logo.svg similarity index 100% rename from src/shared/img/logo.svg rename to src/app/img/logo.svg diff --git a/src/app/jsconfig.json b/src/app/jsconfig.json new file mode 100644 index 00000000..899038cd --- /dev/null +++ b/src/app/jsconfig.json @@ -0,0 +1,26 @@ +// https://code.visualstudio.com/docs/languages/jsconfig +// https://www.typescriptlang.org/docs/handbook/tsconfig-json.html +// https://www.typescriptlang.org/tsconfig + +// https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html +// https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html + +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["**/*.d.ts", "**/*.js", "**/*.svelte"] +} diff --git a/src/app/main.js b/src/app/main.js new file mode 100644 index 00000000..62676104 --- /dev/null +++ b/src/app/main.js @@ -0,0 +1,10 @@ +import "./reset.css"; +import "./variables.css"; +import "./app.css"; +import App from "./App.svelte"; + +const app = new App({ + target: document.getElementById("app"), +}); + +export default app; diff --git a/src/app/reset.css b/src/app/reset.css new file mode 100644 index 00000000..5748075f --- /dev/null +++ b/src/app/reset.css @@ -0,0 +1,44 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +body, +p { + margin: 0; +} + +ul { + list-style: none; + margin: 0; + padding: 0; +} + +button, +input, +select, +textarea { + font-family: inherit; + font-size: 100%; + line-height: inherit; + margin: 0; +} + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; +} + +::-webkit-search-decoration, +::-webkit-search-cancel-button, +::-webkit-search-results-button, +::-webkit-search-results-decoration { + -webkit-appearance: none; +} + +img { + border-style: none; +} diff --git a/src/app/variables.css b/src/app/variables.css new file mode 100644 index 00000000..0c641d23 --- /dev/null +++ b/src/app/variables.css @@ -0,0 +1,52 @@ +:root { + --border-radius: 0.188rem; + --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); + --color-bg-primary: #323639; + --color-bg-secondary: #2f3337; + --color-script-highlighted: #364049; /* rgba(116, 178, 235, 0.1); */ + --color-black: #1d2023; + --color-blue: #74b1eb; + --color-green: #60f36c; + --color-grey: rgb(255 255 255 / 0.15); + --color-red: #ff453a; + --color-yellow: #e4f360; + --letter-spacing-large: -0.031rem; + --letter-spacing-default: -0.029rem; + --letter-spacing-medium: -0.018rem; + --letter-spacing-small: -0.008rem; + --opacity-disabled: 0.3; + --text-color-primary: rgb(255 255 255); + --text-color-secondary: rgb(255 255 255 / 0.65); + --text-color-disabled: rgb(255 255 255 / 0.4); + --font-family: system-ui, -apple-system, "Helvetica Neue", "Helvetica", + sans-serif; + --text-default: 1rem/1.5rem var(--font-family); + --text-large: 1.25rem/1.5rem var(--font-family); + --text-medium: 0.875rem/1.313rem var(--font-family); + --text-small: 0.719rem/1rem var(--font-family); + + /* editor variables */ + --editor-font: monaco, monospace; + --editor-font-size: 14px; + --editor-line-height: 24px; + --editor-invisible: rgb(255 255 255 / 0.15); + --editor-active-line: var(--color-bg-secondary); + --editor-selected-bg: rgb(116 178 235 / 0.35); + --editor-matched-highlight: rgb(116 178 235 / 0.2); + --editor-search-highlight: rgb(255 166 0 / 0.3); + --editor-number: #77e26a; + --editor-comment: rgb(255 255 255 / 0.35); + --editor-def: #efc371; + --editor-default: #cdcfd1; + --editor-keyword: #96c3ed; + --editor-atom: #59ebf5; + --editor-operator: #8c99a7; + --editor-property: #e86c8a; + --editor-string: #f5eea2; + --editor-string-2: #cdabff; + --editor-error: var(--color-red); + --editor-cursor: #e3e7eb; + --editor-matching-bracket-color: #fff; + --editor-matching-bracket-border: var(--editor-number); + --editor-non-matching-bracket: var(--editor-error); +} diff --git a/src/dev/App.svelte b/src/dev/App.svelte new file mode 100644 index 00000000..2ac83b9c --- /dev/null +++ b/src/dev/App.svelte @@ -0,0 +1,46 @@ + + +
+
+ + + +
+

Userscripts dev

+ +

+ + native-app + , + + action-popup + , + + extension-page + +

+ +

This is the development preview environment

+
+ + diff --git a/src/dev/app.css b/src/dev/app.css new file mode 100644 index 00000000..19d0543e --- /dev/null +++ b/src/dev/app.css @@ -0,0 +1,82 @@ +/* stylelint-disable */ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/256-1.png b/src/dev/assets/logo.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/256-1.png rename to src/dev/assets/logo.png diff --git a/src/main.js b/src/dev/main.js similarity index 69% rename from src/main.js rename to src/dev/main.js index 37103c68..b1acd03a 100644 --- a/src/main.js +++ b/src/dev/main.js @@ -2,7 +2,7 @@ import "./app.css"; import App from "./App.svelte"; const app = new App({ - target: document.getElementById("app") + target: document.getElementById("app"), }); export default app; diff --git a/src/vite-env.d.ts b/src/dev/vite-env.d.ts similarity index 100% rename from src/vite-env.d.ts rename to src/dev/vite-env.d.ts diff --git a/src/ext/action-popup/App.svelte b/src/ext/action-popup/App.svelte new file mode 100644 index 00000000..c47d007a --- /dev/null +++ b/src/ext/action-popup/App.svelte @@ -0,0 +1,716 @@ + + +{#if showUpdates} + (showUpdates = false)} + showLoaderOnDisabled={true} + abortClick={abortUpdates} + abort={showUpdates} + > + + +{:else if showInstall} + (showInstall = false)} + showLoaderOnDisabled={true} + > + (showInstall = false)} + installConfirmClick={installConfirm} + /> + +{:else if showAll} + { + showAll = false; + refreshView(); + }} + showLoaderOnDisabled={false} + > + + +{:else} +
+
+ + (showUpdates = true)} + title={"Show updates"} + {disabled} + /> + + +
+ {#if !active} + + {/if} + {#if showInstallPrompt} +
+ Userscript + {#if scriptChecking} + {showInstallPrompt} + {:else} + {scriptInstalled ? "Installed" : "Detected"}: + + {/if} +
+ {/if} + {#if errorNotification} +
+ {errorNotification} + (errorNotification = undefined)} + title={"Clear error"} + /> +
+ {/if} +
+
+ {#if loading} + + {:else if inactive} +
Popup inactive on extension page
+ {:else if firstGuide} +
+

Welcome, first use please: 

+ +

to complete the initialization

+
+ {:else if initError} +
+ Something went wrong:  + +
+ {:else if items.length < 1} +
No matched userscripts
+ {:else} +
+ {#each list as item, i (item.filename)} + toggleItem(item)} + /> + {/each} +
+ {/if} +
+ {#if !inactive && platform === "macos"} + + {/if} +{/if} + + diff --git a/src/ext/action-popup/Components/PopupItem.svelte b/src/ext/action-popup/Components/PopupItem.svelte new file mode 100644 index 00000000..f83fee4b --- /dev/null +++ b/src/ext/action-popup/Components/PopupItem.svelte @@ -0,0 +1,97 @@ + + +
+ +
{name}
+ {#if subframe}
SUB
{/if} + +
+ + diff --git a/src/ext/action-popup/Components/View.svelte b/src/ext/action-popup/Components/View.svelte new file mode 100644 index 00000000..4a7564e7 --- /dev/null +++ b/src/ext/action-popup/Components/View.svelte @@ -0,0 +1,76 @@ + + +
+
+ {headerTitle} + +
+
+ {#if loading && showLoaderOnDisabled} + + {:else} +
Slot content is required...
+ {/if} +
+
+ + diff --git a/src/ext/action-popup/Components/Views/AllItemsView.svelte b/src/ext/action-popup/Components/Views/AllItemsView.svelte new file mode 100644 index 00000000..b5457123 --- /dev/null +++ b/src/ext/action-popup/Components/Views/AllItemsView.svelte @@ -0,0 +1,58 @@ + + +{#if allItems.length} +
+ {#each list as item (item.filename)} + allItemsToggleItem(item)} + /> + {/each} +
+{:else} +
No valid files found in directory
+{/if} + + diff --git a/src/ext/action-popup/Components/Views/InstallView.svelte b/src/ext/action-popup/Components/Views/InstallView.svelte new file mode 100644 index 00000000..d4d23d2b --- /dev/null +++ b/src/ext/action-popup/Components/Views/InstallView.svelte @@ -0,0 +1,176 @@ + + +
+ {#if installError} +
+ + {@html iconError} +
Couldn't install userscript
+

{installError}

+
+ {:else if userscript} +
    +
  • {userscript.name}
  • + {#if userscript.description} +
  • {userscript.description}
  • + {/if} + {#if userscript.match} +
  • +
    @match
    + {#each userscript.match as match} +
    {match}
    + {/each} +
  • + {/if} + {#if userscript.include} +
  • +
    @include
    + {#each userscript.include as include} +
    {include}
    + {/each} +
  • + {/if} + {#if userscript.require} +
  • +
    @require
    + {#each userscript.require as require} +
    {require}
    + {/each} +
  • + {/if} + {#if userscript.grant} +
  • +
    @grant
    + {#each userscript.grant as grant} +
    {grant}
    + {/each} +
  • + {/if} +
+
+ +
{@html iconWarn}
+
+ Be sure you trust the author before installing. Nefarious code can + exploit your security and privacy. +
+
+
+ + +
+ {/if} +
+ + diff --git a/src/ext/action-popup/Components/Views/UpdateView.svelte b/src/ext/action-popup/Components/Views/UpdateView.svelte new file mode 100644 index 00000000..957925c1 --- /dev/null +++ b/src/ext/action-popup/Components/Views/UpdateView.svelte @@ -0,0 +1,83 @@ + + +{#if updates.length} + {#each updates as item (item.name)} + +
+
{item.name}
+ Source + +
+ {/each} +

Be sure you trust the author before saving remote code to your device.

+ +{:else} +
+ + {@html iconUpdate} +
+ There are no file updates available +
+ +
+
+{/if} + + diff --git a/src/ext/action-popup/app.css b/src/ext/action-popup/app.css new file mode 100644 index 00000000..e0a88c6e --- /dev/null +++ b/src/ext/action-popup/app.css @@ -0,0 +1,76 @@ +html { + font-size: 100%; + overflow: hidden; +} + +body { + background-color: var(--color-bg-secondary); + color: var(--text-color-primary); + font: var(--text-medium); + letter-spacing: var(--letter-spacing-medium); + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; + position: relative; + width: 18rem; + height: 26rem; +} + +/* ios */ +@supports (-webkit-touch-callout: none) { + html { + font-size: 125%; + overflow: visible; + overscroll-behavior: none; + } + + body { + width: auto; + min-width: 16rem; + height: auto; + } + + /* non-fixed width need to stretch */ + @media (width < 320px) { + body { + width: 18rem; + min-height: 16rem; + } + } +} + +noscript { + display: block; +} + +#app { + display: flex; + flex-direction: column; + height: 100%; + justify-content: space-between; +} + +button { + border: none; + cursor: pointer; + padding: 0; + user-select: none; +} + +button:disabled { + opacity: var(--opacity-disabled); + pointer-events: none; +} + +a, +.link { + background: none; + color: var(--color-blue); + cursor: pointer; + text-decoration: underline; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/src/ext/action-popup/main.js b/src/ext/action-popup/main.js new file mode 100644 index 00000000..2829a2c0 --- /dev/null +++ b/src/ext/action-popup/main.js @@ -0,0 +1,44 @@ +import "../shared/reset.css"; +import "../shared/variables.css"; +import "./app.css"; +import App from "./App.svelte"; + +// vite feat that only import in dev mode +if (import.meta.env.MODE === "development") { + const modules = import.meta.glob("../shared/dev.js", { eager: true }); + const browser = modules["../shared/dev.js"]["browser"]; + console.debug("DEV-ENV", import.meta.env, modules, browser); + if (!window?.browser?.extension) { + // assign to window simulation WebExtension APIs + window.browser = browser; + // macos popup simulation + const style = document.createElement("style"); + style.textContent = ` +body { + top: 20px; + left: 20px; + box-sizing: content-box; + border: 2px solid #0c0e0f; + border-radius: 10px; + box-shadow: 2px 2px 20px rgba(0, 0, 0, 0.2); +} +body:before { + content: ""; + position: absolute; + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + border: 1px solid #54575a; + border-radius: 9px; +} +`; + browser.platform === "macos" && document.head.append(style); + } +} + +const app = new App({ + target: document.getElementById("app"), +}); + +export default app; diff --git a/src/ext/background/main.js b/src/ext/background/main.js new file mode 100644 index 00000000..4260f153 --- /dev/null +++ b/src/ext/background/main.js @@ -0,0 +1,539 @@ +import { openExtensionPage } from "../shared/utils.js"; +import * as settingsStorage from "../shared/settings.js"; +import { connectNative, sendNativeMessage } from "../shared/native.js"; + +// first sorts files by run-at value, then by weight value +function userscriptSort(a, b) { + // map the run-at values to numeric values + const runAtValues = { + "document-start": 1, + "document-end": 2, + "document-idle": 3, + }; + const runAtA = a.scriptObject["run-at"]; + const runAtB = b.scriptObject["run-at"]; + if (runAtA !== runAtB && runAtValues[runAtA] && runAtValues[runAtB]) { + return runAtValues[runAtA] > runAtValues[runAtB]; + } + return Number(a.scriptObject.weight) < Number(b.scriptObject.weight); +} + +async function readAsDataURL(blob) { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result); // base64data + }); +} + +async function getPlatform() { + let platform = localStorage.getItem("platform"); + if (!platform) { + const message = { name: "REQ_PLATFORM" }; + const response = await sendNativeMessage(message); + if (!response.platform) { + console.error("Failed to get platform"); + return ""; + } + platform = response.platform; + localStorage.setItem("platform", platform); + } + return platform; +} + +function setClipboard(data, type = "text/plain") { + // future enhancement? + // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write + // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText + const onCopy = (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + e.clipboardData.setData(type, data); + document.removeEventListener("copy", onCopy, true); + }; + + const textarea = document.createElement("textarea"); + textarea.textContent = ""; + document.body.appendChild(textarea); + textarea.select(); + document.addEventListener("copy", onCopy, true); + try { + return document.execCommand("copy"); + } catch (error) { + console.warn("setClipboard failed", error); + document.removeEventListener("copy", onCopy, true); + return false; + } finally { + document.body.removeChild(textarea); + } +} + +async function setBadgeCount() { + const clearBadge = () => { + if (import.meta.env.SAFARI_VERSION < 16.4) { + browser.browserAction.setBadgeText({ text: "" }); + } else { + browser.browserAction.setBadgeText({ text: null }); + } + }; + // @todo until better introduce in ios, only set badge on macOS + const platform = await getPlatform(); + // set a text badge or an empty string in visionOS will cause the extension's icon to no longer be displayed + // set it to null to fix already affected users + if (platform === "visionos") { + browser.browserAction.setBadgeText({ text: null }); + return; + } + if (platform !== "macos") return clearBadge(); + // @todo settingsStorage.get("global_exclude_match") + const settings = await settingsStorage.get([ + "global_active", + "toolbar_badge_count", + ]); + if (settings.global_active === false) return clearBadge(); + if (settings.toolbar_badge_count === false) return clearBadge(); + + const currentTab = await browser.tabs.getCurrent(); + // no active tabs exist (user closed all windows) + if (!currentTab) return clearBadge(); + const url = currentTab.url; + // if url doesn't exist, stop + if (!url) return clearBadge(); + // only check for http/s pages + if (!url.startsWith("http://") && !url.startsWith("https://")) + return clearBadge(); + // @todo if url match in global exclude list, clear badge + const frameUrls = new Set(); + const frames = await browser.webNavigation.getAllFrames({ + tabId: currentTab.id, + }); + for (let i = 0; i < frames.length; i++) { + const frameUrl = frames[i].url; + if (frameUrl !== url && frameUrl.startsWith("http")) { + frameUrls.add(frameUrl); + } + } + const message = { + name: "POPUP_BADGE_COUNT", + url, + frameUrls: Array.from(frameUrls), + }; + const response = await sendNativeMessage(message); + if (response?.error) return console.error(response.error); + if (response?.count > 0) { + browser.browserAction.setBadgeText({ text: response.count.toString() }); + } else { + const _url = new URL(url); + if (_url.pathname.endsWith(".user.js")) { + browser.browserAction.setBadgeText({ text: "JS" }); + } else { + clearBadge(); + } + } +} + +// on startup get declarativeNetRequests +// and set the requests for the session +// should also check and refresh when: +// 1. dnr item save event in the page occurs +// 2. dnr item toggle event in the page occurs +// 3. external editor changes script file content +async function setSessionRules() { + // not supported below safari 15.4 + if (!browser.declarativeNetRequest.updateSessionRules) return; + await clearAllSessionRules(); + const message = { name: "REQ_REQUESTS" }; + const response = await sendNativeMessage(message); + if (response.error) { + console.error(response.error); + return; + } + // there are no rules to apply + if (!response.length) return; + // loop through response, parse the rules, push to array and log + const rules = []; + for (let i = 0; i < response.length; i++) { + const rule = response[i]; + const code = JSON.parse(rule.code); + // check if an array or single rule + if (Array.isArray(code)) { + code.forEach((r) => rules.push(r)); + console.info(`Setting session rule: ${rule.name} (${code.length})`); + } else { + rules.push(code); + console.info(`Setting session rule: ${rule.name}`); + } + } + // generate unique ids for all rules to ensure no repeats + const ids = randomNumberSet(1000, rules.length); + rules.map((rule, index) => (rule.id = ids[index])); + try { + await browser.declarativeNetRequest.updateSessionRules({ addRules: rules }); + } catch (error) { + console.error(`Error setting session rules: ${error}`); + return; + } + console.info(`Finished setting ${rules.length} session rules`); +} + +async function clearAllSessionRules() { + const rules = await browser.declarativeNetRequest.getSessionRules(); + if (!rules.length) return; + console.info(`Clearing ${rules.length} session rules`); + const ruleIds = rules.map((a) => a.id); + await browser.declarativeNetRequest.updateSessionRules({ + removeRuleIds: ruleIds, + }); +} + +function randomNumberSet(max, count) { + // generates a set of random unique numbers + // returns an array + const numbers = new Set(); + while (numbers.size < count) { + numbers.add(Math.floor(Math.random() * (max - 1 + 1)) + 1); + } + return [...numbers]; +} + +// the current update logic is similar to setSessionRules() +// this feature needs a more detailed redesign in the future +// https://github.com/quoid/userscripts/issues/453 +async function getContextMenuItems() { + // macos exclusive feature + const platform = await getPlatform(); + if (platform !== "macos") return; + // since it's not possible to get a list of currently active menu items + // on update, all context-menu items are cleared, then re-added + // this is done to ensure fresh code changes appear + await browser.menus.removeAll(); + // get the context-menu scripts + const message = { name: "REQ_CONTEXT_MENU_SCRIPTS" }; + const response = await sendNativeMessage(message); + if (response.error) { + console.error(response.error); + return; + } + // add menus items + const items = response.files?.menu || []; + if (items.length) { + console.info(`Setting ${items.length} context-menu userscripts`); + } + for (let i = 0; i < items.length; i++) { + const item = items[i]; + // context-menu scripts require @match value + // @include values are ignored + if (!item.scriptObject.matches.length) continue; + addContextMenuItem(item); + } +} + +async function addContextMenuItem(userscript) { + // context-menu items persist for a session + // to avoid duplication, when created, save the filename to session storage + const savedItems = sessionStorage.getItem("menu"); + // if the session storage key doesn't exist use empty array + const activeItems = savedItems ? JSON.parse(savedItems) : []; + if (activeItems.indexOf(userscript.scriptObject.filename) !== -1) { + // if already saved, remove it, to get fresh code changes + await browser.menus.remove(userscript.scriptObject.filename); + } + // potential bug? https://developer.apple.com/forums/thread/685273 + // https://stackoverflow.com/q/68431201 + // parse through match values and change pathnames to deal with bug + const patterns = userscript.scriptObject.matches; + patterns.forEach((pattern, index) => { + try { + const url = new URL(pattern); + let pathname = url.pathname; + if (pathname.length > 1 && pathname.endsWith("/")) { + pathname = pathname.slice(0, -1); + } + patterns[index] = `${url.protocol}//${url.hostname}${pathname}`; + } catch (error) { + // prevent breaking when non-url pattern present + } + }); + + browser.menus.create( + { + contexts: ["all"], + documentUrlPatterns: patterns, + id: userscript.scriptObject.filename, + title: userscript.scriptObject.name, + }, + () => { + // add event listener if needed + if (!browser.menus.onClicked.hasListener(contextClick)) { + browser.menus.onClicked.addListener(contextClick); + } + // save the context-menu item reference to sessionStorage + const value = JSON.stringify([userscript.scriptObject.filename]); + sessionStorage.setItem("menu", value); + }, + ); +} + +async function contextClick(info, tab) { + // when any created context-menu item is clicked, send message to tab + // the content script for that tag will have the context-menu code + // which will get send back in the response if/when found + const message = { name: "CONTEXT_RUN", menuItemId: info.menuItemId }; + const response = await browser.tabs.sendMessage(tab.id, message); + // if code is returned, execute on that tab + if (!response.code) return; + browser.tabs.executeScript(tab.id, { code: response.code }); +} + +async function nativeChecks() { + const response = await sendNativeMessage({ + name: "NATIVE_CHECKS", + }); + if (response.error) { + settingsStorage.set({ error_native: response }); + return false; + } + settingsStorage.reset("error_native"); + return true; +} + +/** + * Handles messages sent with `browser.runtime.sendMessage` + * Make sure not to return `undefined` or `rejection`, otherwise the reply may never be delivered + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage#listener} + * @type {Parameters[0]} + * @returns {Promise<{status: "pending"|"fulfilled"|"rejected", result: any}>} + */ +async function handleMessage(message, sender) { + /** @type {Promise} */ + let promise; + switch (message.name) { + case "REQ_USERSCRIPTS": { + // get the page url from the content script that sent request + const url = sender.url; + // use frameId to determine if request came from top level window + // if @noframes true, and isTop false, swift layer won't return code + const isTop = sender.frameId === 0; + // send request to swift layer to provide code for page url + const message = { name: "REQ_USERSCRIPTS", url, isTop }; + try { + const response = await sendNativeMessage(message); + if (import.meta.env.MODE === "development") { + console.debug("REQ_USERSCRIPTS", message, response); + } + // if request failed, send error to content script for logging + if (response.error) return response; + // sort files + response.files.js.sort(userscriptSort); + response.files.css.sort((a, b) => Number(a.weight) < Number(b.weight)); + // return sorted files for injection + return response; + } catch (error) { + console.error(error); + // @ts-ignore -- ignore for now and will reconstruct this in the future. + return { error }; + } + } + case "API_CLOSE_TAB": { + promise = browser.tabs.remove(message.tabId || sender.tab.id); + break; + } + case "API_OPEN_TAB": { + promise = browser.tabs.create({ + active: message.active, + index: sender.tab.index + 1, + url: message.url, + }); + break; + } + case "API_ADD_STYLE": { + promise = browser.tabs.insertCSS(sender.tab.id, { + code: message.css, + cssOrigin: "user", + }); + break; + } + case "API_GET_TAB": { + if (typeof sender.tab === "undefined") { + const error = "unable to deliver tab due to empty tab id"; + return { status: "rejected", result: error }; + } + try { + const tabData = sessionStorage.getItem(`tab-${sender.tab.id}`); + // if tabData is null, can still parse it and return that + const tabObj = JSON.parse(tabData); + return { status: "fulfilled", result: tabObj }; + } catch (error) { + console.error("failed to parse tab data for getTab", error); + return { status: "rejected", result: error }; + } + } + case "API_SAVE_TAB": { + if (sender.tab != null && sender.tab.id) { + const key = `tab-${sender.tab.id}`; + sessionStorage.setItem(key, JSON.stringify(message.tabObj)); + return { status: "fulfilled", result: undefined }; + } else { + const error = "unable to save tab, empty tab id"; + return { status: "rejected", result: error }; + } + } + case "API_SET_CLIPBOARD": { + const result = setClipboard(message.clipboardData, message.type); + return { status: "fulfilled", result }; + } + case "API_XHR": { + // parse details and set up for XMLHttpRequest + const details = message.details; + const method = details.method ? details.method : "GET"; + const user = details.user || null; + const password = details.password || null; + let body = details.data || null; + if (body != null && details.binary != null) { + const len = body.length; + const arr = new Uint8Array(len); + for (let i = 0; i < len; i++) { + arr[i] = body.charCodeAt(i); + } + body = new Blob([arr], { type: "text/plain" }); + } + // establish a long-lived port connection to content script + const port = browser.tabs.connect(sender.tab.id, { + name: message.xhrPortName, + }); + // set up XMLHttpRequest + const xhr = new XMLHttpRequest(); + xhr.withCredentials = details.user && details.password; + xhr.timeout = details.timeout || 0; + if (details.overrideMimeType) { + xhr.overrideMimeType(details.overrideMimeType); + } + // add required listeners and send result back to the content script + for (const e of message.events) { + if (!details[e]) continue; + xhr[e] = async (event) => { + // can not send xhr through postMessage + // construct new object to be sent as "response" + const x = { + readyState: xhr.readyState, + response: xhr.response, + responseHeaders: xhr.getAllResponseHeaders(), + responseType: xhr.responseType, + responseURL: xhr.responseURL, + status: xhr.status, + statusText: xhr.statusText, + timeout: xhr.timeout, + withCredentials: xhr.withCredentials, + }; + // only include responseText when needed + if (["", "text"].indexOf(xhr.responseType) !== -1) { + x.responseText = xhr.responseText; + } + // only process when xhr is complete and data exist + if (xhr.readyState === 4 && xhr.response !== null) { + // need to convert arraybuffer data to postMessage + if (xhr.responseType === "arraybuffer") { + const arr = Array.from(new Uint8Array(xhr.response)); + x.response = arr; + } + // need to convert blob data to postMessage + if (xhr.responseType === "blob") { + const base64data = await readAsDataURL(xhr.response); + x.response = { + data: base64data, + type: xhr.responseType, + }; + } + } + port.postMessage({ name: e, event, response: x }); + }; + } + xhr.open(method, details.url, true, user, password); + xhr.responseType = details.responseType || ""; + if (details.headers) { + for (const key in details.headers) { + if (!key.startsWith("Proxy-") && !key.startsWith("Sec-")) { + const val = details.headers[key]; + xhr.setRequestHeader(key, val); + } + } + } + // receive messages from content script and process them + port.onMessage.addListener((msg) => { + if (msg.name === "ABORT") xhr.abort(); + if (msg.name === "DISCONNECT") port.disconnect(); + }); + // handle port disconnect and clean tasks + port.onDisconnect.addListener((p) => { + if (p?.error) { + console.error( + `port disconnected due to an error: ${p.error.message}`, + ); + } + }); + xhr.send(body); + // if onloadend not set in xhr details + // onloadend event won't be passed to content script + // if that happens port DISCONNECT message won't be posted + // if details lacks onloadend attach listener + if (!details.onloadend) { + xhr.onloadend = (event) => { + port.postMessage({ name: "onloadend", event }); + }; + } + break; + } + case "REFRESH_SESSION_RULES": { + setSessionRules(); + break; + } + case "REFRESH_CONTEXT_MENU_SCRIPTS": { + getContextMenuItems(); + break; + } + } + try { + const result = await promise; + return { status: "fulfilled", result }; + } catch (error) { + console.error(message, sender, error); + return { status: "rejected", result: error }; + } +} +browser.runtime.onInstalled.addListener(async () => { + nativeChecks(); +}); +browser.runtime.onStartup.addListener(async () => { + setSessionRules(); + getContextMenuItems(); +}); +// listens for messages from content script, popup and page +browser.runtime.onMessage.addListener(handleMessage); +// set the badge count +browser.tabs.onActivated.addListener(setBadgeCount); +browser.windows.onFocusChanged.addListener(async (windowId) => { + if (windowId < 1) { + // lose focus + return; + } + nativeChecks(); + setBadgeCount(); + setSessionRules(); + getContextMenuItems(); +}); +browser.webNavigation.onCompleted.addListener(setBadgeCount); + +// handle native app messages +const port = connectNative(); +port.onMessage.addListener((message) => { + // console.info(message); // DEBUG + if (message.name === "SAVE_LOCATION_CHANGED") { + openExtensionPage(); + if (message?.userInfo?.returnApp === true) { + sendNativeMessage({ name: "OPEN_APP" }); + } + } + // if (message.name === "OPEN_EXTENSION_PAGE") { + // openExtensionPage(); + // } +}); diff --git a/src/ext/content-scripts/api.js b/src/ext/content-scripts/api.js new file mode 100644 index 00000000..6865b7d9 --- /dev/null +++ b/src/ext/content-scripts/api.js @@ -0,0 +1,210 @@ +async function setValue(key, value) { + if (typeof key !== "string" || !key.length) { + return Promise.reject(new Error("setValue invalid key arg")); + } + const sid = this.US_filename; + if (typeof sid !== "string" || !sid.length) { + return Promise.reject(new Error("setValue invalid call")); + } + const item = {}; + item[`${sid}---${key}`] = value; + return browser.storage.local.set(item); +} + +async function getValue(key, defaultValue) { + if (typeof key !== "string" || !key.length) { + return Promise.reject(new Error("getValue invalid key arg")); + } + const sid = this.US_filename; + if (typeof sid !== "string" || !sid.length) { + return Promise.reject(new Error("getValue invalid call")); + } + const prefixedKey = `${sid}---${key}`; + const results = await browser.storage.local.get(prefixedKey); + if (prefixedKey in results) return results[prefixedKey]; + if (defaultValue !== undefined) return defaultValue; + return undefined; +} + +async function deleteValue(key) { + if (typeof key !== "string" || !key.length) { + return Promise.reject(new Error("deleteValue missing key arg")); + } + const sid = this.US_filename; + if (typeof sid !== "string" || !sid.length) { + return Promise.reject(new Error("deleteValue invalid call")); + } + const prefixedKey = `${sid}---${key}`; + return browser.storage.local.remove(prefixedKey); +} + +async function listValues() { + const sid = this.US_filename; + if (typeof sid !== "string" || !sid.length) { + return Promise.reject(new Error("listValues invalid call")); + } + const prefix = `${sid}---`; + const results = await browser.storage.local.get(); + const keys = []; + for (const key in results) { + key.startsWith(prefix) && keys.push(key.slice(prefix.length)); + } + return keys; +} + +async function sendMessageProxy(message) { + try { + /** @type {{status: "fulfilled"|"rejected", result: any}} */ + const response = await browser.runtime.sendMessage(message); + if (response.status === "fulfilled") { + return response.result; + } else { + return Promise.reject(response.result); + } + } catch (error) { + console.error(error); + return Promise.reject(error); + } +} + +async function openInTab(url, openInBackground = false) { + try { + new URL(url); + } catch (error) { + return Promise.reject(error); + } + return sendMessageProxy({ + name: "API_OPEN_TAB", + url, + active: !openInBackground, + }); +} + +async function closeTab(tabId) { + return sendMessageProxy({ name: "API_CLOSE_TAB", tabId }); +} + +async function getTab() { + return sendMessageProxy({ name: "API_GET_TAB" }); +} + +async function saveTab(tabObj) { + if (tabObj == null) return Promise.reject(new Error("saveTab invalid arg")); + return sendMessageProxy({ name: "API_SAVE_TAB", tabObj }); +} + +async function addStyle(css) { + if (typeof css !== "string" || !css.length) { + return Promise.reject(new Error("addStyle invalid css arg")); + } + return sendMessageProxy({ name: "API_ADD_STYLE", css }); +} + +async function setClipboard(clipboardData, type) { + return sendMessageProxy({ + name: "API_SET_CLIPBOARD", + clipboardData, + type, + }); +} + +function xhr(details) { + if (details == null) return console.error("xhr invalid details arg"); + if (!details.url) return console.error("xhr details missing url key"); + // generate random port name for single xhr + const xhrPortName = Math.random().toString(36).substring(1, 9); + // strip out functions from details + const detailsParsed = JSON.parse(JSON.stringify(details)); + // get all the "on" events from XMLHttpRequest object + const events = []; + for (const k in XMLHttpRequest.prototype) { + if (k.slice(0, 2) === "on") events.push(k); + } + // check which functions are included in the original details object + // add a bool to indicate if event listeners should be attached + for (const e of events) { + if (typeof details[e] === "function") detailsParsed[e] = true; + } + // define return method, will be populated after port is established + const response = { + abort: () => console.error("xhr has not yet been initialized"), + }; + // port listener, most of the messaging logic goes here + const listener = (port) => { + if (port.name !== xhrPortName) return; + port.onMessage.addListener(async (msg) => { + if ( + events.includes(msg.name) && + typeof details[msg.name] === "function" + ) { + // process xhr response + const r = msg.response; + // only process when xhr is complete and data exist + if (r.readyState === 4 && r.response !== null) { + if (r.responseType === "arraybuffer") { + // arraybuffer responses had their data converted in background + // convert it back to arraybuffer + try { + const buffer = new Uint8Array(r.response).buffer; + r.response = buffer; + } catch (err) { + console.error("error parsing xhr arraybuffer", err); + } + } else if (r.responseType === "blob" && r.response.data) { + // blob responses had their data converted in background + // convert it back to blob + const resp = await fetch(r.response.data); + const b = await resp.blob(); + r.response = b; + } + } + // call userscript method + details[msg.name](msg.response); + } + // all messages received + // tell background it's safe to close port + if (msg.name === "onloadend") { + port.postMessage({ name: "DISCONNECT" }); + } + }); + + // handle port disconnect and clean tasks + port.onDisconnect.addListener((p) => { + if (p?.error) { + console.error(`port disconnected due to an error: ${p.error.message}`); + } + browser.runtime.onConnect.removeListener(listener); + }); + // fill the method returned to the user script + response.abort = () => port.postMessage({ name: "ABORT" }); + }; + // wait for the background to establish a port connection + browser.runtime.onConnect.addListener(listener); + // pass the basic information to the background through a common message + const message = { + name: "API_XHR", + details: detailsParsed, + xhrPortName, + events, + }; + sendMessageProxy(message); + return response; +} + +export default { + setValue, + getValue, + listValues, + deleteValue, + openInTab, + getTab, + saveTab, + closeTab, + addStyle, + setClipboard, + // notification, + // registerMenuCommand, + // getResourceUrl, + xmlHttpRequest: xhr, + GM_xmlhttpRequest: xhr, +}; diff --git a/src/ext/content-scripts/entry-dot-user-js.js b/src/ext/content-scripts/entry-dot-user-js.js new file mode 100644 index 00000000..e69de29b diff --git a/src/ext/content-scripts/entry-greasyfork.js b/src/ext/content-scripts/entry-greasyfork.js new file mode 100644 index 00000000..e69de29b diff --git a/src/ext/content-scripts/entry-userscripts.js b/src/ext/content-scripts/entry-userscripts.js new file mode 100644 index 00000000..ab620622 --- /dev/null +++ b/src/ext/content-scripts/entry-userscripts.js @@ -0,0 +1,259 @@ +import USAPI from "./api.js"; + +// code received from background page will be stored in this variable +// code referenced again when strict CSPs block initial injection attempt +let data; +// determines whether strict csp injection has already run (JS only) +let cspFallbackAttempted = false; + +// label used to distinguish frames in console +const label = randomLabel(); +const usTag = window.self === window.top ? "" : `(${label})`; + +function randomLabel() { + const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const r = Math.random(); + return a[Math.floor(r * a.length)] + r.toString().slice(5, 6); +} + +function triageJS(userscript) { + const runAt = userscript.scriptObject["run-at"]; + if (runAt === "document-start") { + injectJS(userscript); + } else if (runAt === "document-end") { + if (document.readyState !== "loading") { + injectJS(userscript); + } else { + document.addEventListener("DOMContentLoaded", () => { + injectJS(userscript); + }); + } + } else if (runAt === "document-idle") { + if (document.readyState === "complete") { + injectJS(userscript); + } else { + document.addEventListener("readystatechange", () => { + if (document.readyState === "complete") { + injectJS(userscript); + } + }); + } + } +} + +function injectJS(userscript) { + const filename = userscript.scriptObject.filename; + const name = userscript.scriptObject.name; + const code = `${userscript.code} //# sourceURL=${ + filename.replace(/\s/g, "-") + usTag + }`; + let injectInto = userscript.scriptObject["inject-into"]; + // change scope to content since strict CSP event detected + if (injectInto === "auto" && (userscript.fallback || cspFallbackAttempted)) { + injectInto = "content"; + console.warn(`Attempting fallback injection for ${name}`); + } + const world = injectInto === "content" ? "content" : "page"; + if (window.self === window.top) { + console.info(`Injecting: ${name} %c(js/${world})`, "color: #fff600"); + } else { + console.info( + `Injecting: ${name} %c(js/${world})%c - %cframe(${label})(${window.location})`, + "color: #fff600", + "color: inherit", + "color: #006fff", + ); + } + if (world === "page") { + const div = document.createElement("div"); + div.style.display = "none"; + const shadowRoot = div.attachShadow({ mode: "closed" }); + const tag = document.createElement("script"); + tag.textContent = code; + shadowRoot.append(tag); + (document.body ?? document.head ?? document.documentElement).append(div); + } else { + try { + // eslint-disable-next-line no-new-func + return Function( + `{${Object.keys(userscript.apis).join(",")}}`, + code, + )(userscript.apis); + } catch (error) { + console.error(`"${filename}" error:`, error); + } + } +} + +function injectCSS(name, code) { + if (window.self === window.top) { + console.info(`Injecting ${name} %c(css)`, "color: #60f36c"); + } else { + console.info( + `Injecting ${name} %c(css)%c - %cframe(${label})(${window.location})`, + "color: #60f36c", + "color: inherit", + "color: #006fff", + ); + } + // Safari lacks full support for tabs.insertCSS + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/insertCSS + // specifically frameId and cssOrigin + // if support for those details keys arrives, the method below can be used + // NOTE: manifest V3 does support frameId, but not origin + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/insertCSS + + // write the css code to head of the document + const tag = document.createElement("style"); + tag.textContent = code; + document.head.appendChild(tag); +} + +function cspFallback(e) { + // if a security policy violation event has occurred + // and the directive is script-src or script-src-elem + // it's fair to assume that there is a strict CSP for javascript + // and that injection was blocked for all userscripts + // when any script-src violation is detected, re-attempt injection + if ( + e.effectiveDirective === "script-src" || + e.effectiveDirective === "script-src-elem" + ) { + // get all "auto" code + // since other code can trigger a security policy violation event + // make sure data var is not undefined before attempting fallback + if (!data || cspFallbackAttempted) return; + // update global that tracks security policy violations + cspFallbackAttempted = true; + // for all userscripts with @inject-into: auto, attempt re-injection + for (let i = 0; i < data.files.js.length; i++) { + const userscript = data.files.js[i]; + if (userscript.scriptObject["inject-into"] !== "auto") continue; + userscript.fallback = 1; + triageJS(userscript); + } + } +} + +async function injection() { + const response = await browser.runtime.sendMessage({ + name: "REQ_USERSCRIPTS", + }); + // cancel injection if errors detected + if (!response || response.error) { + console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); + return; + } + // save response locally in case CSP events occur + data = response; + // combine regular and context-menu scripts + const scripts = [...data.files.js, ...data.files.menu]; + // loop through each userscript and prepare for processing + for (let i = 0; i < scripts.length; i++) { + const userscript = scripts[i]; + const filename = userscript.scriptObject.filename; + const grants = userscript.scriptObject.grant; + const injectInto = userscript.scriptObject["inject-into"]; + // create GM.info object, all userscripts get access to GM.info + userscript.apis = { GM: {} }; + userscript.apis.GM.info = { + script: userscript.scriptObject, + scriptHandler: data.scriptHandler, + scriptHandlerVersion: data.scriptHandlerVersion, + scriptMetaStr: userscript.scriptMetaStr, + }; + // add GM_info + userscript.apis.GM_info = userscript.apis.GM.info; + // if @grant explicitly set to none, empty grants array + if (grants.includes("none")) grants.length = 0; + // @grant values exist for page scoped userscript + if (grants.length && injectInto === "page") { + // remove grants + grants.length = 0; + // log warning + console.warn( + `${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`, + ); + } + // @grant exist for auto scoped userscript + if (grants.length && injectInto === "auto") { + // change scope + userscript.scriptObject["inject-into"] = "content"; + // log warning + console.warn( + `${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`, + ); + } + // loop through each userscript @grant value, add methods as needed + for (let j = 0; j < grants.length; j++) { + const grant = grants[j]; + const method = grant.split(".")[1] || grant.split(".")[0]; + // ensure API method exists in USAPI object + if (!Object.keys(USAPI).includes(method)) continue; + // add granted methods + switch (method) { + case "info": + case "GM_info": + continue; + case "getValue": + case "setValue": + case "deleteValue": + case "listValues": + userscript.apis.GM[method] = USAPI[method].bind({ + US_filename: filename, + }); + break; + case "GM_xmlhttpRequest": + userscript.apis[method] = USAPI[method]; + break; + default: + userscript.apis.GM[method] = USAPI[method]; + } + } + // triage userjs item for injection + triageJS(userscript); + } + // loop through each usercss and inject + for (let i = 0; i < data.files.css.length; i++) { + const userstyle = data.files.css[i]; + injectCSS(userstyle.name, userstyle.code); + } +} + +function listeners() { + // listens for messages from background, popup, etc... + browser.runtime.onMessage.addListener((request) => { + const name = request.name; + if (name === "CONTEXT_RUN") { + // from bg script when context-menu item is clicked + // double check to ensure context-menu scripts only run in top windows + if (window !== window.top) return; + + // loop through context-menu scripts saved to data object and find match + // if no match found, nothing will execute and error will log + const filename = request.menuItemId; + for (let i = 0; i < data.files.menu.length; i++) { + const item = data.files.menu[i]; + if (item.scriptObject.filename === filename) { + console.info(`Injecting ${filename} %c(js)`, "color: #fff600"); + injectJS(item); + return; + } + } + console.error(`Couldn't find ${filename} code!`); + } + }); + // listen for CSP violations + document.addEventListener("securitypolicyviolation", cspFallback); +} + +async function initialize() { + const results = await browser.storage.local.get("US_GLOBAL_ACTIVE"); + if (results?.US_GLOBAL_ACTIVE === false) + return console.info("Userscripts off"); + // start the injection process and add the listeners + injection(); + listeners(); +} + +initialize(); diff --git a/src/ext/extension-page/App.svelte b/src/ext/extension-page/App.svelte new file mode 100644 index 00000000..a457e035 --- /dev/null +++ b/src/ext/extension-page/App.svelte @@ -0,0 +1,133 @@ + + + + +{#if $state.includes("init")} +
+ + {@html logo} + {#if $state.includes("init-error")} + Failed to initialize app, check the browser console + {:else} + Initializing app... + {/if} +
+{/if} +
+ + +
+
    + {#each $notifications as item (item.id)} + notifications.remove(item.id)} {item} /> + {/each} +
+{#if $state.includes("settings")}{/if} + + diff --git a/src/ext/extension-page/Components/Editor/CodeMirror.svelte b/src/ext/extension-page/Components/Editor/CodeMirror.svelte new file mode 100644 index 00000000..e0c39081 --- /dev/null +++ b/src/ext/extension-page/Components/Editor/CodeMirror.svelte @@ -0,0 +1,422 @@ + + + + + + +{#if instance} + (searchActive = false)} + {instance} + /> +{/if} diff --git a/src/ext/extension-page/Components/Editor/Editor.svelte b/src/ext/extension-page/Components/Editor/Editor.svelte new file mode 100644 index 00000000..5d831d39 --- /dev/null +++ b/src/ext/extension-page/Components/Editor/Editor.svelte @@ -0,0 +1,345 @@ + + +
+ {#if $state.includes("editor-loading") || $state.includes("fetching")} + + {/if} + {#if !activeItem} +
No Item Selected
+ {/if} +
+
+
+ +
{name}
+
+
+
+ {#if $state.includes("saving")} + Saving... + {:else if $state.includes("trashing")} + (◞‸◟) + {:else if $state.includes("updating")} + Updating code, + {:else if remote} + Code was remotely fetched, + check carefully before saving! + {:else if temp} + Ready for code! + {:else} + Last modified: {lastModified} + {/if} +
+
+
+
+ + + +
+
+
+ +
+ +
+ + diff --git a/src/ext/extension-page/Components/Editor/EditorSearch.svelte b/src/ext/extension-page/Components/Editor/EditorSearch.svelte new file mode 100644 index 00000000..cc1f70db --- /dev/null +++ b/src/ext/extension-page/Components/Editor/EditorSearch.svelte @@ -0,0 +1,207 @@ + + +{#if active} + +{/if} + + diff --git a/src/ext/extension-page/Components/Notification.svelte b/src/ext/extension-page/Components/Notification.svelte new file mode 100644 index 00000000..f79e5343 --- /dev/null +++ b/src/ext/extension-page/Components/Notification.svelte @@ -0,0 +1,154 @@ + + +
  • + +
    + +
    {@html icon}
    + {item.message} + +
    +
  • + + diff --git a/src/ext/extension-page/Components/Settings.svelte b/src/ext/extension-page/Components/Settings.svelte new file mode 100644 index 00000000..0d759d11 --- /dev/null +++ b/src/ext/extension-page/Components/Settings.svelte @@ -0,0 +1,352 @@ + + +
    + + +
    state.remove("settings")}>
    + +
    + + diff --git a/src/ext/extension-page/Components/Sidebar/Sidebar.svelte b/src/ext/extension-page/Components/Sidebar/Sidebar.svelte new file mode 100644 index 00000000..99dacae0 --- /dev/null +++ b/src/ext/extension-page/Components/Sidebar/Sidebar.svelte @@ -0,0 +1,264 @@ + + + + + diff --git a/src/ext/extension-page/Components/Sidebar/SidebarFilter.svelte b/src/ext/extension-page/Components/Sidebar/SidebarFilter.svelte new file mode 100644 index 00000000..f0746326 --- /dev/null +++ b/src/ext/extension-page/Components/Sidebar/SidebarFilter.svelte @@ -0,0 +1,122 @@ + + +
    + + + +
    + +
    + +
    + +
    + {#if query} + (query = "")} {disabled} /> + {/if} +
    + + diff --git a/src/ext/extension-page/Components/Sidebar/SidebarItem.svelte b/src/ext/extension-page/Components/Sidebar/SidebarItem.svelte new file mode 100644 index 00000000..c4cd2bd7 --- /dev/null +++ b/src/ext/extension-page/Components/Sidebar/SidebarItem.svelte @@ -0,0 +1,102 @@ + + +
    +
    + +
    {data.name}
    + +
    + {#if description} +
    + {description} +
    + {/if} +
    + + diff --git a/src/ext/extension-page/app.css b/src/ext/extension-page/app.css new file mode 100644 index 00000000..425bcaa4 --- /dev/null +++ b/src/ext/extension-page/app.css @@ -0,0 +1,50 @@ +html { + font-size: 100%; + height: 100vh; + overflow: hidden; +} + +body { + background-color: var(--color-bg-primary); + color: var(--text-color-primary); + font: var(--text-default); + height: 100%; + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; +} + +noscript { + display: block; +} + +#app { + display: flex; + flex-direction: column; + height: 100%; +} + +a, +.link { + background: none; + color: var(--color-blue); + cursor: pointer; + text-decoration: underline; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +button { + border: none; + cursor: pointer; + padding: 0; + user-select: none; +} + +button:disabled { + opacity: var(--opacity-disabled); + pointer-events: none; +} diff --git a/src/ext/extension-page/codemirror.css b/src/ext/extension-page/codemirror.css new file mode 100644 index 00000000..3741538f --- /dev/null +++ b/src/ext/extension-page/codemirror.css @@ -0,0 +1,214 @@ +.CodeMirror { + background-color: transparent; + color: var(--editor-default); + font-family: var(--editor-font); + font-size: var(--editor-font-size); + font-weight: 400; + height: 100%; + line-height: var(--editor-line-height); + text-rendering: unset; + -webkit-font-smoothing: antialiased; +} + +.CodeMirror-gutters { + background-color: transparent; + border-right: 0; +} + +.CodeMirror-activeline-background, +.CodeMirror-activeline-gutter { + background-color: var(--editor-active-line); +} + +.CodeMirror-linenumber { + color: var(--text-color-primary); + font-size: var(--editor-font-size); + opacity: 0.25; +} + +.CodeMirror-activeline .CodeMirror-linenumber { + opacity: 0.5; +} + +.CodeMirror-cursor { + border-left: 2px solid var(--text-color-secondary); /* check */ +} + +.CodeMirror-hints { + background-color: var(--color-black); + border: 1px solid black; + box-shadow: var(--box-shadow); + font: var(--text-small); + font-family: var(--editor-font); + line-height: 1.5rem; +} + +.CodeMirror-hint { + color: var(--text-color-secondary); +} + +li.CodeMirror-hint-active { + background-color: var(--color-blue); + color: var(--color-black); +} + +.cm-s-default .CodeMirror-selected { + background-color: var(--editor-selected-bg); +} + +div.CodeMirror span.CodeMirror-matchingbracket { + color: var(--editor-matching-bracket-color); + border-bottom: 2px solid var(--editor-matching-bracket-border); +} + +div.CodeMirror span.CodeMirror-nonmatchingbracket { + color: var(--editor-non-matching-bracket); +} + +.CodeMirror .CodeMirror-placeholder, +.cm-s-default .cm-comment { + color: var(--editor-comment); +} + +.cm-s-default .cm-keyword { + color: var(--editor-keyword); +} + +.cm-s-default .cm-def { + color: var(--editor-def); +} + +.cm-s-default .cm-variable, +.cm-s-default .cm-variable-2 { + color: inherit; +} + +.cm-s-default .cm-operator { + color: var(--editor-operator); +} + +.cm-s-default .cm-property { + color: var(--editor-property); +} + +.cm-s-default .cm-string { + color: var(--editor-string); +} + +.cm-s-default .cm-string-2 { + color: var(--editor-string-2); +} + +.cm-s-default .cm-number { + color: var(--editor-number); +} + +.cm-s-default .cm-atom { + color: var(--editor-atom); +} + +.cm-s-default .cm-error { + color: var(--color-red); +} + +.CodeMirror .cm-whitespace::before, +.CodeMirror .CodeMirror-line > span::after, +.CodeMirror .CodeMirror-code > div > pre > span::after { + color: var(--editor-invisible); +} + +.cm-s-default .cm-variable-3, +.cm-s-default .cm-builtin, +.cm-s-default .cm-qualifier, +.cm-s-default .cm-tag { + color: var(--editor-def); +} + +.cm-s-default .cm-searching { + background-color: var(--editor-search-highlight); + padding: 3px 0; +} + +.cm-s-default .cm-search-mark { + border-bottom: 2px solid var(--editor-matching-bracket-border); +} + +/* +.cm-s-default .cm-matchhighlight { + background-color: var(--editor-matched-highlight); +} +*/ + +.CodeMirror-lint-markers { + width: 8px; +} + +.CodeMirror-lint-marker { + border-radius: 50%; + cursor: default; + height: 0.375rem; + left: 0.25rem; + position: relative; + width: 0.375rem; +} + +.CodeMirror-lint-marker-error, +.CodeMirror-lint-marker-warning { + background: var(--color-yellow); + top: -1px; +} + +.CodeMirror-lint-marker-error { + background-color: var(--editor-error); +} + +.CodeMirror-lint-marker-multiple { + background: var(--editor-def); + left: 0; + position: absolute; + top: 0; +} + +.CodeMirror-lint-mark-warning { + background: none; + border-bottom: 2px solid var(--color-yellow); +} + +.CodeMirror-lint-mark-error { + background: none; + border-bottom: 2px solid var(--editor-error); +} + +.CodeMirror-lint-tooltip { + background-color: var(--color-black); + border: 1px solid black; + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); + color: var(--text-color-secondary); + font: var(--text-small); + padding: 0.5rem; + transition: opacity 200ms; +} + +.CodeMirror-lint-message-error, +.CodeMirror-lint-message-warning { + background: none; + padding-left: 1rem; + position: relative; +} + +.CodeMirror-lint-message-error::before, +.CodeMirror-lint-message-warning::before { + background-color: var(--color-yellow); + border-radius: 50%; + content: ""; + height: 0.5rem; + left: 0; + position: absolute; + top: 0.25rem; + width: 0.5rem; +} + +.CodeMirror-lint-message-error::before { + background-color: var(--editor-error); +} diff --git a/src/page/codemirror.js b/src/ext/extension-page/codemirror.js similarity index 100% rename from src/page/codemirror.js rename to src/ext/extension-page/codemirror.js diff --git a/src/ext/extension-page/main.js b/src/ext/extension-page/main.js new file mode 100644 index 00000000..ec2580e9 --- /dev/null +++ b/src/ext/extension-page/main.js @@ -0,0 +1,21 @@ +import "../shared/reset.css"; +import "../shared/variables.css"; +import "./app.css"; +import App from "./App.svelte"; + +// vite feat that only import in dev mode +if (import.meta.env.MODE === "development") { + const modules = import.meta.glob("../shared/dev.js", { eager: true }); + const browser = modules["../shared/dev.js"]["browser"]; + console.debug("DEV-ENV", import.meta.env, modules, browser); + if (!window?.browser?.extension) { + // assign to window simulation WebExtension APIs + window.browser = browser; + } +} + +const app = new App({ + target: document.getElementById("app"), +}); + +export default app; diff --git a/src/ext/extension-page/store.js b/src/ext/extension-page/store.js new file mode 100644 index 00000000..dc9b0234 --- /dev/null +++ b/src/ext/extension-page/store.js @@ -0,0 +1,145 @@ +import { writable } from "svelte/store"; +import { uniqueId } from "../shared/utils.js"; +import * as settingsStorage from "../shared/settings.js"; +import { sendNativeMessage } from "../shared/native.js"; + +function notificationStore() { + const { subscribe, update } = writable([]); + const add = (item) => { + update((a) => { + a.push(item); + return a; + }); + }; + const remove = (id) => update((a) => a.filter((b) => b.id !== id)); + return { subscribe, add, remove }; +} +export const notifications = notificationStore(); + +function logStore() { + const { subscribe, set, update } = writable([]); + const add = (message, type, notify) => { + const item = { id: uniqueId(), message, time: Date.now(), type }; + if (notify || type === "error") notifications.add(item); // always notify on error + update((a) => { + a.push(item); + return a; + }); + }; + const remove = (id) => update((a) => a.filter((b) => b.id !== id)); + const reset = () => set([]); + return { subscribe, add, remove, reset }; +} +export const log = logStore(); + +function stateStore() { + const { subscribe, update } = writable(["init"]); + // store oldState to see how state transitioned + // ex. if (newState === foo && oldState === bar) baz(); + let oldState = []; + const add = (stateModifier) => + update((state) => { + // list of acceptable states, mostly for state definition tracking + const states = [ + "init", // the uninitialized app state (start screen) + "init-error", // unique error when initialization fails, shows error on load screen + "settings", // when the settings modal is shown + "items-loading", // when the sidebar items are loading + "saving", // when a file in the editor is being saved + "fetching", // when a new remote file has been added and it being fetched + "trashing", // when deleting the file in the editor + "updating", // when the file in the editor is being updating + ]; + // disallow adding undefined states + if (!states.includes(stateModifier)) { + console.error("invalid state"); + return state; + } + // save pre-changed state to oldState var + oldState = [...state]; + // if current state modifier not present, add it to state array + if (!state.includes(stateModifier)) state.push(stateModifier); + // ready state only when no other states present, remove it if present + if (state.includes("ready")) state.splice(state.indexOf("ready"), 1); + log.add( + `App state updated to: ${state} from: ${oldState}`, + "info", + false, + ); + return state; + }); + const remove = (stateModifier) => + update((state) => { + // save pre-changed state to oldState var + oldState = [...state]; + // if current state modifier present, remove it from state array + if (state.includes(stateModifier)) + state.splice(state.indexOf(stateModifier), 1); + // if no other states, push ready state + if (state.length === 0) state.push("ready"); + log.add( + `App state updated to: ${state} from: ${oldState}`, + "info", + false, + ); + return state; + }); + const getOldState = () => oldState; + return { subscribe, add, getOldState, remove }; +} +export const state = stateStore(); + +function settingsStore() { + const { subscribe, update, set } = writable({}); + const init = async (initData) => { + // import legacy settings data just one-time + await settingsStorage.legacyImport(); + // for compatibility with legacy getting names only + // once all new name is used, use settingsStorage.get() + const settings = await settingsStorage.legacyGet(); + console.info("store.js settingsStore init", initData, settings); + set({ ...initData, ...settings }); + // sync popup, backgound, etc... settings changes + settingsStorage.onChangedSettings((sets, area) => { + console.info(`store.js storage.${area}.onChanged`, sets); + update((obj) => Object.assign(obj, sets)); + }); + }; + const reset = async (keys) => { + await settingsStorage.reset(keys); + // once all new name is used, use settingsStorage.get() + const settings = await settingsStorage.legacyGet(); + console.info("store.js settingsStore reset", settings); + update((obj) => Object.assign(obj, settings)); + }; + + const updateSingleSettingOld = (key, value) => { + // blacklist not stored in normal setting object in manifest, so handle differently + if (key === "blacklist") { + // update blacklist on swift side + const message = { name: "PAGE_UPDATE_BLACKLIST", blacklist: value }; + sendNativeMessage(message).then((response) => { + response.error && log.add(response.error, "error", true); + }); + } + if (key === "active") { + sendNativeMessage({ name: "TOGGLE_EXTENSION", active: String(value) }); + } + }; + const updateSingleSetting = (key, value) => { + // update(settings => (settings[key] = value, settings)); + update((settings) => { + settings[key] = value; + return settings; + }); + // for compatibility with legacy setting names only + // once all new name is used, use settingsStorage.set() + settingsStorage.legacySet({ [key]: value }); // Durable Storage + // temporarily keep the old storage method until it is confirmed that all dependencies are removed + updateSingleSettingOld(key, value); + }; + return { subscribe, set, init, reset, updateSingleSetting }; +} +export const settings = settingsStore(); + +export const items = writable([]); diff --git a/src/ext/global.d.ts b/src/ext/global.d.ts new file mode 100644 index 00000000..167d6d97 --- /dev/null +++ b/src/ext/global.d.ts @@ -0,0 +1,13 @@ +// https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html + +/// +/// + +import type Browser from "webextension-polyfill"; + +declare global { + const browser: Browser.Browser; + interface Window { + browser: Browser.Browser; + } +} diff --git a/src/ext/jsconfig.json b/src/ext/jsconfig.json new file mode 100644 index 00000000..899038cd --- /dev/null +++ b/src/ext/jsconfig.json @@ -0,0 +1,26 @@ +// https://code.visualstudio.com/docs/languages/jsconfig +// https://www.typescriptlang.org/docs/handbook/tsconfig-json.html +// https://www.typescriptlang.org/tsconfig + +// https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html +// https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html + +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["**/*.d.ts", "**/*.js", "**/*.svelte"] +} diff --git a/src/ext/shared/Components/Dropdown.svelte b/src/ext/shared/Components/Dropdown.svelte new file mode 100644 index 00000000..411a5504 --- /dev/null +++ b/src/ext/shared/Components/Dropdown.svelte @@ -0,0 +1,115 @@ + + +
    + +
      + +
    • At least one slot is required...
    • +
      +
    +
    + + diff --git a/src/ext/shared/Components/IconButton.svelte b/src/ext/shared/Components/IconButton.svelte new file mode 100644 index 00000000..b9636042 --- /dev/null +++ b/src/ext/shared/Components/IconButton.svelte @@ -0,0 +1,65 @@ + + + + + diff --git a/src/ext/shared/Components/Loader.svelte b/src/ext/shared/Components/Loader.svelte new file mode 100644 index 00000000..666ae404 --- /dev/null +++ b/src/ext/shared/Components/Loader.svelte @@ -0,0 +1,60 @@ + + +
    + + {@html iconLoader} + {#if abort} +
    + Fetching resources, +
    + {/if} +
    + + diff --git a/src/ext/shared/Components/Tag.svelte b/src/ext/shared/Components/Tag.svelte new file mode 100644 index 00000000..80e00954 --- /dev/null +++ b/src/ext/shared/Components/Tag.svelte @@ -0,0 +1,55 @@ + + +
    + + diff --git a/src/ext/shared/Components/Toggle.svelte b/src/ext/shared/Components/Toggle.svelte new file mode 100644 index 00000000..a724fecb --- /dev/null +++ b/src/ext/shared/Components/Toggle.svelte @@ -0,0 +1,72 @@ + + + + + + + + diff --git a/src/ext/shared/dev.js b/src/ext/shared/dev.js new file mode 100644 index 00000000..6d9a754c --- /dev/null +++ b/src/ext/shared/dev.js @@ -0,0 +1,521 @@ +import { parse, uniqueId, wait } from "./utils"; + +/** + * + * @param {("css"|"js")} type + * @param {boolean?} updates + * @param {boolean?} longName + * @returns + */ +function generateFile( + type, + updates = false, + longName = false, + request = false, +) { + let canUpdate = updates; + const uid = uniqueId(); + let name = request ? `${uid}-request-${type}` : `${uid}-example-${type}`; + if (longName) name = `${uid}${uid}${uid}-example-${type}`; + const randomDate = +( + Number(new Date()) - Math.floor(Math.random() * 10000000000) + ); + let content = + "// ==UserScript==" + + `\n// @name ${name}` + + `\n// @description Custom description for userscript with name "${name}"` + + "\n// @match *://*.*" + + "\n// @exclude-match https://github.com/quoid/userscripts" + + "\n// @version 1.0" + + "\n// @noframes" + + "\n// ==/UserScript==" + + `\n\nconsole.log("I am ${name}");`; + if (type === "css") { + content = content.replace("// ==UserScript==", "/* ==UserStyle=="); + content = content.replace("// ==/UserScript==", "==/UserStyle== */"); + content = content.replaceAll("// @", "@"); + content = content.replace( + `console.log("I am ${name}");`, + "#id,\n.class\n.pseudo-element::after {\n color: red\n}", + ); + canUpdate = false; + } + if (request) { + content = content.replace( + "// @noframes", + "// @noframes\n// @run-at request", + ); + } + if (canUpdate) { + content = content.replace( + "1.0", + "1.0\n// @updateURL https://www.k21p.com/example.user.js", + ); + content = content.replace( + `console.log("I am ${name}");`, + `console.log("I am ${name} and you can update me!");`, + ); + } + return { + content, + filename: `${name}.${type}`, + lastModified: randomDate, + name, + request, + type, + }; +} + +/** + * @typedef {Object} File + * @property {string} content - ... + * @property {string} filename - ... + * @property {number} lastModified - ... + * @property {string} [name] - ... + * @property {boolean} [request] - ... + * @property {"css" | "js"} [type] - ... + */ +/** @type {File[]} */ +const files = [ + generateFile("js", true, true), + generateFile("css"), + generateFile("js", false, false, true), + ...Array.from({ length: 7 }, () => generateFile("js")), +]; + +const _browser = { + delay: 200, + platform: "macos", // ios || macos + runtime: { + getURL() { + return "https://www.example.com/"; + }, + async sendMessage(message, responseCallback) { + const name = message.name; + console.info(`Got message: ${name}`); + let response = {}; + if (name === "REFRESH_SESSION_RULES") { + response = { success: true }; + } + if (!responseCallback) { + return new Promise((resolve) => { + setTimeout(() => resolve(response), _browser.delay); + }); + } + setTimeout(() => responseCallback(response), _browser.delay); + }, + async sendNativeMessage(application, message, responseCallback) { + const name = message.name; + console.info(`Got message: ${name}`); + /** @type {any} */ + let response = {}; + if (name === "PAGE_INIT_DATA") { + response = { + saveLocation: "/Users/someone/Library/Directory", + version: "5.0.0", + build: "55", + }; + } else if (name === "PAGE_LEGACY_IMPORT") { + response = { + active: "true", + autoCloseBrackets: "true", + autoHint: "true", + blacklist: [], + descriptions: "true", + languageCode: "en", + lint: "true", + log: "false", + showCount: "true", + showInvisibles: "true", + sortOrder: "lastModifiedDesc", + tabSize: "4", + }; + } else if (name === "PAGE_ALL_FILES") { + response = []; + files.forEach((file) => { + const content = file.content; + const parsed = parse(content); + const metadata = parsed.metadata; + const canUpdate = !!(metadata.version && metadata.updateURL); + const scriptData = { + canUpdate, + content, + description: metadata.description ? metadata.description[0] : "", + disabled: false, + filename: file.filename, + lastModified: file.lastModified, + metadata, + name: metadata.name[0], + request: file.request, + // type: file.filename.substring(file.filename.lastIndexOf(".") + 1) + type: file.type, + }; + response.push(scriptData); + }); + } else if (name === "TOGGLE_ITEM") { + response = { success: true }; + // response = {error: true}; + } else if (name === "PAGE_UPDATE_SETTINGS") { + response = { success: true }; + } else if (name === "PAGE_UPDATE_BLACKLIST") { + response = { success: true }; + } else if (name === "PAGE_NEW_REMOTE") { + const result = await getRemoteFileContents(message.url); + response = result; + } else if (name === "PAGE_SAVE") { + const newContent = message.content; + const oldFilename = message.item.filename; + const parsed = parse(newContent); + const lastModified = Date.now(); + let canUpdate = false; + // script failed to parse + if (!parsed) { + return { error: "save failed, file has invalid metadata" }; + } + const metaName = parsed.metadata.name[0]; + const newFilename = `${metaName}.${message.item.type}`; + // filename length too long + if (newFilename.length > 255) { + return { error: "save failed, filename too long!" }; + } + + // check if file can be remotely updated + if (parsed.metadata.version && parsed.metadata.updateURL) { + canUpdate = true; + } + + const success = { + canUpdate, + content: newContent, + filename: newFilename, + lastModified, + name, + }; + + // add description if in file metadata + if (parsed.metadata.description) { + // const foo = parsed.metadata; + success.description = parsed.metadata.description[0]; + } + // check if declarative network request + if ( + parsed.metadata["run-at"] && + parsed.metadata["run-at"][0] === "request" + ) { + success.request = true; + } + // overwriting + if (newFilename.toLowerCase() === oldFilename.toLowerCase()) { + saveFile(newContent, lastModified, newFilename, oldFilename); + return success; + } + + // not overwriting, check if filename for that type is taken + if ( + files.find( + (a) => a.filename.toLowerCase() === newFilename.toLowerCase(), + ) + ) { + return { error: "save failed, name already taken!" }; + } + + // not overwriting but all validation passed + saveFile(newContent, lastModified, newFilename, oldFilename); + return success; + } else if (name === "PAGE_UPDATE") { + const url = parse(message.content).metadata.updateURL; + await wait(500); + const result = await getRemoteFileContents(url); + response = result; + // response.error = "Something went wrong!"; + // response.info = "No updates found"; + } else if (name === "TOGGLE_EXTENSION") { + // response = {error: "Failed toggle extension"}; + response = { success: true }; + } else if ( + name === "POPUP_UPDATE_ALL" || + name === "POPUP_UPDATE_SINGLE" + ) { + // response = {error: "Failed refresh scripts"}; + response = { + items: [ + { + name: "Google Images Restored", + filename: "Google Images Restored.js", + disabled: false, + type: "js", + }, + { + name: "Wikipedia Mobile Redirect", + filename: "Wikipedia Mobile Redirect.js", + disabled: true, + type: "js", + }, + { + name: "A Special Userscript", + filename: "A Special Userscript.js", + disabled: false, + type: "js", + }, + { + name: "CSS Adblock", + filename: "CSS Adblock.css", + disabled: false, + type: "css", + }, + { + name: "New Userscript With a Really Really Long Name", + filename: "New Userscript With a Really Really Long Name.css", + disabled: true, + type: "css", + }, + { + name: "Subframe Script Managerial Staffing Company", + filename: "Subframe Script.js", + disabled: false, + subframe: true, + type: "css", + }, + ], + updates: [], + }; + } else if (name === "POPUP_CHECK_UPDATES") { + response = { + updates: [ + { + filename: "Google Images Restored.js", + name: "Google Images Restored", + url: "https://www.k21p.com", + }, + { + filename: "New Userscript With a Really Really Long Name.js", + name: "New Userscript With a Really Really Long Name", + url: "https://www.filmgarb.com", + }, + ], + }; + } else if (name === "POPUP_MATCHES") { + response = { + matches: [ + ...files, + { + name: "Subframe Script Managerial Staffing Company", + filename: "Subframe Script.js", + disabled: false, + subframe: true, + type: "js", + }, + ], + }; + } else if (name === "POPUP_UPDATES") { + response = { + updates: [ + { + filename: "Google Images Restored.js", + name: "Google Images Restored", + url: "https://www.k21p.com", + }, + { + filename: "New Userscript With a Really Really Long Name.js", + name: "New Userscript With a Really Really Long Name", + url: "https://www.filmgarb.com", + }, + ], + }; + response.updates = []; + } else if (name === "REQ_PLATFORM") { + response = { platform: _browser.platform }; + } else if (name === "POPUP_OPEN_EXTENSION_PAGE") { + response = { error: "Failed to get page url" }; + window.open("https://github.com/quoid/userscripts"); + } else if (name === "OPEN_SAVE_LOCATION") { + if (_browser.platform === "macos") { + response = { success: true }; + } else { + response = { + items: [ + { + name: "Google Images Restored", + filename: "Google Images Restored.js", + disabled: false, + type: "js", + metadata: [], + }, + { + name: "Subframe Script Managerial Staffing Company", + filename: "Subframe Script.js", + disabled: false, + type: "css", + metadata: [], + }, + { + name: "Another Script from Managerial Staffing Company", + filename: "Cool Script.js", + disabled: false, + type: "js", + metadata: [], + }, + ], + }; + } + } else if (name === "POPUP_INSTALL_CHECK") { + response = random([ + { success: "Click to install", installed: false }, + { success: "Click to re-install", installed: true }, + ]); + response.metadata = { + description: + 'This userscript re-implements the "View Image" and "Search by image" buttons into google images.', + grant: ["GM.getValue", "GM.setValue", "GM.xmlHttpRequest"], + match: [ + "https://www.example.com/*", + "https://www.example.com/somethingReallylong/goesRightHere", + ], + name: "Test Install Userscript", + require: [ + "https://code.jquery.com/jquery-3.5.1.min.js", + "https://code.jquery.com/jquery-1.7.1.min.js", + ], + source: + "https://greasyforx.org/scripts/00000-something-something-long-name/code/Something%20something%20long20name.user.js", + }; + // response.error = "something went wrong (dev)"; + } + if (!responseCallback) { + return new Promise((resolve) => { + setTimeout(() => resolve(response), _browser.delay); + }); + } + setTimeout(() => responseCallback(response), _browser.delay); + }, + connectNative: () => ({ + onMessage: { + addListener: () => console.info("connectNative - addListener"), + }, + }), + }, + tabs: { + getCurrent(/* responseCallback */) { + const response = random([ + { url: "https://www.filmgarb.com/foo.user.js", id: 101 }, + { + url: `${window.location.origin}/src/ext/shared/dev/DEMO.Alert-URL.user.js`, + id: 102, + }, + // increase probability + { + url: `${window.location.origin}/src/ext/shared/dev/DEMO.Alert-URL.user.js`, + id: 103, + }, + { url: window.location.href, id: 10 }, + ]); + console.info("browser.tabs.getCurrent", response); + return new Promise((resolve) => { + setTimeout(() => resolve(response), _browser.delay); + }); + }, + query(message, responseCallback) { + const response = [ + { url: "https://www.filmgarb.com/foo.user.js", id: 101 }, + ]; + if (!responseCallback) { + return new Promise((resolve) => { + setTimeout(() => resolve(response), _browser.delay); + }); + } + setTimeout(() => responseCallback(response), _browser.delay); + }, + sendMessage(tabId, message, responseCallback) { + let response = {}; + if (message.name === "DEMO_MSG") { + response = {}; + // response.error = "something went wrong (dev)"; + } + if (!responseCallback) { + return new Promise((resolve) => { + setTimeout(() => resolve(response), _browser.delay); + }); + } + setTimeout(() => responseCallback(response), _browser.delay); + }, + }, + webNavigation: { + getAllFrames(message, responseCallback) { + const response = []; + if (!responseCallback) { + return new Promise((resolve) => { + setTimeout(() => resolve(response), _browser.delay); + }); + } + setTimeout(() => responseCallback(response), _browser.delay); + }, + }, + storage: { + local: { + get(items, responseCallback) { + const response = {}; + if (!responseCallback) { + return new Promise((resolve) => { + setTimeout(() => resolve(response), _browser.delay); + }); + } + setTimeout(() => responseCallback(response), _browser.delay); + }, + set() { + return new Promise((resolve) => { + setTimeout(() => resolve(), _browser.delay); + }); + }, + remove: () => + new Promise((resolve) => { + setTimeout(() => resolve(), _browser.delay); + }), + }, + onChanged: { + addListener: () => undefined, + }, + }, +}; + +async function getRemoteFileContents(url) { + const result = {}; + await fetch(url) + .then((response) => { + if (!response.ok) throw Error(response.statusText); + return response.text(); + }) + .then((text) => { + result.content = text; + }) + .catch((error) => { + console.error(error); + result.error = "Remote url bad response!"; + }); + return result; +} + +function saveFile(content, lastMod, newFilename, oldName) { + const ind = files.findIndex((f) => f.filename === oldName); + const s = { + content, + filename: newFilename, + lastModified: lastMod, + }; + if (ind !== -1) { + // overwrite at index + files[ind] = s; + } else { + // add to beginning of array + files.unshift(s); + } +} + +function random(array) { + return array[Math.floor(Math.random() * array.length)]; +} + +export const browser = _browser; + +export default { + browser: _browser, +}; diff --git a/src/shared/dev/DEMO.Alert-URL.user.js b/src/ext/shared/dev/DEMO.Alert-URL.user.js similarity index 83% rename from src/shared/dev/DEMO.Alert-URL.user.js rename to src/ext/shared/dev/DEMO.Alert-URL.user.js index 97868afb..2b093394 100644 --- a/src/shared/dev/DEMO.Alert-URL.user.js +++ b/src/ext/shared/dev/DEMO.Alert-URL.user.js @@ -11,6 +11,6 @@ /* eslint-disable */ (function () { - 'use strict'; - alert("DEBUG.Alert-URL:\n\n" + location); -})(); \ No newline at end of file + "use strict"; + alert("DEBUG.Alert-URL:\n\n" + location); +})(); diff --git a/src/shared/img/icon-arrow-down.svg b/src/ext/shared/img/icon-arrow-down.svg similarity index 100% rename from src/shared/img/icon-arrow-down.svg rename to src/ext/shared/img/icon-arrow-down.svg diff --git a/src/shared/img/icon-arrow-left.svg b/src/ext/shared/img/icon-arrow-left.svg similarity index 100% rename from src/shared/img/icon-arrow-left.svg rename to src/ext/shared/img/icon-arrow-left.svg diff --git a/src/shared/img/icon-arrow-up.svg b/src/ext/shared/img/icon-arrow-up.svg similarity index 100% rename from src/shared/img/icon-arrow-up.svg rename to src/ext/shared/img/icon-arrow-up.svg diff --git a/src/shared/img/icon-check.svg b/src/ext/shared/img/icon-check.svg similarity index 100% rename from src/shared/img/icon-check.svg rename to src/ext/shared/img/icon-check.svg diff --git a/src/shared/img/icon-clear.svg b/src/ext/shared/img/icon-clear.svg similarity index 100% rename from src/shared/img/icon-clear.svg rename to src/ext/shared/img/icon-clear.svg diff --git a/src/shared/img/icon-close.svg b/src/ext/shared/img/icon-close.svg similarity index 100% rename from src/shared/img/icon-close.svg rename to src/ext/shared/img/icon-close.svg diff --git a/src/shared/img/icon-download.svg b/src/ext/shared/img/icon-download.svg similarity index 100% rename from src/shared/img/icon-download.svg rename to src/ext/shared/img/icon-download.svg diff --git a/src/shared/img/icon-edit.svg b/src/ext/shared/img/icon-edit.svg similarity index 100% rename from src/shared/img/icon-edit.svg rename to src/ext/shared/img/icon-edit.svg diff --git a/src/shared/img/icon-error.svg b/src/ext/shared/img/icon-error.svg similarity index 100% rename from src/shared/img/icon-error.svg rename to src/ext/shared/img/icon-error.svg diff --git a/src/shared/img/icon-info.svg b/src/ext/shared/img/icon-info.svg similarity index 100% rename from src/shared/img/icon-info.svg rename to src/ext/shared/img/icon-info.svg diff --git a/src/shared/img/icon-loader.svg b/src/ext/shared/img/icon-loader.svg similarity index 100% rename from src/shared/img/icon-loader.svg rename to src/ext/shared/img/icon-loader.svg diff --git a/src/shared/img/icon-loupe.svg b/src/ext/shared/img/icon-loupe.svg similarity index 100% rename from src/shared/img/icon-loupe.svg rename to src/ext/shared/img/icon-loupe.svg diff --git a/src/shared/img/icon-open.svg b/src/ext/shared/img/icon-open.svg similarity index 100% rename from src/shared/img/icon-open.svg rename to src/ext/shared/img/icon-open.svg diff --git a/src/shared/img/icon-plus.svg b/src/ext/shared/img/icon-plus.svg similarity index 100% rename from src/shared/img/icon-plus.svg rename to src/ext/shared/img/icon-plus.svg diff --git a/src/shared/img/icon-power.svg b/src/ext/shared/img/icon-power.svg similarity index 100% rename from src/shared/img/icon-power.svg rename to src/ext/shared/img/icon-power.svg diff --git a/src/shared/img/icon-refresh.svg b/src/ext/shared/img/icon-refresh.svg similarity index 100% rename from src/shared/img/icon-refresh.svg rename to src/ext/shared/img/icon-refresh.svg diff --git a/src/shared/img/icon-settings.svg b/src/ext/shared/img/icon-settings.svg similarity index 100% rename from src/shared/img/icon-settings.svg rename to src/ext/shared/img/icon-settings.svg diff --git a/src/shared/img/icon-sort.svg b/src/ext/shared/img/icon-sort.svg similarity index 100% rename from src/shared/img/icon-sort.svg rename to src/ext/shared/img/icon-sort.svg diff --git a/src/shared/img/icon-sync.svg b/src/ext/shared/img/icon-sync.svg similarity index 100% rename from src/shared/img/icon-sync.svg rename to src/ext/shared/img/icon-sync.svg diff --git a/src/shared/img/icon-trash.svg b/src/ext/shared/img/icon-trash.svg similarity index 100% rename from src/shared/img/icon-trash.svg rename to src/ext/shared/img/icon-trash.svg diff --git a/src/shared/img/icon-update.svg b/src/ext/shared/img/icon-update.svg similarity index 100% rename from src/shared/img/icon-update.svg rename to src/ext/shared/img/icon-update.svg diff --git a/src/shared/img/icon-warn.svg b/src/ext/shared/img/icon-warn.svg similarity index 100% rename from src/shared/img/icon-warn.svg rename to src/ext/shared/img/icon-warn.svg diff --git a/src/ext/shared/img/logo.svg b/src/ext/shared/img/logo.svg new file mode 100644 index 00000000..9bf95127 --- /dev/null +++ b/src/ext/shared/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/ext/shared/native.js b/src/ext/shared/native.js new file mode 100644 index 00000000..524f3613 --- /dev/null +++ b/src/ext/shared/native.js @@ -0,0 +1,28 @@ +/** + * @file exchange messages between `web extension` and `native application` + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging} + * @see {@link https://developer.apple.com/documentation/safariservices/safari_web_extensions/messaging_between_the_app_and_javascript_in_a_safari_web_extension} + */ + +/** + * @see {@link https://vitejs.dev/guide/env-and-mode.html} + * @returns {string} + */ +const application = () => import.meta.env.NATIVE_APP ?? "application"; + +/** + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative} + * @returns {object} A `runtime.Port` object + */ +export function connectNative() { + return browser.runtime.connectNative(application()); +} + +/** + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage} + * @param {any} message + * @returns {Promise} A response of JSON object or no arguments + */ +export function sendNativeMessage(message) { + return browser.runtime.sendNativeMessage(application(), message); +} diff --git a/src/shared/reset.css b/src/ext/shared/reset.css similarity index 51% rename from src/shared/reset.css rename to src/ext/shared/reset.css index 9ea2b45a..c3329b07 100644 --- a/src/shared/reset.css +++ b/src/ext/shared/reset.css @@ -1,53 +1,53 @@ *, *::before, *::after { - box-sizing: border-box; + box-sizing: border-box; } body, p { - margin: 0; + margin: 0; } ul { - list-style: none; - margin: 0; - padding: 0; + list-style: none; + margin: 0; + padding: 0; } button, input, select, textarea { - font-family: inherit; - font-size: 100%; - line-height: inherit; - margin: 0; + font-family: inherit; + font-size: 100%; + line-height: inherit; + margin: 0; } button, input[type="button"], input[type="reset"], input[type="submit"] { - -webkit-appearance: button; + -webkit-appearance: button; } input[type="search"] { - -webkit-appearance: textfield; + -webkit-appearance: textfield; } ::-webkit-input-placeholder { - color: inherit; - opacity: 0.54; + color: inherit; + opacity: 0.54; } ::-webkit-search-decoration, ::-webkit-search-cancel-button, ::-webkit-search-results-button, ::-webkit-search-results-decoration { - -webkit-appearance: none; + -webkit-appearance: none; } img { - border-style: none; + border-style: none; } diff --git a/src/ext/shared/settings.js b/src/ext/shared/settings.js new file mode 100644 index 00000000..92ed61bf --- /dev/null +++ b/src/ext/shared/settings.js @@ -0,0 +1,789 @@ +/** + * @file wrap a relatively independent settings storage with its own functions + */ + +/** @type {string} */ +const storagePrefix = "US_"; + +/** + * Convert name to storage key + * @param {string} name + * @returns {string} prefixed storage key + */ +const storageKey = (name) => storagePrefix + name.toUpperCase(); + +/** + * Dynamic storage reference + * @param {"sync"|"local"|undefined} area + * @returns {Promise} + */ +const storageRef = async (area) => { + const storages = { + sync: { + area: "sync", + ref: browser.storage.sync, + }, + local: { + area: "local", + ref: browser.storage.local, + }, + }; + // https://developer.apple.com/documentation/safariservices/safari_web_extensions/assessing_your_safari_web_extension_s_browser_compatibility#3584139 + // since storage sync is not implemented in Safari, currently only returns using local storage + if (import.meta.env.BROWSER === "safari") { + return storages.local; + } + if (area in storages) { + return storages[area]; + } else if (area === undefined) { + const key = storageKey("settings_sync"); + const result = await browser.storage.local.get(key); + return result?.[key] ? storages.sync : storages.local; + } else { + return Promise.reject(new Error(`invalid area ${area}`)); + } +}; + +const settingDefault = deepFreeze({ + name: "setting_default", + type: undefined, + local: false, + values: [], + default: undefined, + protect: false, + confirm: false, + platforms: ["macos", "ipados", "ios"], + langLabel: {}, + langTitle: {}, + group: "", + legacy: "", + nodeType: "", + nodeClass: {}, +}); + +export const settingsDefine = deepFreeze( + [ + { + name: "error_native", + type: "object", + local: true, + default: { error: undefined }, + platforms: ["macos", "ipados", "ios"], + group: "Internal", + }, + { + name: "legacy_imported", + type: "number", + local: true, + default: 0, + protect: true, + platforms: ["macos"], + group: "Internal", + }, + { + name: "language_code", + type: "string", + default: "en", + platforms: ["macos", "ipados", "ios"], + group: "Internal", + legacy: "languageCode", + }, + { + name: "scripts_settings", + type: "object", + default: {}, + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "Scripts update check active", + zh_hans: "脚本更新检查激活", + }, + langTitle: { + en: "Whether to enable each single script update check", + zh_hans: "是否开启单个脚本更新检查", + }, + group: "Internal", + nodeType: "Subpage", + }, + // { + // name: "settings_sync", + // type: "boolean", + // local: true, + // default: false, + // protect: true, + // platforms: ["macos", "ipados", "ios"], + // langLabel: { + // en: "Sync settings", + // zh_hans: "同步设置" + // }, + // langTitle: { + // en: "Sync settings across devices", + // zh_hans: "跨设备同步设置" + // }, + // group: "General", + // nodeType: "Toggle" + // }, + { + name: "toolbar_badge_count", + type: "boolean", + default: true, + platforms: ["macos", "ipados"], + langLabel: { + en: "Show Toolbar Count", + zh_hans: "工具栏图标显示计数徽章", + }, + langTitle: { + en: "displays a badge on the toolbar icon with a number that represents how many enabled scripts match the url for the page you are on", + zh_hans: "简体中文描述", + }, + group: "General", + legacy: "showCount", + nodeType: "Toggle", + }, + { + name: "global_active", + type: "boolean", + local: true, + default: true, + platforms: ["macos"], + langLabel: { + en: "Enable Injection", + zh_hans: "启用注入", + }, + langTitle: { + en: "toggle on/off script injection for the pages you visit", + zh_hans: "简体中文描述", + }, + group: "General", + legacy: "active", + nodeType: "Toggle", + nodeClass: { red: false }, + }, + { + name: "global_scripts_update_check", + type: "boolean", + default: true, + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "Global scripts update check", + zh_hans: "全局脚本更新检查", + }, + langTitle: { + en: "Whether to enable global periodic script update check", + zh_hans: "是否开启全局定期脚本更新检查", + }, + group: "General", + nodeType: "Toggle", + }, + { + name: "scripts_update_check_interval", + type: "number", + default: 86400000, + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "Scripts update check interval", + zh_hans: "脚本更新检查间隔", + }, + langTitle: { + en: "The interval for script update check in background", + zh_hans: "脚本更新检查的间隔时间", + }, + group: "General", + nodeType: "Toggle", + }, + { + name: "scripts_update_check_lasttime", + type: "number", + default: 0, + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "Scripts update check lasttime", + zh_hans: "脚本更新上次检查时间", + }, + langTitle: { + en: "The lasttime for script update check in background", + zh_hans: "后台脚本更新上次检查时间", + }, + group: "Internal", + }, + { + name: "scripts_auto_update", + type: "boolean", + default: false, + confirm: true, + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "Scripts silent auto update", + zh_hans: "脚本后台静默自动更新", + }, + langTitle: { + en: "Script silently auto-updates in the background, which is dangerous and may introduce unconfirmed malicious code", + zh_hans: + "脚本在后台静默自动更新,这是危险的,可能引入未经确认的恶意代码", + }, + group: "General", + nodeType: "Toggle", + nodeClass: { warn: true }, + }, + { + name: "global_exclude_match", + type: "object", + default: [], + platforms: ["macos", "ipados", "ios"], + langLabel: { + en: "Global exclude match patterns", + zh_hans: "全局排除匹配模式列表", + }, + langTitle: { + en: "this input accepts a comma separated list of @match patterns, a page url that matches against a pattern in this list will be ignored for script injection", + zh_hans: "简体中文描述", + }, + group: "General", + legacy: "blacklist", + nodeType: "textarea", + nodeClass: { red: "blacklistError" }, + }, + { + name: "editor_close_brackets", + type: "boolean", + default: true, + platforms: ["macos"], + langLabel: { + en: "Auto Close Brackets", + zh_hans: "自动关闭括号", + }, + langTitle: { + en: "toggles on/off auto closing of brackets in the editor, this affects the following characters: () [] {} \"\" ''", + zh_hans: "简体中文描述", + }, + group: "Editor", + legacy: "autoCloseBrackets", + nodeType: "Toggle", + }, + { + name: "editor_auto_hint", + type: "boolean", + default: true, + platforms: ["macos"], + langLabel: { + en: "Auto Hint", + zh_hans: "自动提示(Hint)", + }, + langTitle: { + en: "automatically shows completion hints while editing", + zh_hans: "简体中文描述", + }, + group: "Editor", + legacy: "autoHint", + nodeType: "Toggle", + }, + { + name: "editor_list_sort", + type: "string", + values: ["nameAsc", "nameDesc", "lastModifiedAsc", "lastModifiedDesc"], + default: "lastModifiedDesc", + platforms: ["macos"], + langLabel: { + en: "Sort order", + zh_hans: "排序顺序", + }, + langTitle: { + en: "Display order of items in sidebar", + zh_hans: "侧栏中项目的显示顺序", + }, + group: "Editor", + legacy: "sortOrder", + nodeType: "Dropdown", + }, + { + name: "editor_list_descriptions", + type: "boolean", + default: true, + platforms: ["macos"], + langLabel: { + en: "Show List Descriptions", + zh_hans: "显示列表项目描述", + }, + langTitle: { + en: "show or hides the item descriptions in the sidebar", + zh_hans: "简体中文描述", + }, + group: "Editor", + legacy: "descriptions", + nodeType: "Toggle", + }, + { + name: "editor_javascript_lint", + type: "boolean", + default: false, + platforms: ["macos"], + langLabel: { + en: "Javascript Linter", + zh_hans: "Javascript Linter", + }, + langTitle: { + en: "toggles basic Javascript linting within the editor", + zh_hans: "简体中文描述", + }, + group: "Editor", + legacy: "lint", + nodeType: "Toggle", + }, + { + name: "editor_show_whitespace", + type: "boolean", + default: true, + platforms: ["macos"], + langLabel: { + en: "Show whitespace characters", + zh_hans: "显示空白字符", + }, + langTitle: { + en: "toggles the display of invisible characters in the editor", + zh_hans: "简体中文描述", + }, + group: "Editor", + legacy: "showInvisibles", + nodeType: "Toggle", + }, + { + name: "editor_tab_size", + type: "number", + values: [2, 4], + default: 4, + platforms: ["macos"], + langLabel: { + en: "Tab Size", + zh_hans: "制表符大小", + }, + langTitle: { + en: "the number of spaces a tab is equal to while editing", + zh_hans: "简体中文描述", + }, + group: "Editor", + legacy: "tabSize", + nodeType: "select", + }, + ].reduce(settingsDefineReduceCallback, {}), +); + +/** + * populate the settingsDefine with settingDefault + * and convert settingsDefine to storageKey object + * @param {object} settings new settings object + * @param {object} setting each setting define + * @returns {object} {US_GLOBAL_ACTIVE: {key: US_GLOBAL_ACTIVE, name: global_active, ... }, ...} + */ +function settingsDefineReduceCallback(settings, setting) { + setting.key = storageKey(setting.name); + settings[setting.key] = { ...settingDefault, ...setting }; + return settings; +} + +/** + * prevent settings define from being modified in any case + * otherwise user settings may be lost in the worst case + * @param {object} object any object + * @returns {object} deep frozen object + */ +function deepFreeze(object) { + for (const p in object) { + if (typeof object[p] == "object") { + deepFreeze(object[p]); + } + } + return Object.freeze(object); +} + +/** + * compatibility polyfill for Safari < 15.4 + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn#browser_compatibility} + * @todo remove this polyfill when set safari strict_min_version 15.4 + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings#safari_properties} + */ +if (Object.hasOwn === undefined) { + Object.hasOwn = (obj, prop) => + Object.prototype.hasOwnProperty.call(obj, prop); +} + +// export and define the operation method of settings storage +// they are similar to browser.storage but slightly different + +/** + * settings.get + * @param {string|Array} keys key | array of keys | undefined for all + * @param {"local"|"sync"} area + * @returns {Promise} settings object + */ +export async function get(keys = undefined, area = undefined) { + if (![undefined, "local", "sync"].includes(area)) { + return console.error("Unexpected storage area:", area); + } + // validate setting value and fix surprises to default + const valueFix = (key, val) => { + if (!key || !Object.hasOwn(settingsDefine, key)) return; + const def = settingsDefine[key].default; + // check if value type conforms to settingsDefine + const type = settingsDefine[key].type; + // eslint-disable-next-line valid-typeof -- type known to be valid string literal + if (typeof val != type) { + console.warn( + `Unexpected ${key} value type '${typeof val}' should '${type}', fix to default`, + ); + return def; + } + // check if value conforms to settingsDefine + const values = settingsDefine[key].values; + if (values.length && !values.includes(val)) { + console.warn( + `Unexpected ${key} value '${val}' should one of '${values}', fix to default`, + ); + return def; + } + // verified, pass original value + return val; + }; + // [single setting] + if (typeof keys == "string") { + const key = storageKey(keys); + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { + return console.error("unexpected settings key:", key); + } + // check if only locally stored setting + // eslint-disable-next-line no-param-reassign -- change the area is expected + settingsDefine[key].local === true && (area = "local"); + const storage = await storageRef(area); + const result = await storage.ref.get(key); + if (Object.hasOwn(result, key)) return valueFix(key, result[key]); + return settingsDefine[key].default; + } + const complexGet = async (settingsDefault, areaKeys) => { + const storage = await storageRef(area); + let local = {}, + sync = {}; + if (storage.area === "sync") { + if (areaKeys.sync.length) { + sync = await storage.ref.get(areaKeys.sync); + } + if (areaKeys.local.length) { + local = await browser.storage.local.get(areaKeys.local); + } + } else { + local = await storage.ref.get(areaKeys.all); + } + const result = Object.assign(settingsDefault, local, sync); + // revert settings object property name + return Object.entries(result).reduce((p, c) => { + p[settingsDefine[c[0]].name] = valueFix(...c); + return p; + }, {}); + }; + // [muilt settings] + if (Array.isArray(keys)) { + if (!keys.length) { + return console.error("Settings keys empty:", keys); + } + const settingsDefault = {}; + const areaKeys = { local: [], sync: [], all: [] }; + for (const k of keys) { + const key = storageKey(k); + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { + return console.error("unexpected settings key:", key); + } + settingsDefault[key] = settingsDefine[key].default; + // detach only locally stored settings + settingsDefine[key].local === true + ? areaKeys.local.push(key) + : areaKeys.sync.push(key); + // record all keys in case sync storage is not enabled + areaKeys.all.push(key); + } + return complexGet(settingsDefault, areaKeys); + } + // [all settings] + if (typeof keys == "undefined" || keys === null) { + const settingsDefault = {}; + const areaKeys = { local: [], sync: [], all: [] }; + for (const key of Object.keys(settingsDefine)) { + settingsDefault[key] = settingsDefine[key].default; + // detach only locally stored settings + settingsDefine[key].local === true + ? areaKeys.local.push(key) + : areaKeys.sync.push(key); + // record all keys in case sync storage is not enabled + areaKeys.all.push(key); + } + return complexGet(settingsDefault, areaKeys); + } + return console.error("Unexpected keys type:", keys); +} + +/** + * settings.set + * @param {object} keys settings object + * @param {"local"|"sync"} area + * @returns {Promise} + */ +export async function set(keys, area = undefined) { + if (![undefined, "local", "sync"].includes(area)) { + return console.error("unexpected storage area:", area); + } + if (typeof keys != "object") { + return console.error("Unexpected keys type:", keys); + } + if (!Object.keys(keys).length) { + return console.error("Settings object empty:", keys); + } + const areaKeys = { local: {}, sync: {}, all: {} }; + for (const k of Object.keys(keys)) { + const key = storageKey(k); + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { + return console.error("Unexpected settings keys:", key); + } + // check if value type conforms to settingsDefine + const type = settingsDefine[key].type; + // eslint-disable-next-line valid-typeof -- type known to be valid string literal + if (typeof keys[k] != type) { + if (type === "number" && !Number.isNaN(Number(keys[k]))) { + // compatible with string numbers + keys[k] = Number(keys[k]); // still store it as a number type + } else { + return console.error( + `Unexpected ${k} value type '${typeof keys[k]}' should '${type}'`, + ); + } + } + // check if value conforms to settingsDefine + const values = settingsDefine[key].values; + if (values.length && !values.includes(keys[k])) { + return console.error( + `Unexpected ${k} value '${keys[k]}' should one of '${values}'`, + ); + } + // detach only locally stored settings + settingsDefine[key].local === true + ? (areaKeys.local[key] = keys[k]) + : (areaKeys.sync[key] = keys[k]); + // record all keys in case sync storage is not enabled + areaKeys.all[key] = keys[k]; + } + const storage = await storageRef(area); + // complexSet + try { + if (storage.area === "sync") { + if (Object.keys(areaKeys.sync).length) { + await storage.ref.set(areaKeys.sync); + } + if (Object.keys(areaKeys.local).length) { + await browser.storage.local.set(areaKeys.local); + } + } else { + await storage.ref.set(areaKeys.all); + } + return true; + } catch (error) { + return console.error(error); + } +} + +/** + * settings.reset + * reset to default + * @param {string|Array} keys key | array of keys | undefined for all + * @param {"local"|"sync"} area + * @returns {Promise} + */ +export async function reset(keys = undefined, area = undefined) { + if (![undefined, "local", "sync"].includes(area)) { + return console.error("unexpected storage area:", area); + } + // [single setting] + if (typeof keys == "string") { + const key = storageKey(keys); + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { + return console.error("unexpected settings key:", key); + } + // check if key is protected + if (settingsDefine[key].protect === true) { + return console.error("protected settings key:", key); + } + // eslint-disable-next-line no-param-reassign -- change the area is expected + settingsDefine[key].local === true && (area = "local"); + const storage = await storageRef(area); + return storage.ref.remove(key); + } + const complexRemove = async (areaKeys) => { + const storage = await storageRef(area); + try { + if (storage.area === "sync") { + if (areaKeys.sync.length) { + await storage.ref.remove(areaKeys.sync); + } + if (areaKeys.local.length) { + await browser.storage.local.remove(areaKeys.local); + } + } else { + await storage.ref.remove(areaKeys.all); + } + return true; + } catch (error) { + return console.error(error); + } + }; + // [muilt settings] + if (Array.isArray(keys)) { + if (!keys.length) { + return console.error("Settings keys empty:", keys); + } + const areaKeys = { local: [], sync: [], all: [] }; + for (const k of keys) { + const key = storageKey(k); + // check if key exist in settingsDefine + if (!Object.hasOwn(settingsDefine, key)) { + return console.error("unexpected settings key:", key); + } + // check if key is protected + if (settingsDefine[key].protect === true) { + return console.error("protected settings key:", key); + } + // detach only locally stored settings + settingsDefine[key].local === true + ? areaKeys.local.push(key) + : areaKeys.sync.push(key); + // record all keys in case sync storage is not enabled + areaKeys.all.push(key); + } + return complexRemove(areaKeys); + } + // [all settings] + if (typeof keys == "undefined" || keys === null) { + const areaKeys = { local: [], sync: [], all: [] }; + for (const key in settingsDefine) { + // skip protected keys + if (settingsDefine[key].protect === true) continue; + // detach only locally stored settings + settingsDefine[key].local === true + ? areaKeys.local.push(key) + : areaKeys.sync.push(key); + // record all keys in case sync storage is not enabled + areaKeys.all.push(key); + } + return complexRemove(areaKeys); + } + return console.error("Unexpected keys type:", keys); +} + +/** + * complex onChanged + * this function is convenient for the svelte store to update the state + * @param {Function} callback + * @returns {void} + */ +export function onChangedSettings(callback) { + if (typeof callback != "function") { + return console.error("Unexpected callback:", callback); + } + console.info("storage onChanged addListener"); + /** + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/onChanged#listener} + * @param {object} changes + * @param {"sync"|"local"} area + */ + const listener = (changes, area) => { + // console.log(`storage.${area}.onChanged`, changes); // DEBUG + const settings = {}; + for (const key in changes) { + if (!Object.hasOwn(settingsDefine, key)) continue; + settings[settingsDefine[key].name] = changes[key].newValue; + } + try { + callback(settings, area); + } catch (error) { + console.error("onChanged callback:", error); + } + }; + browser.storage.onChanged.addListener(listener); +} + +// the following functions are used only for compatibility transition periods +// these functions will be removed in the future, perhaps in version 5.0 + +/** + * settings.legacyGet + * @param {string|Array.} keys + * @returns {Promise} settings object with legacy keys + */ +export async function legacyGet(keys = undefined) { + const result = await get(keys); + // console.log("legacy_get", keys, result); + for (const key of Object.keys(result)) { + const legacy = settingsDefine[storageKey(key)]?.legacy; + if (legacy) result[legacy] = result[key]; + } + return result; +} + +/** + * settings.legacySet + * @param {object} keys legacy keys + * @returns {Promise} + */ +export async function legacySet(keys) { + if (typeof keys != "object") { + return console.error("Unexpected arg type:", keys); + } + if (!Object.keys(keys).length) { + return console.error("Settings object empty:", keys); + } + const settings = {}; + for (const key of Object.keys(settingsDefine)) { + const setting = settingsDefine[key]; + if (!setting.legacy) continue; + if (setting.legacy in keys) { + settings[setting.name] = keys[setting.legacy]; + } + } + // console.log("legacy_set", keys, settings); + return set(settings); +} + +export async function legacyImport() { + // if legacy data has already been imported, skip this process + const imported = await get("legacy_imported"); + if (imported) return console.info("Legacy settings has already imported"); + // start the one-time import process + const result = await browser.runtime.sendNativeMessage("app", { + name: "PAGE_LEGACY_IMPORT", + }); + if (result.error) return console.error(result.error); + console.info("Import settings data from legacy manifest file"); + const settings = {}; + for (const key of Object.keys(settingsDefine)) { + const legacy = settingsDefine[key].legacy; + if (legacy in result) { + let value = result[legacy]; + switch (settingsDefine[key].type) { + case "boolean": + value = JSON.parse(value); + break; + case "number": + value = Number(value); + break; + } + console.info(`Importing legacy setting: ${legacy}`, value); + settings[settingsDefine[key].name] = value; + } + } + // import complete tag, to ensure will only be import once + Object.assign(settings, { legacy_imported: Date.now() }); + if (await set(settings, "local")) { + console.info("Import legacy settings complete"); + // send a message to the Swift layer to safely clean up legacy data + // browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); + return true; + } + return console.error("Import legacy settings abort"); +} diff --git a/src/ext/shared/utils.js b/src/ext/shared/utils.js new file mode 100644 index 00000000..ca1788ec --- /dev/null +++ b/src/ext/shared/utils.js @@ -0,0 +1,256 @@ +/** + * @param {number} ms millisecond timestamp + * @returns {string} + */ +export function formatDate(ms) { + const d = new Date(ms); + const yr = new Intl.DateTimeFormat("en", { year: "numeric" }).format(d); + const mo = new Intl.DateTimeFormat("en", { month: "short" }).format(d); + const dd = new Intl.DateTimeFormat("en", { day: "2-digit" }).format(d); + const hr = d.getHours(); + const mn = d.getMinutes(); + return `${mo} ${dd}, ${yr} at ${hr}:${mn}`; +} + +export function uniqueId() { + return Math.random().toString(36).substring(2, 10); +} + +/** + * awaitable function for waiting an arbitrary amount of time + * @param {number} ms the amount of time to wait in milliseconds + * @returns {Promise} + */ +export function wait(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +// TODO: describe the items array that should get passed to this function +/** + * @param {Array} array + * @param {("lastModifiedAsc"|"lastModifiedDesc"|"nameAsc"|"nameDesc")} order + * @returns + */ +export function sortBy(array, order) { + if (order === "nameAsc") { + array.sort((a, b) => a.name.localeCompare(b.name)); + } else if (order === "nameDesc") { + array.sort((a, b) => b.name.localeCompare(a.name)); + } else if (order === "lastModifiedAsc") { + array.sort((a, b) => (a.lastModified < b.lastModified ? -1 : 1)); + } else if (order === "lastModifiedDesc") { + array.sort((a, b) => (a.lastModified > b.lastModified ? -1 : 1)); + } + // always keep temp file pinned to the top, should only ever have one temp script + // if (array.find(f => f.temp)) array.sort((a, b) => a.temp ? -1 : b.temp ? 1 : 0); + return array; +} + +/** + * + * @param {string} description + * @param {string} name + * @param {("css"|"js")} type + * @returns {string} + */ +export function newScriptDefault(description, name, type) { + if (type === "css") { + return `/* ==UserStyle==\n@name ${name}\n@description ${description}\n@match \n==/UserStyle== */`; + } + return `// ==UserScript==\n// @name ${name}\n// @description ${description}\n// @match *://*/*\n// ==/UserScript==`; +} + +/** + * @param {string} str + * @returns {?{code: string, content: str, metablock: string, metadata: object}} + */ +export function parse(str) { + if (typeof str != "string") return null; + const blocksReg = + /(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))/; + const blocks = str.match(blocksReg); + + if (!blocks) return null; + + const metablock = blocks[1] != null ? blocks[1] : blocks[4]; + const metas = blocks[2] != null ? blocks[2] : blocks[5]; + const code = blocks[3] != null ? blocks[3].trim() : blocks[6].trim(); + + const metadata = {}; + const metaArray = metas.split("\n"); + metaArray.forEach((m) => { + const parts = m + .trim() + .match(/^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)/); + const parts2 = m + .trim() + .match(/^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$/); + if (parts) { + metadata[parts[1]] = metadata[parts[1]] || []; + metadata[parts[1]].push(parts[2]); + } else if (parts2) { + metadata[parts2[1]] = metadata[parts2[1]] || []; + metadata[parts2[1]].push(true); + } + }); + // fail if @name is missing or name is empty + if (!metadata.name || metadata.name[0].length < 2) return; + + return { + code, + content: str, + metablock, + metadata, + }; +} + +/** + * @param {string} text editor code + * @returns {{match: boolean, meta: boolean} | {key: string, value: string, text: string}[]} + */ +export function parseMetadata(text) { + const groupsRe = + /(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)/; + const groups = text.match(groupsRe); + // userscript code doesn't match the regex expression + // could be missing opening/closing tags, malformed + // or missing metadata between opening/closing tags (group 2 in regex exp) + if (!groups) { + return { match: false, meta: false }; + } + + // userscript code matches but content between opening/closing tag missing + // ex. opening/closing tags present, but newline characters between the tags + const metas = groups[2]; + if (!metas) return { match: true, meta: false }; + + const metadata = []; + const metaArray = metas.split("\n"); + + for (let i = 0; i < metaArray.length; i++) { + const metaRegex = + /^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]*([^\s]+[^\r\n\t\v\f]*)?/; + const meta = metaArray[i]; + const parts = meta.match(metaRegex); + if (parts) + metadata.push({ key: parts[1], value: parts[2], text: parts[0] }); + } + + // if there is content between the opening/closing tags, match will be found + // this additionally checks that there's at least one properly formed key + // if not keys found, assume metadata is missing + // checking that required keys are present will happen elsewhere + if (!Object.keys(metadata).length) return { match: true, meta: false }; + + return metadata; +} + +export const validGrants = new Set([ + "GM.info", + "GM_info", + "GM.addStyle", + "GM.openInTab", + "GM.closeTab", + "GM.setValue", + "GM.getValue", + "GM.deleteValue", + "GM.listValues", + "GM.setClipboard", + "GM.getTab", + "GM.saveTab", + "GM_xmlhttpRequest", + "GM.xmlHttpRequest", + "none", +]); + +export const validMetaKeys = new Set([ + "author", + "description", + "downloadURL", + "exclude", + "exclude-match", + "grant", + "icon", + "include", + "inject-into", + "match", + "name", + "noframes", + "require", + "run-at", + "updateURL", + "version", + "weight", +]); + +export const extensionPaths = { + popup: "/dist/entry-ext-action-popup.html", + page: "/dist/entry-ext-extension-page.html", +}; + +export async function openExtensionPage() { + const url = browser.runtime.getURL(extensionPaths.page); + const tabs = await browser.tabs.query({ url }); + const tab = tabs.find((e) => e.url.startsWith(url)); + if (!tab) return browser.tabs.create({ url }); + await browser.tabs.update(tab.id, { active: true }); + if (!browser.windows.update) return; + await browser.windows.update(tab.windowId, { focused: true }); +} + +// Safari currently does not honor the target attribute of elements in extension contexts +export async function openInBlank(url) { + browser.tabs.create({ url }); +} + +// Safari currently does not honor the download attribute of elements in extension contexts +// Also not support https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/download +export async function downloadToFile(filename, content, type = "text/plain") { + const url = "https://quoid.github.io/userscripts/serve/download.html"; + const tab = await browser.tabs.create({ url }); + const exchange = { filename, content, type }; + const exscript = (o) => { + // make sure executed only once + // @ts-ignore + if (window.US_DOWNLOAD === 1) return; + // @ts-ignore + window.US_DOWNLOAD = 1; + window.stop(); + document.body.textContent = "Download is starting..."; + const a = document.createElement("a"); + a.download = o.filename; + a.href = URL.createObjectURL(new Blob([o.content], { type: o.type })); + a.click(); + document.body.innerHTML += "
    The download should have started.
    "; + a.textContent = o.filename; + document.body.append(a); + }; + // Safari currently unable to stably executeScript on tab loading status + try { + await browser.tabs.executeScript(tab.id, { + code: `(${exscript})(${JSON.stringify(exchange)});`, + }); + } catch { + const handleUpdated = async (tabId) => { + if (tabId !== tab.id) return; + try { + await browser.tabs.executeScript(tabId, { + code: `(${exscript})(${JSON.stringify(exchange)});`, + }); + console.info(`[${filename}] Download is starting...`); + } catch { + console.info(`[${filename}] Start download failed, retrying...`); + } + }; + browser.tabs.onUpdated.addListener(handleUpdated); + // Remove the listener when tab closing + const handleRemoved = (tabId) => { + if (tabId !== tab.id) return; + browser.tabs.onUpdated.removeListener(handleUpdated); + browser.tabs.onRemoved.removeListener(handleRemoved); + }; + browser.tabs.onRemoved.addListener(handleRemoved); + } +} diff --git a/src/ext/shared/variables.css b/src/ext/shared/variables.css new file mode 100644 index 00000000..0c641d23 --- /dev/null +++ b/src/ext/shared/variables.css @@ -0,0 +1,52 @@ +:root { + --border-radius: 0.188rem; + --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); + --color-bg-primary: #323639; + --color-bg-secondary: #2f3337; + --color-script-highlighted: #364049; /* rgba(116, 178, 235, 0.1); */ + --color-black: #1d2023; + --color-blue: #74b1eb; + --color-green: #60f36c; + --color-grey: rgb(255 255 255 / 0.15); + --color-red: #ff453a; + --color-yellow: #e4f360; + --letter-spacing-large: -0.031rem; + --letter-spacing-default: -0.029rem; + --letter-spacing-medium: -0.018rem; + --letter-spacing-small: -0.008rem; + --opacity-disabled: 0.3; + --text-color-primary: rgb(255 255 255); + --text-color-secondary: rgb(255 255 255 / 0.65); + --text-color-disabled: rgb(255 255 255 / 0.4); + --font-family: system-ui, -apple-system, "Helvetica Neue", "Helvetica", + sans-serif; + --text-default: 1rem/1.5rem var(--font-family); + --text-large: 1.25rem/1.5rem var(--font-family); + --text-medium: 0.875rem/1.313rem var(--font-family); + --text-small: 0.719rem/1rem var(--font-family); + + /* editor variables */ + --editor-font: monaco, monospace; + --editor-font-size: 14px; + --editor-line-height: 24px; + --editor-invisible: rgb(255 255 255 / 0.15); + --editor-active-line: var(--color-bg-secondary); + --editor-selected-bg: rgb(116 178 235 / 0.35); + --editor-matched-highlight: rgb(116 178 235 / 0.2); + --editor-search-highlight: rgb(255 166 0 / 0.3); + --editor-number: #77e26a; + --editor-comment: rgb(255 255 255 / 0.35); + --editor-def: #efc371; + --editor-default: #cdcfd1; + --editor-keyword: #96c3ed; + --editor-atom: #59ebf5; + --editor-operator: #8c99a7; + --editor-property: #e86c8a; + --editor-string: #f5eea2; + --editor-string-2: #cdabff; + --editor-error: var(--color-red); + --editor-cursor: #e3e7eb; + --editor-matching-bracket-color: #fff; + --editor-matching-bracket-border: var(--editor-number); + --editor-non-matching-bracket: var(--editor-error); +} diff --git a/src/page/App.svelte b/src/page/App.svelte deleted file mode 100644 index 62e46a66..00000000 --- a/src/page/App.svelte +++ /dev/null @@ -1,133 +0,0 @@ - - - - -{#if $state.includes("init")} -
    - {@html logo} - {#if $state.includes("init-error")} - Failed to initialize app, check the browser console - {:else} - Initializing app... - {/if} -
    -{/if} -
    - - -
    -
      - {#each $notifications as item (item.id)} - notifications.remove(item.id)} {item}/> - {/each} -
    -{#if $state.includes("settings")}{/if} - - diff --git a/src/page/Components/Editor/CodeMirror.svelte b/src/page/Components/Editor/CodeMirror.svelte deleted file mode 100644 index 9fce5705..00000000 --- a/src/page/Components/Editor/CodeMirror.svelte +++ /dev/null @@ -1,402 +0,0 @@ - - - - -{#if instance} - searchActive = false} - instance={instance} - /> -{/if} diff --git a/src/page/Components/Editor/Editor.svelte b/src/page/Components/Editor/Editor.svelte deleted file mode 100644 index e8f1ed49..00000000 --- a/src/page/Components/Editor/Editor.svelte +++ /dev/null @@ -1,329 +0,0 @@ - - -
    - {#if $state.includes("editor-loading") || $state.includes("fetching")} - - {/if} - {#if !activeItem} -
    No Item Selected
    - {/if} -
    -
    -
    - -
    {name}
    -
    -
    -
    - {#if $state.includes("saving")} - Saving... - {:else if $state.includes("trashing")} - (◞‸◟) - {:else if $state.includes("updating")} - Updating code, - {:else if remote} - Code was remotely fetched, check carefully before saving! - {:else if temp} - Ready for code! - {:else} - Last modified: {lastModified} - {/if} -
    -
    -
    -
    - - - -
    -
    -
    - -
    - -
    - - diff --git a/src/page/Components/Editor/EditorSearch.svelte b/src/page/Components/Editor/EditorSearch.svelte deleted file mode 100644 index ff1833cb..00000000 --- a/src/page/Components/Editor/EditorSearch.svelte +++ /dev/null @@ -1,199 +0,0 @@ - - -{#if active} - -{/if} - - diff --git a/src/page/Components/Notification.svelte b/src/page/Components/Notification.svelte deleted file mode 100644 index c0f07e61..00000000 --- a/src/page/Components/Notification.svelte +++ /dev/null @@ -1,148 +0,0 @@ - - -
  • - -
    -
    {@html icon}
    - {item.message} - -
    -
  • - - diff --git a/src/page/Components/Settings.svelte b/src/page/Components/Settings.svelte deleted file mode 100644 index 40f7f7b3..00000000 --- a/src/page/Components/Settings.svelte +++ /dev/null @@ -1,318 +0,0 @@ - - -
    - - -
    state.remove("settings")}>
    - -
    - - diff --git a/src/page/Components/Sidebar/Sidebar.svelte b/src/page/Components/Sidebar/Sidebar.svelte deleted file mode 100644 index 4504b4a1..00000000 --- a/src/page/Components/Sidebar/Sidebar.svelte +++ /dev/null @@ -1,260 +0,0 @@ - - - - - diff --git a/src/page/Components/Sidebar/SidebarFilter.svelte b/src/page/Components/Sidebar/SidebarFilter.svelte deleted file mode 100644 index 7854163a..00000000 --- a/src/page/Components/Sidebar/SidebarFilter.svelte +++ /dev/null @@ -1,120 +0,0 @@ - - -
    - - - -
    - -
    - -
    - -
    - {#if query} - query = ""} {disabled}/> - {/if} -
    - - diff --git a/src/page/Components/Sidebar/SidebarItem.svelte b/src/page/Components/Sidebar/SidebarItem.svelte deleted file mode 100644 index b7915c49..00000000 --- a/src/page/Components/Sidebar/SidebarItem.svelte +++ /dev/null @@ -1,102 +0,0 @@ - - -
    -
    - -
    {data.name}
    - -
    - {#if description} -
    - {description} -
    - {/if} -
    - - diff --git a/src/page/app.css b/src/page/app.css deleted file mode 100644 index 1ede8fc1..00000000 --- a/src/page/app.css +++ /dev/null @@ -1,50 +0,0 @@ -html { - font-size: 100%; - height: 100vh; - overflow: hidden; -} - -body { - background-color: var(--color-bg-primary); - color: var(--text-color-primary); - font: var(--text-default); - height: 100%; - text-rendering: optimizelegibility; - -webkit-font-smoothing: antialiased; -} - -noscript { - display: block; -} - -#app { - display: flex; - flex-direction: column; - height: 100%; -} - -a, -.link { - background: none; - color: var(--color-blue); - cursor: pointer; - text-decoration: underline; -} - -.truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -button { - border: none; - cursor: pointer; - padding: 0; - user-select: none; -} - -button:disabled { - opacity: var(--opacity-disabled); - pointer-events: none; -} diff --git a/src/page/cm.css b/src/page/cm.css deleted file mode 100644 index 27b7210b..00000000 --- a/src/page/cm.css +++ /dev/null @@ -1,214 +0,0 @@ -.CodeMirror { - background-color: transparent; - color: var(--editor-default); - font-family: var(--editor-font); - font-size: var(--editor-font-size); - font-weight: 400; - height: 100%; - line-height: var(--editor-line-height); - text-rendering: unset; - -webkit-font-smoothing: antialiased; -} - -.CodeMirror-gutters { - background-color: transparent; - border-right: 0; -} - -.CodeMirror-activeline-background, -.CodeMirror-activeline-gutter { - background-color: var(--editor-active-line); -} - -.CodeMirror-linenumber { - color: var(--text-color-primary); - font-size: var(--editor-font-size); - opacity: 0.25; -} - -.CodeMirror-activeline .CodeMirror-linenumber { - opacity: 0.5; -} - -.CodeMirror-cursor { - border-left: 2px solid var(--text-color-secondary); /* check */ -} - -.CodeMirror-hints { - background-color: var(--color-black); - border: 1px solid black; - box-shadow: var(--box-shadow); - font: var(--text-small); - font-family: var(--editor-font); - line-height: 1.5rem; -} - -.CodeMirror-hint { - color: var(--text-color-secondary); -} - -li.CodeMirror-hint-active { - background-color: var(--color-blue); - color: var(--color-black); -} - -.cm-s-default .CodeMirror-selected { - background-color: var(--editor-selected-bg); -} - -div.CodeMirror span.CodeMirror-matchingbracket { - color: var(--editor-matching-bracket-color); - border-bottom: 2px solid var(--editor-matching-bracket-border); -} - -div.CodeMirror span.CodeMirror-nonmatchingbracket { - color: var(--editor-non-matching-bracket); -} - -.CodeMirror .CodeMirror-placeholder, -.cm-s-default .cm-comment { - color: var(--editor-comment); -} - -.cm-s-default .cm-keyword { - color: var(--editor-keyword); -} - -.cm-s-default .cm-def { - color: var(--editor-def); -} - -.cm-s-default .cm-variable, -.cm-s-default .cm-variable-2 { - color: inherit; -} - -.cm-s-default .cm-operator { - color: var(--editor-operator); -} - -.cm-s-default .cm-property { - color: var(--editor-property); -} - -.cm-s-default .cm-string { - color: var(--editor-string); -} - -.cm-s-default .cm-string-2 { - color: var(--editor-string-2); -} - -.cm-s-default .cm-number { - color: var(--editor-number); -} - -.cm-s-default .cm-atom { - color: var(--editor-atom); -} - -.cm-s-default .cm-error { - color: var(--color-red); -} - -.CodeMirror .cm-whitespace::before, -.CodeMirror .CodeMirror-line > span::after, -.CodeMirror .CodeMirror-code > div > pre > span::after { - color: var(--editor-invisible); -} - -.cm-s-default .cm-variable-3, -.cm-s-default .cm-builtin, -.cm-s-default .cm-qualifier, -.cm-s-default .cm-tag { - color: var(--editor-def); -} - -.cm-s-default .cm-searching { - background-color: var(--editor-search-highlight); - padding: 3px 0; -} - -.cm-s-default .cm-search-mark { - border-bottom: 2px solid var(--editor-matching-bracket-border); -} - -/* -.cm-s-default .cm-matchhighlight { - background-color: var(--editor-matched-highlight); -} -*/ - -.CodeMirror-lint-markers { - width: 8px; -} - -.CodeMirror-lint-marker { - border-radius: 50%; - cursor: default; - height: 0.375rem; - left: 0.25rem; - position: relative; - width: 0.375rem; -} - -.CodeMirror-lint-marker-error, -.CodeMirror-lint-marker-warning { - background: var(--color-yellow); - top: -1px; -} - -.CodeMirror-lint-marker-error { - background-color: var(--editor-error); -} - -.CodeMirror-lint-marker-multiple { - background: var(--editor-def); - left: 0; - position: absolute; - top: 0; -} - -.CodeMirror-lint-mark-warning { - background: none; - border-bottom: 2px solid var(--color-yellow); -} - -.CodeMirror-lint-mark-error { - background: none; - border-bottom: 2px solid var(--editor-error); -} - -.CodeMirror-lint-tooltip { - background-color: var(--color-black); - border: 1px solid black; - border-radius: var(--border-radius); - box-shadow: var(--box-shadow); - color: var(--text-color-secondary); - font: var(--text-small); - padding: 0.5rem; - transition: opacity 200ms; -} - -.CodeMirror-lint-message-error, -.CodeMirror-lint-message-warning { - background: none; - padding-left: 1rem; - position: relative; -} - -.CodeMirror-lint-message-error::before, -.CodeMirror-lint-message-warning::before { - background-color: var(--color-yellow); - border-radius: 50%; - content: ""; - height: 0.5rem; - left: 0; - position: absolute; - top: 0.25rem; - width: 0.5rem; -} - -.CodeMirror-lint-message-error::before { - background-color: var(--editor-error); -} diff --git a/src/page/main.js b/src/page/main.js deleted file mode 100644 index 66ad9d11..00000000 --- a/src/page/main.js +++ /dev/null @@ -1,17 +0,0 @@ -import "./app.css"; -import "../shared/reset.css"; -import "../shared/variables.css"; -import App from "./App.svelte"; -import "./cm.css"; - -if (import.meta.env.DEV) { // vite feat that only import in dev mode - const modules = import.meta.glob("../shared/dev.js", {eager: true}); - window.browser = modules["../shared/dev.js"].browser; - console.info("DEV-ENV", import.meta.env, modules, browser); -} - -const app = new App({ - target: document.getElementById("app") -}); - -export default app; diff --git a/src/page/store.js b/src/page/store.js deleted file mode 100644 index 4b607bba..00000000 --- a/src/page/store.js +++ /dev/null @@ -1,133 +0,0 @@ -import {writable} from "svelte/store"; -import {uniqueId} from "../shared/utils.js"; -// eslint-disable-next-line import/no-namespace -import * as settingsStorage from "../shared/settings.js"; - -function notificationStore() { - const {subscribe, update} = writable([]); - const add = item => { - update(a => { - a.push(item); - return a; - }); - }; - const remove = id => update(a => a.filter(b => b.id !== id)); - return {subscribe, add, remove}; -} -export const notifications = notificationStore(); - -function logStore() { - const {subscribe, set, update} = writable([]); - const add = (message, type, notify) => { - const item = {id: uniqueId(), message, time: Date.now(), type}; - if (notify || type === "error") notifications.add(item); // always notify on error - update(a => { - a.push(item); - return a; - }); - }; - const remove = id => update(a => a.filter(b => b.id !== id)); - const reset = () => set([]); - return {subscribe, add, remove, reset}; -} -export const log = logStore(); - -function stateStore() { - const {subscribe, update} = writable(["init"]); - // store oldState to see how state transitioned - // ex. if (newState === foo && oldState === bar) baz(); - let oldState = []; - const add = stateModifier => update(state => { - // list of acceptable states, mostly for state definition tracking - const states = [ - "init", // the uninitialized app state (start screen) - "init-error", // unique error when initialization fails, shows error on load screen - "settings", // when the settings modal is shown - "items-loading", // when the sidebar items are loading - "saving", // when a file in the editor is being saved - "fetching", // when a new remote file has been added and it being fetched - "trashing", // when deleting the file in the editor - "updating" // when the file in the editor is being updating - ]; - // disallow adding undefined states - if (!states.includes(stateModifier)) return console.error("invalid state"); - // save pre-changed state to oldState var - oldState = [...state]; - // if current state modifier not present, add it to state array - if (!state.includes(stateModifier)) state.push(stateModifier); - // ready state only when no other states present, remove it if present - if (state.includes("ready")) state.splice(state.indexOf("ready"), 1); - log.add(`App state updated to: ${state} from: ${oldState}`, "info", false); - return state; - }); - const remove = stateModifier => update(state => { - // save pre-changed state to oldState var - oldState = [...state]; - // if current state modifier present, remove it from state array - if (state.includes(stateModifier)) state.splice(state.indexOf(stateModifier), 1); - // if no other states, push ready state - if (state.length === 0) state.push("ready"); - log.add(`App state updated to: ${state} from: ${oldState}`, "info", false); - return state; - }); - const getOldState = () => oldState; - return {subscribe, add, getOldState, remove}; -} -export const state = stateStore(); - -function settingsStore() { - const {subscribe, update, set} = writable({}); - const init = async initData => { - // import legacy settings data just one-time - await settingsStorage.legacyImport(); - // for compatibility with legacy getting names only - // once all new name is used, use settingsStorage.get() - const settings = await settingsStorage.legacyGet(); - console.info("store.js settingsStore init", initData, settings); - set({...initData, ...settings}); - // sync popup, backgound, etc... settings changes - settingsStorage.onChanged((s, area) => { - console.info(`store.js storage.${area}.onChanged`, s); - update(obj => Object.assign(obj, s)); - }); - }; - const reset = async keys => { - await settingsStorage.reset(keys); - // once all new name is used, use settingsStorage.get() - const settings = await settingsStorage.legacyGet(); - console.info("store.js settingsStore reset", settings); - update(obj => Object.assign(obj, settings)); - }; - - const updateSingleSettingOld = (key, value) => { - // blacklist not stored in normal setting object in manifest, so handle differently - if (key === "blacklist") { - // update blacklist on swift side - const message = {name: "PAGE_UPDATE_BLACKLIST", blacklist: value}; - browser.runtime.sendNativeMessage(message, response => { - if (response.error) { - log.add("Failed to save blacklist to disk", "error", true); - } - }); - } - if (key === "active") { - browser.runtime.sendNativeMessage({name: "TOGGLE_EXTENSION", active: String(value)}); - } - }; - const updateSingleSetting = (key, value) => { - // update(settings => (settings[key] = value, settings)); - update(settings => { - settings[key] = value; - return settings; - }); - // for compatibility with legacy setting names only - // once all new name is used, use settingsStorage.set() - settingsStorage.legacySet({[key]: value}); // Durable Storage - // temporarily keep the old storage method until it is confirmed that all dependencies are removed - updateSingleSettingOld(key, value); - }; - return {subscribe, set, init, reset, updateSingleSetting}; -} -export const settings = settingsStore(); - -export const items = writable([]); diff --git a/src/popup/App.svelte b/src/popup/App.svelte deleted file mode 100644 index 52cb2cb8..00000000 --- a/src/popup/App.svelte +++ /dev/null @@ -1,740 +0,0 @@ - - - -
    - - showUpdates = true} - title={"Show updates"} - {disabled} - /> - - -
    -{#if !active} - -{/if} -{#if showInstallPrompt} -
    - Userscript - {#if scriptChecking} - {showInstallPrompt} - {:else} - {scriptInstalled ? "Installed" : "Detected"}: - - {/if} -
    -{/if} -{#if errorNotification} -
    - {errorNotification} - errorNotification = undefined} - title={"Clear error"} - /> -
    -{/if} -
    - {#if loading} - - {:else} - {#if inactive} -
    Popup inactive on extension page
    - {:else if firstGuide} -
    - Welcome, first please:  - -
    - {:else if initError} -
    - Something went wrong:  - -
    - {:else if items.length < 1} -
    No matched userscripts
    - {:else} -
    - {#each list as item, i (item.filename)} - toggleItem(item)} - /> - {/each} -
    - {/if} - {/if} -
    -{#if !inactive && platform === "macos"} - -{/if} -{#if showUpdates} - showUpdates = false} - showLoaderOnDisabled={true} - abortClick={abortUpdates} - abort={showUpdates} - > - - -{:else if showInstall} - showInstall = false} - showLoaderOnDisabled={true} - > - showInstall = false} - installConfirmClick={installConfirm} - /> - -{:else if showAll} - { - showAll = false; - refreshView(); - }} - showLoaderOnDisabled={false} - > - - -{/if} - - diff --git a/src/popup/Components/PopupItem.svelte b/src/popup/Components/PopupItem.svelte deleted file mode 100644 index 01e091de..00000000 --- a/src/popup/Components/PopupItem.svelte +++ /dev/null @@ -1,97 +0,0 @@ - - -
    - -
    {name}
    - {#if subframe}
    SUB
    {/if} - -
    - - diff --git a/src/popup/Components/View.svelte b/src/popup/Components/View.svelte deleted file mode 100644 index 71115f7b..00000000 --- a/src/popup/Components/View.svelte +++ /dev/null @@ -1,80 +0,0 @@ - - -
    -
    - {headerTitle} - -
    -
    - {#if loading && showLoaderOnDisabled} - - {:else} -
    Slot content is required...
    - {/if} -
    -
    - - diff --git a/src/popup/Components/Views/AllItemsView.svelte b/src/popup/Components/Views/AllItemsView.svelte deleted file mode 100644 index a5a59553..00000000 --- a/src/popup/Components/Views/AllItemsView.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -{#if allItems.length} -
    - {#each list as item (item.filename)} - allItemsToggleItem(item)} - /> - {/each} -
    -{:else} -
    No valid files found in directory
    -{/if} - - diff --git a/src/popup/Components/Views/InstallView.svelte b/src/popup/Components/Views/InstallView.svelte deleted file mode 100644 index 9d9cd3fe..00000000 --- a/src/popup/Components/Views/InstallView.svelte +++ /dev/null @@ -1,180 +0,0 @@ - - -
    - {#if installError} -
    - {@html iconError} -
    Couldn't install userscript
    -

    {installError}

    -
    - {:else if userscript} -
      -
    • {userscript.name}
    • - {#if userscript.description} -
    • {userscript.description}
    • - {/if} - {#if userscript.match} -
    • -
      @match
      - {#each userscript.match as match} -
      {match}
      - {/each} -
    • - {/if} - {#if userscript.include} -
    • -
      @include
      - {#each userscript.include as include} -
      {include}
      - {/each} -
    • - {/if} - {#if userscript.require} -
    • -
      @require
      - {#each userscript.require as require} -
      {require}
      - {/each} -
    • - {/if} - {#if userscript.grant} -
    • -
      @grant
      - {#each userscript.grant as grant} -
      {grant}
      - {/each} -
    • - {/if} -
    -
    -
    {@html iconWarn}
    -
    Be sure you trust the author before installing. Nefarious code can exploit your security and privacy.
    -
    -
    - - -
    - {/if} -
    - - diff --git a/src/popup/Components/Views/UpdateView.svelte b/src/popup/Components/Views/UpdateView.svelte deleted file mode 100644 index fd121c84..00000000 --- a/src/popup/Components/Views/UpdateView.svelte +++ /dev/null @@ -1,88 +0,0 @@ - - -{#if updates.length} - {#each updates as item (item.name)} - -
    -
    {item.name}
    -
    - Source - - -
    - {/each} -

    Be sure you trust the author before saving remote code to your device.

    - -{:else} -
    - {@html iconUpdate} -
    - There are no file updates available -
    - -
    -
    -{/if} - - diff --git a/src/popup/app.css b/src/popup/app.css deleted file mode 100644 index 8bbc54fe..00000000 --- a/src/popup/app.css +++ /dev/null @@ -1,86 +0,0 @@ -html { - font-size: 100%; - overflow: hidden; -} - -body { - background-color: var(--color-bg-secondary); - color: var(--text-color-primary); - font: var(--text-medium); - letter-spacing: var(--letter-spacing-medium); - position: relative; - text-rendering: optimizelegibility; - width: 18rem; - -webkit-font-smoothing: antialiased; -} - -/* attempts to detect iOS */ -@media (hover: none) { - html { - font-size: 125%; - } - - body { - width: 100vw; - } -} - -@media screen and (width >= 481px) and (orientation: portrait) and (hover: none) { - body { - width: 18rem; - } -} - -@media screen and (width >= 1024px) and (orientation: landscape) and (hover: none) { - body { - width: 18rem; - } -} - -body.ipados { - width: 18rem; - height: 100vh; -} - -body.ipados #app { - height: 100%; -} - -body.ios { - height: 100vh; - width: 100vw; -} - -body.ios #app { - height: 100%; -} - -noscript { - display: block; -} - -button { - border: none; - cursor: pointer; - padding: 0; - user-select: none; -} - -button:disabled { - opacity: var(--opacity-disabled); - pointer-events: none; -} - -a, -.link { - background: none; - color: var(--color-blue); - cursor: pointer; - text-decoration: underline; -} - -.truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/src/popup/main.js b/src/popup/main.js deleted file mode 100644 index 03752163..00000000 --- a/src/popup/main.js +++ /dev/null @@ -1,39 +0,0 @@ -import "./app.css"; -import "../shared/reset.css"; -import "../shared/variables.css"; -import App from "./App.svelte"; - -if (import.meta.env.DEV) { // vite feat that only import in dev mode - const modules = import.meta.glob("../shared/dev.js", {eager: true}); - window.browser = modules["../shared/dev.js"].browser; - console.info("DEV-ENV", import.meta.env, modules, browser); - // macos popup simulation - const style = document.createElement("style"); - style.textContent = ` -body { - top: 20px; - left: 20px; - box-sizing: content-box; - border: 2px solid #0c0e0f; - border-radius: 10px; - box-shadow: 2px 2px 20px rgba(0, 0, 0, 0.2); -} -body:before { - content: ""; - position: absolute; - top: -1px; - left: -1px; - right: -1px; - bottom: -1px; - border: 1px solid #54575a; - border-radius: 9px; -} -`; - browser.platform === "macos" && document.head.append(style); -} - -const app = new App({ - target: document.getElementById("app") -}); - -export default app; diff --git a/src/shared/Components/Dropdown.svelte b/src/shared/Components/Dropdown.svelte deleted file mode 100644 index 6e7b5c75..00000000 --- a/src/shared/Components/Dropdown.svelte +++ /dev/null @@ -1,115 +0,0 @@ - - -
    - -
      - -
    • At least one slot is required...
    • -
      -
    -
    - - diff --git a/src/shared/Components/IconButton.svelte b/src/shared/Components/IconButton.svelte deleted file mode 100644 index 40197ace..00000000 --- a/src/shared/Components/IconButton.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - - - - diff --git a/src/shared/Components/Loader.svelte b/src/shared/Components/Loader.svelte deleted file mode 100644 index 3e1d5f0d..00000000 --- a/src/shared/Components/Loader.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - -
    - {@html iconLoader} - {#if abort} -
    - Fetching resources, -
    - {/if} -
    - - diff --git a/src/shared/Components/Tag.svelte b/src/shared/Components/Tag.svelte deleted file mode 100644 index 13e80cf2..00000000 --- a/src/shared/Components/Tag.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - -
    - - diff --git a/src/shared/Components/Toggle.svelte b/src/shared/Components/Toggle.svelte deleted file mode 100644 index 72a43a6b..00000000 --- a/src/shared/Components/Toggle.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - diff --git a/src/shared/dev.js b/src/shared/dev.js deleted file mode 100644 index 03ab9296..00000000 --- a/src/shared/dev.js +++ /dev/null @@ -1,471 +0,0 @@ -import {parse, uniqueId, wait} from "./utils"; - -/** - * - * @param {("css"|"js")} type - * @param {boolean?} updates - * @param {boolean?} longName - * @returns - */ -function generateFile(type, updates = false, longName = false, request = false) { - let canUpdate = updates; - const uid = uniqueId(); - let name = request ? `${uid}-request-${type}` : `${uid}-example-${type}`; - if (longName) name = `${uid}${uid}${uid}-example-${type}`; - const randomDate = +(new Date() - Math.floor(Math.random() * 10000000000)); - let content = "// ==UserScript==" - + `\n// @name ${name}` - + `\n// @description Custom description for userscript with name "${name}"` - + "\n// @match *://*.*" - + "\n// @exclude-match https://github.com/quoid/userscripts" - + "\n// @version 1.0" - + "\n// @noframes" - + "\n// ==/UserScript==" - + `\n\nconsole.log("I am ${name}");`; - if (type === "css") { - content = content.replace("// ==UserScript==", "/* ==UserStyle=="); - content = content.replace("// ==/UserScript==", "==/UserStyle== */"); - content = content.replaceAll("// @", "@"); - content = content.replace( - `console.log("I am ${name}");`, - "#id,\n.class\n.pseudo-element::after {\n color: red\n}" - ); - canUpdate = false; - } - if (request) { - content = content.replace("// @noframes", "// @noframes\n// @run-at request"); - } - if (canUpdate) { - content = content.replace( - "1.0", - "1.0\n// @updateURL https://www.k21p.com/example.user.js" - ); - content = content.replace( - `console.log("I am ${name}");`, - `console.log("I am ${name} and you can update me!");` - ); - } - return { - content, - filename: `${name}.${type}`, - lastModified: randomDate, - name, - request, - type - }; -} - -const files = [ - generateFile("js", true, true), - generateFile("css"), - generateFile("js", false, false, true), - ...Array.from({length: 7}, () => generateFile("js")) -]; - -const _browser = { - delay: 200, - platform: "macos", // ios || macos - runtime: { - getURL() { - return "https://www.example.com/"; - }, - async sendMessage(message, responseCallback) { - const name = message.name; - console.info(`Got message: ${name}`); - let response = {}; - if (name === "REFRESH_SESSION_RULES") { - response = {success: true}; - } - if (!responseCallback) { - return new Promise(resolve => { - setTimeout(() => resolve(response), _browser.delay); - }); - } - setTimeout(() => responseCallback(response), _browser.delay); - }, - async sendNativeMessage(message, responseCallback) { - const name = message.name; - console.info(`Got message: ${name}`); - let response = {}; - if (name === "PAGE_INIT_DATA") { - response = { - saveLocation: "/Users/someone/Library/Directory", - version: "5.0.0", - build: "55" - }; - } else if (name === "PAGE_LEGACY_IMPORT") { - response = { - active: "true", - autoCloseBrackets: "true", - autoHint: "true", - blacklist: [], - descriptions: "true", - languageCode: "en", - lint: "true", - log: "false", - showCount: "true", - showInvisibles: "true", - sortOrder: "lastModifiedDesc", - tabSize: "4" - }; - } else if (name === "PAGE_ALL_FILES") { - response = []; - files.forEach(file => { - const content = file.content; - const parsed = parse(content); - const metadata = parsed.metadata; - const canUpdate = !!(metadata.version && metadata.updateURL); - const scriptData = { - canUpdate, - content, - description: metadata.description ? metadata.description[0] : "", - disabled: false, - filename: file.filename, - lastModified: file.lastModified, - metadata, - name: metadata.name[0], - request: file.request, - // type: file.filename.substring(file.filename.lastIndexOf(".") + 1) - type: file.type - }; - response.push(scriptData); - }); - } else if (name === "TOGGLE_ITEM") { - response = {success: true}; - // response = {error: true}; - } else if (name === "PAGE_UPDATE_SETTINGS") { - response = {success: true}; - } else if (name === "PAGE_UPDATE_BLACKLIST") { - response = {success: true}; - } else if (name === "PAGE_NEW_REMOTE") { - const result = await getRemoteFileContents(message.url); - response = result; - } else if (name === "PAGE_SAVE") { - const newContent = message.content; - const oldFilename = message.item.filename; - const parsed = parse(newContent); - const lastModified = Date.now(); - let canUpdate = false; - // script failed to parse - if (!parsed) { - return {error: "save failed, file has invalid metadata"}; - } - const metaName = parsed.metadata.name[0]; - const newFilename = `${metaName}.${message.item.type}`; - // filename length too long - if (newFilename.length > 255) { - return {error: "save failed, filename too long!"}; - } - - // check if file can be remotely updated - if (parsed.metadata.version && parsed.metadata.updateURL) { - canUpdate = true; - } - - const success = { - canUpdate, - content: newContent, - filename: newFilename, - lastModified, - name - }; - - // add description if in file metadata - if (parsed.metadata.description) { - // const foo = parsed.metadata; - success.description = parsed.metadata.description[0]; - } - // check if declarative network request - if ( - parsed.metadata["run-at"] - && parsed.metadata["run-at"][0] === "request" - ) { - success.request = true; - } - // overwriting - if (newFilename.toLowerCase() === oldFilename.toLowerCase()) { - saveFile(newContent, lastModified, newFilename, oldFilename); - return success; - } - - // not overwriting, check if filename for that type is taken - if (files.find(a => a.filename.toLowerCase() === newFilename.toLowerCase())) { - return {error: "save failed, name already taken!"}; - } - - // not overwriting but all validation passed - saveFile(newContent, lastModified, newFilename, oldFilename); - return success; - } else if (name === "PAGE_UPDATE") { - const url = parse(message.content).metadata.updateURL; - await wait(500); - const result = await getRemoteFileContents(url); - response = result; - // response.error = "Something went wrong!"; - // response.info = "No updates found"; - } else if (name === "TOGGLE_EXTENSION") { - // response = {error: "Failed toggle extension"}; - response = {success: true}; - } else if (name === "POPUP_UPDATE_ALL" || name === "POPUP_UPDATE_SINGLE") { - // response = {error: "Failed refresh scripts"}; - response = { - items: [ - { - name: "Google Images Restored", - filename: "Google Images Restored.js", - disabled: false, - type: "js" - }, - { - name: "Wikipedia Mobile Redirect", - filename: "Wikipedia Mobile Redirect.js", - disabled: true, - type: "js" - }, - { - name: "A Special Userscript", - filename: "A Special Userscript.js", - disabled: false, - type: "js" - }, - { - name: "CSS Adblock", - filename: "CSS Adblock.css", - disabled: false, - type: "css" - }, - { - name: "New Userscript With a Really Really Long Name", - filename: "New Userscript With a Really Really Long Name.css", - disabled: true, - type: "css" - }, - { - name: "Subframe Script Managerial Staffing Company", - filename: "Subframe Script.js", - disabled: false, - subframe: true, - type: "css" - } - ], - updates: [] - }; - } else if (name === "POPUP_CHECK_UPDATES") { - response = { - updates: [ - { - filename: "Google Images Restored.js", - name: "Google Images Restored", - url: "https://www.k21p.com" - }, - { - filename: "New Userscript With a Really Really Long Name.js", - name: "New Userscript With a Really Really Long Name", - url: "https://www.filmgarb.com" - } - ] - }; - } else if (name === "POPUP_MATCHES") { - response = { - matches: [ - ...files, - { - name: "Subframe Script Managerial Staffing Company", - filename: "Subframe Script.js", - disabled: false, - subframe: true, - type: "js" - } - ] - }; - } else if (name === "POPUP_UPDATES") { - response = { - updates: [ - { - filename: "Google Images Restored.js", - name: "Google Images Restored", - url: "https://www.k21p.com" - }, - { - filename: "New Userscript With a Really Really Long Name.js", - name: "New Userscript With a Really Really Long Name", - url: "https://www.filmgarb.com" - } - ] - }; - response.updates = []; - } else if (name === "REQ_PLATFORM") { - response = {platform: _browser.platform}; - } else if (name === "POPUP_OPEN_EXTENSION_PAGE") { - response = {error: "Failed to get page url"}; - window.open("https://github.com/quoid/userscripts"); - } else if (name === "OPEN_SAVE_LOCATION") { - if (_browser.platform === "macos") { - response = {success: true}; - } else { - response = { - items: [ - { - name: "Google Images Restored", - filename: "Google Images Restored.js", - disabled: false, - type: "js", - metadata: [] - }, - { - name: "Subframe Script Managerial Staffing Company", - filename: "Subframe Script.js", - disabled: false, - type: "css", - metadata: [] - }, - { - name: "Another Script from Managerial Staffing Company", - filename: "Cool Script.js", - disabled: false, - type: "js", - metadata: [] - } - ] - }; - } - } else if (name === "POPUP_INSTALL_CHECK") { - response = random([ - {success: "Click to install", installed: false}, - {success: "Click to re-install", installed: true} - ]); - response.metadata = { - description: "This userscript re-implements the \"View Image\" and \"Search by image\" buttons into google images.", - grant: ["GM.getValue", "GM.setValue", "GM.xmlHttpRequest"], - match: ["https://www.example.com/*", "https://www.example.com/somethingReallylong/goesRightHere"], - name: "Test Install Userscript", - require: ["https://code.jquery.com/jquery-3.5.1.min.js", "https://code.jquery.com/jquery-1.7.1.min.js"], - source: "https://greasyforx.org/scripts/00000-something-something-long-name/code/Something%20something%20long20name.user.js" - }; - // response.error = "something went wrong (dev)"; - } - if (!responseCallback) { - return new Promise(resolve => { - setTimeout(() => resolve(response), _browser.delay); - }); - } - setTimeout(() => responseCallback(response), _browser.delay); - }, - connectNative: () => ({ - onMessage: { - addListener: () => console.info("connectNative - addListener") - } - }) - }, - tabs: { - getCurrent(/* responseCallback */) { - const response = random([ - {url: "https://www.filmgarb.com/foo.user.js", id: 101}, - {url: `${window.location.origin}/src/shared/dev/DEMO.Alert-URL.user.js`, id: 102}, - {url: `${window.location.origin}/src/shared/dev/DEMO.Alert-URL.user.js`, id: 103}, // increase probability - {url: window.location.href, id: 10} - ]); - console.info("browser.tabs.getCurrent", response); - return new Promise(resolve => { - setTimeout(() => resolve(response), _browser.delay); - }); - }, - query(message, responseCallback) { - const response = [{url: "https://www.filmgarb.com/foo.user.js", id: 101}]; - if (!responseCallback) { - return new Promise(resolve => { - setTimeout(() => resolve(response), _browser.delay); - }); - } - setTimeout(() => responseCallback(response), _browser.delay); - }, - sendMessage(tabId, message, responseCallback) { - let response = {}; - if (message.name === "DEMO_MSG") { - response = {}; - // response.error = "something went wrong (dev)"; - } - if (!responseCallback) { - return new Promise(resolve => { - setTimeout(() => resolve(response), _browser.delay); - }); - } - setTimeout(() => responseCallback(response), _browser.delay); - } - }, - webNavigation: { - getAllFrames(message, responseCallback) { - const response = []; - if (!responseCallback) { - return new Promise(resolve => { - setTimeout(() => resolve(response), _browser.delay); - }); - } - setTimeout(() => responseCallback(response), _browser.delay); - } - }, - storage: { - local: { - get(items, responseCallback) { - const response = {}; - if (!responseCallback) { - return new Promise(resolve => { - setTimeout(() => resolve(response), _browser.delay); - }); - } - setTimeout(() => responseCallback(response), _browser.delay); - }, - set() { - return new Promise(resolve => { - setTimeout(() => resolve(), _browser.delay); - }); - }, - remove: () => new Promise(resolve => { - setTimeout(() => resolve(), _browser.delay); - }), - onChanged: { - addListener: () => undefined - } - } - } -}; - -async function getRemoteFileContents(url) { - const result = {}; - await fetch(url).then(response => { - if (!response.ok) throw Error(response.statusText); - return response.text(); - }).then(text => { - result.content = text; - }).catch(error => { - console.error(error); - result.error = "Remote url bad response!"; - }); - return result; -} - -function saveFile(content, lastMod, newFilename, oldName) { - const ind = files.findIndex(f => f.filename === oldName); - const s = { - content, - filename: newFilename, - lastModified: lastMod - }; - if (ind !== -1) { - // overwrite at index - files[ind] = s; - } else { - // add to beginning of array - files.unshift(s); - } -} - -function random(array) { - return array[Math.floor(Math.random() * array.length)]; -} - -export const browser = _browser; - -export default { - browser: _browser -}; diff --git a/src/shared/settings.js b/src/shared/settings.js deleted file mode 100644 index 059870b6..00000000 --- a/src/shared/settings.js +++ /dev/null @@ -1,695 +0,0 @@ -// wrap a relatively independent settings storage with its own functions - -const storagePrefix = "US_"; -const storageKey = key => storagePrefix + key.toUpperCase(); -// const storageRef = async area => { // dynamic storage reference -// browser.storage.sync.area = "sync"; -// browser.storage.local.area = "local"; -// if (area === "sync") return browser.storage.sync; -// if (area === "local") return browser.storage.local; -// const key = storageKey("settings_sync"); -// const result = await browser.storage.local.get(key); -// if (result?.[key] === true) { -// return browser.storage.sync; -// } else { -// return browser.storage.local; -// } -// }; - -// https://developer.apple.com/documentation/safariservices/safari_web_extensions/assessing_your_safari_web_extension_s_browser_compatibility#3584139 -// since storage sync is not implemented in Safari, currently only returns using local storage -const storageRef = async () => { - browser.storage.local.area = "local"; - return browser.storage.local; -}; - -const settingDefault = deepFreeze({ - name: "setting_default", - type: undefined, - local: false, - values: [], - default: undefined, - protect: false, - confirm: false, - platforms: ["macos", "ipados", "ios"], - langLabel: {}, - langTitle: {}, - group: "", - legacy: "", - nodeType: "", - nodeClass: {} -}); - -export const settingsDefine = deepFreeze([ - { - name: "error_native", - type: "object", - local: true, - default: {error: undefined}, - platforms: ["macos", "ipados", "ios"], - group: "Internal" - }, - { - name: "legacy_imported", - type: "number", - local: true, - default: 0, - protect: true, - platforms: ["macos"], - group: "Internal" - }, - { - name: "language_code", - type: "string", - default: "en", - platforms: ["macos", "ipados", "ios"], - group: "Internal", - legacy: "languageCode" - }, - { - name: "scripts_settings", - type: "object", - default: {}, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Scripts update check active", - zh_hans: "脚本更新检查激活" - }, - langTitle: { - en: "Whether to enable each single script update check", - zh_hans: "是否开启单个脚本更新检查" - }, - group: "Internal", - nodeType: "Subpage" - }, - // { - // name: "settings_sync", - // type: "boolean", - // local: true, - // default: false, - // protect: true, - // platforms: ["macos", "ipados", "ios"], - // langLabel: { - // en: "Sync settings", - // zh_hans: "同步设置" - // }, - // langTitle: { - // en: "Sync settings across devices", - // zh_hans: "跨设备同步设置" - // }, - // group: "General", - // nodeType: "Toggle" - // }, - { - name: "toolbar_badge_count", - type: "boolean", - default: true, - platforms: ["macos", "ipados"], - langLabel: { - en: "Show Toolbar Count", - zh_hans: "工具栏图标显示计数徽章" - }, - langTitle: { - en: "displays a badge on the toolbar icon with a number that represents how many enabled scripts match the url for the page you are on", - zh_hans: "简体中文描述" - }, - group: "General", - legacy: "showCount", - nodeType: "Toggle" - }, - { - name: "global_active", - type: "boolean", - local: true, - default: true, - platforms: ["macos"], - langLabel: { - en: "Enable Injection", - zh_hans: "启用注入" - }, - langTitle: { - en: "toggle on/off script injection for the pages you visit", - zh_hans: "简体中文描述" - }, - group: "General", - legacy: "active", - nodeType: "Toggle", - nodeClass: {red: false} - }, - { - name: "global_scripts_update_check", - type: "boolean", - default: true, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Global scripts update check", - zh_hans: "全局脚本更新检查" - }, - langTitle: { - en: "Whether to enable global periodic script update check", - zh_hans: "是否开启全局定期脚本更新检查" - }, - group: "General", - nodeType: "Toggle" - }, - { - name: "scripts_update_check_interval", - type: "number", - default: 86400000, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Scripts update check interval", - zh_hans: "脚本更新检查间隔" - }, - langTitle: { - en: "The interval for script update check in background", - zh_hans: "脚本更新检查的间隔时间" - }, - group: "General", - nodeType: "Toggle" - }, - { - name: "scripts_update_check_lasttime", - type: "number", - default: 0, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Scripts update check lasttime", - zh_hans: "脚本更新上次检查时间" - }, - langTitle: { - en: "The lasttime for script update check in background", - zh_hans: "后台脚本更新上次检查时间" - }, - group: "Internal" - }, - { - name: "scripts_auto_update", - type: "boolean", - default: false, - confirm: true, - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Scripts silent auto update", - zh_hans: "脚本后台静默自动更新" - }, - langTitle: { - en: "Script silently auto-updates in the background, which is dangerous and may introduce unconfirmed malicious code", - zh_hans: "脚本在后台静默自动更新,这是危险的,可能引入未经确认的恶意代码" - }, - group: "General", - nodeType: "Toggle", - nodeClass: {warn: true} - }, - { - name: "global_exclude_match", - type: "object", - default: [], - platforms: ["macos", "ipados", "ios"], - langLabel: { - en: "Global exclude match patterns", - zh_hans: "全局排除匹配模式列表" - }, - langTitle: { - en: "this input accepts a comma separated list of @match patterns, a page url that matches against a pattern in this list will be ignored for script injection", - zh_hans: "简体中文描述" - }, - group: "General", - legacy: "blacklist", - nodeType: "textarea", - nodeClass: {red: "blacklistError"} - }, - { - name: "editor_close_brackets", - type: "boolean", - default: true, - platforms: ["macos"], - langLabel: { - en: "Auto Close Brackets", - zh_hans: "自动关闭括号" - }, - langTitle: { - en: "toggles on/off auto closing of brackets in the editor, this affects the following characters: () [] {} \"\" ''", - zh_hans: "简体中文描述" - }, - group: "Editor", - legacy: "autoCloseBrackets", - nodeType: "Toggle" - }, - { - name: "editor_auto_hint", - type: "boolean", - default: true, - platforms: ["macos"], - langLabel: { - en: "Auto Hint", - zh_hans: "自动提示(Hint)" - }, - langTitle: { - en: "automatically shows completion hints while editing", - zh_hans: "简体中文描述" - }, - group: "Editor", - legacy: "autoHint", - nodeType: "Toggle" - }, - { - name: "editor_list_sort", - type: "string", - values: ["nameAsc", "nameDesc", "lastModifiedAsc", "lastModifiedDesc"], - default: "lastModifiedDesc", - platforms: ["macos"], - langLabel: { - en: "Sort order", - zh_hans: "排序顺序" - }, - langTitle: { - en: "Display order of items in sidebar", - zh_hans: "侧栏中项目的显示顺序" - }, - group: "Editor", - legacy: "sortOrder", - nodeType: "Dropdown" - }, - { - name: "editor_list_descriptions", - type: "boolean", - default: true, - platforms: ["macos"], - langLabel: { - en: "Show List Descriptions", - zh_hans: "显示列表项目描述" - }, - langTitle: { - en: "show or hides the item descriptions in the sidebar", - zh_hans: "简体中文描述" - }, - group: "Editor", - legacy: "descriptions", - nodeType: "Toggle" - }, - { - name: "editor_javascript_lint", - type: "boolean", - default: false, - platforms: ["macos"], - langLabel: { - en: "Javascript Linter", - zh_hans: "Javascript Linter" - }, - langTitle: { - en: "toggles basic Javascript linting within the editor", - zh_hans: "简体中文描述" - }, - group: "Editor", - legacy: "lint", - nodeType: "Toggle" - }, - { - name: "editor_show_whitespace", - type: "boolean", - default: true, - platforms: ["macos"], - langLabel: { - en: "Show whitespace characters", - zh_hans: "显示空白字符" - }, - langTitle: { - en: "toggles the display of invisible characters in the editor", - zh_hans: "简体中文描述" - }, - group: "Editor", - legacy: "showInvisibles", - nodeType: "Toggle" - }, - { - name: "editor_tab_size", - type: "number", - values: [2, 4], - default: 4, - platforms: ["macos"], - langLabel: { - en: "Tab Size", - zh_hans: "制表符大小" - }, - langTitle: { - en: "the number of spaces a tab is equal to while editing", - zh_hans: "简体中文描述" - }, - group: "Editor", - legacy: "tabSize", - nodeType: "select" - } -].reduce(settingsDefineReduceCallback, {})); - -// populate the settingsDefine with settingDefault -// and convert settingsDefine to storageKey object -function settingsDefineReduceCallback(settings, setting) { - setting.key = storageKey(setting.name); - settings[setting.key] = {...settingDefault, ...setting}; - return settings; -} - -// prevent settings define from being modified in any case -// otherwise user settings may be lost in the worst case -function deepFreeze(object) { - for (const p in object) { - if (typeof object[p] == "object") { - deepFreeze(object[p]); - } - } - return Object.freeze(object); -} - -// @todo remove this polyfill when browser limitation is set above 15.4 -// compatibility polyfill for Safari < 15.4 -if (Object.hasOwn === undefined) { - Object.hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); -} - -// export and define the operation method of settings storage -// they are similar to browser.storage but slightly different - -export async function get(keys, area) { - if (![undefined, "local", "sync"].includes(area)) { - return console.error("Unexpected storage area:", area); - } - // validate setting value and fix surprises to default - const valueFix = (key, val) => { - if (!key || !Object.hasOwn(settingsDefine, key)) return; - const def = settingsDefine[key].default; - // check if value type conforms to settingsDefine - const type = settingsDefine[key].type; - // eslint-disable-next-line valid-typeof -- type known to be valid string literal - if (typeof val != type) { - console.warn(`Unexpected ${key} value type '${typeof val}' should '${type}', fix to default`); - return def; - } - // check if value conforms to settingsDefine - const values = settingsDefine[key].values; - if (values.length && !values.includes(val)) { - console.warn(`Unexpected ${key} value '${val}' should one of '${values}', fix to default`); - return def; - } - // verified, pass original value - return val; - }; - if (typeof keys == "string") { // [single setting] - const key = storageKey(keys); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { - return console.error("unexpected settings key:", key); - } - // check if only locally stored setting - // eslint-disable-next-line no-param-reassign -- change the area is expected - settingsDefine[key].local === true && (area = "local"); - const storage = await storageRef(area); - const result = await storage.get(key); - if (Object.hasOwn(result, key)) return valueFix(key, result[key]); - return settingsDefine[key].default; - } - const complexGet = async (settingsDefault, areaKeys) => { - const storage = await storageRef(area); - let local = {}, sync = {}; - if (storage.area === "sync") { - if (areaKeys.sync.length) { - sync = await storage.get(areaKeys.sync); - } - if (areaKeys.local.length) { - const storageLocal = await storageRef("local"); - local = await storageLocal.get(areaKeys.local); - } - } else { - local = await storage.get(areaKeys.all); - } - const result = Object.assign(settingsDefault, local, sync); - // revert settings object property name - return Object.entries(result).reduce((p, c) => { - p[settingsDefine[c[0]].name] = valueFix(...c); - return p; - }, {}); - }; - if (Array.isArray(keys)) { // [muilt settings] - if (!keys.length) { - return console.error("Settings keys empty:", keys); - } - const settingsDefault = {}; - const areaKeys = {local: [], sync: [], all: []}; - for (const k of keys) { - const key = storageKey(k); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { - return console.error("unexpected settings key:", key); - } - settingsDefault[key] = settingsDefine[key].default; - // detach only locally stored settings - settingsDefine[key].local === true - ? areaKeys.local.push(key) - : areaKeys.sync.push(key); - // record all keys in case sync storage is not enabled - areaKeys.all.push(key); - } - return complexGet(settingsDefault, areaKeys); - } - if (typeof keys == "undefined" || keys === null) { // [all settings] - const settingsDefault = {}; - const areaKeys = {local: [], sync: [], all: []}; - for (const key of Object.keys(settingsDefine)) { - settingsDefault[key] = settingsDefine[key].default; - // detach only locally stored settings - settingsDefine[key].local === true - ? areaKeys.local.push(key) - : areaKeys.sync.push(key); - // record all keys in case sync storage is not enabled - areaKeys.all.push(key); - } - return complexGet(settingsDefault, areaKeys); - } - return console.error("Unexpected keys type:", keys); -} - -export async function set(keys, area) { - if (![undefined, "local", "sync"].includes(area)) { - return console.error("unexpected storage area:", area); - } - if (typeof keys != "object") { - return console.error("Unexpected keys type:", keys); - } - if (!Object.keys(keys).length) { - return console.error("Settings object empty:", keys); - } - const areaKeys = {local: {}, sync: {}, all: {}}; - for (const k of Object.keys(keys)) { - const key = storageKey(k); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { - return console.error("Unexpected settings keys:", key); - } - // check if value type conforms to settingsDefine - const type = settingsDefine[key].type; - // eslint-disable-next-line valid-typeof -- type known to be valid string literal - if (typeof keys[k] != type) { - if (type === "number" && !Number.isNaN(Number(keys[k]))) { // compatible with string numbers - keys[k] = Number(keys[k]); // still store it as a number type - } else { - return console.error(`Unexpected ${k} value type '${typeof keys[k]}' should '${type}'`); - } - } - // check if value conforms to settingsDefine - const values = settingsDefine[key].values; - if (values.length && !values.includes(keys[k])) { - return console.error(`Unexpected ${k} value '${keys[k]}' should one of '${values}'`); - } - // detach only locally stored settings - settingsDefine[key].local === true - ? areaKeys.local[key] = keys[k] - : areaKeys.sync[key] = keys[k]; - // record all keys in case sync storage is not enabled - areaKeys.all[key] = keys[k]; - } - const storage = await storageRef(area); - // complexSet - try { - if (storage.area === "sync") { - if (Object.keys(areaKeys.sync).length) { - await storage.set(areaKeys.sync); - } - if (Object.keys(areaKeys.local).length) { - const storageLocal = await storageRef("local"); - await storageLocal.set(areaKeys.local); - } - } else { - await storage.set(areaKeys.all); - } - return true; - } catch (error) { - return console.error(error); - } -} - -export async function reset(keys, area) { // reset to default - if (![undefined, "local", "sync"].includes(area)) { - return console.error("unexpected storage area:", area); - } - if (typeof keys == "string") { // [single setting] - const key = storageKey(keys); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { - return console.error("unexpected settings key:", key); - } - // check if key is protected - if (settingsDefine[key].protect === true) { - return console.error("protected settings key:", key); - } - // eslint-disable-next-line no-param-reassign -- change the area is expected - settingsDefine[key].local === true && (area = "local"); - const storage = await storageRef(area); - return storage.remove(key); - } - const complexRemove = async areaKeys => { - const storage = await storageRef(area); - try { - if (storage.area === "sync") { - if (areaKeys.sync.length) { - await storage.remove(areaKeys.sync); - } - if (areaKeys.local.length) { - const storageLocal = await storageRef("local"); - await storageLocal.remove(areaKeys.local); - } - } else { - await storage.remove(areaKeys.all); - } - return true; - } catch (error) { - return console.error(error); - } - }; - if (Array.isArray(keys)) { // [muilt settings] - if (!keys.length) { - return console.error("Settings keys empty:", keys); - } - const areaKeys = {local: [], sync: [], all: []}; - for (const k of keys) { - const key = storageKey(k); - // check if key exist in settingsDefine - if (!Object.hasOwn(settingsDefine, key)) { - return console.error("unexpected settings key:", key); - } - // check if key is protected - if (settingsDefine[key].protect === true) { - return console.error("protected settings key:", key); - } - // detach only locally stored settings - settingsDefine[key].local === true - ? areaKeys.local.push(key) - : areaKeys.sync.push(key); - // record all keys in case sync storage is not enabled - areaKeys.all.push(key); - } - return complexRemove(areaKeys); - } - if (typeof keys == "undefined" || keys === null) { // [all settings] - const areaKeys = {local: [], sync: [], all: []}; - for (const key in settingsDefine) { - // skip protected keys - if (settingsDefine[key].protect === true) continue; - // detach only locally stored settings - settingsDefine[key].local === true - ? areaKeys.local.push(key) - : areaKeys.sync.push(key); - // record all keys in case sync storage is not enabled - areaKeys.all.push(key); - } - return complexRemove(areaKeys); - } - return console.error("Unexpected keys type:", keys); -} - -// this function is convenient for the svelte store to update the state -export function onChanged(callback) { // complex onChanged - if (typeof callback != "function") { - return console.error("Unexpected callback:", callback); - } - console.info("storage onChanged addListener"); - const handle = (changes, area) => { - // console.log(`storage.${area}.onChanged`, changes); - try { - const settings = {}; - for (const key in changes) { - if (!Object.hasOwn(settingsDefine, key)) continue; - settings[settingsDefine[key].name] = changes[key].newValue; - } - callback(settings, area); - } catch (error) { - console.error("onChanged callback:", error); - } - }; - // comment for now same reason as `storageRef` function - // browser.storage.sync.onChanged.addListener(c => handle(c, "sync")); - browser.storage.local.onChanged.addListener(c => handle(c, "local")); -} - -// the following functions are used only for compatibility transition periods -// these functions will be removed in the future, perhaps in version 5.0 - -export async function legacyGet(keys) { - const result = await get(keys); - // console.log("legacy_get", keys, result); - for (const key of Object.keys(result)) { - const legacy = settingsDefine[storageKey(key)]?.legacy; - if (legacy) result[legacy] = result[key]; - } - return result; -} - -export async function legacySet(keys) { - if (typeof keys != "object") { - return console.error("Unexpected arg type:", keys); - } - if (!Object.keys(keys).length) { - return console.error("Settings object empty:", keys); - } - const settings = {}; - for (const key of Object.keys(settingsDefine)) { - const setting = settingsDefine[key]; - if (!setting.legacy) continue; - if (setting.legacy in keys) { - settings[setting.name] = keys[setting.legacy]; - } - } - // console.log("legacy_set", keys, settings); - return set(settings); -} - -export async function legacyImport() { - // if legacy data has already been imported, skip this process - const imported = await get("legacy_imported"); - if (imported) return console.info("Legacy settings has already imported"); - // start the one-time import process - const result = await browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORT"}); - if (result.error) return console.error(result.error); - console.info("Import settings data from legacy manifest file"); - const settings = {}; - for (const key of Object.keys(settingsDefine)) { - const legacy = settingsDefine[key].legacy; - if (legacy in result) { - let value = result[legacy]; - switch (settingsDefine[key].type) { - case "boolean": value = JSON.parse(value); break; - case "number": value = Number(value); break; - } - console.info(`Importing legacy setting: ${legacy}`, value); - settings[settingsDefine[key].name] = value; - } - } - // import complete tag, to ensure will only be import once - Object.assign(settings, {legacy_imported: Date.now()}); - if (await set(settings, "local")) { - console.info("Import legacy settings complete"); - // send a message to the Swift layer to safely clean up legacy data - // browser.runtime.sendNativeMessage({name: "PAGE_LEGACY_IMPORTED"}); - return true; - } - return console.error("Import legacy settings abort"); -} diff --git a/src/shared/utils.js b/src/shared/utils.js deleted file mode 100644 index 46e7661c..00000000 --- a/src/shared/utils.js +++ /dev/null @@ -1,244 +0,0 @@ -/** - * @param {number} ms millisecond timestamp - * @returns {string} - */ -export function formatDate(ms) { - const d = new Date(ms); - const yr = new Intl.DateTimeFormat("en", {year: "numeric"}).format(d); - const mo = new Intl.DateTimeFormat("en", {month: "short"}).format(d); - const dd = new Intl.DateTimeFormat("en", {day: "2-digit"}).format(d); - const hr = d.getHours(); - const mn = d.getMinutes(); - return `${mo} ${dd}, ${yr} at ${hr}:${mn}`; -} - -export function uniqueId() { - return Math.random().toString(36).substring(2, 10); -} - -/** - * awaitable function for waiting an arbitrary amount of time - * @param {number} ms the amount of time to wait in milliseconds - * @returns {Promise} - */ -export function wait(ms) { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} - -// TODO: describe the items array that should get passed to this function -/** - * @param {Array} array - * @param {("lastModifiedAsc"|"lastModifiedDesc"|"nameAsc"|"nameDesc")} order - * @returns - */ -export function sortBy(array, order) { - if (order === "nameAsc") { - array.sort((a, b) => a.name.localeCompare(b.name)); - } else if (order === "nameDesc") { - array.sort((a, b) => b.name.localeCompare(a.name)); - } else if (order === "lastModifiedAsc") { - array.sort((a, b) => (a.lastModified < b.lastModified) ? -1 : 1); - } else if (order === "lastModifiedDesc") { - array.sort((a, b) => (a.lastModified > b.lastModified) ? -1 : 1); - } - // always keep temp file pinned to the top, should only ever have one temp script - // if (array.find(f => f.temp)) array.sort((a, b) => a.temp ? -1 : b.temp ? 1 : 0); - return array; -} - -/** - * - * @param {string} description - * @param {string} name - * @param {("css"|"js")} type - * @returns {string} - */ -export function newScriptDefault(description, name, type) { - if (type === "css") { - return `/* ==UserStyle==\n@name ${name}\n@description ${description}\n@match \n==/UserStyle== */`; - } - return `// ==UserScript==\n// @name ${name}\n// @description ${description}\n// @match *://*/*\n// ==/UserScript==`; -} - -/** - * @param {string} str - * @returns {?{code: string, content: str, metablock: string, metadata: {string: string[]}}} - */ -export function parse(str) { - if (typeof str != "string") return null; - const blocksReg = /(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))/; - const blocks = str.match(blocksReg); - - if (!blocks) return null; - - const metablock = (blocks[1] != null) ? blocks[1] : blocks[4]; - const metas = (blocks[2] != null) ? blocks[2] : blocks[5]; - const code = (blocks[3] != null) ? blocks[3].trim() : blocks[6].trim(); - - const metadata = {}; - const metaArray = metas.split("\n"); - metaArray.forEach(m => { - const parts = m.trim().match(/^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]+([^\s]+[^\r\n\t\v\f]*)/); - const parts2 = m.trim().match(/^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$/); - if (parts) { - metadata[parts[1]] = metadata[parts[1]] || []; - metadata[parts[1]].push(parts[2]); - } else if (parts2) { - metadata[parts2[1]] = metadata[parts2[1]] || []; - metadata[parts2[1]].push(true); - } - }); - // fail if @name is missing or name is empty - if (!metadata.name || metadata.name[0].length < 2) return; - - return { - code, - content: str, - metablock, - metadata - }; -} - -/** - * @param {string} text editor code - * @returns {{match: boolean, meta: boolean} | {key: string, value: string, text: string}[]} - */ -export function parseMetadata(text) { - const groupsRe = /(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)/; - const groups = text.match(groupsRe); - // userscript code doesn't match the regex expression - // could be missing opening/closing tags, malformed - // or missing metadata between opening/closing tags (group 2 in regex exp) - if (!groups) { - return {match: false}; - } - - // userscript code matches but content between opening/closing tag missing - // ex. opening/closing tags present, but newline characters between the tags - const metas = groups[2]; - if (!metas) return {match: true, meta: false}; - - const metadata = []; - const metaArray = metas.split("\n"); - - for (let i = 0; i < metaArray.length; i++) { - const metaRegex = /^(?:[ \t]*(?:\/\/)?[ \t]*@)([\w-]+)[ \t]*([^\s]+[^\r\n\t\v\f]*)?/; - const meta = metaArray[i]; - const parts = meta.match(metaRegex); - if (parts) metadata.push({key: parts[1], value: parts[2], text: parts[0]}); - } - - // if there is content between the opening/closing tags, match will be found - // this additionally checks that there's at least one properly formed key - // if not keys found, assume metadata is missing - // checking that required keys are present will happen elsewhere - if (!Object.keys(metadata).length) return {match: true, meta: false}; - - return metadata; -} - -export const validGrants = new Set([ - "GM.info", - "GM_info", - "GM.addStyle", - "GM.openInTab", - "GM.closeTab", - "GM.setValue", - "GM.getValue", - "GM.deleteValue", - "GM.listValues", - "GM.setClipboard", - "GM.getTab", - "GM.saveTab", - "GM_xmlhttpRequest", - "GM.xmlHttpRequest", - "none" -]); - -export const validMetaKeys = new Set([ - "author", - "description", - "downloadURL", - "exclude", - "exclude-match", - "grant", - "icon", - "include", - "inject-into", - "match", - "name", - "noframes", - "require", - "run-at", - "updateURL", - "version", - "weight" -]); - -export const extensionPaths = { - page: "/dist/entry-page.html", - popup: "/dist/entry-popup.html" -}; - -export async function openExtensionPage() { - const url = browser.runtime.getURL(extensionPaths.page); - const tabs = await browser.tabs.query({url}); - const tab = tabs.find(e => e.url.startsWith(url)); - if (!tab) return browser.tabs.create({url}); - browser.tabs.update(tab.id, {active: true}); - browser.windows.update(tab.windowId, {focused: true}); -} - -// Safari currently does not honor the target attribute of elements in extension contexts -export async function openInBlank(url) { - browser.tabs.create({url}); -} - -// Safari currently does not honor the download attribute of elements in extension contexts -// Also not support https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/download -export async function downloadToFile(filename, content, type = "text/plain") { - const url = "https://quoid.github.io/userscripts/serve/download.html"; - const tab = await browser.tabs.create({url}); - const exchange = {filename, content, type}; - const exscript = o => { - // make sure executed only once - if (window.US_DOWNLOAD === 1) return; window.US_DOWNLOAD = 1; - window.stop(); - document.body.textContent = "Download is starting..."; - const a = document.createElement("a"); - a.download = o.filename; - a.href = URL.createObjectURL(new Blob([o.content], {type: o.type})); - a.click(); - document.body.innerHTML += "
    The download should have started.
    "; - a.textContent = o.filename; - document.body.append(a); - }; - // Safari currently unable to stably executeScript on tab loading status - try { - await browser.tabs.executeScript(tab.id, { - code: `(${exscript})(${JSON.stringify(exchange)});` - }); - } catch { - const handleUpdated = async tabId => { - if (tabId !== tab.id) return; - try { - await browser.tabs.executeScript(tabId, { - code: `(${exscript})(${JSON.stringify(exchange)});` - }); - console.info(`[${filename}] Download is starting...`); - } catch { - console.info(`[${filename}] Start download failed, retrying...`); - } - }; - browser.tabs.onUpdated.addListener(handleUpdated); - // Remove the listener when tab closing - const handleRemoved = tabId => { - if (tabId !== tab.id) return; - browser.tabs.onUpdated.removeListener(handleUpdated); - browser.tabs.onRemoved.removeListener(handleRemoved); - }; - browser.tabs.onRemoved.addListener(handleRemoved); - } -} diff --git a/src/shared/variables.css b/src/shared/variables.css deleted file mode 100644 index acdc671d..00000000 --- a/src/shared/variables.css +++ /dev/null @@ -1,51 +0,0 @@ -:root { - --border-radius: 0.188rem; - --box-shadow: 0 0.5rem 1rem 0 rgb(0 0 0 / 0.25); - --color-bg-primary: #323639; - --color-bg-secondary: #2f3337; - --color-script-highlighted: #364049; /* rgba(116, 178, 235, 0.1); */ - --color-black: #1d2023; - --color-blue: #74b1eb; - --color-green: #60f36c; - --color-grey: rgb(255 255 255 / 0.15); - --color-red: #ff453a; - --color-yellow: #e4f360; - --letter-spacing-large: -0.031rem; - --letter-spacing-default: -0.029rem; - --letter-spacing-medium: -0.018rem; - --letter-spacing-small: -0.008rem; - --opacity-disabled: 0.3; - --text-color-primary: rgb(255 255 255); - --text-color-secondary: rgb(255 255 255 / 0.65); - --text-color-disabled: rgb(255 255 255 / 0.4); - --font-family: system-ui, -apple-system, "Helvetica Neue", "Helvetica", sans-serif; - --text-default: 1rem/1.5rem var(--font-family); - --text-large: 1.25rem/1.5rem var(--font-family); - --text-medium: 0.875rem/1.313rem var(--font-family); - --text-small: 0.719rem/1rem var(--font-family); - - /* editor variables */ - --editor-font: monaco, monospace; - --editor-font-size: 14px; - --editor-line-height: 24px; - --editor-invisible: rgb(255 255 255 / 0.15); - --editor-active-line: var(--color-bg-secondary); - --editor-selected-bg: rgb(116 178 235 / 0.35); - --editor-matched-highlight: rgb(116 178 235 / 0.2); - --editor-search-highlight: rgb(255 166 0 / 0.3); - --editor-number: #77e26a; - --editor-comment: rgb(255 255 255 / 0.35); - --editor-def: #efc371; - --editor-default: #cdcfd1; - --editor-keyword: #96c3ed; - --editor-atom: #59ebf5; - --editor-operator: #8c99a7; - --editor-property: #e86c8a; - --editor-string: #f5eea2; - --editor-string-2: #cdabff; - --editor-error: var(--color-red); - --editor-cursor: #e3e7eb; - --editor-matching-bracket-color: #fff; - --editor-matching-bracket-border: var(--editor-number); - --editor-non-matching-bracket: var(--editor-error); -} diff --git a/vite.config.js b/vite.config.js index 044b986d..f7cf2b45 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,44 +1,25 @@ -import {resolve} from "path"; -import {defineConfig} from "vite"; -import {svelte} from "@sveltejs/vite-plugin-svelte"; +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; -// [inline-svg] -// TODO: remove this once vite resolved issue -// https://github.com/vitejs/vite/issues/1204 -// import fs from "fs"; -// import svgpkg from "svg-inline-loader"; -// export function svgInline() { // custom plugin -// return { -// name: "transform-file", -// transform(code, id) { -// if (id.endsWith(".svg")) { -// const svg = fs.readFileSync(id, "utf8"); -// const ret = svgpkg.getExtractedSVG(svg, {}); -// return `export default '${ret}'`; -// } -// } -// }; -// } -// [inline-svg] -// NOW: use `?raw` suffix import svg assets as inline -// https://vitejs.dev/guide/assets.html#importing-asset-as-string +/** + * About `inline-svg` + * use `?raw` suffix import svg assets as inline + * @see {@link https://vitejs.dev/guide/assets.html#importing-asset-as-string} + * Ignore lint errors `{@html}`, since these are deterministic local svg resources, + * there is no risk of Cross-Site Scripting (XSS) attacks and can be safely disabled. + * @see {@link https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-html-tags/} + */ -// [autoprefixer] -// https://vitejs.dev/guide/features.html#postcss -// have config with `.postcssrc.json` file -// about `missing peer postcss` error, ignore it +/** + * About `autoprefixer` + * have config with `.postcssrc.json` file + * @see {@link https://vitejs.dev/guide/features.html#postcss} + * about `missing peer postcss` error, ignore it + */ -// https://vitejs.dev/config/ +/** @see {@link https://vitejs.dev/config/} */ export default defineConfig({ - plugins: [svelte()], - base: "./", - build: { - outDir: "xcode/Safari-Extension/Resources/dist/", - rollupOptions: { - input: { - page: resolve(__dirname, "entry-page.html"), - popup: resolve(__dirname, "entry-popup.html") - } - } - } + publicDir: "public/ext/vendor/", + plugins: [svelte()], + base: "./", }); diff --git a/xcode/Mac-App/App.entitlements b/xcode/App-Mac/App.entitlements similarity index 100% rename from xcode/Mac-App/App.entitlements rename to xcode/App-Mac/App.entitlements diff --git a/xcode/App-Mac/AppDelegate.swift b/xcode/App-Mac/AppDelegate.swift new file mode 100644 index 00000000..ffdf0758 --- /dev/null +++ b/xcode/App-Mac/AppDelegate.swift @@ -0,0 +1,70 @@ +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + private var window: NSWindow! + private var windowForego = false + private var windowLoaded = false + + @IBOutlet weak var enbaleNativeLogger: NSMenuItem! + + func application(_ application: NSApplication, open urls: [URL]) { + // if open panel is already open, stop processing the URL scheme + if NSApplication.shared.keyWindow?.accessibilityIdentifier() == "open-panel" { return } + for url in urls { + if url.host == "changesavelocation" { + // avoid opening the panel repeatedly and playing unnecessary warning sounds + if NSApplication.shared.keyWindow?.identifier?.rawValue == "changeSaveLocation" { continue } + if windowLoaded { + let viewController = window.contentViewController as? ViewController + viewController?.changeSaveLocation(nil) + } else { + windowForego = true + schemeChangeSaveLocation() + } + } + } + } + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Initialize menu items + enbaleNativeLogger.state = Preferences.enableLogger ? .on : .off + if windowForego { return } + let storyboard = NSStoryboard(name: "View", bundle: Bundle.main) + let windowController = storyboard.instantiateInitialController() as! NSWindowController +// let viewController = windowController.contentViewController as! ViewController + window = windowController.window + window.setIsVisible(true) + windowLoaded = true + } + + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + return true + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + @IBAction func enableLogger(_ sender: NSMenuItem) { + if sender.state == .on { + Preferences.enableLogger = false + sender.state = .off + } else { + Preferences.enableLogger = true + sender.state = .on + } + } + + @IBAction func applicationHelp(_ sender: NSMenuItem) { + if let url = URL(string: "https://github.com/quoid/userscripts") { + NSWorkspace.shared.open(url) + } + } + +} diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/1024.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/1024.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/128.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/128.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/128.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/128.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/16.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/16.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/16.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/16.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/256.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/256-1.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/256.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/256-1.png diff --git a/xcode/iOS-App/Resources/Icon.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/256.png similarity index 100% rename from xcode/iOS-App/Resources/Icon.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/256.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/32-1.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/32-1.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/32-1.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/32-1.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/32.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/32.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/32.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/32.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/512-1.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/512-1.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/512-1.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/512-1.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/512.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/512.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/512.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/512.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/64.png b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/64.png similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/64.png rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/64.png diff --git a/xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/Contents.json b/xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from xcode/Mac-App/Assets.xcassets/AppIcon.appiconset/Contents.json rename to xcode/App-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/xcode/Mac-App/Assets.xcassets/Contents.json b/xcode/App-Mac/Assets.xcassets/Contents.json similarity index 100% rename from xcode/Mac-App/Assets.xcassets/Contents.json rename to xcode/App-Mac/Assets.xcassets/Contents.json diff --git a/xcode/Mac-App/Base.lproj/Main.storyboard b/xcode/App-Mac/Base.lproj/Main.storyboard similarity index 57% rename from xcode/Mac-App/Base.lproj/Main.storyboard rename to xcode/App-Mac/Base.lproj/Main.storyboard index ed7ecf33..859f7dde 100644 --- a/xcode/Mac-App/Base.lproj/Main.storyboard +++ b/xcode/App-Mac/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -30,13 +30,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + diff --git a/xcode/Mac-App/Base.lproj/View.storyboard b/xcode/App-Mac/Base.lproj/View.storyboard similarity index 100% rename from xcode/Mac-App/Base.lproj/View.storyboard rename to xcode/App-Mac/Base.lproj/View.storyboard diff --git a/xcode/App-Mac/Functions.swift b/xcode/App-Mac/Functions.swift new file mode 100644 index 00000000..48770bfa --- /dev/null +++ b/xcode/App-Mac/Functions.swift @@ -0,0 +1,70 @@ +import AppKit +import SafariServices + +private let logger = USLogger(#fileID) + +func getSaveLocationURL() -> URL { + return Preferences.scriptsDirectoryUrl +} + +func setSaveLocationURL(url: URL) -> Bool { + guard FileManager.default.isWritableFile(atPath: url.path) else { + let alert = NSAlert() + alert.messageText = "Can not write to path. Choose a different path." + alert.runModal() + return false + } + Preferences.scriptsDirectoryUrl = url + return Preferences.scriptsDirectoryUrl == url +} + +func sendExtensionMessage(name: String, userInfo: [String : Any]? = nil, completion: ((Error?) -> Void)? = nil) { + SFSafariApplication.dispatchMessage( + withName: name, + toExtensionWithIdentifier: extIdentifier, + userInfo: userInfo + ) { error in // always be called + if error != nil { + debugPrint("Message attempted. Error info: \(String.init(describing: error))") + } + if let userHandle = completion { userHandle(error) } + } +} + +func changeSaveLocationPanel(directoryURL: URL? = nil) -> NSOpenPanel { + let panel = NSOpenPanel() + panel.allowsMultipleSelection = false + panel.canChooseDirectories = true + panel.canCreateDirectories = true + panel.canChooseFiles = false + if directoryURL != nil { + panel.directoryURL = directoryURL + } + panel.title = "Change Save Location - Userscripts" + panel.identifier = NSUserInterfaceItemIdentifier("changeSaveLocation") + return panel +} + +func schemeChangeSaveLocation() { + let saveLocationURL = getSaveLocationURL() + let panel = changeSaveLocationPanel(directoryURL: saveLocationURL) + // shows the path selection panel + let response = panel.runModal() + // check if clicked open button and there is a valid result + guard response == .OK, let url: URL = panel.urls.first else { return } + // revoke implicitly starts security-scoped access + defer { url.stopAccessingSecurityScopedResource() } + // check if path has indeed changed + if url.absoluteString == saveLocationURL.absoluteString { return } + // try set new save location path to bookmark + guard setSaveLocationURL(url: url) else { return } + // use semaphore to ensure the async func executed before app exits + let semaphore = DispatchSemaphore(value: 0) + // notify browser extension of relevant updates + sendExtensionMessage( + name: "SAVE_LOCATION_CHANGED", + userInfo: ["saveLocation": url.absoluteString.removingPercentEncoding ?? url.absoluteString], + completion: { _ in semaphore.signal() } + ) + semaphore.wait() +} diff --git a/xcode/Mac-App/Info.plist b/xcode/App-Mac/Info.plist similarity index 100% rename from xcode/Mac-App/Info.plist rename to xcode/App-Mac/Info.plist diff --git a/xcode/App-Mac/ViewController.swift b/xcode/App-Mac/ViewController.swift new file mode 100644 index 00000000..fc7598ee --- /dev/null +++ b/xcode/App-Mac/ViewController.swift @@ -0,0 +1,82 @@ +import SafariServices.SFSafariApplication + +private let logger = USLogger(#fileID) + +class ViewController: NSViewController { + + @IBOutlet var appName: NSTextField! + @IBOutlet var saveLocation: NSTextField! + @IBOutlet weak var enabledText: NSTextField! + @IBOutlet weak var enabledIcon: NSView! + @IBOutlet weak var openButton: NSButton! + + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" + let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" + + override func viewDidLoad() { + super.viewDidLoad() + self.appName.stringValue = "Userscripts Safari Version \(appVersion) (\(buildNumber))" + setExtensionState() + NotificationCenter.default.addObserver( + self, + selector: #selector(setExtensionState), + name: NSApplication.didBecomeActiveNotification, + object: nil + ) + let url = getSaveLocationURL() + self.saveLocation.stringValue = url.absoluteString + self.saveLocation.toolTip = url.absoluteString + if #available(macOS 13, *) { + self.openButton.title = "Open Safari Settings" + } + } + + @objc func setExtensionState() { + SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extIdentifier) { (state, error) in + guard let state = state else { + self.enabledText.stringValue = "Safari Extension State Unknown" + if let error = error { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } else { + logger?.error("\(#function, privacy: .public) - couldn't get safari extension state in containing app") + } + return + } + DispatchQueue.main.async { + self.enabledIcon.layer?.backgroundColor = state.isEnabled ? NSColor.green.cgColor : NSColor.red.cgColor + self.enabledText.stringValue = state.isEnabled ? "Safari Extension Enabled" : "Safari Extension Disabled" + } + } + } + + @IBAction func changeSaveLocation(_ sender: AnyObject?) { + guard let window = self.view.window else { return } + let saveLocationURL = getSaveLocationURL() + let panel = changeSaveLocationPanel(directoryURL: saveLocationURL) + panel.beginSheetModal(for: window, completionHandler: { response in + // check if clicked open button and there is a valid result + guard response == .OK, let url: URL = panel.urls.first else { return } + // revoke implicitly starts security-scoped access + defer { url.stopAccessingSecurityScopedResource() } + // check if path has indeed changed + if url.absoluteString == saveLocationURL.absoluteString { return } + // try set new save location path to bookmark + guard setSaveLocationURL(url: url) else { return } + // update user interface text display + self.saveLocation.stringValue = url.absoluteString + self.saveLocation.toolTip = url.absoluteString + // notify browser extension of relevant updates + sendExtensionMessage( + name: "SAVE_LOCATION_CHANGED", + userInfo: [ + "saveLocation": url.absoluteString.removingPercentEncoding ?? url.absoluteString, + "returnApp": true + ] + ) + }) + } + + @IBAction func openSafariExtensionPreferences(_ sender: AnyObject?) { + SFSafariApplication.showPreferencesForExtension(withIdentifier: extIdentifier) + } +} diff --git a/xcode/App-Shared/USchemeHandler.swift b/xcode/App-Shared/USchemeHandler.swift new file mode 100644 index 00000000..28fa301b --- /dev/null +++ b/xcode/App-Shared/USchemeHandler.swift @@ -0,0 +1,66 @@ +import WebKit +import UniformTypeIdentifiers + +private let logger = USLogger(#fileID) +let AppWebViewUrlScheme = "userscriptsapp" +let AppWebViewEntryPage = "entry-app-webview.html" + +// https://developer.apple.com/documentation/webkit/wkurlschemehandler +// https://developer.apple.com/documentation/webkit/wkurlschemetask +class USchemeHandler: NSObject, WKURLSchemeHandler { + + func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + // wrapper of didFailWithError + func failHandler(_ errmsg: String) { + logger?.error("\(#function) - \(errmsg, privacy: .public)") + // redirect to a customized error page +// DispatchQueue.main.async { +// webView.load(URLRequest(url: URL(string: "\(AppWebViewUrlScheme):///")!)) +// } + urlSchemeTask.didFailWithError(NSError(domain: "USchemeHandler", code: 0, userInfo: nil)) + } + + // https://developer.apple.com/documentation/dispatch/dispatchqueue + DispatchQueue.global(qos: .userInteractive).async { + guard let url = urlSchemeTask.request.url else { + failHandler("failed to get request url") + return + } + guard url.scheme == AppWebViewUrlScheme else { + failHandler("unexpected url scheme: \(url)") + return + } + var name: String + if #available(macOS 13.0, iOS 16.0, *) { + name = url.path(percentEncoded: false) + } else { + name = url.path + } + if ["", "/"].contains(name) { + name = AppWebViewEntryPage + } + guard let file = Bundle.main.url(forResource: name, withExtension: nil, subdirectory: "dist") else { + failHandler("file not found: \(url)") + return + } + guard let data = try? Data(contentsOf: file) else { + failHandler("faild to get data from: \(url)") + return + } + + // https://developer.apple.com/documentation/uniformtypeidentifiers + // let mime = UTType.init(filenameExtension: file.pathExtension)?.preferredMIMEType ?? "application/octet-stream" + let mime = UTTypeReference.init(filenameExtension: file.pathExtension)?.preferredMIMEType ?? "application/octet-stream" + + let response = URLResponse(url: url, mimeType: mime, expectedContentLength: data.count, textEncodingName: nil) + urlSchemeTask.didReceive(response) + urlSchemeTask.didReceive(data) + urlSchemeTask.didFinish() + } + } + + func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + + } + +} diff --git a/xcode/App-Shared/ViewController.swift b/xcode/App-Shared/ViewController.swift new file mode 100644 index 00000000..15430135 --- /dev/null +++ b/xcode/App-Shared/ViewController.swift @@ -0,0 +1,110 @@ +import WebKit + +private let logger = USLogger(#fileID) + +class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler, UIDocumentPickerDelegate { + + @IBOutlet var webView: WKWebView! + + // https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview + override func loadView() { + // https://developer.apple.com/documentation/webkit/wkwebviewconfiguration + let configuration = WKWebViewConfiguration() + // https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler + configuration.setURLSchemeHandler(USchemeHandler(), forURLScheme: AppWebViewUrlScheme) + // https://developer.apple.com/documentation/webkit/wkusercontentcontroller + configuration.userContentController.add(self, name: "controller") + // https://developer.apple.com/documentation/webkit/wkwebview + self.webView = WKWebView(frame: .zero, configuration: configuration) + // https://developer.apple.com/documentation/webkit/wknavigationdelegate + self.webView.navigationDelegate = self +#if DEBUG + // https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/ + if #available(macOS 13.3, iOS 16.4, tvOS 16.4, *) { + // https://developer.apple.com/documentation/webkit/wkwebview/4111163-inspectable/ + self.webView.isInspectable = true + } + logger?.debug("\(#function, privacy: .public) - DEBUG mode: isInspectable = true") +#endif + view = webView + self.webView.scrollView.isScrollEnabled = false + self.webView.isOpaque = false + let backgroundColor = UIColor.init(red: (47/255.0), green: (51/255.0), blue: (55/255.0), alpha: 1.0) + view.setValue(backgroundColor, forKey: "backgroundColor") + self.webView.backgroundColor = backgroundColor + } + + // https://developer.apple.com/documentation/uikit/uiviewcontroller/1621495-viewdidload + override func viewDidLoad() { + super.viewDidLoad() + webView.load(URLRequest(url: URL(string: "\(AppWebViewUrlScheme):///")!)) + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" + let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" + webView.evaluateJavaScript("APP.printVersion('v\(appVersion)', '(\(buildNumber))')") + webView.evaluateJavaScript("APP.printDirectory('\(getCurrentScriptsDirectoryString())')") + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if navigationAction.navigationType == .linkActivated { + guard let url = navigationAction.request.url else { + decisionHandler(.allow) + return + } + // allow registration scheme + if url.scheme == AppWebViewUrlScheme { + decisionHandler(.allow) + return + } + // allow specified url prefixes +// let allowPrefixes = [ +// "https://github.com/quoid/userscripts" +// ] +// for prefix in allowPrefixes { +// if url.absoluteString.lowercased().hasPrefix(prefix) { +// decisionHandler(.allow) +// return +// } +// } + // open from external app like safari + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + decisionHandler(.cancel) + return + } + decisionHandler(.allow) + } + decisionHandler(.allow) + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let name = message.body as? String else { + logger?.error("\(#function, privacy: .public) - Userscripts iOS received a message without a name") + return + } + if name == "CHANGE_DIRECTORY" { + // https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories + logger?.info("\(#function, privacy: .public) - Userscripts iOS has requested to set the readLocation") + let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) + documentPicker.delegate = self + documentPicker.directoryURL = getDocumentsDirectory() + present(documentPicker, animated: true, completion: nil) + } + if name == "OPEN_DIRECTORY" { + guard var components = URLComponents(url: Preferences.scriptsDirectoryUrl, resolvingAgainstBaseURL: true) else { + return + } + components.scheme = "shareddocuments" + if let url = components.url, UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { + Preferences.scriptsDirectoryUrl = url + webView.evaluateJavaScript("APP.printDirectory('\(getCurrentScriptsDirectoryString())')") + } +} diff --git a/xcode/iOS-App/App.entitlements b/xcode/App-iOS/App.entitlements similarity index 100% rename from xcode/iOS-App/App.entitlements rename to xcode/App-iOS/App.entitlements diff --git a/xcode/App-iOS/AppDelegate.swift b/xcode/App-iOS/AppDelegate.swift new file mode 100644 index 00000000..9da1b4c6 --- /dev/null +++ b/xcode/App-iOS/AppDelegate.swift @@ -0,0 +1,27 @@ +// +// AppDelegate.swift +// Userscripts-iOS +// +// Created by Justin Wasack on 10/3/21. +// Copyright © 2021 Justin Wasack. All rights reserved. +// + +import UIKit +import os + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + initializeFirstStart() + return true + } + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + +} diff --git a/xcode/iOS-App/Assets.xcassets/AccentColor.colorset/Contents.json b/xcode/App-iOS/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AccentColor.colorset/Contents.json rename to xcode/App-iOS/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/1024.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/1024.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/1024.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/120-1.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/120-1.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/120-1.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/120-1.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/120.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/120.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/120.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/120.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/152.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/152.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/152.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/152.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/167.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/167.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/167.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/167.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/180.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/180.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/180.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/180.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/20.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/20.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/20.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/20.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/29.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/29.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/29.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/29.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40-1.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40-1.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40-1.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40-1.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40-2.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40-2.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40-2.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40-2.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/40.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/40.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/58-1.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/58-1.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/58-1.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/58-1.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/58.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/58.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/58.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/58.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/60.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/60.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/60.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/60.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/76.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/76.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/76.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/76.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/80-1.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/80-1.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/80-1.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/80-1.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/80.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/80.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/80.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/80.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/87.png b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/87.png similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/87.png rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/87.png diff --git a/xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/Contents.json b/xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/AppIcon.appiconset/Contents.json rename to xcode/App-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/Contents.json b/xcode/App-iOS/Assets.xcassets/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/Contents.json rename to xcode/App-iOS/Assets.xcassets/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/LargeIcon.imageset/Contents.json b/xcode/App-iOS/Assets.xcassets/LargeIcon.imageset/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/LargeIcon.imageset/Contents.json rename to xcode/App-iOS/Assets.xcassets/LargeIcon.imageset/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/LaunchScreen.imageset/Contents.json b/xcode/App-iOS/Assets.xcassets/LaunchScreen.imageset/Contents.json similarity index 100% rename from xcode/iOS-App/Assets.xcassets/LaunchScreen.imageset/Contents.json rename to xcode/App-iOS/Assets.xcassets/LaunchScreen.imageset/Contents.json diff --git a/xcode/iOS-App/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf b/xcode/App-iOS/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf similarity index 100% rename from xcode/iOS-App/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf rename to xcode/App-iOS/Assets.xcassets/LaunchScreen.imageset/LaunchScreen.pdf diff --git a/xcode/iOS-App/Base.lproj/LaunchScreen.storyboard b/xcode/App-iOS/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from xcode/iOS-App/Base.lproj/LaunchScreen.storyboard rename to xcode/App-iOS/Base.lproj/LaunchScreen.storyboard diff --git a/xcode/iOS-App/Base.lproj/Main.storyboard b/xcode/App-iOS/Base.lproj/Main.storyboard similarity index 100% rename from xcode/iOS-App/Base.lproj/Main.storyboard rename to xcode/App-iOS/Base.lproj/Main.storyboard diff --git a/xcode/iOS-App/Info.plist b/xcode/App-iOS/Info.plist similarity index 87% rename from xcode/iOS-App/Info.plist rename to xcode/App-iOS/Info.plist index a5539a85..b6d4f637 100644 --- a/xcode/iOS-App/Info.plist +++ b/xcode/App-iOS/Info.plist @@ -38,7 +38,13 @@ UIStatusBarStyleLightContent UIViewControllerBasedStatusBarAppearance + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + US_SHARED_GID $(IOS_SHARED_GID) + US_EXT_IDENTIFIER + $(EXT_IDENTIFIER) diff --git a/xcode/App-iOS/Initialization.swift b/xcode/App-iOS/Initialization.swift new file mode 100644 index 00000000..7585ff7d --- /dev/null +++ b/xcode/App-iOS/Initialization.swift @@ -0,0 +1,49 @@ +import Foundation + +private let logger = USLogger(#fileID) + +private func createDemoScript() { + let demoScript = """ +// ==UserScript== +// @name Demo user script +// @description I am a demo user script that you can safely delete (add any files to this folder and I will no longer automatically generate) +// @author Userscripts +// @version 0.0.1 +// @match *://*/* +// @grant none +// @inject-into content +// ==/UserScript== + +(function () { + 'use strict'; + // here is your code +})(); +""" + let url = getDocumentsDirectory() + // get a list of non-hidden files + guard let contents = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: [], options: .skipsHiddenFiles) else { + logger?.error("\(#function, privacy: .public) - failed get contentsOfDirectory") + return + } + if contents.isEmpty { + let fileURL = url.appendingPathComponent("Demo_user_script.user.js") + do { + logger?.info("\(#function, privacy: .public) - try to write demo user script") + try demoScript.write(to: fileURL, atomically: true, encoding: .utf8) + } catch { + logger?.error("\(#function, privacy: .public) - failed to write demo user script") + } + } +} + +func initializeFirstStart() { + // set the scripts directory to the app document on first use + if isCurrentDefaultScriptsDirectory() { + logger?.info("\(#function, privacy: .public) - Initialize default directory") + Preferences.scriptsDirectoryUrl = getDocumentsDirectory() + } + // put a visible file to display the documents directory in files app + if isCurrentInitialScriptsDirectory() { + createDemoScript() + } +} diff --git a/xcode/iOS-App/SceneDelegate.swift b/xcode/App-iOS/SceneDelegate.swift similarity index 50% rename from xcode/iOS-App/SceneDelegate.swift rename to xcode/App-iOS/SceneDelegate.swift index 1d5426bd..e733d27e 100644 --- a/xcode/iOS-App/SceneDelegate.swift +++ b/xcode/App-iOS/SceneDelegate.swift @@ -10,10 +10,10 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? + var window: UIWindow? - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - guard let _ = (scene as? UIWindowScene) else { return } - } + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let _ = (scene as? UIWindowScene) else { return } + } } diff --git a/xcode/App-iOS/Settings.bundle/Root.plist b/xcode/App-iOS/Settings.bundle/Root.plist new file mode 100644 index 00000000..b9ad077e --- /dev/null +++ b/xcode/App-iOS/Settings.bundle/Root.plist @@ -0,0 +1,31 @@ + + + + + ApplicationGroupContainerIdentifier + // US_SHARED_GID will be introduced in the custom build script + StringsTable + Root + PreferenceSpecifiers + + + Type + PSGroupSpecifier + Title + GroupDebug + FooterText + FooterText + + + Type + PSToggleSwitchSpecifier + Title + EnableLogger + Key + EnableLogger + DefaultValue + + + + + diff --git a/xcode/App-iOS/Settings.bundle/en.lproj/Root.strings b/xcode/App-iOS/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 00000000..dc826ef0 Binary files /dev/null and b/xcode/App-iOS/Settings.bundle/en.lproj/Root.strings differ diff --git a/xcode/Ext-Safari/Functions.swift b/xcode/Ext-Safari/Functions.swift new file mode 100644 index 00000000..9ce1f86b --- /dev/null +++ b/xcode/Ext-Safari/Functions.swift @@ -0,0 +1,1957 @@ +import SafariServices + +private let logger = USLogger(#fileID) + +// helpers +func getRequireLocation() -> URL { + // simple helper in case required code save directory needs to change + return getDocumentsDirectory().appendingPathComponent("require") +} + +func dateToMilliseconds(_ date: Date) -> Int { + let since1970 = date.timeIntervalSince1970 + return Int(since1970 * 1000) +} + +func sanitize(_ str: String) -> String { + // removes invalid filename characters from strings + var sanitized = str + if sanitized.first == "." { + sanitized = "%2" + str.dropFirst() + } + sanitized = sanitized.replacingOccurrences(of: "/", with: "%2F") + sanitized = sanitized.replacingOccurrences(of: ":", with: "%3A") + sanitized = sanitized.replacingOccurrences(of: "\\", with: "%5C") + return sanitized +} + +func unsanitize(_ str: String) -> String { + var s = str + if s.hasPrefix("%2") && !s.hasPrefix("%2F") { + s = "." + s.dropFirst(2) + } + if s.removingPercentEncoding != s { + s = s.removingPercentEncoding ?? s + } + return s +} + +func normalizeWeight(_ weight: String) -> String { + if let w = Int(weight) { + if w > 999 { + return "999" + } else if w < 1 { + return "1" + } else { + return weight + } + } else { + return "1" + } +} + +func getSaveLocation() -> URL? { +#if os(iOS) + if isCurrentDefaultScriptsDirectory() { + logger?.info("\(#function, privacy: .public) - Uninitialized save location") + return nil + } +#endif + let url = Preferences.scriptsDirectoryUrl + logger?.debug("\(#function, privacy: .public) - \(url, privacy: .public)") + return url +} + +func openSaveLocation() -> Bool { + #if os(macOS) + guard let saveLocation = getSaveLocation() else { + return false + } + let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() + defer { + if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} + } + NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: saveLocation.path) + #endif + return true +} + +func validateUrl(_ urlString: String) -> Bool { + guard + let parts = jsLikeURL(urlString), + let ptcl = parts["protocol"], + let path = parts["pathname"] + else { + logger?.error("\(#function, privacy: .public) - Invalid URL: \(urlString, privacy: .public)") + return false + } + if + (ptcl != "https:" && ptcl != "http:") + || (!path.hasSuffix(".css") && !path.hasSuffix(".js")) + { + return false + } + return true +} + +func isVersionNewer(_ oldVersion: String, _ newVersion: String) -> Bool { + let oldVersions = oldVersion.components(separatedBy: ".") + let newVersions = newVersion.components(separatedBy: ".") + for (index, version) in newVersions.enumerated() { + let a = Int(version) ?? 0 + let oldVersionValue = oldVersions.indices.contains(index) ? oldVersions[index] : "0" + let b = Int(oldVersionValue) ?? 0 + if a > b { + return true + } + if a < b { + return false + } + } + return false +} + +func isEncoded(_ str: String) -> Bool { + if let decoded = str.removingPercentEncoding { + return decoded != str + } + return false +} + +// parser +func parse(_ content: String) -> [String: Any]? { + // returns structured data from content of file + // will fail to parse if metablock or required @name key missing + let pattern = #"(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))"# + // force try b/c pattern is known to be valid regex + let regex = try! NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(location: 0, length: content.utf16.count) + // return nil/fail if metablock missing + guard let match = regex.firstMatch(in: content, options: [], range: range) else { + return nil + } + + // at this point the text content has passed initial validation, it contains valid metadata + // the metadata can be in userscript or userstyle format, need to check for this and adjust group numbers + // rather than being too strict, text content can precede the opening userscript tag, however it will be ignored + // adjust start index of file content while assigning group numbers to account for any text content preceding opening tag + let contentStartIndex = content.index(content.startIndex, offsetBy: match.range.lowerBound) + var g1, g2, g3:Int + if (content[contentStartIndex.. Bool { + let content = data + let url = getDocumentsDirectory().appendingPathComponent("manifest.json") + do { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + let encoded = try encoder.encode(content) + let fileContent = String(decoding: encoded, as: UTF8.self) + try fileContent.write(to: url, atomically: false, encoding: .utf8) + return true + } catch { + logger?.error("\(#function, privacy: .public) - failed to update manifest: \(error.localizedDescription, privacy: .public)") + return false + } +} + +func getManifest() -> Manifest { + let url = getDocumentsDirectory().appendingPathComponent("manifest.json") + if + let content = try? String(contentsOf: url, encoding: .utf8), + let data = content.data(using: .utf8), + let decoded = try? JSONDecoder().decode(Manifest.self, from: Data(data)) + { + return decoded + } else { + // manifest missing, improperly formatted or missing key + // create new manifest with default key/vals + let manifest = Manifest( + blacklist: [], + declarativeNetRequest: [], + disabled: [], + exclude: [:], + excludeMatch: [:], + include: [:], + match: [:], + require: [:], + settings: defaultSettings + ) + _ = updateManifest(with: manifest) + return manifest + } +} + +func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { + logger?.info("\(#function, privacy: .public) - started") + // only get all files if files were not provided + var files = [[String: Any]]() + if optionalFilesArray.isEmpty { + guard let getFiles = getAllFiles() else {return false} + files = getFiles + } else { + files = optionalFilesArray + } + var manifest = getManifest() + for file in files { + // can be force unwrapped because getAllFiles didn't return nil + let metadata = file["metadata"] as! [String: [String]] + let filename = file["filename"] as! String + // skip request type userscripts + let runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + continue + } + // populate excludes & matches + var excludeMatched = [String]() + var matched = [String]() + var excluded = [String]() + var included = [String]() + if metadata["exclude-match"] != nil { + excludeMatched.append(contentsOf: metadata["exclude-match"]!) + } + if metadata["match"] != nil { + matched.append(contentsOf: metadata["match"]!) + } + if metadata["include"] != nil { + included.append(contentsOf: metadata["include"]!) + } + if metadata["exclude"] != nil { + excluded.append(contentsOf: metadata["exclude"]!) + } + // if in declarativeNetRequest array, remove it + if manifest.declarativeNetRequest.contains(filename) { + if let index = manifest.declarativeNetRequest.firstIndex(of: filename) { + manifest.declarativeNetRequest.remove(at: index) + } else { + logger?.error("\(#function, privacy: .public) - failed to remove \(filename, privacy: .public) from dNR array") + } + } + + // update manifest values + manifest.excludeMatch = updatePatternDict(filename, excludeMatched, manifest.excludeMatch) + manifest.match = updatePatternDict(filename, matched, manifest.match) + manifest.exclude = updatePatternDict(filename, excluded, manifest.exclude) + manifest.include = updatePatternDict(filename, included, manifest.include) + + if !updateManifest(with: manifest) { + logger?.error("\(#function, privacy: .public) - failed to update manifest matches") + return false + } + } + logger?.info("\(#function, privacy: .public) - completed") + return true +} + +func updatePatternDict(_ filename: String, _ filePatterns: [String], _ manifestKeys: [String: [String]]) -> [String: [String]] { + // will hold the exclude/match patterns in manifest that have file name as value + var patternsInManifestForFile = [String]() + // new var from func argument, so it can be manipulated + var returnDictionary = manifestKeys + // patterns from manifest + let keys = returnDictionary.keys + // determine what patterns already have this filename as a value + for key in keys { + // key is an array of filenames + guard let filenames = returnDictionary[key] else { + logger?.error("\(#function, privacy: .public) - failed to get values for manifest key, \(key, privacy: .public)") + continue + } + for name in filenames { + // name is a single filename + // if name is same as filename, file already added for this pattern + // add it to patternsInManifestForFile for later comparison + if name == filename { + patternsInManifestForFile.append(key) + } + } + } + // patterns in file metadata and patterns in manifest that have filename as a value + // filename already present in manifest for these patterns, do nothing with these + // let common = filePatterns.filter{patternsInManifestForFile.contains($0)} + // patterns in file metadata, but don't have the filename as a value within the manifest + // these are the manifest patterns that the filename needs to be added to + let addFilenameTo = filePatterns.filter{!patternsInManifestForFile.contains($0)} + + // the patterns that have the filename as a value, but not present in file metadata + // ie. these are the manifest patterns we need to remove the filename from + let removeFilenameFrom = patternsInManifestForFile.filter{!filePatterns.contains($0)} + + // check if filename needs to be added or new key/val needs to be created + for pattern in addFilenameTo { + if returnDictionary[pattern] != nil { + returnDictionary[pattern]?.append(filename) + } else { + returnDictionary[pattern] = [filename] + } + } + + for pattern in removeFilenameFrom { + // get the index of the filename within the array + let ind = returnDictionary[pattern]?.firstIndex(of: filename) + // remove filename from array by index + returnDictionary[pattern]?.remove(at: ind!) + // if filename was the last item in array, remove the url pattern from dictionary + if returnDictionary[pattern]!.isEmpty { + returnDictionary.removeValue(forKey: pattern) + } + } + + return returnDictionary +} + +func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool { + logger?.info("\(#function, privacy: .public) - started") + // only get all files if files were not provided + var files = [[String: Any]]() + if optionalFilesArray.isEmpty { + guard let getFiles = getAllFiles() else { + logger?.info("\(#function, privacy: .public) - count not get files") + return false + } + files = getFiles + } else { + files = optionalFilesArray + } + logger?.info("\(#function, privacy: .public) - will loop through \(files.count, privacy: .public)") + var manifest = getManifest() + for file in files { + // can be force unwrapped because getAllFiles didn't return nil + let filename = file["filename"] as! String + let metadata = file["metadata"] as! [String: [String]] + let type = file["type"] as! String + let required = metadata["require"] ?? [] + logger?.info("\(#function, privacy: .public) - begin \(filename, privacy: .public)") + // get required resources for file, if fail, skip updating manifest + if !getRequiredCode(filename, required, type) { + logger?.error("\(#function, privacy: .public) - couldn't fetch remote content for \(filename, privacy: .public)") + continue + } + + // create filenames from sanitized resource urls + // getRequiredCode does the same thing when saving to disk + // populate array with entries for manifest + var r = [String]() + for resource in required { + let sanitizedResourceName = sanitize(resource) + r.append(sanitizedResourceName) + } + + // if there are values, write them to manifest + // if failed to write to manifest, continue to next file & log error + if !r.isEmpty && r != manifest.require[filename] { + manifest.require[filename] = r + if !updateManifest(with: manifest) { + logger?.error("\(#function, privacy: .public) - couldn't update manifest when getting required resources") + } + } + logger?.info("\(#function, privacy: .public) - end \(filename, privacy: .public)") + } + logger?.info("\(#function, privacy: .public) - completed") + return true +} + +func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] = []) -> Bool { + logger?.info("\(#function, privacy: .public) - started") + var files = [[String: Any]]() + if optionalFilesArray.isEmpty { + guard let getFiles = getAllFiles() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return false + } + files = getFiles + } else { + files = optionalFilesArray + } + var manifest = getManifest() + for file in files { + // can be force unwrapped because getAllFiles didn't return nil + // and getAllFiles always returns the following + let metadata = file["metadata"] as! [String: [String]] + let filename = file["filename"] as! String + let runAt = metadata["run-at"]?[0] ?? "document-end" + // if not a request type, ignore + if runAt != "request" { + continue + } + var update = false + // if filename already in manifest + if !manifest.declarativeNetRequest.contains(filename) { + manifest.declarativeNetRequest.append(filename) + update = true + } + // if filename in another array remove it + for (pattern, filenames) in manifest.match { + for fn in filenames { + if fn == filename, let index = manifest.match[pattern]?.firstIndex(of: filename) { + manifest.match[pattern]?.remove(at: index) + update = true + } + } + } + for (pattern, filenames) in manifest.excludeMatch { + for fn in filenames { + if fn == filename, let index = manifest.excludeMatch[pattern]?.firstIndex(of: filename) { + manifest.excludeMatch[pattern]?.remove(at: index) + update = true + } + } + } + for (pattern, filenames) in manifest.include { + for fn in filenames { + if fn == filename, let index = manifest.include[pattern]?.firstIndex(of: filename) { + manifest.include[pattern]?.remove(at: index) + update = true + } + } + } + for (pattern, filenames) in manifest.exclude { + for fn in filenames { + if fn == filename, let index = manifest.exclude[pattern]?.firstIndex(of: filename) { + manifest.exclude[pattern]?.remove(at: index) + update = true + } + } + } + if update, !updateManifest(with: manifest) { + logger?.error("\(#function, privacy: .public) - failed at (2)") + return false + } + } + logger?.info("\(#function, privacy: .public) - completed") + return true +} + +func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { + logger?.info("\(#function, privacy: .public) - started") + // purge all manifest keys of any stale entries + var update = false, manifest = getManifest(), allSaveLocationFilenames = [String]() + // only get all files if files were not provided + var allFiles = [[String: Any]]() + if optionalFilesArray.isEmpty { + // if getAllFiles fails to return, ignore and pass an empty array + let getFiles = getAllFiles() ?? [] + allFiles = getFiles + } else { + allFiles = optionalFilesArray + } + // populate array with filenames + for file in allFiles { + if let filename = file["filename"] as? String { + allSaveLocationFilenames.append(filename) + } + } + // loop through manifest keys, if no file exists for value, remove value from manifest + // if there are no more filenames in pattern, remove pattern from manifest + for (pattern, filenames) in manifest.match { + for filename in filenames { + if !allSaveLocationFilenames.contains(filename) { + if let index = manifest.match[pattern]?.firstIndex(of: filename) { + manifest.match[pattern]?.remove(at: index) + update = true + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from match pattern - \(pattern, privacy: .public)") + } + } + } + if let length = manifest.match[pattern]?.count { + if length < 1, let ind = manifest.match.index(forKey: pattern) { + manifest.match.remove(at: ind) + logger?.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) match pattern, removed from manifest") + } + } + } + for (pattern, filenames) in manifest.excludeMatch { + for filename in filenames { + if !allSaveLocationFilenames.contains(filename) { + if let index = manifest.excludeMatch[pattern]?.firstIndex(of: filename) { + manifest.excludeMatch[pattern]?.remove(at: index) + update = true + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude-match pattern - \(pattern, privacy: .public)") + } + } + } + if let length = manifest.excludeMatch[pattern]?.count { + if length < 1, let ind = manifest.excludeMatch.index(forKey: pattern) { + manifest.excludeMatch.remove(at: ind) + logger?.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude-match pattern, removed from manifest") + } + } + } + for (pattern, filenames) in manifest.exclude { + for filename in filenames { + if !allSaveLocationFilenames.contains(filename) { + if let index = manifest.exclude[pattern]?.firstIndex(of: filename) { + manifest.exclude[pattern]?.remove(at: index) + update = true + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude pattern - \(pattern, privacy: .public)") + } + } + } + if let length = manifest.exclude[pattern]?.count { + if length < 1, let ind = manifest.exclude.index(forKey: pattern) { + manifest.exclude.remove(at: ind) + logger?.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude pattern, removed from manifest") + } + } + } + for (pattern, filenames) in manifest.include { + for filename in filenames { + if !allSaveLocationFilenames.contains(filename) { + if let index = manifest.include[pattern]?.firstIndex(of: filename) { + manifest.include[pattern]?.remove(at: index) + update = true + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude pattern - \(pattern, privacy: .public)") + } + } + } + if let length = manifest.include[pattern]?.count { + if length < 1, let ind = manifest.include.index(forKey: pattern) { + manifest.include.remove(at: ind) + logger?.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude pattern, removed from manifest") + } + } + } + // loop through manifest required + for (filename, _) in manifest.require { + if !allSaveLocationFilenames.contains(filename) { + if let index = manifest.require.index(forKey: filename) { + manifest.require.remove(at: index) + // remove associated resources + if !getRequiredCode(filename, [], (filename as NSString).pathExtension) { + logger?.error("\(#function, privacy: .public) - failed to remove required resources when purging \(filename, privacy: .public) from manifest required records") + } + update = true + logger?.info("\(#function, privacy: .public) - No more required resources for \(filename, privacy: .public), removed from manifest along with resource folder") + } + } + } + // loop through manifest disabled + for filename in manifest.disabled { + if !allSaveLocationFilenames.contains(filename) { + if let index = manifest.disabled.firstIndex(of: filename) { + manifest.disabled.remove(at: index) + update = true + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from disabled") + } + } + } + // loop through manifest declarativeNetRequest + for filename in manifest.declarativeNetRequest { + if !allSaveLocationFilenames.contains(filename) { + if let index = manifest.declarativeNetRequest.firstIndex(of: filename) { + manifest.declarativeNetRequest.remove(at: index) + update = true + logger?.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from dNR") + } + } + } + // remove obsolete settings + for setting in manifest.settings { + if !defaultSettings.keys.contains(setting.key) { + manifest.settings.removeValue(forKey: setting.key) + update = true + logger?.info("\(#function, privacy: .public) - Removed obsolete setting - \(setting.key, privacy: .public)") + } + } + if update, !updateManifest(with: manifest) { + logger?.error("\(#function, privacy: .public) - failed to purge manifest") + return false + } + logger?.info("\(#function, privacy: .public) - completed") + return true +} + +// settings +func checkSettings() -> Bool { + // iterate over default settings and individually check if each present + // if missing add setting to manifest about to be returned + // missing keys will occur when new settings introduced + var manifest = getManifest() + var update = false + for (key, value) in defaultSettings { + if manifest.settings[key] == nil { + manifest.settings[key] = value + update = true + } + } + if update, !updateManifest(with: manifest) { + logger?.error("\(#function, privacy: .public) - failed to update manifest settings") + return false + } + return true +} + +func updateSettings(_ settings: [String: String]) -> Bool { + var manifest = getManifest() + manifest.settings = settings + if updateManifest(with: manifest) != true { + logger?.error("\(#function, privacy: .public) - failed to update settings") + return false + } + return true +} + +// files +func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { + logger?.info("\(#function, privacy: .public) - started") + // returns all files of proper type with filenames, metadata & more + var files = [[String: Any]]() + let fm = FileManager.default + let manifest = getManifest() + guard let saveLocation = getSaveLocation() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + // security scope + let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() + defer { + if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} + } + // get all file urls within save location + guard let urls = try? fm.contentsOfDirectory(at: saveLocation, includingPropertiesForKeys: []) else { + logger?.error("\(#function, privacy: .public) - failed at (2)") + return nil + } + for url in urls { + var fileData = [String: Any]() + // only read contents for css & js files + let filename = url.lastPathComponent + if (!filename.hasSuffix(".css") && !filename.hasSuffix(".js")) { + continue + } + // file will be skipped if metablock is missing + guard + let content = try? String(contentsOf: url, encoding: .utf8), + let dateMod = try? fm.attributesOfItem(atPath: url.path)[.modificationDate] as? Date, + let parsed = parse(content), + let metadata = parsed["metadata"] as? [String: [String]], + let type = filename.split(separator: ".").last + else { + logger?.info("\(#function, privacy: .public) - ignoring \(filename, privacy: .public), file missing or metadata missing from file contents") + continue + } + fileData["canUpdate"] = false + fileData["content"] = content + fileData["disabled"] = manifest.disabled.contains(filename) + fileData["filename"] = filename + fileData["lastModified"] = dateToMilliseconds(dateMod) + fileData["metadata"] = metadata + // force unwrap name since parser ensure it exists + fileData["name"] = metadata["name"]![0] + fileData["type"] = "\(type)" + // add extra data if a request userscript + let runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + fileData["request"] = true + } + if metadata["description"] != nil { + fileData["description"] = metadata["description"]![0] + } + if metadata["version"] != nil && metadata["updateURL"] != nil { + fileData["canUpdate"] = true + } + fileData["noframes"] = metadata["noframes"] != nil ? true : false + // if asked, also return the code string + if (includeCode) { + // can force unwrap because always returned from parser + fileData["code"] = parsed["code"] as! String + } + files.append(fileData) + } + logger?.info("\(#function, privacy: .public) - completed") + return files +} + +func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: String) -> Bool { + let directory = getRequireLocation().appendingPathComponent(filename) + // if file requires no resource but directory exists, remove it + // this resource is not user-generated and can be downloaded again, so just remove it instead of moves to the trash + // also in ios, the volume “Data” has no trash and item can only be removed directly + if resources.isEmpty { + if FileManager.default.fileExists(atPath: directory.path) { + do { + try FileManager.default.removeItem(at: directory) + } catch { + logger?.error("\(#function, privacy: .public) - failed to remove directory: \(error.localizedDescription, privacy: .public)") + } + } + return true + } + // record URLs for subsequent processing + var resourceUrls = Set() + // loop through resource urls and attempt to fetch it + for resourceUrlString in resources { + // get the path of the url string + guard let resourceUrlPath = URLComponents(string: resourceUrlString)?.path else { + // if path can not be obtained, skip and log + logger?.info("\(#function, privacy: .public) - failed to get path on \(filename, privacy: .public) for \(resourceUrlString, privacy: .public)") + continue + } + // skip urls pointing to files of different types + if resourceUrlPath.hasSuffix(fileType) { + let resourceFilename = sanitize(resourceUrlString) + let fileURL = directory.appendingPathComponent(resourceFilename) + // insert url to resolve symlink into set + resourceUrls.insert(fileURL.standardizedFileURL) + // only attempt to get resource if it does not yet exist + if FileManager.default.fileExists(atPath: fileURL.path) {continue} + // get the remote file contents + guard let contents = getRemoteFileContents(resourceUrlString) else {continue} + // check if file specific folder exists at requires directory + if !FileManager.default.fileExists(atPath: directory.path) { + guard ((try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false)) != nil) else { + logger?.info("\(#function, privacy: .public) - failed to create required code directory for \(filename, privacy: .public)") + return false + } + } + // finally write file to directory + guard ((try? contents.write(to: fileURL, atomically: false, encoding: .utf8)) != nil) else { + logger?.info("\(#function, privacy: .public) - failed to write content to file for \(filename, privacy: .public) from \(resourceUrlString, privacy: .public)") + return false + } + } + } + // cleanup downloaded files that are no longer required + do { + var downloadedUrls = Set() + // get all downloaded resources url + let fileUrls = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: []) + // insert url to resolve symlink into set + for url in fileUrls { downloadedUrls.insert(url.standardizedFileURL) } + // exclude currently required resources + let abandonedUrls = downloadedUrls.subtracting(resourceUrls) + // loop through abandoned urls and attempt to remove it + for abandonFileUrl in abandonedUrls { + do { + try FileManager.default.removeItem(at: abandonFileUrl) + logger?.info("\(#function, privacy: .public) - cleanup abandoned resource: \(unsanitize(abandonFileUrl.lastPathComponent), privacy: .public)") + } catch { + logger?.error("\(#function, privacy: .public) - failed to remove abandoned resource: \(error.localizedDescription, privacy: .public)") + } + } + } catch { + logger?.error("\(#function, privacy: .public) - failed to cleanup resources: \(error.localizedDescription, privacy: .public)") + } + return true +} + +func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) -> [[String: String]]? { + // only get all files if files were not provided + var files = [[String: Any]]() + if optionalFilesArray.isEmpty { + guard let getFiles = getAllFiles() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + files = getFiles + } else { + files = optionalFilesArray + } + + var hasUpdates = [[String: String]]() + for file in files { + // can be force unwrapped because getAllFiles didn't return nil + let filename = file["filename"] as! String + let canUpdate = file["canUpdate"] as! Bool + let metadata = file["metadata"] as! [String: [String]] + let type = file["type"] as! String + let name = metadata["name"]![0] + logger?.info("\(#function, privacy: .public) - Checking for remote updates for \(filename, privacy: .public)") + if canUpdate { + let currentVersion = metadata["version"]![0] + let updateUrl = metadata["updateURL"]![0] + // before fetching remote contents, ensure it points to a file of the same type + if !updateUrl.hasSuffix(type) {continue} + guard + let remoteFileContents = getRemoteFileContents(updateUrl), + let remoteFileContentsParsed = parse(remoteFileContents), + let remoteMetadata = remoteFileContentsParsed["metadata"] as? [String: [String]], + let remoteVersion = remoteMetadata["version"]?[0] + else { + logger?.error("\(#function, privacy: .public) - failed to parse remote file contents") + return nil + } + let remoteVersionNewer = isVersionNewer(currentVersion, remoteVersion) + if remoteVersionNewer { + hasUpdates.append(["name": name, "filename": filename, "type": type, "url": updateUrl]) + } + } + } + logger?.info("\(#function, privacy: .public) - Finished checking for remote updates for \(files.count, privacy: .public) files") + return hasUpdates +} + +func getRemoteFileContents(_ url: String) -> String? { + logger?.info("\(#function, privacy: .public) - started for \(url, privacy: .public)") + // if url is http change to https + var urlChecked = url + if urlChecked.hasPrefix("http:") { + urlChecked = urlChecked.replacingOccurrences(of: "http:", with: "https:") + logger?.info("\(#function, privacy: .public) - \(url, privacy: .public) is using insecure http, attempt to fetch remote content with https") + } + // convert url string to url + guard let solidURL = fixedURL(string: urlChecked) else { + logger?.error("\(#function, privacy: .public) - failed at (1), invalid URL: \(url, privacy: .public)") + return nil + } + var contents = "" + // get remote file contents, synchronously + let semaphore = DispatchSemaphore(value: 0) + var task: URLSessionDataTask? + task = URLSession.shared.dataTask(with: solidURL) { data, response, error in + if let r = response as? HTTPURLResponse, data != nil, error == nil { + if r.statusCode == 200 { + contents = String(data: data!, encoding: .utf8) ?? "" + } else { + logger?.error("\(#function, privacy: .public) - http statusCode (\(r.statusCode, privacy: .public)): \(url, privacy: .public)") + } + } + if let error = error { + logger?.error("\(#function, privacy: .public) - task error: \(error.localizedDescription, privacy: .public) (\(url, privacy: .public))") + } + semaphore.signal() + } + task?.resume() + // wait 30 seconds before timing out + if semaphore.wait(timeout: .now() + 30) == .timedOut { + task?.cancel() + logger?.error("\(#function, privacy: .public) - 30 seconds timeout: \(url, privacy: .public)") + } + + // if made it to this point and contents still an empty string, something went wrong with the request + if contents.isEmpty { + logger?.error("\(#function, privacy: .public) - failed at (2), contents empty: \(url, privacy: .public)") + return nil + } + logger?.info("\(#function, privacy: .public) - completed for \(url, privacy: .public)") + return contents +} + +func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) -> Bool { + // get names of all files with updates available + guard + let filesWithUpdates = checkForRemoteUpdates(optionalFilesArray), + let saveLocation = getSaveLocation() + else { + logger?.error("\(#function, privacy: .public) - failed to update files (1)") + return false + } + // security scope + let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() + defer { + if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} + } + for file in filesWithUpdates { + // can be force unwrapped because checkForRemoteUpdates didn't return nil + let filename = file["filename"]! + let fileUrl = saveLocation.appendingPathComponent(filename) + guard + let content = try? String(contentsOf: fileUrl, encoding: .utf8), + let parsed = parse(content), + let metadata = parsed["metadata"] as? [String: [String]], + let updateUrl = metadata["updateURL"]?[0] + else { + logger?.error("\(#function, privacy: .public) - failed to update files (2)") + continue + } + let downloadUrl = metadata["downloadURL"] != nil ? metadata["downloadURL"]![0] : updateUrl + guard + let remoteFileContents = getRemoteFileContents(downloadUrl), + ((try? remoteFileContents.write(to: fileUrl, atomically: false, encoding: .utf8)) != nil) + else { + logger?.error("\(#function, privacy: .public) - failed to update files (3)") + continue + } + logger?.info("\(#function, privacy: .public) - updated \(filename, privacy: .public) with contents fetched from \(downloadUrl, privacy: .public)") + } + return true +} + +func toggleFile(_ filename: String,_ action: String) -> Bool { + // if file doesn't exist return false + guard let saveLocation = getSaveLocation() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return false + } + let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() + defer { + if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} + } + let path = saveLocation.appendingPathComponent(filename).path + if !FileManager.default.fileExists(atPath: path) { + logger?.error("\(#function, privacy: .public) - failed at (2)") + return false + } + var manifest = getManifest() + // if file is already disabled + if action == "disable" && manifest.disabled.contains(filename) || action == "enabled" && !manifest.disabled.contains(filename) { + return true + } + // add filename to disabled array if disabling + if (action == "disable") {manifest.disabled.append(filename)} + // remove filename from disabled array if enabling + if (action == "enable") { + guard let index = manifest.disabled.firstIndex(of: filename) else { + logger?.error("\(#function, privacy: .public) - failed at (3)") + return false + } + manifest.disabled.remove(at: index) + } + if !updateManifest(with: manifest) { + logger?.error("\(#function, privacy: .public) - failed at (4)") + return false + } + return true +} + +func checkDefaultDirectories() -> Bool { + let defaultSaveLocation = getDocumentsDirectory().appendingPathComponent("scripts") + let requireLocation = getRequireLocation() + let urls = [defaultSaveLocation, requireLocation] + for url in urls { + if !FileManager.default.fileExists(atPath: url.path) { + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) + } catch { + // could not create the save location directory, show error + logger?.error("\(#function, privacy: .public) - failed at (1) - \(url, privacy: .public) - \(error.localizedDescription, privacy: .public)") + return false + } + } + } + return true +} + +// matching +func stringToRegex(_ stringPattern: String) -> NSRegularExpression? { + let pattern = #"[\.|\?|\^|\$|\+|\{|\}|\[|\]|\||\\(|\)|\/]"# + var patternReplace = "^\(stringPattern.replacingOccurrences(of: pattern, with: #"\\$0"#, options: .regularExpression))$" + patternReplace = patternReplace.replacingOccurrences(of: "*", with: ".*") + guard let regex = try? NSRegularExpression(pattern: patternReplace, options: .caseInsensitive) else { + return nil + } + return regex +} + +func match(_ url: String, _ matchPattern: String) -> Bool { + guard + let parts = jsLikeURL(url), + let ptcl = parts["protocol"], + let host = parts["hostname"], + var path = parts["pathname"] + else { + logger?.error("\(#function, privacy: .public) - invalid url \(url, privacy: .public)") + return false + } + + // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#path + // The value for the path matches against the string which is the URL path plus the URL query string + if let search = parts["search"], search.count > 0 { + path += search + } + + // matchPattern is the value from metatdata key @match or @exclude-match + if (matchPattern == "") { + return true + } + // currently only http/s supported + if (ptcl != "http:" && ptcl != "https:") { + return false + } + let partsPattern = #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*|[a-z0-9]+)(\/[^\s]*)$"# + let partsPatternReg = try! NSRegularExpression(pattern: partsPattern, options: .caseInsensitive) + let range = NSMakeRange(0, matchPattern.utf16.count) + guard let parts = partsPatternReg.firstMatch(in: matchPattern, options: [], range: range) else { + logger?.error("\(#function, privacy: .public) - malformed regex match pattern") + return false + } + // ensure url protocol matches pattern protocol + let protocolPattern = matchPattern[Range(parts.range(at: 1), in: matchPattern)!] + if (protocolPattern != "*:" && ptcl != protocolPattern) { + return false + } + // construct host regex from matchPattern + let matchPatternHost = matchPattern[Range(parts.range(at: 2), in: matchPattern)!] + var hostPattern = "^\(matchPatternHost.replacingOccurrences(of: ".", with: "\\."))$" + hostPattern = hostPattern.replacingOccurrences(of: "^*$", with: ".*") + hostPattern = hostPattern.replacingOccurrences(of: "*\\.", with: "(.*\\.)?") + guard let hostRegEx = try? NSRegularExpression(pattern: hostPattern, options: .caseInsensitive) else { + logger?.error("\(#function, privacy: .public) - invalid host regex") + return false + } + // construct path regex from matchPattern + let matchPatternPath = matchPattern[Range(parts.range(at: 3), in: matchPattern)!] + guard let pathRegEx = stringToRegex(String(matchPatternPath)) else { + logger?.error("\(#function, privacy: .public) - invalid path regex") + return false + } + guard + (hostRegEx.firstMatch(in: host, options: [], range: NSMakeRange(0, host.utf16.count)) != nil), + (pathRegEx.firstMatch(in: path, options: [], range: NSMakeRange(0, path.utf16.count)) != nil) + else { + return false + } + + return true +} + +func include(_ url: String,_ pattern: String) -> Bool { + var regex:NSRegularExpression + if pattern.hasPrefix("/") && pattern.hasSuffix("/") { + let p = String(pattern.dropFirst().dropLast()) + guard let exp = try? NSRegularExpression(pattern: p, options: .caseInsensitive) else { + logger?.error("\(#function, privacy: .public) - invalid regex") + return false + } + regex = exp + } else { + guard let exp = stringToRegex(pattern) else { + logger?.error("\(#function, privacy: .public) - coudn't convert string to regex") + return false + } + regex = exp + } + if (regex.firstMatch(in: url, options: [], range: NSMakeRange(0, url.utf16.count)) == nil) { + return false + } + return true +} + +func getMatchedFiles(_ url: String, _ optionalManifest: Manifest?, _ checkBlocklist: Bool) -> [String] { + logger?.info("\(#function, privacy: .public) - Getting matched files for \(url, privacy: .private(mask: .hash))") + // logger?.debug("\(#function, privacy: .public) - Getting matched files for \(url, privacy: .public)") + let manifest = optionalManifest ?? getManifest() + + // filenames that should not load for the passed url + // the manifest values from @exclude and @exclude-match populate this set + var excludedFilenames: Set = [] + // filenames that should load for the passed url + // the manifest values from @include and @match populate this set + var matchedFilenames: Set = [] + // all exclude-match patterns from manifest + let excludeMatchPatterns = manifest.excludeMatch.keys + // all match patterns from manifest + let matchPatterns = manifest.match.keys + // all include expressions from manifest + let includeExpressions = manifest.include.keys + // all exclude expressions from manifest + let excludeExpressions = manifest.exclude.keys + + // if url matches a pattern in blocklist, no injection for this url + if (checkBlocklist) { + for pattern in manifest.blacklist { + if match(url, pattern) { + // return empty array + return Array(matchedFilenames) + } + } + } + + // loop through all the @exclude-match patterns + // if any match passed url, push all filenames to excludedFilenames set + for pattern in excludeMatchPatterns { + if match(url, pattern) { + guard let filenames = manifest.excludeMatch[pattern] else { + logger?.error("\(#function, privacy: .public) - failed at (2) for \(pattern, privacy: .public)") + continue + } + excludedFilenames = excludedFilenames.union(filenames) + } + } + for exp in excludeExpressions { + if include(url, exp) { + guard let filenames = manifest.exclude[exp] else { + logger?.error("\(#function, privacy: .public) - failed at (3) for \(exp, privacy: .public)") + continue + } + excludedFilenames = excludedFilenames.union(filenames) + } + } + for pattern in matchPatterns { + if match(url, pattern) { + guard let filenames = manifest.match[pattern] else { + logger?.error("\(#function, privacy: .public) - failed at (4) for \(pattern, privacy: .public)") + continue + } + matchedFilenames = matchedFilenames.union(filenames) + } + } + for exp in includeExpressions { + if include(url, exp) { + guard let filenames = manifest.include[exp] else { + logger?.error("\(#function, privacy: .public) - failed at (5) for \(exp, privacy: .public)") + continue + } + matchedFilenames = matchedFilenames.union(filenames) + } + } + matchedFilenames = matchedFilenames.subtracting(excludedFilenames) + logger?.info("\(#function, privacy: .public) - Got \(matchedFilenames.count) matched files for \(url, privacy: .private(mask: .hash))") + // logger?.debug("\(#function, privacy: .public) - Got \(matchedFilenames.count) matched files for \(url, privacy: .public)") + return Array(matchedFilenames) +} + +// injection +func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { + var cssFiles = [Any]() + var jsFiles = [Any]() + var menuFiles = [Any]() + + guard let saveLocation = getSaveLocation() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + + for filename in filenames { + guard + let contents = getFileContentsParsed(saveLocation.appendingPathComponent(filename)), + var code = contents["code"] as? String, + let type = filename.split(separator: ".").last + else { + // if guard fails, log error continue to next file + logger?.error("\(#function, privacy: .public) - failed at (2) for \(filename, privacy: .public)") + continue + } + // can force unwrap b/c getFileContentsParsed ensures metadata exists + let metadata = contents["metadata"] as! [String: [String]] + let name = metadata["name"]![0] + + // if metadata has noframes option and the url is not the top window, don't load + if (metadata["noframes"] != nil && !isTop) { + continue + } + + // get run-at values and set default if missing + // if type request, ignore + var runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + continue + } + + // normalize weight + var weight = metadata["weight"]?[0] ?? "1" + weight = normalizeWeight(weight) + + // get inject-into and set default if missing + var injectInto = metadata["inject-into"]?[0] ?? "auto" + let injectVals: Set = ["auto", "content", "page"] + let runAtVals: Set = ["context-menu", "document-start", "document-end", "document-idle"] + let validGrants: Set = [ + "GM.info", + "GM_info", + "GM.addStyle", + "GM.openInTab", + "GM.closeTab", + "GM.setValue", + "GM.getValue", + "GM.deleteValue", + "GM.listValues", + "GM.setClipboard", + "GM.getTab", + "GM.saveTab", + "GM_xmlhttpRequest", + "GM.xmlHttpRequest" + ] + // if either is invalid use default value + if !injectVals.contains(injectInto) { + injectInto = "auto" + } + if !runAtVals.contains(runAt) { + runAt = "document-end" + } + + // attempt to get all @grant value + var grants = metadata["grant"] ?? [] + // remove duplicates, if any exist + if !grants.isEmpty { + grants = Array(Set(grants)) + } + + // filter out grant values that are not in validGrant set + grants = grants.filter{validGrants.contains($0)} + + // set GM.info data + let description = metadata["description"]?[0] ?? "" + let excludes = metadata["exclude"] ?? [] + let excludeMatches = metadata["exclude-match"] ?? [] + let icon = metadata["icon"]?[0] ?? "" + let includes = metadata["include"] ?? [] + let matches = metadata["match"] ?? [] + let requires = metadata["require"] ?? [] + let version = metadata["version"]?[0] ?? "" + let noframes = metadata["noframes"] != nil ? true : false + var scriptObject:[String: Any] = [ + "description": description, + "excludes": excludes, + "exclude-match": excludeMatches, + "filename": filename, + "grant": grants, + "icon": icon, + "includes": includes, + "inject-into": injectInto, + "matches": matches, + "name": name, + "noframes": noframes, + "namespace": "", + "resources": "", + "require": requires, + "run-at": runAt, + "version": version + ] + // certain metadata keys use a different key name then the actual key name + // for compatibility keeping this when applicable, although the rationale is not clear to me + // for unique keys passed to scriptObject, using the same key name that is present in actual userscript + // this key map is used to check for existence of keys in next loop + let keyMap = [ + "exclude": "excludes", + "include": "includes", + "match": "matches", + "resource": "resources", + ] + for metaline in metadata { + let key = keyMap[metaline.key] ?? metaline.key + if !scriptObject.keys.contains(key) { + let value = metaline.value + // metalines without values aren't included in parsed metadata object + // the only exception is @noframes + scriptObject[key] = !value.isEmpty ? value : value[0] + } + } + let scriptMetaStr = contents["metablock"] as? String ?? "??" + + // attempt to get require resource from disk + // if required resource is inaccessible, log error and continue + if let required = metadata["require"] { + // reverse required metadata + // if required is ["A", "B", "C"], C gets added above B which is above A, etc.. + // the reverse of that is desired + for require in required.reversed() { + let sanitizedName = sanitize(require) + let requiredFileURL = getRequireLocation().appendingPathComponent(filename).appendingPathComponent(sanitizedName) + if let requiredContent = try? String(contentsOf: requiredFileURL, encoding: .utf8) { + code = "\(requiredContent)\n\(code)" + } else { + logger?.error("\(#function, privacy: .public) - failed at (3) for \(require, privacy: .public)") + } + } + } + + if type == "css" { + cssFiles.append([ + "code": code, + "filename": filename, + "name": name, + "type": "css", + "weight": weight + ]) + } else if type == "js" { + if runAt == "context-menu" { + #if os(macOS) + menuFiles.append([ + "code": code, + "scriptMetaStr": scriptMetaStr, + "scriptObject": scriptObject, + "type": "js", + "weight": weight + ]) + #endif + } else { + jsFiles.append([ + "code": code, + "scriptMetaStr": scriptMetaStr, + "scriptObject": scriptObject, + "type": "js", + "weight": weight + ]) + } + } + } + let resp = [ + "files": ["css": cssFiles, "js": jsFiles, "menu": menuFiles], + "scriptHandler": "Userscripts", + "scriptHandlerVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" + ] as [String : Any] + return resp +} + +func getFileContentsParsed(_ url: URL) -> [String: Any]? { + guard let saveLocation = getSaveLocation() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + // security scope + let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() + defer { + if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} + } + // check that url is a valid path to a directory or single file + guard + FileManager.default.fileExists(atPath: url.path), + let content = try? String(contentsOf: url, encoding: .utf8), + let parsed = parse(content) + else { + return nil + } + return parsed +} + +func getInjectionFilenames(_ url: String) -> [String]? { + var filenames = [String]() + let manifest = getManifest() + let matched = getMatchedFiles(url, manifest, true) + guard let active = manifest.settings["active"] else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + // if injection is disabled return empty array + if active != "true" { + return filenames + } + // filter out all disabled files + filenames = matched.filter{!manifest.disabled.contains($0)} + return filenames +} + +func getRequestScripts() -> [[String: String]]? { + var requestScripts = [[String: String]]() + // check the manifest to see if injection is enabled + let manifest = getManifest() + guard let active = manifest.settings["active"] else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + // if not enabled, do not apply any net requests, ie. return empty array + if active != "true" { + return requestScripts + } + guard let files = getAllFiles(includeCode: true) else { + logger?.error("\(#function, privacy: .public) - failed at (2)") + return nil + } + for file in files { + let isRequest = file["request"] as? Bool ?? false + // skip any non-request userscripts + if !isRequest { + continue + } + // can be force unwrapped because getAllFiles always returns these + let name = file["name"] as! String + let code = file["code"] as! String + let filename = file["filename"] as! String + + if !manifest.disabled.contains(filename) { + requestScripts.append(["name": name, "code": code]) + } + } + return requestScripts +} + +func getContextMenuScripts() -> [String: Any]? { + var menuFilenames = [String]() + // check the manifest to see if injection is enabled + let manifest = getManifest() + guard let active = manifest.settings["active"] else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + // if not enabled return empty array + if active != "true" { + return ["files": ["menu": []]] + } + // get all files at save location + guard let files = getAllFiles() else { + logger?.error("\(#function, privacy: .public) - failed at (2)") + return nil + } + // loop through files and find @run-at context-menu script filenames + for file in files { + guard let filename = file["filename"] as? String else { + logger?.error("\(#function, privacy: .public) - failed at (3), couldn't get filename") + continue + } + guard let fileMetadata = file["metadata"] as? [String: [String]] else { + logger?.error("\(#function, privacy: .public) - failed at (4), couldn't get metadata for \(filename, privacy: .public)") + continue + } + let runAt = fileMetadata["run-at"]?[0] ?? "document-end" + if runAt != "context-menu" || manifest.disabled.contains(filename) { + continue + } + menuFilenames.append(filename) + } + // get and return script objects for all context-menu scripts + guard let scripts = getCode(menuFilenames, true) else { + logger?.error("\(#function, privacy: .public) - failed at (5)") + return nil + } + return scripts +} + +// popup +func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { + var matches = [[String: Any]]() + // if the url doesn't start with http/s return empty array + if !url.starts(with: "http://") && !url.starts(with: "https://") { + return matches + } + // get all the files saved to manifest that match the passed url + let matched = getMatchedFiles(url, nil, false) + // get all the files at the save location + guard + let files = getAllFiles() + else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + // filter out the files that are present in both files and matched + // force unwrap filename to string since getAllFiles always returns it + matches = files.filter{matched.contains($0["filename"] as! String)} + + // get the subframe url matches + var frameUrlsMatched = [[String: Any]]() + var frameUrlsMatches = [String]() + // filter out the top page url from the frame urls + let frameUrls = subframeUrls.filter{$0 != url} + // for each url just pushed to frameUrls, get all the files saved to manifest that match their url + for frameUrl in frameUrls { + let frameMatches = getMatchedFiles(frameUrl, nil, false) + for frameMatch in frameMatches { + // for the match against the frameUrl, see if it has @noframes + // if so, it should not be appended to frameUrlsMatches + // filter all files for the first one that matches the frameMatch filename + // can force unwrap filename b/c getAllFiles always returns it + let frameMatchMetadata = files.filter{$0["filename"] as! String == frameMatch}.first + // can force unwrap noframes b/c getAllFiles always returns it + let noFrames = frameMatchMetadata?["noframes"] as? Bool ?? false + if !matched.contains(frameMatch) && !noFrames { + frameUrlsMatches.append(frameMatch) + } + } + } + + // filter out the files that are present in both files and frameUrlsMatches + // force unwrap filename to string since getAllFiles always returns it + frameUrlsMatched = files.filter{frameUrlsMatches.contains($0["filename"] as! String)} + // loop through frameUrlsMatched and add subframe key/val + for (index, var frameUrlsMatch) in frameUrlsMatched.enumerated() { + frameUrlsMatch["subframe"] = true + frameUrlsMatched[index] = frameUrlsMatch + } + // add frameUrlsMatched to matches array + matches.append(contentsOf: frameUrlsMatched) + return matches +} + +func popupUpdateAll() -> Bool { + guard + let files = getAllFiles(), + updateAllFiles(files), + updateManifestMatches(files), + updateManifestRequired(files), + purgeManifest(files) + else { + return false + } + return true +} + +func getPopupBadgeCount(_ url: String, _ subframeUrls: [String]) -> Int? { + if !url.starts(with: "http://") && !url.starts(with: "https://") { + return 0 + } + let manifest = getManifest() + guard + var matches = getPopupMatches(url, subframeUrls) + else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + for pattern in manifest.blacklist { + if match(url, pattern) { + return 0 + } + } + matches = matches.filter{!manifest.disabled.contains($0["filename"] as! String)} + return matches.count +} + +func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { + guard let saveLocation = getSaveLocation() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + // security scope + let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() + defer { + if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} + } + let fileUrl = saveLocation.appendingPathComponent(filename) + guard + let content = try? String(contentsOf: fileUrl, encoding: .utf8), + let parsed = parse(content), + let metadata = parsed["metadata"] as? [String: [String]], + let updateUrl = metadata["updateURL"]?[0] + else { + logger?.error("\(#function, privacy: .public) - failed at (2)") + return nil + } + let downloadUrl = metadata["downloadURL"] != nil ? metadata["downloadURL"]![0] : updateUrl + guard + let remoteFileContents = getRemoteFileContents(downloadUrl), + ((try? remoteFileContents.write(to: fileUrl, atomically: false, encoding: .utf8)) != nil) + else { + logger?.error("\(#function, privacy: .public) - failed at (3)") + return nil + } + guard + let files = getAllFiles(), + updateManifestMatches(files), + updateManifestRequired(files), + purgeManifest(files), + let matches = getPopupMatches(url, subframeUrls) + else { + logger?.error("\(#function, privacy: .public) - failed at (4)") + return nil + } + return matches +} + +// page +func getInitData() -> [String: Any]? { + guard let saveLocation = getSaveLocation() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return nil + } + return [ + "saveLocation": saveLocation.path, + "version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??", + "build": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" + ] +} + +func getLegacyData() -> [String: Any]? { + let manifest = getManifest() + var data:[String: Any] = manifest.settings + data["blacklist"] = manifest.blacklist + return data +} + +func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { + var response = [String: Any]() + let newContent = content + guard let saveLocation = getSaveLocation() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return ["error": "failed to get save location when attempting to save"] + } + guard + let oldFilename = item["filename"] as? String, + let type = item["type"] as? String + else { + return ["error": "invalid argument in save function"] + } + guard + let parsed = parse(newContent), + let metadata = parsed["metadata"] as? [String: [String]] + else { + return ["error": "failed to parse metadata"] + } + guard let n = metadata["name"]?[0] else { + return ["error": "@name not found in metadata"] + } + var name = sanitize(n) + + // construct new file name + let newFilename = "\(name).user.\(type)" + + // security scope + let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() + defer { + if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} + } + guard + let allFilesUrls = try? FileManager.default.contentsOfDirectory(at: saveLocation, includingPropertiesForKeys: []) + else { + return ["error": "failed to read save urls in save function"] + } + + // validate file before save + var allFilenames:[String] = [] // stores the indv filenames for later comparison + // old and new filenames are equal, overwriting and can skip + if oldFilename.lowercased() != newFilename.lowercased() { + // loop through all the file urls in the save location and save filename to var + for fileUrl in allFilesUrls { + // skip file if it is not of the proper type + let filename = fileUrl.lastPathComponent + if (!filename.hasSuffix(type)) { + continue + } + // if file is of the proper type, add it to the allFilenames array + allFilenames.append(filename.lowercased()) + } + } + + if allFilenames.contains(newFilename.lowercased()) { + return ["error": "filename already taken"] + } + + if newFilename.count > 250 { + return ["error": "filename too long"] + } + + // file passed validation + + // attempt to save to disk + let newFileUrl = saveLocation.appendingPathComponent(newFilename) + do { + try newContent.write(to: newFileUrl, atomically: false, encoding: .utf8) + } catch { + logger?.error("\(#function, privacy: .public) - failed at (2)") + return ["error": "failed to write file to disk"] + } + + // saved to disk successfully + + // get the file last modified date + guard + let dateMod = try? FileManager.default.attributesOfItem(atPath: newFileUrl.path)[.modificationDate] as? Date + else { + logger?.error("\(#function, privacy: .public) - failed at (3)") + return ["error": "failed to read modified date in save function"] + } + + // remove old file and manifest records for old file if they exist + if oldFilename.lowercased() != newFilename.lowercased() { + // if user changed the filename, remove file with old filename + let oldFileUrl = saveLocation.appendingPathComponent(oldFilename) + // however, when creating a new file, if user changes the temp given name by app... + // oldFilename (the temp name in activeItem) and newFilename (@name in file contents) will differ + // the file with oldFilename will not be on the filesystem and can not be deleted + // for that edge case, using try? rather than try(!) to allow failures + try? FileManager.default.trashItem(at: oldFileUrl, resultingItemURL: nil) + } + + // update manifest for new file and purge anything from old file + guard + let allFiles = getAllFiles(), + updateManifestMatches(allFiles), + updateManifestRequired(allFiles), + updateManifestDeclarativeNetRequests(allFiles), + purgeManifest(allFiles) + else { + logger?.error("\(#function, privacy: .public) - failed at (4)") + return ["error": "file save but manifest couldn't be updated"] + } + + // un-santized name + name = unsanitize(name) + + // build response dict + response["canUpdate"] = false + response["content"] = newContent + response["filename"] = newFilename + response["lastModified"] = dateToMilliseconds(dateMod) + response["name"] = name + if metadata["description"] != nil { + response["description"] = metadata["description"]![0] + } + if metadata["version"] != nil && metadata["updateURL"] != nil { + response["canUpdate"] = true + } + // if a request "type" userscript add key/val + let runAt = metadata["run-at"]?[0] ?? "document-end" + if runAt == "request" { + response["request"] = true + } + + return response +} + +func trashFile(_ item: [String: Any]) -> Bool { + guard + let saveLocation = getSaveLocation(), + let filename = item["filename"] as? String + else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return false + } + // security scope + let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() + defer { + if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} + } + let url = saveLocation.appendingPathComponent(filename) + // if file is already removed from path, assume it was removed by user and return true + if (FileManager.default.fileExists(atPath: url.path)) { + do { + try FileManager.default.trashItem(at: url, resultingItemURL: nil) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + return false + } + } + // update manifest + guard updateManifestMatches(), updateManifestRequired(), purgeManifest() else { + logger?.error("\(#function, privacy: .public) - failed at (2)") + return false + } + return true; +} + +func getFileRemoteUpdate(_ content: String) -> [String: String] { + guard + let parsed = parse(content), + let metadata = parsed["metadata"] as? [String: [String]] + else { + // can't parse editor contents + return ["error": "Update failed, metadata missing"] + } + // editor contents missing version value + guard let version = metadata["version"]?[0] else { + return ["error": "Update failed, version value required"] + } + // editor contents missing updateURL + guard let updateURL = metadata["updateURL"]?[0] else { + return ["error": "Update failed, update url required"] + } + // set download url + let downloadURL = (metadata["downloadURL"] != nil) ? metadata["downloadURL"]![0] : updateURL + // basic url validation + guard validateUrl(updateURL) else { + return ["error": "Update failed, invalid updateURL"] + } + guard validateUrl(downloadURL) else { + return ["error": "Update failed, invalid downloadURL"] + } + // get the remote file contents for checking version + guard var remoteContent = getRemoteFileContents(updateURL) else { + return ["error": "Update failed, updateURL unreachable"] + } + // parse remote file contents + guard + let remoteParsed = parse(remoteContent), + let remoteMetadata = remoteParsed["metadata"] as? [String: [String]], + let remoteVersion = remoteMetadata["version"]?[0] + else { + // can't parse editor contents + return ["error": "Update failed, couldn't parse remote file contents"] + } + // check if update is needed + if version >= remoteVersion { + return ["info": "No updates found"] + } + // at this point it is known an update is available, get new code from downloadURL + // is there's a specific downloadURL overwrite remoteContents with code from downloadURL + if updateURL != downloadURL { + guard let remoteDownloadContent = getRemoteFileContents(downloadURL) else { + return ["error": "Update failed, downloadURL unreachable"] + } + remoteContent = remoteDownloadContent + } + return ["content": remoteContent] +} + +// background +func nativeChecks() -> [String: String] { + logger?.info("\(#function, privacy: .public) - started") + #if os(iOS) + // check the save location is set + guard (getSaveLocation() != nil) else { + logger?.error("\(#function, privacy: .public) - save location unset (iOS)") + return [ + "error": "Native checks error (0)", + "saveLocation": "unset", + "scheme": Bundle.main.infoDictionary?["US_URL_SCHEME"] as! String + ] + } + #endif + // check the default directories + guard checkDefaultDirectories() else { + logger?.error("\(#function, privacy: .public) - checkDefaultDirectories failed") + return ["error": "Native checks error (1)"] + } + // check the settings + guard checkSettings() else { + logger?.error("\(#function, privacy: .public) - checkSettings failed") + return ["error": "Native checks error (2)"] + } + // get all files to pass as arguments to function below + guard let allFiles = getAllFiles() else { + logger?.error("\(#function, privacy: .public) - getAllFiles failed") + return ["error": "Native checks error (3)"] + } + // purge the manifest of old records + guard purgeManifest(allFiles) else { + logger?.error("\(#function, privacy: .public) - purgeManifest failed") + return ["error": "Native checks error (4)"] + } + // update matches in manifest + guard updateManifestMatches(allFiles) else { + logger?.error("\(#function, privacy: .public) - updateManifestMatches failed") + return ["error": "Native checks error (5)"] + } + // update the required resources + guard updateManifestRequired(allFiles) else { + logger?.error("\(#function, privacy: .public) - updateManifestRequired failed") + return ["error": "Native checks error (6)"] + } + // update declarativeNetRequest + guard updateManifestDeclarativeNetRequests(allFiles) else { + logger?.error("\(#function, privacy: .public) - updateManifestDeclarativeNetRequests failed") + return ["error": "Native checks error (7)"] + } + // pass some info in response + logger?.info("\(#function, privacy: .public) - completed") + return ["success": "Native checks complete"] +} + +// userscript install +func installCheck(_ content: String) -> [String: Any] { + // this func checks a userscript's metadata to determine if it's already installed + + guard let files = getAllFiles() else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return ["error": "installCheck failed at (1)"] + } + + guard + let parsed = parse(content), + let metadata = parsed["metadata"] as? [String: [String]], + let newName = metadata["name"]?[0] + else { + return ["error": "userscript metadata is invalid"] + } + + // loop through all files nad get their names and filenames + // we will check the new name/filename to see if this is a unique userscript + // or if it will overwrite an existing userscript + var names = [String]() + for file in files { + // can be force unwrapped because getAllFiles didn't return nil + let name = file["name"] as! String + + // populate array + names.append(name) + } + + var directive = "" + #if os(macOS) + directive = "Click" + #elseif os(iOS) + directive = "Tap" + #endif + + if names.contains(newName) { + return [ + "success": "\(directive) to re-install", + "metadata": metadata, + "installed": true + ] + } + + return [ + "success": "\(directive) to install", + "metadata": metadata, + "installed": false + ]; +} + +func installUserscript(_ content: String) -> [String: Any] { + guard + let parsed = parse(content), + let metadata = parsed["metadata"] as? [String: [String]], + let n = metadata["name"]?[0] + else { + logger?.error("\(#function, privacy: .public) - failed at (1)") + return ["error": "installUserscript failed at (1)"] + } + let name = sanitize(n) + let filename = "\(name).user.js" + + let saved = saveFile(["filename": filename, "type": "js"], content) + return saved +} diff --git a/xcode/Safari-Extension/Info.plist b/xcode/Ext-Safari/Info.plist similarity index 95% rename from xcode/Safari-Extension/Info.plist rename to xcode/Ext-Safari/Info.plist index eda50006..2cc72fe3 100644 --- a/xcode/Safari-Extension/Info.plist +++ b/xcode/Ext-Safari/Info.plist @@ -33,6 +33,8 @@ $(MAC_SHARED_GID) US_SHARED_GID-iphoneos $(IOS_SHARED_GID) + US_EXT_IDENTIFIER + $(EXT_IDENTIFIER) US_URL_SCHEME $(APP_URL_SCHEME) diff --git a/xcode/Safari-Extension/Mac.entitlements b/xcode/Ext-Safari/Mac.entitlements similarity index 87% rename from xcode/Safari-Extension/Mac.entitlements rename to xcode/Ext-Safari/Mac.entitlements index f123ee4d..aa2f466a 100644 --- a/xcode/Safari-Extension/Mac.entitlements +++ b/xcode/Ext-Safari/Mac.entitlements @@ -8,8 +8,6 @@ $(MAC_SHARED_GID) - com.apple.security.files.bookmarks.app-scope - com.apple.security.files.user-selected.read-write com.apple.security.network.client diff --git a/xcode/Ext-Safari/SafariWebExtensionHandler.swift b/xcode/Ext-Safari/SafariWebExtensionHandler.swift new file mode 100644 index 00000000..ac5cec13 --- /dev/null +++ b/xcode/Ext-Safari/SafariWebExtensionHandler.swift @@ -0,0 +1,308 @@ +import SafariServices + +class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { + func beginRequest(with context: NSExtensionContext) { + let logger = USLogger(#fileID) + let item = context.inputItems[0] as? NSExtensionItem + let message = item?.userInfo?[SFExtensionMessageKey] as? [String: Any] + // if message received without name, ignore + guard let name = message?["name"] as? String else { + logger?.error("\(#function, privacy: .public) - could not get message name from web extension") + return + } + logger?.info("\(#function, privacy: .public) - Got message with name: \(name, privacy: .public)") + // got a valid message, construct response based on message received + let response = NSExtensionItem() + // send standard error when there's an issue parsing inbound message + var inBoundError = false + // these if/else if statement are formatted so that they can be neatly collapsed in Xcode + // typically the "else if" would be on the same line as the preceding statements close bracket + // ie. } else if { + if name == "OPEN_APP" { + if let scheme = Bundle.main.infoDictionary?["US_URL_SCHEME"], + let url = URL(string: "\(scheme):") { + #if os(macOS) + NSWorkspace.shared.open(url) + #endif + } + } + else if name == "NATIVE_CHECKS" { + let result = nativeChecks() + response.userInfo = [SFExtensionMessageKey: result] + } + else if name == "REQ_PLATFORM" { + let platform = getPlatform() + response.userInfo = [SFExtensionMessageKey: ["platform": platform]] + } + else if name == "REQ_USERSCRIPTS" { + if let url = message?["url"] as? String, let isTop = message?["isTop"] as? Bool { + if + let matches = getInjectionFilenames(url), + let code = getCode(matches, isTop) + { + response.userInfo = [SFExtensionMessageKey: code] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "REQ_USERSCRIPTS failed"]] + } + } else { + inBoundError = true + } + } + else if name == "REQ_REQUESTS" { + if let requestScripts = getRequestScripts() { + response.userInfo = [SFExtensionMessageKey: requestScripts] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get requestScripts"]] + } + } + else if name == "REQ_CONTEXT_MENU_SCRIPTS" { + if let contextMenuScripts = getContextMenuScripts() { + response.userInfo = [SFExtensionMessageKey: contextMenuScripts] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get contextMenuScripts"]] + } + } + else if name == "POPUP_BADGE_COUNT" { + if let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { + if let matches = getPopupBadgeCount(url, frameUrls) { + response.userInfo = [SFExtensionMessageKey: ["count": matches]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to update badge count"]] + } + } else { + inBoundError = true + } + } + else if name == "POPUP_MATCHES"{ + if let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { + if let matches = getPopupMatches(url, frameUrls) { + response.userInfo = [SFExtensionMessageKey: ["matches": matches]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get matches"]] + } + } else { + inBoundError = true + } + } + else if name == "POPUP_UPDATES" { + if let updates = checkForRemoteUpdates() { + response.userInfo = [SFExtensionMessageKey: ["updates": updates]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get updates"]] + } + } + else if name == "POPUP_UPDATE_ALL" { + if popupUpdateAll(), let updates = checkForRemoteUpdates() { + response.userInfo = [SFExtensionMessageKey: ["updates": updates]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to run update sequence"]] + } + } + else if name == "POPUP_UPDATE_SINGLE" { + if + let filename = message?["filename"] as? String, + let url = message?["url"] as? String, + let frameUrls = message?["frameUrls"] as? [String] + { + if let matches = popupUpdateSingle(filename, url, frameUrls) { + response.userInfo = [SFExtensionMessageKey: ["items": matches]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to update file"]] + } + } else { + inBoundError = true + } + } + else if name == "POPUP_CHECK_UPDATES" { + if let updates = checkForRemoteUpdates() { + response.userInfo = [SFExtensionMessageKey: ["updates": updates]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to check for updates"]] + } + } + else if name == "TOGGLE_EXTENSION" { + if let active = message?["active"] as? String, ["true", "false"].contains(active) { + var manifest = getManifest() + manifest.settings["active"] = active + if updateManifest(with: manifest) { + response.userInfo = [SFExtensionMessageKey: ["success": true]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to update injection state"]] + } + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "missing or wrong message content"]] + } + } + else if name == "TOGGLE_ITEM" { + // the current status of the item to toggle comes in as 0 || 1 + if + let item = message?["item"] as? [String: Any], + let filename = item["filename"] as? String, + let current = item["disabled"] as? Int + { + let action = current == 0 ? "disable" : "enable" + if toggleFile(filename, action) { + response.userInfo = [SFExtensionMessageKey: ["success": true]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to toggle file"]] + } + } else { + inBoundError = true + } + } + else if name == "OPEN_SAVE_LOCATION" { + #if os(macOS) + if openSaveLocation() { + response.userInfo = [SFExtensionMessageKey: ["success": true]] + } + #elseif os(iOS) + if let files = getAllFiles() { + response.userInfo = [SFExtensionMessageKey: ["items": files]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get all files"]] + } + #endif + } + else if name == "CHANGE_SAVE_LOCATION" { + #if os(macOS) + if let scheme = Bundle.main.infoDictionary?["US_URL_SCHEME"], + let url = URL(string: "\(scheme)://changesavelocation") { + NSWorkspace.shared.open(url) + } + #endif + } + else if name == "POPUP_INSTALL_CHECK" { + if let content = message?["content"] as? String { + response.userInfo = [SFExtensionMessageKey: installCheck(content)] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content"]] + } + } + else if name == "POPUP_INSTALL_SCRIPT" { + if let content = message?["content"] as? String { + response.userInfo = [SFExtensionMessageKey: installUserscript(content)] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content (2)"]] + } + } + else if name == "PAGE_INIT_DATA" { + #if os(macOS) + if let initData = getInitData(), checkDefaultDirectories() { + response.userInfo = [SFExtensionMessageKey: initData] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get init data"]] + } + #endif + } + else if name == "PAGE_LEGACY_IMPORT" { + #if os(macOS) + if let settings = getLegacyData() { + response.userInfo = [SFExtensionMessageKey: settings] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get legacy data"]] + } + #endif + } + else if name == "PAGE_ALL_FILES" { + #if os(macOS) + if let files = getAllFiles() { + response.userInfo = [SFExtensionMessageKey: files] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to get all files"]] + } + #endif + } + else if name == "PAGE_SAVE" { + #if os(macOS) + if + let item = message?["item"] as? [String: Any], + let content = message?["content"] as? String + { + let saveResponse = saveFile(item, content) + response.userInfo = [SFExtensionMessageKey: saveResponse] + } else { + inBoundError = true + } + #endif + } + else if name == "PAGE_TRASH" { + #if os(macOS) + if let item = message?["item"] as? [String: Any] { + if trashFile(item) { + response.userInfo = [SFExtensionMessageKey: ["success": true]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "failed to trash file"]] + } + } else { + inBoundError = true + } + #endif + } + else if name == "PAGE_UPDATE" { + #if os(macOS) + if let content = message?["content"] as? String { + let updateResponse = getFileRemoteUpdate(content) + response.userInfo = [SFExtensionMessageKey: updateResponse] + } else { + inBoundError = true + } + #endif + } + else if name == "CANCEL_REQUESTS" { + URLSession.shared.getAllTasks { tasks in + for task in tasks { + task.cancel() + } + } + response.userInfo = [SFExtensionMessageKey: ["success": true]] + } + else if name == "PAGE_NEW_REMOTE" { + #if os(macOS) + if let url = message?["url"] as? String { + if !validateUrl(url) { + response.userInfo = [SFExtensionMessageKey: ["error": "Failed to get remote content, invalid url"]] + } else if let content = getRemoteFileContents(url) { + response.userInfo = [SFExtensionMessageKey: content] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "Failed to get remote content"]] + } + } else { + inBoundError = true + } + #endif + } + else if name == "PAGE_UPDATE_SETTINGS" { + #if os(macOS) + if let settings = message?["settings"] as? [String: String] { + if updateSettings(settings) { + response.userInfo = [SFExtensionMessageKey: ["success": true]] + } else { + response.userInfo = [SFExtensionMessageKey: ["error": "Failed to save settings to disk"]] + } + } else { + inBoundError = true + } + #endif + } + else if name == "PAGE_UPDATE_BLACKLIST" { + #if os(macOS) + if let blacklist = message?["blacklist"] as? [String] { + var manifest = getManifest() + manifest.blacklist = blacklist + if !updateManifest(with: manifest) { + response.userInfo = [SFExtensionMessageKey: ["error": "Failed to save blacklist to disk"]] + } else { + response.userInfo = [SFExtensionMessageKey: ["success": true]] + } + } else { + inBoundError = true + } + #endif + } + // send inBoundError if found + if inBoundError { + response.userInfo = [SFExtensionMessageKey: ["error": "Failed to parse inbound message"]] + } + context.completeRequest(returningItems: [response], completionHandler: nil) + } +} diff --git a/xcode/Safari-Extension/iOS.entitlements b/xcode/Ext-Safari/iOS.entitlements similarity index 100% rename from xcode/Safari-Extension/iOS.entitlements rename to xcode/Ext-Safari/iOS.entitlements diff --git a/xcode/Mac-App/AppDelegate.swift b/xcode/Mac-App/AppDelegate.swift deleted file mode 100644 index 23e8a127..00000000 --- a/xcode/Mac-App/AppDelegate.swift +++ /dev/null @@ -1,50 +0,0 @@ -import Cocoa - -@NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate { - - private var window: NSWindow! - private var windowForego = false - private var windowLoaded = false - - func application(_ application: NSApplication, open urls: [URL]) { - // if open panel is already open, stop processing the URL scheme - if NSApplication.shared.keyWindow?.accessibilityIdentifier() == "open-panel" { return } - for url in urls { - if url.host == "changesavelocation" { - // avoid opening the panel repeatedly and playing unnecessary warning sounds - if NSApplication.shared.keyWindow?.identifier?.rawValue == "changeSaveLocation" { continue } - if windowLoaded { - let viewController = window.contentViewController as? ViewController - viewController?.changeSaveLocation(nil) - } else { - windowForego = true - schemeChangeSaveLocation() - } - } - } - } - - func applicationDidFinishLaunching(_ aNotification: Notification) { - if windowForego { return } - let storyboard = NSStoryboard(name: "View", bundle: Bundle.main) - let windowController = storyboard.instantiateInitialController() as! NSWindowController -// let viewController = windowController.contentViewController as! ViewController - window = windowController.window - window.setIsVisible(true) - windowLoaded = true - } - - func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - return true - } - - func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application - } - - func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - -} diff --git a/xcode/Mac-App/Functions.swift b/xcode/Mac-App/Functions.swift deleted file mode 100644 index 61d753b7..00000000 --- a/xcode/Mac-App/Functions.swift +++ /dev/null @@ -1,99 +0,0 @@ -import AppKit -import SafariServices -import os - -let extensionIdentifier = Bundle.main.infoDictionary?["US_EXT_IDENTIFIER"] as! String -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) - -func getSaveLocationURL() -> URL { - var url: URL - // default url - if #available(macOS 13.0, *) { - url = getDocumentsDirectory().appending(path: "scripts") - } else { - url = getDocumentsDirectory().appendingPathComponent("scripts") - } - // if not in safari extension environment, replace with extension path - if let bundleIdentifier = Bundle.main.bundleIdentifier, bundleIdentifier != extensionIdentifier { - let s = url.absoluteString.replacingOccurrences(of: bundleIdentifier, with: extensionIdentifier) - // avoid being encode again, decode first - if let decodePath = s.removingPercentEncoding { - url = URL(fileURLWithPath: decodePath, isDirectory: true) - } - } - // bookmark url - if let data = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName) { - if let bookmarkURL = readBookmark(data: data, isSecure: false) { - if directoryExists(path: bookmarkURL.path) { - url = bookmarkURL - } else { - UserDefaults(suiteName: SharedDefaults.suiteName)?.removeObject(forKey: SharedDefaults.keyName) - NSLog("removed shared bookmark because it's directory is non-existent, permanently deleted or in trash") - } - } - } - return url -} - -func setSaveLocationURL(url: URL) -> Bool { - guard FileManager.default.isWritableFile(atPath: url.path) else { - let alert = NSAlert() - alert.messageText = "Can not write to path. Choose a different path." - alert.runModal() - return false - } - guard saveBookmark(url: url, isShared: true, keyName: SharedDefaults.keyName, isSecure: false) else { - logger.error("\(#function, privacy: .public) - couldn't save new location from host app") - return false - } - return true -} - -func sendExtensionMessage(name: String, userInfo: [String : Any]? = nil, completion: ((Error?) -> Void)? = nil) { - SFSafariApplication.dispatchMessage( - withName: name, - toExtensionWithIdentifier: extensionIdentifier, - userInfo: userInfo - ) { error in // always be called - if error != nil { - debugPrint("Message attempted. Error info: \(String.init(describing: error))") - } - if let userHandle = completion { userHandle(error) } - } -} - -func changeSaveLocationPanel(directoryURL: URL? = nil) -> NSOpenPanel { - let panel = NSOpenPanel() - panel.allowsMultipleSelection = false - panel.canChooseDirectories = true - panel.canCreateDirectories = true - panel.canChooseFiles = false - if directoryURL != nil { - panel.directoryURL = directoryURL - } - panel.title = "Change Save Location - Userscripts" - panel.identifier = NSUserInterfaceItemIdentifier("changeSaveLocation") - return panel -} - -func schemeChangeSaveLocation() { - let saveLocationURL = getSaveLocationURL() - let panel = changeSaveLocationPanel(directoryURL: saveLocationURL) - // shows the path selection panel - let response = panel.runModal() - // check if clicked open button and there is a valid result - guard response == .OK, let url: URL = panel.urls.first else { return } - // check if path has indeed changed - if url.absoluteString == saveLocationURL.absoluteString { return } - // try set new save location path to bookmark - guard setSaveLocationURL(url: url) else { return } - // use semaphore to ensure the async func executed before app exits - let semaphore = DispatchSemaphore(value: 0) - // notify browser extension of relevant updates - sendExtensionMessage( - name: "SAVE_LOCATION_CHANGED", - userInfo: ["saveLocation": url.absoluteString.removingPercentEncoding ?? url.absoluteString], - completion: { _ in semaphore.signal() } - ) - semaphore.wait() -} diff --git a/xcode/Mac-App/ViewController.swift b/xcode/Mac-App/ViewController.swift deleted file mode 100644 index 3e2ffeaf..00000000 --- a/xcode/Mac-App/ViewController.swift +++ /dev/null @@ -1,83 +0,0 @@ -import Cocoa -import SafariServices.SFSafariApplication -import os - -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) - -class ViewController: NSViewController { - - @IBOutlet var appName: NSTextField! - @IBOutlet var saveLocation: NSTextField! - @IBOutlet weak var enabledText: NSTextField! - @IBOutlet weak var enabledIcon: NSView! - @IBOutlet weak var openButton: NSButton! - - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" - let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" - let extensionID = extensionIdentifier - - override func viewDidLoad() { - super.viewDidLoad() - self.appName.stringValue = "Userscripts Safari Version \(appVersion) (\(buildNumber))" - setExtensionState() - NotificationCenter.default.addObserver( - self, - selector: #selector(setExtensionState), - name: NSApplication.didBecomeActiveNotification, - object: nil - ) - let url = getSaveLocationURL() - self.saveLocation.stringValue = url.absoluteString - self.saveLocation.toolTip = url.absoluteString - if #available(macOS 13, *) { - self.openButton.title = "Open Safari Settings" - } - } - - @objc func setExtensionState() { - SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionID) { (state, error) in - guard let state = state else { - self.enabledText.stringValue = "Safari Extension State Unknown" - if let error = error { - logger.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") - } else { - logger.error("\(#function, privacy: .public) - couldn't get safari extension state in containing app") - } - return - } - DispatchQueue.main.async { - self.enabledIcon.layer?.backgroundColor = state.isEnabled ? NSColor.green.cgColor : NSColor.red.cgColor - self.enabledText.stringValue = state.isEnabled ? "Safari Extension Enabled" : "Safari Extension Disabled" - } - } - } - - @IBAction func changeSaveLocation(_ sender: AnyObject?) { - guard let window = self.view.window else { return } - let saveLocationURL = getSaveLocationURL() - let panel = changeSaveLocationPanel(directoryURL: saveLocationURL) - panel.beginSheetModal(for: window, completionHandler: { response in - // check if clicked open button and there is a valid result - guard response == .OK, let url: URL = panel.urls.first else { return } - // check if path has indeed changed - if url.absoluteString == saveLocationURL.absoluteString { return } - // try set new save location path to bookmark - guard setSaveLocationURL(url: url) else { return } - // update user interface text display - self.saveLocation.stringValue = url.absoluteString - self.saveLocation.toolTip = url.absoluteString - // notify browser extension of relevant updates - sendExtensionMessage( - name: "SAVE_LOCATION_CHANGED", - userInfo: [ - "saveLocation": url.absoluteString.removingPercentEncoding ?? url.absoluteString, - "returnApp": true - ] - ) - }) - } - - @IBAction func openSafariExtensionPreferences(_ sender: AnyObject?) { - SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionID) - } -} diff --git a/xcode/Mac-Tests/UserscriptsTests.swift b/xcode/Mac-Tests/UserscriptsTests.swift deleted file mode 100644 index 12c91bd0..00000000 --- a/xcode/Mac-Tests/UserscriptsTests.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// UserscriptsTests.swift -// UserscriptsTests -// -// Created by Justin Wasack on 1/23/22. -// Copyright © 2022 Justin Wasack. All rights reserved. -// - -import XCTest -@testable import Userscripts_Mac_Safari_Extension - -class UserscriptsTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testStringSanitization() throws { - // given - let strs = [ - "String", - "https://something.com/?foo=12", - "I have backslashes \\\\", - ".....ok", - ":Akneh.,><>dkie:lm", - "..解锁B站大会员番剧、", - "解锁B站大会员番剧、B站视频解析下载;全网VIP视频免费破解去广告;全网音乐直接下载;油管、Facebook等国外视频解析下载;网盘搜索引擎破解无限下载等", - "5CLksm3AAbb2F2F2f----___--+87363&^#%o%3O3", - "Example Userscript Name" - ] - - // when - var result = [String]() - for str in strs { - let sanitizedString = sanitize(str) - let unsanitizedString = unsanitize(sanitizedString) - result.append(unsanitizedString) - } - - // then - XCTAssert(result.elementsEqual(strs)) - } - - func testEncodedCheck() throws { - let urls = [ - "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect%20外链跳转.user.js", - "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", - "https://cdn.frankerfacez.com/static/ffz_injector.user.js", - "http://www.k21p.com/example.user.js", // add http protocol - "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js" - ] - var result = [String]() - for url in urls { - if isEncoded(url) { - result.append(url) - } - } - // 2 urls already percent encoded - XCTAssert(result.count == 2) - } - - func testGetRemoteFileContents() throws { - let urls = [ - "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect%20外链跳转.user.js", - "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js", - "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", - "https://cdn.frankerfacez.com/static/ffz_injector.user.js", - "http://www.k21p.com/example.user.js" // add http protocol - ] - var result = [String]() - for url in urls { - if let contents = getRemoteFileContents(url) { - result.append(contents) - } - } - XCTAssert(result.count == urls.count) - } - - func testFileRemoteUpdate() throws { - let urls = [ - "https://www.k21p.com/example.user.js", - "https://www.k21p.com/example.user.js?foo=bar", // query string - "http://www.k21p.com/example.user.js", // http protocol - "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js", // non latin chars - "https://www.k21p.com/example.user.jsx" // should fail - - ] - var result = [Int]() - for url in urls { - let content = """ - // ==UserScript== - // @name test - // @match *://*/* - // @version 0.1 - // @updateURL http://www.k21p.com/example.user.js - // @downloadURL \(url) - // ==/UserScript== - """; - let response = getFileRemoteUpdate(content) - if !response.keys.contains("error") { - result.append(1) - } - } - XCTAssert(result.count == (urls.count - 1)) - } - - func testMatching() throws { - var count = 0 - var result = [String]() - let patternDict = [ - "*://*/*": [ - "https://www.bing.com/", - "https://example.org/foo/bar.html", - "https://a.org/some/path/" - ], - "*://*.mozilla.org/*": [ - "http://mozilla.org/", - "https://mozilla.org/", - "https://b.mozilla.org/path/" - ], - "*://www.google.com/*": [ - "https://www.google.com/://aa", - "https://www.google.com/preferences?prev=https://www.google.com/", - "https://www.google.com/preferences?prev=", - "https://www.google.com/" - ], - "*://localhost/*": [ - "http://localhost:8000/", - "https://localhost:3000/foo.html" - ], - "http://127.0.0.1/*": [ - "http://127.0.0.1/", - "http://127.0.0.1/foo/bar.html" - ], - "*://*.example.com/*?a=1*": [ - "http://example.com/?a=1", - "https://www.example.com/index?a=1&b=2" - ] - ] - let patternDictFails = [ - "https://www.example.com/*": [ - "file://www.example.com/", - "ftp://www.example.com/", - "ws://www.example.com/", - "http://www.example.com/" - ], - "http://www.example.com/index.html": [ - "http://www.example.com/", - "https://www.example.com/index.html" - ], - "*://localhost/*": [ - "https://localhost.com/", - "ftp://localhost:8080/" - ], - "https://www.example*/*": [ - "https://www.example.com/" - ], - "*://*.example.com/*?a=1*": [ - "http://example.com/", - "https://www.example.com/?a=2" - ] - ] - for (pattern, urls) in patternDict { - count = count + urls.count - for url in urls { - if match(url, pattern) { - result.append("1") - } - } - } - for (pattern, urls) in patternDictFails { - // don't increment count since these tests should fail - for url in urls { - if match(url, pattern) { - result.removeLast() - } - } - } - XCTAssert(result.count == count) - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/xcode/Safari-Extension/Functions.swift b/xcode/Safari-Extension/Functions.swift deleted file mode 100644 index bd15f4ca..00000000 --- a/xcode/Safari-Extension/Functions.swift +++ /dev/null @@ -1,2065 +0,0 @@ -import Foundation -import SafariServices -import os - -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) - -// helpers -func getRequireLocation() -> URL { - // simple helper in case required code save directory needs to change - return getDocumentsDirectory().appendingPathComponent("require") -} - -func dateToMilliseconds(_ date: Date) -> Int { - let since1970 = date.timeIntervalSince1970 - return Int(since1970 * 1000) -} - -func sanitize(_ str: String) -> String { - // removes invalid filename characters from strings - var sanitized = str - if sanitized.first == "." { - sanitized = "%2" + str.dropFirst() - } - sanitized = sanitized.replacingOccurrences(of: "/", with: "%2F") - sanitized = sanitized.replacingOccurrences(of: ":", with: "%3A") - sanitized = sanitized.replacingOccurrences(of: "\\", with: "%5C") - return sanitized -} - -func unsanitize(_ str: String) -> String { - var s = str - if s.hasPrefix("%2") && !s.hasPrefix("%2F") { - s = "." + s.dropFirst(2) - } - if s.removingPercentEncoding != s { - s = s.removingPercentEncoding ?? s - } - return s -} - -func normalizeWeight(_ weight: String) -> String { - if let w = Int(weight) { - if w > 999 { - return "999" - } else if w < 1 { - return "1" - } else { - return weight - } - } else { - return "1" - } -} - -func getSaveLocation() -> URL? { - #if os(iOS) - if - let sharedBookmarkData = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName), - let bookmarkUrl = readBookmark(data: sharedBookmarkData, isSecure: true) - { - return bookmarkUrl - } else { - return nil - } - #elseif os(macOS) - let standardDefaults = UserDefaults.standard - let userSaveLocationKey = "userSaveLocation" - var defaultSaveLocation:URL - - // get the default save location, if key doesn't exist write it to user defaults - if let saveLocationValue = standardDefaults.url(forKey: "saveLocation") { - defaultSaveLocation = saveLocationValue - } else { - logger.info("\(#function, privacy: .public) - default save location not set, writing to user defaults") - let url = getDocumentsDirectory().appendingPathComponent("scripts") - UserDefaults.standard.set(url, forKey: "saveLocation") - defaultSaveLocation = url - } - - // check if shared bookmark data exists - // check if can get shared bookmark url - // won't be able to if directory trashed - guard - let sharedBookmarkData = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName), - let sharedBookmark = readBookmark(data: sharedBookmarkData, isSecure: false), - directoryExists(path: sharedBookmark.path) - else { - // can't get shared bookmark, use default location and remove shared bookmark key from shared user defaults - UserDefaults(suiteName: SharedDefaults.suiteName)?.removeObject(forKey: SharedDefaults.keyName) - logger.info("\(#function, privacy: .public) - removed sharedbookmark because it was either permanently deleted or in trash") - return defaultSaveLocation - } - - // at this point, it's known sharedbookmark exists - // check local bookmark exists, can read url from bookmark and if bookmark url == shared bookmark url - // if local bookmark exists, no need to check if directory exists for it - // can't think of an instance where shared bookmark directory exists (checked above), yet local bookmark directory does not - if - let userSaveLocationData = standardDefaults.data(forKey: userSaveLocationKey), - let userSaveLocation = readBookmark(data: userSaveLocationData, isSecure: true), - sharedBookmark == userSaveLocation - { - return userSaveLocation - } - - // at this point one of the following conditions met - // - local bookmark data doesn't exist - // - for some reason can't get url from local bookmark data - // - local bookmark url != shared bookmark url (user updated save location) - // when any of those conditions are met, create new local bookmark from shared bookmark - if saveBookmark(url: sharedBookmark, isShared: false, keyName: userSaveLocationKey, isSecure: true) { - // read the newly saved bookmark and return it - guard - let localBookmarkData = standardDefaults.data(forKey: userSaveLocationKey), - let localBookmarkUrl = readBookmark(data: localBookmarkData, isSecure: true) - else { - logger.error("\(#function, privacy: .public) - failed reading local bookmark") - return nil - } - return localBookmarkUrl - } else { - logger.error("\(#function, privacy: .public) - could not save local version of shared bookmark") - return nil - } - #endif -} - -func openSaveLocation() -> Bool { - #if os(macOS) - guard let saveLocation = getSaveLocation() else { - return false - } - let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() - defer { - if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} - } - NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: saveLocation.path) - #endif - return true -} - -func validateUrl(_ urlString: String) -> Bool { - var urlChecked = urlString - // if the url is already encoded, decode it - if isEncoded(urlChecked) { - if let decodedUrl = urlChecked.removingPercentEncoding { - urlChecked = decodedUrl - } else { - logger.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(urlString, privacy: .public)") - return false - } - } - // encode all urls strings - if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - urlChecked = encodedUrl - } else { - logger.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(urlString, privacy: .public)") - return false - } - guard - let parts = getUrlProps(urlChecked), - let ptcl = parts["protocol"], - let path = parts["pathname"] - else { - logger.error("\(#function, privacy: .public) - failed at (3) for \(urlString, privacy: .public)") - return false - } - if - (ptcl != "https:" && ptcl != "http:") - || (!path.hasSuffix(".css") && !path.hasSuffix(".js")) - { - return false - } - return true -} - -func isVersionNewer(_ oldVersion: String, _ newVersion: String) -> Bool { - let oldVersions = oldVersion.components(separatedBy: ".") - let newVersions = newVersion.components(separatedBy: ".") - for (index, version) in newVersions.enumerated() { - let a = Int(version) ?? 0 - let oldVersionValue = oldVersions.indices.contains(index) ? oldVersions[index] : "0" - let b = Int(oldVersionValue) ?? 0 - if a > b { - return true - } - if a < b { - return false - } - } - return false -} - -func isEncoded(_ str: String) -> Bool { - return str.removingPercentEncoding != str -} - -// parser -func parse(_ content: String) -> [String: Any]? { - // returns structured data from content of file - // will fail to parse if metablock or required @name key missing - let pattern = #"(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))"# - // force try b/c pattern is known to be valid regex - let regex = try! NSRegularExpression(pattern: pattern, options: []) - let range = NSRange(location: 0, length: content.utf16.count) - // return nil/fail if metablock missing - guard let match = regex.firstMatch(in: content, options: [], range: range) else { - return nil - } - - // at this point the text content has passed initial validation, it contains valid metadata - // the metadata can be in userscript or userstyle format, need to check for this and adjust group numbers - // rather than being too strict, text content can precede the opening userscript tag, however it will be ignored - // adjust start index of file content while assigning group numbers to account for any text content preceding opening tag - let contentStartIndex = content.index(content.startIndex, offsetBy: match.range.lowerBound) - var g1, g2, g3:Int - if (content[contentStartIndex.. Bool { - let content = data - let url = getDocumentsDirectory().appendingPathComponent("manifest.json") - do { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - let encoded = try encoder.encode(content) - let fileContent = String(decoding: encoded, as: UTF8.self) - try fileContent.write(to: url, atomically: false, encoding: .utf8) - return true - } catch { - logger.error("\(#function, privacy: .public) - failed to update manifest: \(error.localizedDescription, privacy: .public)") - return false - } -} - -func getManifest() -> Manifest { - let url = getDocumentsDirectory().appendingPathComponent("manifest.json") - if - let content = try? String(contentsOf: url, encoding: .utf8), - let data = content.data(using: .utf8), - let decoded = try? JSONDecoder().decode(Manifest.self, from: Data(data)) - { - return decoded - } else { - // manifest missing, improperly formatted or missing key - // create new manifest with default key/vals - let manifest = Manifest( - blacklist: [], - declarativeNetRequest: [], - disabled: [], - exclude: [:], - excludeMatch: [:], - include: [:], - match: [:], - require: [:], - settings: defaultSettings - ) - _ = updateManifest(with: manifest) - return manifest - } -} - -func updateManifestMatches(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - logger.info("\(#function, privacy: .public) - started") - // only get all files if files were not provided - var files = [[String: Any]]() - if optionalFilesArray.isEmpty { - guard let getFiles = getAllFiles() else {return false} - files = getFiles - } else { - files = optionalFilesArray - } - var manifest = getManifest() - for file in files { - // can be force unwrapped because getAllFiles didn't return nil - let metadata = file["metadata"] as! [String: [String]] - let filename = file["filename"] as! String - // skip request type userscripts - let runAt = metadata["run-at"]?[0] ?? "document-end" - if runAt == "request" { - continue - } - // populate excludes & matches - var excludeMatched = [String]() - var matched = [String]() - var excluded = [String]() - var included = [String]() - if metadata["exclude-match"] != nil { - excludeMatched.append(contentsOf: metadata["exclude-match"]!) - } - if metadata["match"] != nil { - matched.append(contentsOf: metadata["match"]!) - } - if metadata["include"] != nil { - included.append(contentsOf: metadata["include"]!) - } - if metadata["exclude"] != nil { - excluded.append(contentsOf: metadata["exclude"]!) - } - // if in declarativeNetRequest array, remove it - if manifest.declarativeNetRequest.contains(filename) { - if let index = manifest.declarativeNetRequest.firstIndex(of: filename) { - manifest.declarativeNetRequest.remove(at: index) - } else { - logger.error("\(#function, privacy: .public) - failed to remove \(filename, privacy: .public) from dNR array") - } - } - - // update manifest values - manifest.excludeMatch = updatePatternDict(filename, excludeMatched, manifest.excludeMatch) - manifest.match = updatePatternDict(filename, matched, manifest.match) - manifest.exclude = updatePatternDict(filename, excluded, manifest.exclude) - manifest.include = updatePatternDict(filename, included, manifest.include) - - if !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed to update manifest matches") - return false - } - } - logger.info("\(#function, privacy: .public) - complete") - return true -} - -func updatePatternDict(_ filename: String, _ filePatterns: [String], _ manifestKeys: [String: [String]]) -> [String: [String]] { - // will hold the exclude/match patterns in manifest that have file name as value - var patternsInManifestForFile = [String]() - // new var from func argument, so it can be manipulated - var returnDictionary = manifestKeys - // patterns from manifest - let keys = returnDictionary.keys - // determine what patterns already have this filename as a value - for key in keys { - // key is an array of filenames - guard let filenames = returnDictionary[key] else { - logger.error("\(#function, privacy: .public) - failed to get values for manifest key, \(key, privacy: .public)") - continue - } - for name in filenames { - // name is a single filename - // if name is same as filename, file already added for this pattern - // add it to patternsInManifestForFile for later comparison - if name == filename { - patternsInManifestForFile.append(key) - } - } - } - // patterns in file metadata and patterns in manifest that have filename as a value - // filename already present in manifest for these patterns, do nothing with these - // let common = filePatterns.filter{patternsInManifestForFile.contains($0)} - // patterns in file metadata, but don't have the filename as a value within the manifest - // these are the manifest patterns that the filename needs to be added to - let addFilenameTo = filePatterns.filter{!patternsInManifestForFile.contains($0)} - - // the patterns that have the filename as a value, but not present in file metadata - // ie. these are the manifest patterns we need to remove the filename from - let removeFilenameFrom = patternsInManifestForFile.filter{!filePatterns.contains($0)} - - // check if filename needs to be added or new key/val needs to be created - for pattern in addFilenameTo { - if returnDictionary[pattern] != nil { - returnDictionary[pattern]?.append(filename) - } else { - returnDictionary[pattern] = [filename] - } - } - - for pattern in removeFilenameFrom { - // get the index of the filename within the array - let ind = returnDictionary[pattern]?.firstIndex(of: filename) - // remove filename from array by index - returnDictionary[pattern]?.remove(at: ind!) - // if filename was the last item in array, remove the url pattern from dictionary - if returnDictionary[pattern]!.isEmpty { - returnDictionary.removeValue(forKey: pattern) - } - } - - return returnDictionary -} - -func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - logger.info("\(#function, privacy: .public) - started") - // only get all files if files were not provided - var files = [[String: Any]]() - if optionalFilesArray.isEmpty { - guard let getFiles = getAllFiles() else { - logger.info("\(#function, privacy: .public) - count not get files") - return false - } - files = getFiles - } else { - files = optionalFilesArray - } - logger.info("\(#function, privacy: .public) - will loop through \(files.count, privacy: .public)") - var manifest = getManifest() - for file in files { - // can be force unwrapped because getAllFiles didn't return nil - let filename = file["filename"] as! String - let metadata = file["metadata"] as! [String: [String]] - let type = file["type"] as! String - let required = metadata["require"] ?? [] - logger.info("\(#function, privacy: .public) - begin \(filename, privacy: .public)") - // get required resources for file, if fail, skip updating manifest - if !getRequiredCode(filename, required, type) { - logger.error("\(#function, privacy: .public) - couldn't fetch remote content for \(filename, privacy: .public)") - continue - } - - // create filenames from sanitized resource urls - // getRequiredCode does the same thing when saving to disk - // populate array with entries for manifest - var r = [String]() - for resource in required { - let sanitizedResourceName = sanitize(resource) - r.append(sanitizedResourceName) - } - - // if there are values, write them to manifest - // if failed to write to manifest, continue to next file & log error - if !r.isEmpty && r != manifest.require[filename] { - manifest.require[filename] = r - if !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - couldn't update manifest when getting required resources") - } - } - logger.info("\(#function, privacy: .public) - end \(filename, privacy: .public)") - } - logger.info("\(#function, privacy: .public) - complete") - return true -} - -func updateManifestDeclarativeNetRequests(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - logger.info("\(#function, privacy: .public) - started") - var files = [[String: Any]]() - if optionalFilesArray.isEmpty { - guard let getFiles = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return false - } - files = getFiles - } else { - files = optionalFilesArray - } - var manifest = getManifest() - for file in files { - // can be force unwrapped because getAllFiles didn't return nil - // and getAllFiles always returns the following - let metadata = file["metadata"] as! [String: [String]] - let filename = file["filename"] as! String - let runAt = metadata["run-at"]?[0] ?? "document-end" - // if not a request type, ignore - if runAt != "request" { - continue - } - var update = false - // if filename already in manifest - if !manifest.declarativeNetRequest.contains(filename) { - manifest.declarativeNetRequest.append(filename) - update = true - } - // if filename in another array remove it - for (pattern, filenames) in manifest.match { - for fn in filenames { - if fn == filename, let index = manifest.match[pattern]?.firstIndex(of: filename) { - manifest.match[pattern]?.remove(at: index) - update = true - } - } - } - for (pattern, filenames) in manifest.excludeMatch { - for fn in filenames { - if fn == filename, let index = manifest.excludeMatch[pattern]?.firstIndex(of: filename) { - manifest.excludeMatch[pattern]?.remove(at: index) - update = true - } - } - } - for (pattern, filenames) in manifest.include { - for fn in filenames { - if fn == filename, let index = manifest.include[pattern]?.firstIndex(of: filename) { - manifest.include[pattern]?.remove(at: index) - update = true - } - } - } - for (pattern, filenames) in manifest.exclude { - for fn in filenames { - if fn == filename, let index = manifest.exclude[pattern]?.firstIndex(of: filename) { - manifest.exclude[pattern]?.remove(at: index) - update = true - } - } - } - if update, !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed at (2)") - return false - } - } - logger.info("\(#function, privacy: .public) - complete") - return true -} - -func purgeManifest(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - logger.info("\(#function, privacy: .public) - started") - // purge all manifest keys of any stale entries - var update = false, manifest = getManifest(), allSaveLocationFilenames = [String]() - // only get all files if files were not provided - var allFiles = [[String: Any]]() - if optionalFilesArray.isEmpty { - // if getAllFiles fails to return, ignore and pass an empty array - let getFiles = getAllFiles() ?? [] - allFiles = getFiles - } else { - allFiles = optionalFilesArray - } - // populate array with filenames - for file in allFiles { - if let filename = file["filename"] as? String { - allSaveLocationFilenames.append(filename) - } - } - // loop through manifest keys, if no file exists for value, remove value from manifest - // if there are no more filenames in pattern, remove pattern from manifest - for (pattern, filenames) in manifest.match { - for filename in filenames { - if !allSaveLocationFilenames.contains(filename) { - if let index = manifest.match[pattern]?.firstIndex(of: filename) { - manifest.match[pattern]?.remove(at: index) - update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from match pattern - \(pattern, privacy: .public)") - } - } - } - if let length = manifest.match[pattern]?.count { - if length < 1, let ind = manifest.match.index(forKey: pattern) { - manifest.match.remove(at: ind) - logger.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) match pattern, removed from manifest") - } - } - } - for (pattern, filenames) in manifest.excludeMatch { - for filename in filenames { - if !allSaveLocationFilenames.contains(filename) { - if let index = manifest.excludeMatch[pattern]?.firstIndex(of: filename) { - manifest.excludeMatch[pattern]?.remove(at: index) - update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude-match pattern - \(pattern, privacy: .public)") - } - } - } - if let length = manifest.excludeMatch[pattern]?.count { - if length < 1, let ind = manifest.excludeMatch.index(forKey: pattern) { - manifest.excludeMatch.remove(at: ind) - logger.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude-match pattern, removed from manifest") - } - } - } - for (pattern, filenames) in manifest.exclude { - for filename in filenames { - if !allSaveLocationFilenames.contains(filename) { - if let index = manifest.exclude[pattern]?.firstIndex(of: filename) { - manifest.exclude[pattern]?.remove(at: index) - update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude pattern - \(pattern, privacy: .public)") - } - } - } - if let length = manifest.exclude[pattern]?.count { - if length < 1, let ind = manifest.exclude.index(forKey: pattern) { - manifest.exclude.remove(at: ind) - logger.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude pattern, removed from manifest") - } - } - } - for (pattern, filenames) in manifest.include { - for filename in filenames { - if !allSaveLocationFilenames.contains(filename) { - if let index = manifest.include[pattern]?.firstIndex(of: filename) { - manifest.include[pattern]?.remove(at: index) - update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from exclude pattern - \(pattern, privacy: .public)") - } - } - } - if let length = manifest.include[pattern]?.count { - if length < 1, let ind = manifest.include.index(forKey: pattern) { - manifest.include.remove(at: ind) - logger.info("\(#function, privacy: .public) - No more files for \(pattern, privacy: .public) exclude pattern, removed from manifest") - } - } - } - // loop through manifest required - for (filename, _) in manifest.require { - if !allSaveLocationFilenames.contains(filename) { - if let index = manifest.require.index(forKey: filename) { - manifest.require.remove(at: index) - // remove associated resources - if !getRequiredCode(filename, [], (filename as NSString).pathExtension) { - logger.error("\(#function, privacy: .public) - failed to remove required resources when purging \(filename, privacy: .public) from manifest required records") - } - update = true - logger.info("\(#function, privacy: .public) - No more required resources for \(filename, privacy: .public), removed from manifest along with resource folder") - } - } - } - // loop through manifest disabled - for filename in manifest.disabled { - if !allSaveLocationFilenames.contains(filename) { - if let index = manifest.disabled.firstIndex(of: filename) { - manifest.disabled.remove(at: index) - update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from disabled") - } - } - } - // loop through manifest declarativeNetRequest - for filename in manifest.declarativeNetRequest { - if !allSaveLocationFilenames.contains(filename) { - if let index = manifest.declarativeNetRequest.firstIndex(of: filename) { - manifest.declarativeNetRequest.remove(at: index) - update = true - logger.info("\(#function, privacy: .public) - Could not find \(filename, privacy: .public) in save location, removed from dNR") - } - } - } - // remove obsolete settings - for setting in manifest.settings { - if !defaultSettings.keys.contains(setting.key) { - manifest.settings.removeValue(forKey: setting.key) - update = true - logger.info("\(#function, privacy: .public) - Removed obsolete setting - \(setting.key, privacy: .public)") - } - } - if update, !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed to purge manifest") - return false - } - logger.info("\(#function, privacy: .public) - complete") - return true -} - -// settings -func checkSettings() -> Bool { - // iterate over default settings and individually check if each present - // if missing add setting to manifest about to be returned - // missing keys will occur when new settings introduced - var manifest = getManifest() - var update = false - for (key, value) in defaultSettings { - if manifest.settings[key] == nil { - manifest.settings[key] = value - update = true - } - } - if update, !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed to update manifest settings") - return false - } - return true -} - -func updateSettings(_ settings: [String: String]) -> Bool { - var manifest = getManifest() - manifest.settings = settings - if updateManifest(with: manifest) != true { - logger.error("\(#function, privacy: .public) - failed to update settings") - return false - } - return true -} - -// files -func getAllFiles(includeCode: Bool = false) -> [[String: Any]]? { - // returns all files of proper type with filenames, metadata & more - var files = [[String: Any]]() - let fm = FileManager.default - let manifest = getManifest() - guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - // security scope - let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() - defer { - if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} - } - // get all file urls within save location - guard let urls = try? fm.contentsOfDirectory(at: saveLocation, includingPropertiesForKeys: []) else { - logger.error("\(#function, privacy: .public) - failed at (2)") - return nil - } - for url in urls { - var fileData = [String: Any]() - // only read contents for css & js files - let filename = url.lastPathComponent - if (!filename.hasSuffix(".css") && !filename.hasSuffix(".js")) { - continue - } - // file will be skipped if metablock is missing - guard - let content = try? String(contentsOf: url, encoding: .utf8), - let dateMod = try? fm.attributesOfItem(atPath: url.path)[.modificationDate] as? Date, - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]], - let type = filename.split(separator: ".").last - else { - logger.info("\(#function, privacy: .public) - ignoring \(filename, privacy: .public), file missing or metadata missing from file contents") - continue - } - fileData["canUpdate"] = false - fileData["content"] = content - fileData["disabled"] = manifest.disabled.contains(filename) - fileData["filename"] = filename - fileData["lastModified"] = dateToMilliseconds(dateMod) - fileData["metadata"] = metadata - // force unwrap name since parser ensure it exists - fileData["name"] = metadata["name"]![0] - fileData["type"] = "\(type)" - // add extra data if a request userscript - let runAt = metadata["run-at"]?[0] ?? "document-end" - if runAt == "request" { - fileData["request"] = true - } - if metadata["description"] != nil { - fileData["description"] = metadata["description"]![0] - } - if metadata["version"] != nil && metadata["updateURL"] != nil { - fileData["canUpdate"] = true - } - fileData["noframes"] = metadata["noframes"] != nil ? true : false - // if asked, also return the code string - if (includeCode) { - // can force unwrap because always returned from parser - fileData["code"] = parsed["code"] as! String - } - files.append(fileData) - } - logger.info("\(#function, privacy: .public) - completed") - return files -} - -func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: String) -> Bool { - let directory = getRequireLocation().appendingPathComponent(filename) - // if file requires no resource but directory exists, remove it - // this resource is not user-generated and can be downloaded again, so just remove it instead of moves to the trash - // also in ios, the volume “Data” has no trash and item can only be removed directly - if resources.isEmpty { - if FileManager.default.fileExists(atPath: directory.path) { - do { - try FileManager.default.removeItem(at: directory) - } catch { - logger.error("\(#function, privacy: .public) - failed to remove directory: \(error.localizedDescription, privacy: .public)") - } - } - return true - } - // record URLs for subsequent processing - var resourceUrls = Set() - // loop through resource urls and attempt to fetch it - for resourceUrlString in resources { - // get the path of the url string - guard let resourceUrlPath = URLComponents(string: resourceUrlString)?.path else { - // if path can not be obtained, skip and log - logger.info("\(#function, privacy: .public) - failed to get path on \(filename, privacy: .public) for \(resourceUrlString, privacy: .public)") - continue - } - // skip urls pointing to files of different types - if resourceUrlPath.hasSuffix(fileType) { - let resourceFilename = sanitize(resourceUrlString) - let fileURL = directory.appendingPathComponent(resourceFilename) - // insert url to resolve symlink into set - resourceUrls.insert(fileURL.standardizedFileURL) - // only attempt to get resource if it does not yet exist - if FileManager.default.fileExists(atPath: fileURL.path) {continue} - // get the remote file contents - guard let contents = getRemoteFileContents(resourceUrlString) else {continue} - // check if file specific folder exists at requires directory - if !FileManager.default.fileExists(atPath: directory.path) { - guard ((try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false)) != nil) else { - logger.info("\(#function, privacy: .public) - failed to create required code directory for \(filename, privacy: .public)") - return false - } - } - // finally write file to directory - guard ((try? contents.write(to: fileURL, atomically: false, encoding: .utf8)) != nil) else { - logger.info("\(#function, privacy: .public) - failed to write content to file for \(filename, privacy: .public) from \(resourceUrlString, privacy: .public)") - return false - } - } - } - // cleanup downloaded files that are no longer required - do { - var downloadedUrls = Set() - // get all downloaded resources url - let fileUrls = try FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: []) - // insert url to resolve symlink into set - for url in fileUrls { downloadedUrls.insert(url.standardizedFileURL) } - // exclude currently required resources - let abandonedUrls = downloadedUrls.subtracting(resourceUrls) - // loop through abandoned urls and attempt to remove it - for abandonFileUrl in abandonedUrls { - do { - try FileManager.default.removeItem(at: abandonFileUrl) - logger.info("\(#function, privacy: .public) - cleanup abandoned resource: \(unsanitize(abandonFileUrl.lastPathComponent), privacy: .public)") - } catch { - logger.error("\(#function, privacy: .public) - failed to remove abandoned resource: \(error.localizedDescription, privacy: .public)") - } - } - } catch { - logger.error("\(#function, privacy: .public) - failed to cleanup resources: \(error.localizedDescription, privacy: .public)") - } - return true -} - -func checkForRemoteUpdates(_ optionalFilesArray: [[String: Any]] = []) -> [[String: String]]? { - // only get all files if files were not provided - var files = [[String: Any]]() - if optionalFilesArray.isEmpty { - guard let getFiles = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - files = getFiles - } else { - files = optionalFilesArray - } - - var hasUpdates = [[String: String]]() - for file in files { - // can be force unwrapped because getAllFiles didn't return nil - let filename = file["filename"] as! String - let canUpdate = file["canUpdate"] as! Bool - let metadata = file["metadata"] as! [String: [String]] - let type = file["type"] as! String - let name = metadata["name"]![0] - logger.info("\(#function, privacy: .public) - Checking for remote updates for \(filename, privacy: .public)") - if canUpdate { - let currentVersion = metadata["version"]![0] - let updateUrl = metadata["updateURL"]![0] - // before fetching remote contents, ensure it points to a file of the same type - if !updateUrl.hasSuffix(type) {continue} - guard - let remoteFileContents = getRemoteFileContents(updateUrl), - let remoteFileContentsParsed = parse(remoteFileContents), - let remoteMetadata = remoteFileContentsParsed["metadata"] as? [String: [String]], - let remoteVersion = remoteMetadata["version"]?[0] - else { - logger.error("\(#function, privacy: .public) - failed to parse remote file contents") - return nil - } - let remoteVersionNewer = isVersionNewer(currentVersion, remoteVersion) - if remoteVersionNewer { - hasUpdates.append(["name": name, "filename": filename, "type": type, "url": updateUrl]) - } - } - } - logger.info("\(#function, privacy: .public) - Finished checking for remote updates for \(files.count, privacy: .public) files") - return hasUpdates -} - -func getRemoteFileContents(_ url: String) -> String? { - logger.info("\(#function, privacy: .public) - started for \(url, privacy: .public)") - // if url is http change to https - var urlChecked = url - if urlChecked.hasPrefix("http:") { - urlChecked = urlChecked.replacingOccurrences(of: "http:", with: "https:") - logger.info("\(#function, privacy: .public) - \(url, privacy: .public) is using insecure http, attempt to fetch remote content with https") - } - // if the url is already encoded, decode it - if isEncoded(urlChecked) { - if let decodedUrl = urlChecked.removingPercentEncoding { - urlChecked = decodedUrl - } else { - logger.error("\(#function, privacy: .public) - failed at (1), couldn't decode url, \(url, privacy: .public)") - return nil - } - } - // encode all urls strings - if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - urlChecked = encodedUrl - } else { - logger.error("\(#function, privacy: .public) - failed at (2), couldn't percent encode url, \(url, privacy: .public)") - return nil - } - // convert url string to url - guard let solidURL = URL(string: urlChecked) else { - logger.error("\(#function, privacy: .public) - failed at (3), couldn't convert string to url, \(url, privacy: .public)") - return nil - } - var contents = "" - // get remote file contents, synchronously - let semaphore = DispatchSemaphore(value: 0) - var task: URLSessionDataTask? - task = URLSession.shared.dataTask(with: solidURL) { data, response, error in - if let r = response as? HTTPURLResponse, data != nil, error == nil { - if r.statusCode == 200 { - contents = String(data: data!, encoding: .utf8) ?? "" - } - } - semaphore.signal() - } - task?.resume() - // wait 30 seconds before timing out - if semaphore.wait(timeout: .now() + 30) == .timedOut { - task?.cancel() - } - - // if made it to this point and contents still an empty string, something went wrong with the request - if contents.isEmpty { - logger.error("\(#function, privacy: .public) - failed at (4), contents empty, \(url, privacy: .public)") - return nil - } - logger.info("\(#function, privacy: .public) - complete for \(url, privacy: .public)") - return contents -} - -func updateAllFiles(_ optionalFilesArray: [[String: Any]] = []) -> Bool { - // get names of all files with updates available - guard - let filesWithUpdates = checkForRemoteUpdates(optionalFilesArray), - let saveLocation = getSaveLocation() - else { - logger.error("\(#function, privacy: .public) - failed to update files (1)") - return false - } - // security scope - let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() - defer { - if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} - } - for file in filesWithUpdates { - // can be force unwrapped because checkForRemoteUpdates didn't return nil - let filename = file["filename"]! - let fileUrl = saveLocation.appendingPathComponent(filename) - guard - let content = try? String(contentsOf: fileUrl, encoding: .utf8), - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]], - let updateUrl = metadata["updateURL"]?[0] - else { - logger.error("\(#function, privacy: .public) - failed to update files (2)") - continue - } - let downloadUrl = metadata["downloadURL"] != nil ? metadata["downloadURL"]![0] : updateUrl - guard - let remoteFileContents = getRemoteFileContents(downloadUrl), - ((try? remoteFileContents.write(to: fileUrl, atomically: false, encoding: .utf8)) != nil) - else { - logger.error("\(#function, privacy: .public) - failed to update files (3)") - continue - } - logger.info("\(#function, privacy: .public) - updated \(filename, privacy: .public) with contents fetched from \(downloadUrl, privacy: .public)") - } - return true -} - -func toggleFile(_ filename: String,_ action: String) -> Bool { - // if file doesn't exist return false - guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return false - } - let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() - defer { - if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} - } - let path = saveLocation.appendingPathComponent(filename).path - if !FileManager.default.fileExists(atPath: path) { - logger.error("\(#function, privacy: .public) - failed at (2)") - return false - } - var manifest = getManifest() - // if file is already disabled - if action == "disable" && manifest.disabled.contains(filename) || action == "enabled" && !manifest.disabled.contains(filename) { - return true - } - // add filename to disabled array if disabling - if (action == "disable") {manifest.disabled.append(filename)} - // remove filename from disabled array if enabling - if (action == "enable") { - guard let index = manifest.disabled.firstIndex(of: filename) else { - logger.error("\(#function, privacy: .public) - failed at (3)") - return false - } - manifest.disabled.remove(at: index) - } - if !updateManifest(with: manifest) { - logger.error("\(#function, privacy: .public) - failed at (4)") - return false - } - return true -} - -func checkDefaultDirectories() -> Bool { - let defaultSaveLocation = getDocumentsDirectory().appendingPathComponent("scripts") - let requireLocation = getRequireLocation() - let urls = [defaultSaveLocation, requireLocation] - for url in urls { - if !FileManager.default.fileExists(atPath: url.path) { - do { - try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false) - } catch { - // could not create the save location directory, show error - logger.error("\(#function, privacy: .public) - failed at (1) - \(url, privacy: .public) - \(error.localizedDescription, privacy: .public)") - return false - } - } - } - return true -} - -// matching -func getUrlProps(_ url: String) -> [String: String]? { - guard - let parts = URLComponents(string: url), - let ptcl = parts.scheme, - let host = parts.host - else { - logger.error("\(#function, privacy: .public) - failed to parse url") - return nil - } - var search = "" - if let query = parts.query { - search = "?" + query - } - return [ - "protocol": "\(ptcl):", - "host": host, - "pathname": parts.path, - "search": search, - "href": url - ] -} - -func stringToRegex(_ stringPattern: String) -> NSRegularExpression? { - let pattern = #"[\.|\?|\^|\$|\+|\{|\}|\[|\]|\||\\(|\)|\/]"# - var patternReplace = "^\(stringPattern.replacingOccurrences(of: pattern, with: #"\\$0"#, options: .regularExpression))$" - patternReplace = patternReplace.replacingOccurrences(of: "*", with: ".*") - guard let regex = try? NSRegularExpression(pattern: patternReplace, options: .caseInsensitive) else { - return nil - } - return regex -} - -func match(_ url: String, _ matchPattern: String) -> Bool { - guard - let parts = getUrlProps(url), - let ptcl = parts["protocol"], - let host = parts["host"], - var path = parts["pathname"] - else { - logger.error("\(#function, privacy: .public) - invalid url \(url, privacy: .public)") - return false - } - - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#path - // The value for the path matches against the string which is the URL path plus the URL query string - if let search = parts["search"], search.count > 0 { - path += search - } - - // matchPattern is the value from metatdata key @match or @exclude-match - if (matchPattern == "") { - return true - } - // currently only http/s supported - if (ptcl != "http:" && ptcl != "https:") { - return false - } - let partsPattern = #"^(http:|https:|\*:)\/\/((?:\*\.)?(?:[a-z0-9-]+\.)+(?:[a-z0-9]+)|\*\.[a-z]+|\*|[a-z0-9]+)(\/[^\s]*)$"# - let partsPatternReg = try! NSRegularExpression(pattern: partsPattern, options: .caseInsensitive) - let range = NSMakeRange(0, matchPattern.utf16.count) - guard let parts = partsPatternReg.firstMatch(in: matchPattern, options: [], range: range) else { - logger.error("\(#function, privacy: .public) - malformed regex match pattern") - return false - } - // ensure url protocol matches pattern protocol - let protocolPattern = matchPattern[Range(parts.range(at: 1), in: matchPattern)!] - if (protocolPattern != "*:" && ptcl != protocolPattern) { - return false - } - // construct host regex from matchPattern - let matchPatternHost = matchPattern[Range(parts.range(at: 2), in: matchPattern)!] - var hostPattern = "^\(matchPatternHost.replacingOccurrences(of: ".", with: "\\."))$" - hostPattern = hostPattern.replacingOccurrences(of: "^*$", with: ".*") - hostPattern = hostPattern.replacingOccurrences(of: "*\\.", with: "(.*\\.)?") - guard let hostRegEx = try? NSRegularExpression(pattern: hostPattern, options: .caseInsensitive) else { - logger.error("\(#function, privacy: .public) - invalid host regex") - return false - } - // construct path regex from matchPattern - let matchPatternPath = matchPattern[Range(parts.range(at: 3), in: matchPattern)!] - guard let pathRegEx = stringToRegex(String(matchPatternPath)) else { - logger.error("\(#function, privacy: .public) - invalid path regex") - return false - } - guard - (hostRegEx.firstMatch(in: host, options: [], range: NSMakeRange(0, host.utf16.count)) != nil), - (pathRegEx.firstMatch(in: path, options: [], range: NSMakeRange(0, path.utf16.count)) != nil) - else { - return false - } - - return true -} - -func include(_ url: String,_ pattern: String) -> Bool { - var regex:NSRegularExpression - if pattern.hasPrefix("/") && pattern.hasSuffix("/") { - let p = String(pattern.dropFirst().dropLast()) - guard let exp = try? NSRegularExpression(pattern: p, options: .caseInsensitive) else { - logger.error("\(#function, privacy: .public) - invalid regex") - return false - } - regex = exp - } else { - guard let exp = stringToRegex(pattern) else { - logger.error("\(#function, privacy: .public) - coudn't convert string to regex") - return false - } - regex = exp - } - if (regex.firstMatch(in: url, options: [], range: NSMakeRange(0, url.utf16.count)) == nil) { - return false - } - return true -} - -func getMatchedFiles(_ url: String, _ optionalManifest: Manifest?, _ checkBlocklist: Bool) -> [String] { - logger.info("\(#function, privacy: .public) - Getting matched files for \(url, privacy: .private(mask: .hash))") - // logger.debug("\(#function, privacy: .public) - Getting matched files for \(url, privacy: .public)") - let manifest = optionalManifest ?? getManifest() - - // filenames that should not load for the passed url - // the manifest values from @exclude and @exclude-match populate this set - var excludedFilenames: Set = [] - // filenames that should load for the passed url - // the manifest values from @include and @match populate this set - var matchedFilenames: Set = [] - // all exclude-match patterns from manifest - let excludeMatchPatterns = manifest.excludeMatch.keys - // all match patterns from manifest - let matchPatterns = manifest.match.keys - // all include expressions from manifest - let includeExpressions = manifest.include.keys - // all exclude expressions from manifest - let excludeExpressions = manifest.exclude.keys - - // if url matches a pattern in blocklist, no injection for this url - if (checkBlocklist) { - for pattern in manifest.blacklist { - if match(url, pattern) { - // return empty array - return Array(matchedFilenames) - } - } - } - - // loop through all the @exclude-match patterns - // if any match passed url, push all filenames to excludedFilenames set - for pattern in excludeMatchPatterns { - if match(url, pattern) { - guard let filenames = manifest.excludeMatch[pattern] else { - logger.error("\(#function, privacy: .public) - failed at (2) for \(pattern, privacy: .public)") - continue - } - excludedFilenames = excludedFilenames.union(filenames) - } - } - for exp in excludeExpressions { - if include(url, exp) { - guard let filenames = manifest.exclude[exp] else { - logger.error("\(#function, privacy: .public) - failed at (3) for \(exp, privacy: .public)") - continue - } - excludedFilenames = excludedFilenames.union(filenames) - } - } - for pattern in matchPatterns { - if match(url, pattern) { - guard let filenames = manifest.match[pattern] else { - logger.error("\(#function, privacy: .public) - failed at (4) for \(pattern, privacy: .public)") - continue - } - matchedFilenames = matchedFilenames.union(filenames) - } - } - for exp in includeExpressions { - if include(url, exp) { - guard let filenames = manifest.include[exp] else { - logger.error("\(#function, privacy: .public) - failed at (5) for \(exp, privacy: .public)") - continue - } - matchedFilenames = matchedFilenames.union(filenames) - } - } - matchedFilenames = matchedFilenames.subtracting(excludedFilenames) - logger.info("\(#function, privacy: .public) - Got \(matchedFilenames.count) matched files for \(url, privacy: .private(mask: .hash))") - // logger.debug("\(#function, privacy: .public) - Got \(matchedFilenames.count) matched files for \(url, privacy: .public)") - return Array(matchedFilenames) -} - -// injection -func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { - var cssFiles = [Any]() - var jsFiles = [Any]() - var menuFiles = [Any]() - - guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - - for filename in filenames { - guard - let contents = getFileContentsParsed(saveLocation.appendingPathComponent(filename)), - var code = contents["code"] as? String, - let type = filename.split(separator: ".").last - else { - // if guard fails, log error continue to next file - logger.error("\(#function, privacy: .public) - failed at (2) for \(filename, privacy: .public)") - continue - } - // can force unwrap b/c getFileContentsParsed ensures metadata exists - let metadata = contents["metadata"] as! [String: [String]] - let name = metadata["name"]![0] - - // if metadata has noframes option and the url is not the top window, don't load - if (metadata["noframes"] != nil && !isTop) { - continue - } - - // get run-at values and set default if missing - // if type request, ignore - var runAt = metadata["run-at"]?[0] ?? "document-end" - if runAt == "request" { - continue - } - - // normalize weight - var weight = metadata["weight"]?[0] ?? "1" - weight = normalizeWeight(weight) - - // get inject-into and set default if missing - var injectInto = metadata["inject-into"]?[0] ?? "auto" - let injectVals: Set = ["auto", "content", "page"] - let runAtVals: Set = ["context-menu", "document-start", "document-end", "document-idle"] - let validGrants: Set = [ - "GM.info", - "GM_info", - "GM.addStyle", - "GM.openInTab", - "GM.closeTab", - "GM.setValue", - "GM.getValue", - "GM.deleteValue", - "GM.listValues", - "GM.setClipboard", - "GM.getTab", - "GM.saveTab", - "GM_xmlhttpRequest", - "GM.xmlHttpRequest" - ] - // if either is invalid use default value - if !injectVals.contains(injectInto) { - injectInto = "auto" - } - if !runAtVals.contains(runAt) { - runAt = "document-end" - } - - // attempt to get all @grant value - var grants = metadata["grant"] ?? [] - // remove duplicates, if any exist - if !grants.isEmpty { - grants = Array(Set(grants)) - } - - // filter out grant values that are not in validGrant set - grants = grants.filter{validGrants.contains($0)} - - // set GM.info data - let description = metadata["description"]?[0] ?? "" - let excludes = metadata["exclude"] ?? [] - let excludeMatches = metadata["exclude-match"] ?? [] - let icon = metadata["icon"]?[0] ?? "" - let includes = metadata["include"] ?? [] - let matches = metadata["match"] ?? [] - let requires = metadata["require"] ?? [] - let version = metadata["version"]?[0] ?? "" - let noframes = metadata["noframes"] != nil ? true : false - var scriptObject:[String: Any] = [ - "description": description, - "excludes": excludes, - "exclude-match": excludeMatches, - "filename": filename, - "grant": grants, - "icon": icon, - "includes": includes, - "inject-into": injectInto, - "matches": matches, - "name": name, - "noframes": noframes, - "namespace": "", - "resources": "", - "require": requires, - "run-at": runAt, - "version": version - ] - // certain metadata keys use a different key name then the actual key name - // for compatibility keeping this when applicable, although the rationale is not clear to me - // for unique keys passed to scriptObject, using the same key name that is present in actual userscript - // this key map is used to check for existence of keys in next loop - let keyMap = [ - "exclude": "excludes", - "include": "includes", - "match": "matches", - "resource": "resources", - ] - for metaline in metadata { - let key = keyMap[metaline.key] ?? metaline.key - if !scriptObject.keys.contains(key) { - let value = metaline.value - // metalines without values aren't included in parsed metadata object - // the only exception is @noframes - scriptObject[key] = !value.isEmpty ? value : value[0] - } - } - let scriptMetaStr = contents["metablock"] as? String ?? "??" - - // attempt to get require resource from disk - // if required resource is inaccessible, log error and continue - if let required = metadata["require"] { - // reverse required metadata - // if required is ["A", "B", "C"], C gets added above B which is above A, etc.. - // the reverse of that is desired - for require in required.reversed() { - let sanitizedName = sanitize(require) - let requiredFileURL = getRequireLocation().appendingPathComponent(filename).appendingPathComponent(sanitizedName) - if let requiredContent = try? String(contentsOf: requiredFileURL, encoding: .utf8) { - code = "\(requiredContent)\n\(code)" - } else { - logger.error("\(#function, privacy: .public) - failed at (3) for \(require, privacy: .public)") - } - } - } - - if type == "css" { - cssFiles.append([ - "code": code, - "filename": filename, - "name": name, - "type": "css", - "weight": weight - ]) - } else if type == "js" { - if runAt == "context-menu" { - #if os(macOS) - menuFiles.append([ - "code": code, - "scriptMetaStr": scriptMetaStr, - "scriptObject": scriptObject, - "type": "js", - "weight": weight - ]) - #endif - } else { - jsFiles.append([ - "code": code, - "scriptMetaStr": scriptMetaStr, - "scriptObject": scriptObject, - "type": "js", - "weight": weight - ]) - } - } - } - let resp = [ - "files": ["css": cssFiles, "js": jsFiles, "menu": menuFiles], - "scriptHandler": "Userscripts", - "scriptHandlerVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" - ] as [String : Any] - return resp -} - -func getFileContentsParsed(_ url: URL) -> [String: Any]? { - guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - // security scope - let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() - defer { - if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} - } - // check that url is a valid path to a directory or single file - guard - FileManager.default.fileExists(atPath: url.path), - let content = try? String(contentsOf: url, encoding: .utf8), - let parsed = parse(content) - else { - return nil - } - return parsed -} - -func getInjectionFilenames(_ url: String) -> [String]? { - var filenames = [String]() - let manifest = getManifest() - let matched = getMatchedFiles(url, manifest, true) - guard let active = manifest.settings["active"] else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - // if injection is disabled return empty array - if active != "true" { - return filenames - } - // filter out all disabled files - filenames = matched.filter{!manifest.disabled.contains($0)} - return filenames -} - -func getRequestScripts() -> [[String: String]]? { - var requestScripts = [[String: String]]() - // check the manifest to see if injection is enabled - let manifest = getManifest() - guard let active = manifest.settings["active"] else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - // if not enabled, do not apply any net requests, ie. return empty array - if active != "true" { - return requestScripts - } - guard let files = getAllFiles(includeCode: true) else { - logger.error("\(#function, privacy: .public) - failed at (2)") - return nil - } - for file in files { - let isRequest = file["request"] as? Bool ?? false - // skip any non-request userscripts - if !isRequest { - continue - } - // can be force unwrapped because getAllFiles always returns these - let name = file["name"] as! String - let code = file["code"] as! String - let filename = file["filename"] as! String - - if !manifest.disabled.contains(filename) { - requestScripts.append(["name": name, "code": code]) - } - } - return requestScripts -} - -func getContextMenuScripts() -> [String: Any]? { - var menuFilenames = [String]() - // check the manifest to see if injection is enabled - let manifest = getManifest() - guard let active = manifest.settings["active"] else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - // if not enabled return empty array - if active != "true" { - return ["files": ["menu": []]] - } - // get all files at save location - guard let files = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (2)") - return nil - } - // loop through files and find @run-at context-menu script filenames - for file in files { - guard let filename = file["filename"] as? String else { - logger.error("\(#function, privacy: .public) - failed at (3), couldn't get filename") - continue - } - guard let fileMetadata = file["metadata"] as? [String: [String]] else { - logger.error("\(#function, privacy: .public) - failed at (4), couldn't get metadata for \(filename, privacy: .public)") - continue - } - let runAt = fileMetadata["run-at"]?[0] ?? "document-end" - if runAt != "context-menu" || manifest.disabled.contains(filename) { - continue - } - menuFilenames.append(filename) - } - // get and return script objects for all context-menu scripts - guard let scripts = getCode(menuFilenames, true) else { - logger.error("\(#function, privacy: .public) - failed at (5)") - return nil - } - return scripts -} - -// popup -func getPopupMatches(_ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { - var matches = [[String: Any]]() - // if the url doesn't start with http/s return empty array - if !url.starts(with: "http://") && !url.starts(with: "https://") { - return matches - } - // get all the files saved to manifest that match the passed url - let matched = getMatchedFiles(url, nil, false) - // get all the files at the save location - guard - let files = getAllFiles() - else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - // filter out the files that are present in both files and matched - // force unwrap filename to string since getAllFiles always returns it - matches = files.filter{matched.contains($0["filename"] as! String)} - - // get the subframe url matches - var frameUrlsMatched = [[String: Any]]() - var frameUrlsMatches = [String]() - // filter out the top page url from the frame urls - let frameUrls = subframeUrls.filter{$0 != url} - // for each url just pushed to frameUrls, get all the files saved to manifest that match their url - for frameUrl in frameUrls { - let frameMatches = getMatchedFiles(frameUrl, nil, false) - for frameMatch in frameMatches { - // for the match against the frameUrl, see if it has @noframes - // if so, it should not be appended to frameUrlsMatches - // filter all files for the first one that matches the frameMatch filename - // can force unwrap filename b/c getAllFiles always returns it - let frameMatchMetadata = files.filter{$0["filename"] as! String == frameMatch}.first - // can force unwrap noframes b/c getAllFiles always returns it - let noFrames = frameMatchMetadata?["noframes"] as? Bool ?? false - if !matched.contains(frameMatch) && !noFrames { - frameUrlsMatches.append(frameMatch) - } - } - } - - // filter out the files that are present in both files and frameUrlsMatches - // force unwrap filename to string since getAllFiles always returns it - frameUrlsMatched = files.filter{frameUrlsMatches.contains($0["filename"] as! String)} - // loop through frameUrlsMatched and add subframe key/val - for (index, var frameUrlsMatch) in frameUrlsMatched.enumerated() { - frameUrlsMatch["subframe"] = true - frameUrlsMatched[index] = frameUrlsMatch - } - // add frameUrlsMatched to matches array - matches.append(contentsOf: frameUrlsMatched) - return matches -} - -func popupUpdateAll() -> Bool { - guard - let files = getAllFiles(), - updateAllFiles(files), - updateManifestMatches(files), - updateManifestRequired(files), - purgeManifest(files) - else { - return false - } - return true -} - -func getPopupBadgeCount(_ url: String, _ subframeUrls: [String]) -> Int? { - if !url.starts(with: "http://") && !url.starts(with: "https://") { - return 0 - } - let manifest = getManifest() - guard - var matches = getPopupMatches(url, subframeUrls) - else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - for pattern in manifest.blacklist { - if match(url, pattern) { - return 0 - } - } - matches = matches.filter{!manifest.disabled.contains($0["filename"] as! String)} - return matches.count -} - -func popupUpdateSingle(_ filename: String, _ url: String, _ subframeUrls: [String]) -> [[String: Any]]? { - guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - // security scope - let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() - defer { - if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} - } - let fileUrl = saveLocation.appendingPathComponent(filename) - guard - let content = try? String(contentsOf: fileUrl, encoding: .utf8), - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]], - let updateUrl = metadata["updateURL"]?[0] - else { - logger.error("\(#function, privacy: .public) - failed at (2)") - return nil - } - let downloadUrl = metadata["downloadURL"] != nil ? metadata["downloadURL"]![0] : updateUrl - guard - let remoteFileContents = getRemoteFileContents(downloadUrl), - ((try? remoteFileContents.write(to: fileUrl, atomically: false, encoding: .utf8)) != nil) - else { - logger.error("\(#function, privacy: .public) - failed at (3)") - return nil - } - guard - let files = getAllFiles(), - updateManifestMatches(files), - updateManifestRequired(files), - purgeManifest(files), - let matches = getPopupMatches(url, subframeUrls) - else { - logger.error("\(#function, privacy: .public) - failed at (4)") - return nil - } - return matches -} - -// page -func getInitData() -> [String: Any]? { - guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return nil - } - return [ - "saveLocation": saveLocation.path, - "version": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??", - "build": Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" - ] -} - -func getLegacyData() -> [String: Any]? { - let manifest = getManifest() - var data:[String: Any] = manifest.settings - data["blacklist"] = manifest.blacklist - return data -} - -func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { - var response = [String: Any]() - let newContent = content - guard let saveLocation = getSaveLocation() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return ["error": "failed to get save location when attempting to save"] - } - guard - let oldFilename = item["filename"] as? String, - let type = item["type"] as? String - else { - return ["error": "invalid argument in save function"] - } - guard - let parsed = parse(newContent), - let metadata = parsed["metadata"] as? [String: [String]] - else { - return ["error": "failed to parse metadata"] - } - guard let n = metadata["name"]?[0] else { - return ["error": "@name not found in metadata"] - } - var name = sanitize(n) - - // construct new file name - let newFilename = "\(name).user.\(type)" - - // security scope - let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() - defer { - if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} - } - guard - let allFilesUrls = try? FileManager.default.contentsOfDirectory(at: saveLocation, includingPropertiesForKeys: []) - else { - return ["error": "failed to read save urls in save function"] - } - - // validate file before save - var allFilenames:[String] = [] // stores the indv filenames for later comparison - // old and new filenames are equal, overwriting and can skip - if oldFilename.lowercased() != newFilename.lowercased() { - // loop through all the file urls in the save location and save filename to var - for fileUrl in allFilesUrls { - // skip file if it is not of the proper type - let filename = fileUrl.lastPathComponent - if (!filename.hasSuffix(type)) { - continue - } - // if file is of the proper type, add it to the allFilenames array - allFilenames.append(filename.lowercased()) - } - } - - if allFilenames.contains(newFilename.lowercased()) { - return ["error": "filename already taken"] - } - - if newFilename.count > 250 { - return ["error": "filename too long"] - } - - // file passed validation - - // attempt to save to disk - let newFileUrl = saveLocation.appendingPathComponent(newFilename) - do { - try newContent.write(to: newFileUrl, atomically: false, encoding: .utf8) - } catch { - logger.error("\(#function, privacy: .public) - failed at (2)") - return ["error": "failed to write file to disk"] - } - - // saved to disk successfully - - // get the file last modified date - guard - let dateMod = try? FileManager.default.attributesOfItem(atPath: newFileUrl.path)[.modificationDate] as? Date - else { - logger.error("\(#function, privacy: .public) - failed at (3)") - return ["error": "failed to read modified date in save function"] - } - - // remove old file and manifest records for old file if they exist - if oldFilename.lowercased() != newFilename.lowercased() { - // if user changed the filename, remove file with old filename - let oldFileUrl = saveLocation.appendingPathComponent(oldFilename) - // however, when creating a new file, if user changes the temp given name by app... - // oldFilename (the temp name in activeItem) and newFilename (@name in file contents) will differ - // the file with oldFilename will not be on the filesystem and can not be deleted - // for that edge case, using try? rather than try(!) to allow failures - try? FileManager.default.trashItem(at: oldFileUrl, resultingItemURL: nil) - } - - // update manifest for new file and purge anything from old file - guard - let allFiles = getAllFiles(), - updateManifestMatches(allFiles), - updateManifestRequired(allFiles), - updateManifestDeclarativeNetRequests(allFiles), - purgeManifest(allFiles) - else { - logger.error("\(#function, privacy: .public) - failed at (4)") - return ["error": "file save but manifest couldn't be updated"] - } - - // un-santized name - name = unsanitize(name) - - // build response dict - response["canUpdate"] = false - response["content"] = newContent - response["filename"] = newFilename - response["lastModified"] = dateToMilliseconds(dateMod) - response["name"] = name - if metadata["description"] != nil { - response["description"] = metadata["description"]![0] - } - if metadata["version"] != nil && metadata["updateURL"] != nil { - response["canUpdate"] = true - } - // if a request "type" userscript add key/val - let runAt = metadata["run-at"]?[0] ?? "document-end" - if runAt == "request" { - response["request"] = true - } - - return response -} - -func trashFile(_ item: [String: Any]) -> Bool { - guard - let saveLocation = getSaveLocation(), - let filename = item["filename"] as? String - else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return false - } - // security scope - let didStartAccessing = saveLocation.startAccessingSecurityScopedResource() - defer { - if didStartAccessing {saveLocation.stopAccessingSecurityScopedResource()} - } - let url = saveLocation.appendingPathComponent(filename) - // if file is already removed from path, assume it was removed by user and return true - if (FileManager.default.fileExists(atPath: url.path)) { - do { - try FileManager.default.trashItem(at: url, resultingItemURL: nil) - } catch { - logger.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") - return false - } - } - // update manifest - guard updateManifestMatches(), updateManifestRequired(), purgeManifest() else { - logger.error("\(#function, privacy: .public) - failed at (2)") - return false - } - return true; -} - -func getFileRemoteUpdate(_ content: String) -> [String: String] { - guard - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]] - else { - // can't parse editor contents - return ["error": "Update failed, metadata missing"] - } - // editor contents missing version value - guard let version = metadata["version"]?[0] else { - return ["error": "Update failed, version value required"] - } - // editor contents missing updateURL - guard let updateURL = metadata["updateURL"]?[0] else { - return ["error": "Update failed, update url required"] - } - // set download url - let downloadURL = (metadata["downloadURL"] != nil) ? metadata["downloadURL"]![0] : updateURL - // basic url validation - guard validateUrl(updateURL) else { - return ["error": "Update failed, invalid updateURL"] - } - guard validateUrl(downloadURL) else { - return ["error": "Update failed, invalid downloadURL"] - } - // get the remote file contents for checking version - guard var remoteContent = getRemoteFileContents(updateURL) else { - return ["error": "Update failed, updateURL unreachable"] - } - // parse remote file contents - guard - let remoteParsed = parse(remoteContent), - let remoteMetadata = remoteParsed["metadata"] as? [String: [String]], - let remoteVersion = remoteMetadata["version"]?[0] - else { - // can't parse editor contents - return ["error": "Update failed, couldn't parse remote file contents"] - } - // check if update is needed - if version >= remoteVersion { - return ["info": "No updates found"] - } - // at this point it is known an update is available, get new code from downloadURL - // is there's a specific downloadURL overwrite remoteContents with code from downloadURL - if updateURL != downloadURL { - guard let remoteDownloadContent = getRemoteFileContents(downloadURL) else { - return ["error": "Update failed, downloadURL unreachable"] - } - remoteContent = remoteDownloadContent - } - return ["content": remoteContent] -} - -// background -func nativeChecks() -> [String: String] { - logger.info("\(#function, privacy: .public) - started") - #if os(iOS) - // check the save location is set - guard (getSaveLocation() != nil) else { - logger.error("\(#function, privacy: .public) - save location unset (iOS)") - return [ - "error": "Native checks error (0)", - "saveLocation": "unset", - "scheme": Bundle.main.infoDictionary?["US_URL_SCHEME"] as! String - ] - } - #endif - // check the default directories - guard checkDefaultDirectories() else { - logger.error("\(#function, privacy: .public) - checkDefaultDirectories failed") - return ["error": "Native checks error (1)"] - } - // check the settings - guard checkSettings() else { - logger.error("\(#function, privacy: .public) - checkSettings failed") - return ["error": "Native checks error (2)"] - } - // get all files to pass as arguments to function below - guard let allFiles = getAllFiles() else { - logger.error("\(#function, privacy: .public) - getAllFiles failed") - return ["error": "Native checks error (3)"] - } - // purge the manifest of old records - guard purgeManifest(allFiles) else { - logger.error("\(#function, privacy: .public) - purgeManifest failed") - return ["error": "Native checks error (4)"] - } - // update matches in manifest - guard updateManifestMatches(allFiles) else { - logger.error("\(#function, privacy: .public) - updateManifestMatches failed") - return ["error": "Native checks error (5)"] - } - // update the required resources - guard updateManifestRequired(allFiles) else { - logger.error("\(#function, privacy: .public) - updateManifestRequired failed") - return ["error": "Native checks error (6)"] - } - // update declarativeNetRequest - guard updateManifestDeclarativeNetRequests(allFiles) else { - logger.error("\(#function, privacy: .public) - updateManifestDeclarativeNetRequests failed") - return ["error": "Native checks error (7)"] - } - // pass some info in response - logger.info("\(#function, privacy: .public) - complete") - return ["success": "Native checks complete"] -} - -// userscript install -func installCheck(_ content: String) -> [String: Any] { - // this func checks a userscript's metadata to determine if it's already installed - - guard let files = getAllFiles() else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return ["error": "installCheck failed at (1)"] - } - - guard - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]], - let newName = metadata["name"]?[0] - else { - return ["error": "userscript metadata is invalid"] - } - - // loop through all files nad get their names and filenames - // we will check the new name/filename to see if this is a unique userscript - // or if it will overwrite an existing userscript - var names = [String]() - for file in files { - // can be force unwrapped because getAllFiles didn't return nil - let name = file["name"] as! String - - // populate array - names.append(name) - } - - var directive = "" - #if os(macOS) - directive = "Click" - #elseif os(iOS) - directive = "Tap" - #endif - - if names.contains(newName) { - return [ - "success": "\(directive) to re-install", - "metadata": metadata, - "installed": true - ] - } - - return [ - "success": "\(directive) to install", - "metadata": metadata, - "installed": false - ]; -} - -func installUserscript(_ content: String) -> [String: Any] { - guard - let parsed = parse(content), - let metadata = parsed["metadata"] as? [String: [String]], - let n = metadata["name"]?[0] - else { - logger.error("\(#function, privacy: .public) - failed at (1)") - return ["error": "installUserscript failed at (1)"] - } - let name = sanitize(n) - let filename = "\(name).user.js" - - let saved = saveFile(["filename": filename, "type": "js"], content) - return saved -} diff --git a/xcode/Safari-Extension/Resources/_locales/en/messages.json b/xcode/Safari-Extension/Resources/_locales/en/messages.json deleted file mode 100644 index a7cee631..00000000 --- a/xcode/Safari-Extension/Resources/_locales/en/messages.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extension_name": { - "message": "Userscripts", - "description": "The display name for the extension." - }, - "extension_description": { - "message": "Save and run javascript for the web pages you visit", - "description": "Description of what the extension does." - } -} diff --git a/xcode/Safari-Extension/Resources/background.js b/xcode/Safari-Extension/Resources/background.js deleted file mode 100644 index 5e0a8feb..00000000 --- a/xcode/Safari-Extension/Resources/background.js +++ /dev/null @@ -1,509 +0,0 @@ -// functions from "src/shared/utils.js"; -async function openExtensionPage() { - const url = browser.runtime.getURL("dist/entry-page.html"); - const tabs = await browser.tabs.query({url}); - const tab = tabs.find(e => e.url.startsWith(url)); - if (!tab) return browser.tabs.create({url}); - browser.tabs.update(tab.id, {active: true}); - browser.windows.update(tab.windowId, {focused: true}); -} - -// functions from "src/shared/settings.js"; - -// first sorts files by run-at value, then by weight value -function userscriptSort(a, b) { - // map the run-at values to numeric values - const runAtValues = { - "document-start": 1, - "document-end": 2, - "document-idle": 3 - }; - const runAtA = a.scriptObject["run-at"]; - const runAtB = b.scriptObject["run-at"]; - if (runAtA !== runAtB && runAtValues[runAtA] && runAtValues[runAtB]) { - return runAtValues[runAtA] > runAtValues[runAtB]; - } - return Number(a.scriptObject.weight) < Number(b.scriptObject.weight); -} - -async function readAsDataURL(blob) { - return new Promise(resolve => { - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => resolve(reader.result); // base64data - }); -} - -async function getPlatform() { - let platform = localStorage.getItem("platform"); - if (!platform) { - const message = {name: "REQ_PLATFORM"}; - const response = await browser.runtime.sendNativeMessage(message); - if (!response.platform) { - console.error("Failed to get platform"); - return ""; - } - platform = response.platform; - localStorage.setItem("platform", platform); - } - return platform; -} - -function setClipboard(data, type = "text/plain") { - // future enhancement? - // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write - // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText - const onCopy = e => { - e.stopImmediatePropagation(); - e.preventDefault(); - e.clipboardData.setData(type, data); - document.removeEventListener("copy", onCopy, true); - }; - - const textarea = document.createElement("textarea"); - textarea.textContent = ""; - document.body.appendChild(textarea); - textarea.select(); - document.addEventListener("copy", onCopy, true); - try { - return document.execCommand("copy"); - } catch (error) { - console.warn("setClipboard failed", error); - document.removeEventListener("copy", onCopy, true); - return false; - } finally { - document.body.removeChild(textarea); - } -} - -async function setBadgeCount() { - const clearBadge = () => browser.browserAction.setBadgeText({text: ""}); - // @todo until better introduce in ios, only set badge on macOS - const platform = await getPlatform(); - if (platform !== "macos") return clearBadge(); - // @todo after the background script is modularized, import and use: - // settingsStorage.get(["global_active","toolbar_badge_count","global_exclude_match"]) - const results = await browser.storage.local.get(["US_GLOBAL_ACTIVE", "US_TOOLBAR_BADGE_COUNT"]); - if (results?.US_GLOBAL_ACTIVE === false) return clearBadge(); - if (results?.US_TOOLBAR_BADGE_COUNT === false) return clearBadge(); - - const currentTab = await browser.tabs.getCurrent(); - // no active tabs exist (user closed all windows) - if (!currentTab) return clearBadge(); - const url = currentTab.url; - // if url doesn't exist, stop - if (!url) return clearBadge(); - // only check for http/s pages - if (!url.startsWith("http://") && !url.startsWith("https://")) return clearBadge(); - // @todo if url match in global exclude list, clear badge - const frameUrls = new Set(); - const frames = await browser.webNavigation.getAllFrames({tabId: currentTab.id}); - for (let i = 0; i < frames.length; i++) { - const frameUrl = frames[i].url; - if (frameUrl !== url && frameUrl.startsWith("http")) { - frameUrls.add(frameUrl); - } - } - const message = { - name: "POPUP_BADGE_COUNT", - url, - frameUrls: Array.from(frameUrls) - }; - browser.runtime.sendNativeMessage(message, response => { - if (response.error) return console.error(response.error); - const count = response.count; - if (count > 0) { - browser.browserAction.setBadgeText({text: count.toString()}); - } else { - const _url = new URL(url); - if (_url.pathname.endsWith(".user.js")) { - browser.browserAction.setBadgeText({text: "JS"}); - } else { - clearBadge(); - } - } - }); -} - -// on startup get declarativeNetRequests -// and set the requests for the session -// should also check and refresh when: -// 1. dnr item save event in the page occurs -// 2. dnr item toggle event in the page occurs -// 3. external editor changes script file content -async function setSessionRules() { - // not supported below safari 15.4 - if (!browser.declarativeNetRequest.updateSessionRules) return; - await clearAllSessionRules(); - const message = {name: "REQ_REQUESTS"}; - const response = await browser.runtime.sendNativeMessage(message); - if (response.error) { - console.error(response.error); - return; - } - // there are no rules to apply - if (!response.length) return; - // loop through response, parse the rules, push to array and log - const rules = []; - for (let i = 0; i < response.length; i++) { - const rule = response[i]; - const code = JSON.parse(rule.code); - // check if an array or single rule - if (Array.isArray(code)) { - code.forEach(r => rules.push(r)); - console.info(`Setting session rule: ${rule.name} (${code.length})`); - } else { - rules.push(code); - console.info(`Setting session rule: ${rule.name}`); - } - } - // generate unique ids for all rules to ensure no repeats - const ids = randomNumberSet(1000, rules.length); - rules.map((rule, index) => rule.id = ids[index]); - try { - await browser.declarativeNetRequest.updateSessionRules({addRules: rules}); - } catch (error) { - console.error(`Error setting session rules: ${error}`); - return; - } - console.info(`Finished setting ${rules.length} session rules`); -} - -async function clearAllSessionRules() { - const rules = await browser.declarativeNetRequest.getSessionRules(); - if (!rules.length) return; - console.info(`Clearing ${rules.length} session rules`); - const ruleIds = rules.map(a => a.id); - await browser.declarativeNetRequest.updateSessionRules({ - removeRuleIds: ruleIds - }); -} - -function randomNumberSet(max, count) { - // generates a set of random unique numbers - // returns an array - const numbers = new Set(); - while (numbers.size < count) { - numbers.add((Math.floor(Math.random() * (max - 1 + 1)) + 1)); - } - return [...numbers]; -} - -// the current update logic is similar to setSessionRules() -// this feature needs a more detailed redesign in the future -// https://github.com/quoid/userscripts/issues/453 -async function getContextMenuItems() { - // macos exclusive feature - const platform = await getPlatform(); - if (platform !== "macos") return; - // since it's not possible to get a list of currently active menu items - // on update, all context-menu items are cleared, then re-added - // this is done to ensure fresh code changes appear - await browser.menus.removeAll(); - // get the context-menu scripts - const message = {name: "REQ_CONTEXT_MENU_SCRIPTS"}; - const response = await browser.runtime.sendNativeMessage(message); - if (response.error) { - console.error(response.error); - return; - } - // add menus items - const items = response.files?.menu || []; - if (items.length) { - console.info(`Setting ${items.length} context-menu userscripts`); - } - for (let i = 0; i < items.length; i++) { - const item = items[i]; - // context-menu scripts require @match value - // @include values are ignored - if (!item.scriptObject.matches.length) continue; - addContextMenuItem(item); - } -} - -async function addContextMenuItem(userscript) { - // context-menu items persist for a session - // to avoid duplication, when created, save the filename to session storage - const savedItems = sessionStorage.getItem("menu"); - // if the session storage key doesn't exist use empty array - const activeItems = savedItems ? JSON.parse(savedItems) : []; - if (activeItems.indexOf(userscript.scriptObject.filename) !== -1) { - // if already saved, remove it, to get fresh code changes - await browser.menus.remove(userscript.scriptObject.filename); - } - // potential bug? https://developer.apple.com/forums/thread/685273 - // https://stackoverflow.com/q/68431201 - // parse through match values and change pathnames to deal with bug - const patterns = userscript.scriptObject.matches; - patterns.forEach((pattern, index) => { - try { - const url = new URL(pattern); - let pathname = url.pathname; - if (pathname.length > 1 && pathname.endsWith("/")) { - pathname = pathname.slice(0, -1); - } - patterns[index] = `${url.protocol}//${url.hostname}${pathname}`; - } catch (error) { - // prevent breaking when non-url pattern present - } - }); - - browser.menus.create({ - contexts: ["all"], - documentUrlPatterns: patterns, - id: userscript.scriptObject.filename, - title: userscript.scriptObject.name - }, () => { - // add event listener if needed - if (!browser.menus.onClicked.hasListener(contextClick)) { - browser.menus.onClicked.addListener(contextClick); - } - // save the context-menu item reference to sessionStorage - const value = JSON.stringify([userscript.scriptObject.filename]); - sessionStorage.setItem("menu", value); - }); -} - -function contextClick(info, tab) { - // when any created context-menu item is clicked, send message to tab - // the content script for that tag will have the context-menu code - // which will get send back in the response if/when found - const message = {name: "CONTEXT_RUN", menuItemId: info.menuItemId}; - browser.tabs.sendMessage(tab.id, message, response => { - // if code is returned, execute on that tab - if (!response.code) return; - browser.tabs.executeScript(tab.id, { - code: response.code - }); - }); -} - -async function nativeChecks() { - const response = await browser.runtime.sendNativeMessage({name: "NATIVE_CHECKS"}); - // note: use settings.js once background page modularization - if (response.error) { - browser.storage.local.set({US_ERROR_NATIVE: response}); - return false; - } - browser.storage.local.remove("US_ERROR_NATIVE"); - return true; -} - -// handles messages sent with browser.runtime.sendMessage -function handleMessage(request, sender, sendResponse) { - switch (request.name) { - case "REQ_USERSCRIPTS": { - // get the page url from the content script that sent request - const url = sender.url; - // use frameId to determine if request came from top level window - // if @noframes true, and isTop false, swift layer won't return code - const isTop = sender.frameId === 0; - // send request to swift layer to provide code for page url - const message = {name: "REQ_USERSCRIPTS", url, isTop}; - browser.runtime.sendNativeMessage(message, response => { - // if request failed, send error to content script for logging - if (response.error) return sendResponse(response); - // sort files - response.files.js.sort(userscriptSort); - response.files.css.sort((a, b) => { - return Number(a.weight) < Number(b.weight); - }); - // return sorted files for injection - sendResponse(response); - }); - return true; - } - case "API_CLOSE_TAB": { - const tabId = request.tabId || sender.tab.id; - browser.tabs.remove(tabId, () => sendResponse({success: 1})); - return true; - } - case "API_OPEN_TAB": { - const props = { - active: request.active, - index: sender.tab.index + 1, - url: request.url - }; - browser.tabs.create(props, response => sendResponse(response)); - return true; - } - case "API_ADD_STYLE": { - const tabId = sender.tab.id; - const details = {code: request.css, cssOrigin: "user"}; - browser.tabs.insertCSS(tabId, details, () => { - sendResponse(request.css); - }); - return true; - } - case "API_GET_TAB": { - let tab = null; - if (typeof sender.tab !== "undefined") { - const tabData = sessionStorage.getItem(`tab-${sender.tab.id}`); - try { - // if tabData is null, can still parse it and return that - tab = JSON.parse(tabData); - } catch (error) { - console.error("failed to parse tab data for getTab"); - } - } else { - console.error("unable to deliver tab due to empty tab id"); - } - sendResponse(tab == null ? {} : tab); - break; - } - case "API_SAVE_TAB": { - if (sender.tab != null && sender.tab.id) { - const key = `tab-${sender.tab.id}`; - sessionStorage.setItem(key, JSON.stringify(request.tab)); - sendResponse({success: true}); - } else { - console.error("unable to save tab, empty tab id"); - sendResponse({success: false}); - } - break; - } - case "API_SET_CLIPBOARD": { - const result = setClipboard(request.clipboardData, request.type); - sendResponse(result); - break; - } - case "API_XHR": { - // parse details and set up for XMLHttpRequest - const details = request.details; - const method = details.method ? details.method : "GET"; - const user = details.user || null; - const password = details.password || null; - let body = details.data || null; - if (body != null && details.binary != null) { - const len = body.length; - const arr = new Uint8Array(len); - for (let i = 0; i < len; i++) { - arr[i] = body.charCodeAt(i); - } - body = new Blob([arr], {type: "text/plain"}); - } - // establish a long-lived port connection to content script - const port = browser.tabs.connect(sender.tab.id, { - name: request.xhrPortName - }); - // set up XMLHttpRequest - const xhr = new XMLHttpRequest(); - xhr.withCredentials = (details.user && details.password); - xhr.timeout = details.timeout || 0; - if (details.overrideMimeType) { - xhr.overrideMimeType(details.overrideMimeType); - } - // add required listeners and send result back to the content script - for (const e of request.events) { - if (!details[e]) continue; - xhr[e] = async event => { - // can not send xhr through postMessage - // construct new object to be sent as "response" - const x = { - readyState: xhr.readyState, - response: xhr.response, - responseHeaders: xhr.getAllResponseHeaders(), - responseType: xhr.responseType, - responseURL: xhr.responseURL, - status: xhr.status, - statusText: xhr.statusText, - timeout: xhr.timeout, - withCredentials: xhr.withCredentials - }; - // only include responseText when needed - if (["", "text"].indexOf(xhr.responseType) !== -1) { - x.responseText = xhr.responseText; - } - // need to convert arraybuffer data to postMessage - if (xhr.responseType === "arraybuffer") { - const arr = Array.from(new Uint8Array(xhr.response)); - x.response = arr; - } - // need to blob arraybuffer data to postMessage - if (xhr.responseType === "blob") { - const base64data = await readAsDataURL(xhr.response); - x.response = { - data: base64data, - type: xhr.responseType - }; - } - port.postMessage({name: e, event, response: x}); - }; - } - xhr.open(method, details.url, true, user, password); - xhr.responseType = details.responseType || ""; - if (details.headers) { - for (const key in details.headers) { - if (!key.startsWith("Proxy-") && !key.startsWith("Sec-")) { - const val = details.headers[key]; - xhr.setRequestHeader(key, val); - } - } - } - // receive messages from content script and process them - port.onMessage.addListener(msg => { - if (msg.name === "ABORT") xhr.abort(); - if (msg.name === "DISCONNECT") port.disconnect(); - }); - // handle port disconnect and clean tasks - port.onDisconnect.addListener(p => { - if (p?.error) { - console.error(`port disconnected due to an error: ${p.error.message}`); - } - }); - xhr.send(body); - // if onloadend not set in xhr details - // onloadend event won't be passed to content script - // if that happens port DISCONNECT message won't be posted - // if details lacks onloadend attach listener - if (!details.onloadend) { - xhr.onloadend = event => { - port.postMessage({name: "onloadend", event}); - }; - } - break; - } - case "REFRESH_SESSION_RULES": { - setSessionRules(); - break; - } - case "REFRESH_CONTEXT_MENU_SCRIPTS": { - getContextMenuItems(); - break; - } - } -} -browser.runtime.onInstalled.addListener(async () => { - nativeChecks(); -}); -browser.runtime.onStartup.addListener(async () => { - setSessionRules(); - getContextMenuItems(); -}); -// listens for messages from content script, popup and page -browser.runtime.onMessage.addListener(handleMessage); -// set the badge count -browser.tabs.onActivated.addListener(setBadgeCount); -browser.windows.onFocusChanged.addListener(async windowId => { - if (windowId < 1) { // lose focus - return; - } - nativeChecks(); - setBadgeCount(); - setSessionRules(); - getContextMenuItems(); -}); -browser.webNavigation.onCompleted.addListener(setBadgeCount); - -// handle native app messages -const port = browser.runtime.connectNative(); -port.onMessage.addListener(message => { - // console.info(message); // DEBUG - if (message.name === "SAVE_LOCATION_CHANGED") { - openExtensionPage(); - if (message?.userInfo?.returnApp === true) browser.runtime.sendNativeMessage({name: "OPEN_APP"}); - } - // if (message.name === "OPEN_EXTENSION_PAGE") { - // openExtensionPage(); - // } -}); diff --git a/xcode/Safari-Extension/Resources/content.js b/xcode/Safari-Extension/Resources/content.js deleted file mode 100644 index 2060a321..00000000 --- a/xcode/Safari-Extension/Resources/content.js +++ /dev/null @@ -1,469 +0,0 @@ -// code received from background page will be stored in this variable -// code referenced again when strict CSPs block initial injection attempt -let data; -// determines whether strict csp injection has already run (JS only) -let cspFallbackAttempted = false; -// save reference to window's browser object -const browser = window.browser; -// GM APIs -const apis = { - closeTab(tabId) { - return new Promise(resolve => { - const message = { - name: "API_CLOSE_TAB", - tabId - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); - }, - openInTab(url, openInBackground = false) { - if (!url) return console.error("openInTab missing url arg"); - return new Promise(resolve => { - const message = { - name: "API_OPEN_TAB", - url, - active: !openInBackground - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); - }, - setValue(key, value) { - if (typeof key !== "string" || !key.length) { - return console.error("setValue invalid key arg"); - } - if (value == null) { - return console.error("setValue invalid value arg"); - } - return new Promise(resolve => { - const item = {}; - item[`${this.US_filename}---${key}`] = value; - browser.storage.local.set(item, () => resolve({success: 1})); - }); - }, - getValue(key, defaultValue) { - if (typeof key !== "string" || !key.length) { - return console.error("getValue invalid key arg"); - } - const prefixedKey = `${this.US_filename}---${key}`; - return new Promise(resolve => { - browser.storage.local.get(prefixedKey, item => { - if (Object.keys(item).length === 0) { - if (defaultValue != null) { - resolve(defaultValue); - } else { - resolve(undefined); - } - } else { - resolve(Object.values(item)[0]); - } - }); - }); - }, - deleteValue(key) { - if (typeof key !== "string" || !key.length) { - return console.error("deleteValue missing key arg"); - } - return new Promise(resolve => { - const prefixedKey = `${this.US_filename}---${key}`; - browser.storage.local.remove(prefixedKey, () => { - resolve({success: 1}); - }); - }); - }, - listValues() { - return new Promise(resolve => { - const prefix = `${this.US_filename}---`; - const keys = []; - browser.storage.local.get().then(items => { - for (const key in items) { - if (key.startsWith(prefix)) { - const k = key.replace(prefix, ""); - keys.push(k); - } - } - resolve(keys); - }); - }); - }, - addStyle(css) { - if (typeof css !== "string") { - return console.error("addStyle invalid css arg"); - } - return new Promise(resolve => { - const message = { - name: "API_ADD_STYLE", - css - }; - browser.runtime.sendMessage(message, response => resolve(response)); - }); - }, - getTab() { - return new Promise(resolve => { - const message = {name: "API_GET_TAB"}; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); - }, - saveTab(tab) { - if (tab == null) return console.error("saveTab invalid arg"); - return new Promise(resolve => { - const message = { - name: "API_SAVE_TAB", - tab - }; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); - }, - setClipboard(clipboardData, type) { - return new Promise(resolve => { - const message = { - name: "API_SET_CLIPBOARD", - clipboardData, - type - }; - browser.runtime.sendMessage(message, response => { - resolve(response); - }); - }); - }, - xhr(details) { - if (details == null) return console.error("xhr invalid details arg"); - if (!details.url) return console.error("xhr details missing url key"); - // generate random port name for single xhr - const xhrPortName = Math.random().toString(36).substring(1, 9); - // strip out functions from details - const detailsParsed = JSON.parse(JSON.stringify(details)); - // get all the "on" events from XMLHttpRequest object - const events = []; - for (const k in XMLHttpRequest.prototype) { - if (k.slice(0, 2) === "on") events.push(k); - } - // check which functions are included in the original details object - // add a bool to indicate if event listeners should be attached - for (const e of events) { - if (typeof details[e] === "function") detailsParsed[e] = true; - } - // define return method, will be populated after port is established - const response = { - abort: () => console.error("xhr has not yet been initialized") - }; - // port listener, most of the messaging logic goes here - const listener = port => { - if (port.name !== xhrPortName) return; - port.onMessage.addListener(async msg => { - if ( - events.includes(msg.name) - && typeof details[msg.name] === "function" - ) { - // process xhr response - const r = msg.response; - if (r.responseType === "arraybuffer") { - // arraybuffer responses had their data converted in background - // convert it back to arraybuffer - try { - const buffer = new Uint8Array(r.response).buffer; - r.response = buffer; - } catch (err) { - console.error("error parsing xhr arraybuffer", err); - } - } else if (r.responseType === "blob" && r.response.data) { - // blob responses had their data converted in background - // convert it back to blob - const resp = await fetch(r.response.data); - const b = await resp.blob(); - r.response = b; - } - // call userscript method - details[msg.name](msg.response); - } - // all messages received - // tell background it's safe to close port - if (msg.name === "onloadend") { - port.postMessage({name: "DISCONNECT"}); - } - }); - - // handle port disconnect and clean tasks - port.onDisconnect.addListener(p => { - if (p?.error) { - console.error(`port disconnected due to an error: ${p.error.message}`); - } - browser.runtime.onConnect.removeListener(listener); - }); - // fill the method returned to the user script - response.abort = () => port.postMessage({name: "ABORT"}); - }; - // wait for the background to establish a port connection - browser.runtime.onConnect.addListener(listener); - // pass the basic information to the background through a common message - const message = { - name: "API_XHR", - details: detailsParsed, - xhrPortName, - events - }; - browser.runtime.sendMessage(message); - return response; - }, - // include method names so they don't get skipped when adding to userscript - xmlHttpRequest: true, - GM_xmlhttpRequest: true -}; -// remote window's browser object -delete window.browser; - -// label used to distinguish frames in console -const label = randomLabel(); - -function randomLabel() { - const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", r = Math.random(); - return a[Math.floor(r * a.length)] + r.toString().slice(5, 6); -} - -function processJS(userscript) { - const runAt = userscript.scriptObject["run-at"]; - if (runAt === "document-start") { - injectJS(userscript); - } else if (runAt === "document-end") { - if (document.readyState !== "loading") { - injectJS(userscript); - } else { - document.addEventListener("DOMContentLoaded", () => { - injectJS(userscript); - }); - } - } else if (runAt === "document-idle") { - if (document.readyState === "complete") { - injectJS(userscript); - } else { - document.addEventListener("readystatechange", () => { - if (document.readyState === "complete") { - injectJS(userscript); - } - }); - } - } -} - -function wrapCode(preCode, code, filename) { - const tag = window.self === window.top ? "" : `(${label})`; - return ` - (function() { - ${preCode} - (function() { - const US_filename = "${filename}"; - const apis = undefined; - const browser = undefined; - // userscript code below - ${code} - //# sourceURL=${filename.replace(/\s/g, "-") + tag} - })(); - })(); - `; -} - -function injectJS(userscript) { - const filename = userscript.scriptObject.filename; - const code = wrapCode(userscript.preCode, userscript.code, filename); - const name = userscript.scriptObject.name; - let injectInto = userscript.scriptObject["inject-into"]; - // change scope to content since strict CSP event detected - if (injectInto === "auto" && (userscript.fallback || cspFallbackAttempted)) { - injectInto = "content"; - console.warn(`Attempting fallback injection for ${name}`); - } else if (window.self === window.top) { - console.info(`Injecting ${name} %c(js)`, "color: #fff600"); - } else { - console.info(`Injecting ${name} %c(js)%c - %cframe(${label})(${window.location})`, "color: #fff600", "color: inherit", "color: #006fff"); - } - if (injectInto !== "content") { - const tag = document.createElement("script"); - tag.textContent = code; - document.head.appendChild(tag); - } else { - try { - // eslint-disable-next-line no-new-func - return Function(code)(); - } catch (error) { - console.error(`${filename} error`, error); - } - } -} - -function injectCSS(name, code) { - if (window.self === window.top) { - console.info(`Injecting ${name} %c(css)`, "color: #60f36c"); - } else { - console.info(`Injecting ${name} %c(css)%c - %cframe(${label})(${window.location})`, "color: #60f36c", "color: inherit", "color: #006fff"); - } - // Safari lacks full support for tabs.insertCSS - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/insertCSS - // specifically frameId and cssOrigin - // if support for those details keys arrives, the method below can be used - // NOTE: manifest V3 does support frameId, but not origin - // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/insertCSS - - // write the css code to head of the document - const tag = document.createElement("style"); - tag.textContent = code; - document.head.appendChild(tag); -} - -function cspFallback(e) { - // if a security policy violation event has occurred - // and the directive is script-src or script-src-elem - // it's fair to assume that there is a strict CSP for javascript - // and that injection was blocked for all userscripts - // when any script-src violation is detected, re-attempt injection - if ( - e.effectiveDirective === "script-src" - || e.effectiveDirective === "script-src-elem" - ) { - // get all "auto" code - // since other code can trigger a security policy violation event - // make sure data var is not undefined before attempting fallback - if (!data || cspFallbackAttempted) return; - // update global that tracks security policy violations - cspFallbackAttempted = 1; - // for all userscripts with @inject-into: auto, attempt re-injection - for (let i = 0; i < data.files.js.length; i++) { - const userscript = data.files.js[i]; - if (userscript.scriptObject["inject-into"] !== "auto") continue; - userscript.fallback = 1; - processJS(userscript); - } - } -} - -function injection() { - browser.runtime.sendMessage({name: "REQ_USERSCRIPTS"}, response => { - // cancel injection if errors detected - if (!response || response.error) { - console.error(response?.error || "REQ_USERSCRIPTS returned undefined"); - return; - } - // save response locally in case CSP events occur - data = response; - // combine regular and context-menu scripts - const scripts = [...data.files.js, ...data.files.menu]; - // loop through each userscript and prepare for processing - for (let i = 0; i < scripts.length; i++) { - const userscript = scripts[i]; - userscript.preCode = ""; - // pass references to the api methods as needed - const gmMethods = []; - const filename = userscript.scriptObject.filename; - const grants = userscript.scriptObject.grant; - const injectInto = userscript.scriptObject["inject-into"]; - // create GM.info object - const scriptData = { - script: userscript.scriptObject, - scriptHandler: data.scriptHandler, - scriptHandlerVersion: data.scriptHandlerVersion, - scriptMetaStr: userscript.scriptMetaStr - }; - // all userscripts get access to GM.info - gmMethods.push("info: GM_info"); - // if @grant explicitly set to none, empty grants array - if (grants.includes("none")) grants.length = 0; - // @grant values exist for page scoped userscript - if (grants.length && injectInto === "page") { - // remove grants - grants.length = 0; - // log warning - console.warn(`${filename} @grant values removed due to @inject-into value: ${injectInto} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // @grant exist for auto scoped userscript - if (grants.length && injectInto === "auto") { - // change scope - userscript.scriptObject["inject-into"] = "content"; - // log warning - console.warn(`${filename} @inject-into value set to 'content' due to @grant values: ${grants} - https://github.com/quoid/userscripts/issues/265#issuecomment-1213462394`); - } - // loop through each userscript @grant value, add methods as needed - for (let j = 0; j < grants.length; j++) { - const grant = grants[j]; - const method = grant.split(".")[1] || grant.split(".")[0]; - // ensure API method exists in apis object - if (!Object.keys(apis).includes(method)) continue; - // create the method string to be pushed to methods array - let methodStr = `${method}: apis.${method}`; - // add require variables to specific methods - switch (method) { - case "getValue": - case "setValue": - case "deleteValue": - case "listValues": - methodStr += `.bind({"US_filename": "${filename}"})`; - break; - case "info": - case "GM_info": - continue; - case "xmlHttpRequest": - gmMethods.push("xmlHttpRequest: apis.xhr"); - continue; - case "GM_xmlhttpRequest": - userscript.preCode += "const GM_xmlhttpRequest = apis.xhr;"; - continue; - } - gmMethods.push(methodStr); - } - // add GM.info - userscript.preCode += `const GM_info = ${JSON.stringify(scriptData)};`; - // add other included GM API methods - userscript.preCode += `const GM = {${gmMethods.join(",")}};`; - // process file for injection - processJS(userscript); - } - for (let i = 0; i < data.files.css.length; i++) { - const userstyle = data.files.css[i]; - injectCSS(userstyle.name, userstyle.code); - } - }); -} - -function listeners() { - // listens for messages from background, popup, etc... - browser.runtime.onMessage.addListener((request, sender, sendResponse) => { - const name = request.name; - if (name === "CONTEXT_RUN") { - // from bg script when context-menu item is clicked - // double check to ensure context-menu scripts only run in top windows - if (window !== window.top) return; - - // loop through context-menu scripts saved to data object and find match - // if no match found, nothing will execute and error will log - const filename = request.menuItemId; - for (let i = 0; i < data.files.menu.length; i++) { - const item = data.files.menu[i]; - if (item.scriptObject.filename === filename) { - console.info(`Injecting ${filename} %c(js)`, "color: #fff600"); - sendResponse({ - code: wrapCode( - item.preCode, - item.code, - filename - ) - }); - return; - } - } - console.error(`Couldn't find ${filename} code!`); - } - }); - // listen for CSP violations - document.addEventListener("securitypolicyviolation", cspFallback); -} - -async function initialize() { - const results = await browser.storage.local.get("US_GLOBAL_ACTIVE"); - if (results?.US_GLOBAL_ACTIVE === false) return console.info("Userscripts off"); - // start the injection process and add the listeners - injection(); - listeners(); -} - -initialize(); diff --git a/xcode/Safari-Extension/Resources/manifest.json b/xcode/Safari-Extension/Resources/manifest.json deleted file mode 100644 index 85e80919..00000000 --- a/xcode/Safari-Extension/Resources/manifest.json +++ /dev/null @@ -1,47 +0,0 @@ - -{ - "manifest_version": 2, - "default_locale": "en", - "name": "__MSG_extension_name__", - "description": "__MSG_extension_description__", - "version": "4.4.4", - "icons": { - "48": "images/icon-48.png", - "96": "images/icon-96.png", - "128": "images/icon-128.png", - "256": "images/icon-256.png", - "512": "images/icon-512.png" - }, - "background": { - "scripts": ["background.js"], - "persistent": false - }, - "browser_action": { - "default_popup": "dist/entry-popup.html", - "default_icon": { - "16": "images/toolbar-icon-16.png", - "32": "images/toolbar-icon-32.png" - } - }, - "content_scripts": [ - { - "js": ["content.js"], - "matches": [""], - "run_at": "document_start", - "all_frames": true - } - ], - "permissions": [ - "", - "clipboardWrite", - "contextMenus", - "declarativeNetRequest", - "declarativeNetRequestWithHostAccess", - "menus", - "nativeMessaging", - "storage", - "tabs", - "unlimitedStorage", - "webNavigation" - ] -} diff --git a/xcode/Safari-Extension/SafariWebExtensionHandler.swift b/xcode/Safari-Extension/SafariWebExtensionHandler.swift deleted file mode 100644 index d6bb4afc..00000000 --- a/xcode/Safari-Extension/SafariWebExtensionHandler.swift +++ /dev/null @@ -1,309 +0,0 @@ -import SafariServices -import os - -class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { - func beginRequest(with context: NSExtensionContext) { - let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) - let item = context.inputItems[0] as? NSExtensionItem - let message = item?.userInfo?[SFExtensionMessageKey] as? [String: Any] - // if message received without name, ignore - guard let name = message?["name"] as? String else { - logger.error("\(#function, privacy: .public) - could not get message name from web extension") - return - } - logger.info("\(#function, privacy: .public) - Got message with name: \(name, privacy: .public)") - // got a valid message, construct response based on message received - let response = NSExtensionItem() - // send standard error when there's an issue parsing inbound message - var inBoundError = false - // these if/else if statement are formatted so that they can be neatly collapsed in Xcode - // typically the "else if" would be on the same line as the preceding statements close bracket - // ie. } else if { - if name == "OPEN_APP" { - if let scheme = Bundle.main.infoDictionary?["US_URL_SCHEME"], - let url = URL(string: "\(scheme):") { - #if os(macOS) - NSWorkspace.shared.open(url) - #endif - } - } - else if name == "NATIVE_CHECKS" { - let result = nativeChecks() - response.userInfo = [SFExtensionMessageKey: result] - } - else if name == "REQ_PLATFORM" { - let platform = getPlatform() - response.userInfo = [SFExtensionMessageKey: ["platform": platform]] - } - else if name == "REQ_USERSCRIPTS" { - if let url = message?["url"] as? String, let isTop = message?["isTop"] as? Bool { - if - let matches = getInjectionFilenames(url), - let code = getCode(matches, isTop) - { - response.userInfo = [SFExtensionMessageKey: code] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "REQ_USERSCRIPTS failed"]] - } - } else { - inBoundError = true - } - } - else if name == "REQ_REQUESTS" { - if let requestScripts = getRequestScripts() { - response.userInfo = [SFExtensionMessageKey: requestScripts] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get requestScripts"]] - } - } - else if name == "REQ_CONTEXT_MENU_SCRIPTS" { - if let contextMenuScripts = getContextMenuScripts() { - response.userInfo = [SFExtensionMessageKey: contextMenuScripts] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get contextMenuScripts"]] - } - } - else if name == "POPUP_BADGE_COUNT" { - if let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { - if let matches = getPopupBadgeCount(url, frameUrls) { - response.userInfo = [SFExtensionMessageKey: ["count": matches]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to update badge count"]] - } - } else { - inBoundError = true - } - } - else if name == "POPUP_MATCHES"{ - if let url = message?["url"] as? String, let frameUrls = message?["frameUrls"] as? [String] { - if let matches = getPopupMatches(url, frameUrls) { - response.userInfo = [SFExtensionMessageKey: ["matches": matches]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get matches"]] - } - } else { - inBoundError = true - } - } - else if name == "POPUP_UPDATES" { - if let updates = checkForRemoteUpdates() { - response.userInfo = [SFExtensionMessageKey: ["updates": updates]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get updates"]] - } - } - else if name == "POPUP_UPDATE_ALL" { - if popupUpdateAll(), let updates = checkForRemoteUpdates() { - response.userInfo = [SFExtensionMessageKey: ["updates": updates]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to run update sequence"]] - } - } - else if name == "POPUP_UPDATE_SINGLE" { - if - let filename = message?["filename"] as? String, - let url = message?["url"] as? String, - let frameUrls = message?["frameUrls"] as? [String] - { - if let matches = popupUpdateSingle(filename, url, frameUrls) { - response.userInfo = [SFExtensionMessageKey: ["items": matches]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to update file"]] - } - } else { - inBoundError = true - } - } - else if name == "POPUP_CHECK_UPDATES" { - if let updates = checkForRemoteUpdates() { - response.userInfo = [SFExtensionMessageKey: ["updates": updates]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to check for updates"]] - } - } - else if name == "TOGGLE_EXTENSION" { - if let active = message?["active"] as? String, ["true", "false"].contains(active) { - var manifest = getManifest() - manifest.settings["active"] = active - if updateManifest(with: manifest) { - response.userInfo = [SFExtensionMessageKey: ["success": true]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to update injection state"]] - } - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "missing or wrong message content"]] - } - } - else if name == "TOGGLE_ITEM" { - // the current status of the item to toggle comes in as 0 || 1 - if - let item = message?["item"] as? [String: Any], - let filename = item["filename"] as? String, - let current = item["disabled"] as? Int - { - let action = current == 0 ? "disable" : "enable" - if toggleFile(filename, action) { - response.userInfo = [SFExtensionMessageKey: ["success": true]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to toggle file"]] - } - } else { - inBoundError = true - } - } - else if name == "OPEN_SAVE_LOCATION" { - #if os(macOS) - if openSaveLocation() { - response.userInfo = [SFExtensionMessageKey: ["success": true]] - } - #elseif os(iOS) - if let files = getAllFiles() { - response.userInfo = [SFExtensionMessageKey: ["items": files]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get all files"]] - } - #endif - } - else if name == "CHANGE_SAVE_LOCATION" { - #if os(macOS) - if let scheme = Bundle.main.infoDictionary?["US_URL_SCHEME"], - let url = URL(string: "\(scheme)://changesavelocation") { - NSWorkspace.shared.open(url) - } - #endif - } - else if name == "POPUP_INSTALL_CHECK" { - if let content = message?["content"] as? String { - response.userInfo = [SFExtensionMessageKey: installCheck(content)] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content"]] - } - } - else if name == "POPUP_INSTALL_SCRIPT" { - if let content = message?["content"] as? String { - response.userInfo = [SFExtensionMessageKey: installUserscript(content)] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get script content (2)"]] - } - } - else if name == "PAGE_INIT_DATA" { - #if os(macOS) - if let initData = getInitData(), checkDefaultDirectories() { - response.userInfo = [SFExtensionMessageKey: initData] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get init data"]] - } - #endif - } - else if name == "PAGE_LEGACY_IMPORT" { - #if os(macOS) - if let settings = getLegacyData() { - response.userInfo = [SFExtensionMessageKey: settings] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get legacy data"]] - } - #endif - } - else if name == "PAGE_ALL_FILES" { - #if os(macOS) - if let files = getAllFiles() { - response.userInfo = [SFExtensionMessageKey: files] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to get all files"]] - } - #endif - } - else if name == "PAGE_SAVE" { - #if os(macOS) - if - let item = message?["item"] as? [String: Any], - let content = message?["content"] as? String - { - let saveResponse = saveFile(item, content) - response.userInfo = [SFExtensionMessageKey: saveResponse] - } else { - inBoundError = true - } - #endif - } - else if name == "PAGE_TRASH" { - #if os(macOS) - if let item = message?["item"] as? [String: Any] { - if trashFile(item) { - response.userInfo = [SFExtensionMessageKey: ["success": true]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "failed to trash file"]] - } - } else { - inBoundError = true - } - #endif - } - else if name == "PAGE_UPDATE" { - #if os(macOS) - if let content = message?["content"] as? String { - let updateResponse = getFileRemoteUpdate(content) - response.userInfo = [SFExtensionMessageKey: updateResponse] - } else { - inBoundError = true - } - #endif - } - else if name == "CANCEL_REQUESTS" { - URLSession.shared.getAllTasks { tasks in - for task in tasks { - task.cancel() - } - } - response.userInfo = [SFExtensionMessageKey: ["success": true]] - } - else if name == "PAGE_NEW_REMOTE" { - #if os(macOS) - if let url = message?["url"] as? String { - if !validateUrl(url) { - response.userInfo = [SFExtensionMessageKey: ["error": "Failed to get remote content, invalid url"]] - } else if let content = getRemoteFileContents(url) { - response.userInfo = [SFExtensionMessageKey: content] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "Failed to get remote content"]] - } - } else { - inBoundError = true - } - #endif - } - else if name == "PAGE_UPDATE_SETTINGS" { - #if os(macOS) - if let settings = message?["settings"] as? [String: String] { - if updateSettings(settings) { - response.userInfo = [SFExtensionMessageKey: ["success": true]] - } else { - response.userInfo = [SFExtensionMessageKey: ["error": "Failed to save settings to disk"]] - } - } else { - inBoundError = true - } - #endif - } - else if name == "PAGE_UPDATE_BLACKLIST" { - #if os(macOS) - if let blacklist = message?["blacklist"] as? [String] { - var manifest = getManifest() - manifest.blacklist = blacklist - if !updateManifest(with: manifest) { - response.userInfo = [SFExtensionMessageKey: ["error": "Failed to save blacklist to disk"]] - } else { - response.userInfo = [SFExtensionMessageKey: ["success": true]] - } - } else { - inBoundError = true - } - #endif - } - // send inBoundError if found - if inBoundError { - response.userInfo = [SFExtensionMessageKey: ["error": "Failed to parse inbound message"]] - } - context.completeRequest(returningItems: [response], completionHandler: nil) - } -} diff --git a/xcode/Shared/Preferences.swift b/xcode/Shared/Preferences.swift new file mode 100644 index 00000000..f486cede --- /dev/null +++ b/xcode/Shared/Preferences.swift @@ -0,0 +1,326 @@ +import Foundation + +private let logger = USLogger(#fileID) +private let suiteName = Bundle.main.infoDictionary!["US_SHARED_GID"] as! String +private let SDefaults = UserDefaults(suiteName: suiteName) // Shared UserDefaults + +let bundleIdentifier = Bundle.main.bundleIdentifier! +let extIdentifier = Bundle.main.infoDictionary?["US_EXT_IDENTIFIER"] as! String + +func getDefaultScriptsDirectoryUrl() -> URL { + var url: URL + if #available(macOS 13.0, iOS 16.0, *) { + url = getDocumentsDirectory().appending(path: "scripts") + } else { + url = getDocumentsDirectory().appendingPathComponent("scripts") + } + // if not in safari extension environment, replace with extension path + if bundleIdentifier != extIdentifier { + var path: String + if #available(macOS 13.0, iOS 16.0, *) { + path = url.path(percentEncoded: false) + } else { + path = url.path.removingPercentEncoding ?? url.path + } + let truePath = path.replacingOccurrences(of: bundleIdentifier, with: extIdentifier) + url = URL(fileURLWithPath: truePath, isDirectory: true) + } + return url +} + +func isCurrentDefaultScriptsDirectory() -> Bool { + return Preferences.scriptsDirectoryUrl == getDefaultScriptsDirectoryUrl() +} + +func isCurrentInitialScriptsDirectory() -> Bool { + let url = Preferences.scriptsDirectoryUrl.standardizedFileURL + return url == getDocumentsDirectory().standardizedFileURL +} + +func getCurrentScriptsDirectoryString() -> String { + let url = Preferences.scriptsDirectoryUrl.standardizedFileURL + if url == getDocumentsDirectory().standardizedFileURL { + return "Userscripts App Documents" + } + if #available(macOS 13.0, iOS 16.0, *) { + return url.path(percentEncoded: false) + } else { + return url.path.removingPercentEncoding ?? url.path + } +} + +// https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties#Property-Wrappers +// https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/ +@propertyWrapper +private struct SharedUserDefaults { + private let key: String + private let defaultValue: T + + init(wrappedValue: T, _ key: String) { + self.key = key + self.defaultValue = wrappedValue + SDefaults?.register(defaults: [key: wrappedValue]) + } + + var wrappedValue: T { + get { SDefaults?.object(forKey: key) as? T ?? defaultValue } + set { SDefaults?.set(newValue, forKey: key) } + } +} + +#if os(macOS) +/* https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox + # Design ideas and processes + Since the security-scoped URL bookmark is only available in the current app, + pass to extensions can only use URL bookmark with an implicit security scope. + app set URL write to: + - `_SetterId` (setter app bundleIdentifier) + - `_Transfer` (with an implicit security scope) + - `_SecurityScoped_` (with an explicit security scope) + app get URL read from: `_SecurityScoped_` + ext get URL read flow: + - if `_Transfer` exist, remove to `_SecurityScoped_` + - if `_Transfer` empty, read from `_SecurityScoped_` + */ +@propertyWrapper +private struct SecurityScopedBookmark { + private let key: String + private let keySetter: String + private let keyTransfer: String + private let keySecurityScoped: String + private let defaultValue: URL + + init(wrappedValue: URL, _ key: String) { + self.key = key + self.keySetter = key + "/SetterId" + self.keyTransfer = key + "/Transfer" + self.keySecurityScoped = key + "/SecurityScoped/" + bundleIdentifier + self.defaultValue = wrappedValue + } + + private func createBookmark(_ url: URL, _ withSecurityScope: Bool) -> Data? { + do { + return try url.bookmarkData( + options: withSecurityScope ? .withSecurityScope : [], + includingResourceValuesForKeys: nil, + relativeTo: nil + ) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } + return nil + } + + private func resolvBookmark(_ data: Data, _ withSecurityScope: Bool, _ isStale: inout Bool) -> URL? { + do { + return try URL( + resolvingBookmarkData: data, + options: withSecurityScope ? .withSecurityScope : [], + relativeTo: nil, + bookmarkDataIsStale: &isStale + ) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } + return nil + } + + private func updateBookmark() { + guard let setterIdentifier = SDefaults?.string(forKey: keySetter) else { + logger?.debug("\(#function, privacy: .public) - setterId not exist: \(key, privacy: .public)") + return + } + guard bundleIdentifier != setterIdentifier else { return } // update only in non-setter environment + guard let data = SDefaults?.data(forKey: keyTransfer) else { // no need to update due empty data + logger?.debug("\(#function, privacy: .public) - no update: \(key, privacy: .public)") + return + } + var isStale = false // no need to renew since will remove anyway + guard let url = resolvBookmark(data, false, &isStale) else { // get URL bookmark with an implicit security scope + removeBookmark() + return + } + defer { url.stopAccessingSecurityScopedResource() } // revoke implicitly starts security-scoped access + if let data = createBookmark(url, true) { // set URL bookmark with an explicit security scope + logger?.info("\(#function, privacy: .public) - update bookmark: \(key, privacy: .public) \(url, privacy: .public)") + SDefaults?.removeObject(forKey: keyTransfer) + SDefaults?.set(data, forKey: keySecurityScoped) + } + } + + private func removeBookmark() { + logger?.info("\(#function, privacy: .public) - remove invalid bookmark: \(key, privacy: .public)") + SDefaults?.removeObject(forKey: keyTransfer) + SDefaults?.removeObject(forKey: keySecurityScoped) + if let setterIdentifier = SDefaults?.string(forKey: keySetter) { + SDefaults?.removeObject(forKey: key + "/SecurityScoped/" + setterIdentifier) + } + } + + // NOTE: This function will be deleted after several public versions of v4.5.0 + private func legacyBookmarkImporter() { + guard key == "ScriptsDirectoryUrlBookmarkData" else { return } + guard bundleIdentifier == extIdentifier else { return } + func legacyCleanup() { + logger?.debug("\(#function, privacy: .public) - cleanup legacy bookmark data") + SDefaults?.removeObject(forKey: "hostSelectedSaveLocation") // shared + UserDefaults.standard.removeObject(forKey: "saveLocation") // ext + UserDefaults.standard.removeObject(forKey: "userSaveLocation") // ext + } + guard let data = UserDefaults.standard.data(forKey: "userSaveLocation") else { // get from old key + logger?.debug("\(#function, privacy: .public) - legacy bookmark not exist") + return + } + guard SDefaults?.string(forKey: keySetter) == nil else { // already a new key + return legacyCleanup() + } + logger?.debug("\(#function, privacy: .public) - Import legacy bookmark data") + var isStale = false // no need to renew since will remove anyway + guard let url = resolvBookmark(data, true, &isStale) else { // get URL bookmark with an explicit security scope + return legacyCleanup() + } + guard url.startAccessingSecurityScopedResource() else { + return legacyCleanup() + } + defer { url.stopAccessingSecurityScopedResource() } + guard let transferData = createBookmark(url, false) else { + return legacyCleanup() + } + SDefaults?.set(bundleIdentifier, forKey: keySetter) + SDefaults?.set(transferData, forKey: keyTransfer) // set URL bookmark with an implicit security scope + SDefaults?.set(data, forKey: keySecurityScoped) // set URL bookmark with an explicit security scope + legacyCleanup() + } + + var wrappedValue: URL { + get { + legacyBookmarkImporter() + updateBookmark() + logger?.info("\(#function, privacy: .public) - try get bookmark: \(key, privacy: .public)") + guard let data = SDefaults?.data(forKey: keySecurityScoped) else { + logger?.debug("\(#function, privacy: .public) - bookmark not exist: \(key, privacy: .public)") + return defaultValue + } + var isStale = false + guard let url = resolvBookmark(data, true, &isStale) else { // get URL bookmark with an explicit security scope + removeBookmark() // remove data that cannot be resolved + return defaultValue + } + if isStale, url.startAccessingSecurityScopedResource() { // renew URL bookmark + defer { url.stopAccessingSecurityScopedResource() } + if let data = createBookmark(url, true) { // set URL bookmark with an explicit security scope + logger?.info("\(#function, privacy: .public) - renew bookmark: \(key, privacy: .public) \(url, privacy: .public)") + SDefaults?.set(data, forKey: keySecurityScoped) + } + } + return url + } + set(url) { + let k = key // key cannot be log directly with error: Escaping autoclosure captures mutating 'self' parameter + logger?.info("\(#function, privacy: .public) - try set bookmark: \(k, privacy: .public) \(url, privacy: .public)") + guard let tdata = createBookmark(url, false), let sdata = createBookmark(url, true) else { + logger?.info("\(#function, privacy: .public) - failed create bookmark: \(k, privacy: .public) \(url, privacy: .public)") + return + } + SDefaults?.set(bundleIdentifier, forKey: keySetter) + SDefaults?.set(tdata, forKey: keyTransfer) // set URL bookmark with an implicit security scope + SDefaults?.set(sdata, forKey: keySecurityScoped) // set URL bookmark with an explicit security scope + } + } +} +#elseif os(iOS) +/* https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories#3331285 + iOS and macOS have completely different processes, iOS has no implicit security scope + The security-scoped URL is not bind to the app and can be shared directly between the app and ext + Separating the wrappers for the two platforms is intentional for ease of viewing code logic + */ +@propertyWrapper +private struct SecurityScopedBookmark { + private let key: String + private let defaultValue: URL + + init(wrappedValue: URL, _ key: String) { + self.key = key + self.defaultValue = wrappedValue + } + + private func createBookmark(_ url: URL) -> Data? { + do { + return try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } + return nil + } + + private func resolvBookmark(_ data: Data, _ isStale: inout Bool) -> URL? { + do { + return try URL(resolvingBookmarkData: data, bookmarkDataIsStale: &isStale) + } catch { + logger?.error("\(#function, privacy: .public) - \(error.localizedDescription, privacy: .public)") + } + return nil + } + + // NOTE: This function will be deleted after several public versions of v4.5.0 + private func legacyBookmarkImporter() { + guard let data = SDefaults?.data(forKey: "iosReadLocation") else { // get from old key + logger?.debug("\(#function, privacy: .public) - legacy bookmark not exist") + return + } + logger?.debug("\(#function, privacy: .public) - Import legacy bookmark data") + SDefaults?.set(data, forKey: key) + SDefaults?.removeObject(forKey: "iosReadLocation") + } + + var wrappedValue: URL { + get { + legacyBookmarkImporter() + logger?.info("\(#function, privacy: .public) - try get bookmark: \(key, privacy: .public)") + guard let data = SDefaults?.data(forKey: key) else { + logger?.debug("\(#function, privacy: .public) - bookmark not exist: \(key, privacy: .public)") + return defaultValue + } + var isStale = false + guard let url = resolvBookmark(data, &isStale) else { // get security-scoped URL + SDefaults?.removeObject(forKey: key) // remove data that cannot be resolved + return defaultValue + } + if isStale, url.startAccessingSecurityScopedResource() { // renew URL bookmark + defer { url.stopAccessingSecurityScopedResource() } + if let data = createBookmark(url) { // set security-scoped URL + logger?.info("\(#function, privacy: .public) - renew bookmark: \(key, privacy: .public) \(url, privacy: .public)") + SDefaults?.set(data, forKey: key) + } + } + return url + } + set(url) { + let k = key // key cannot be log directly with error: Escaping autoclosure captures mutating 'self' parameter + logger?.info("\(#function, privacy: .public) - try set bookmark: \(k, privacy: .public) \(url, privacy: .public)") + let didStartAccessing = url.startAccessingSecurityScopedResource() + defer { + if didStartAccessing { url.stopAccessingSecurityScopedResource() } + } + guard let data = createBookmark(url) else { + logger?.info("\(#function, privacy: .public) - failed create bookmark: \(k, privacy: .public) \(url, privacy: .public)") + return + } + SDefaults?.set(data, forKey: key) // set security-scoped URL + } + } +} +#endif + +// Define shared preferences, default values ​​determine data type +struct Preferences { +/* Example preference, can be get or set with: Preferences.propertyName + @SharedUserDefaults("SharedUserDefaultsKeyName") + static var propertyName = "DefaultValue" // String +*/ + @SecurityScopedBookmark("ScriptsDirectoryUrlBookmarkData") + static var scriptsDirectoryUrl = getDefaultScriptsDirectoryUrl() + + @SharedUserDefaults("EnableLogger") + static var enableLogger = false +} diff --git a/xcode/Shared/Shared.swift b/xcode/Shared/Shared.swift deleted file mode 100644 index 7f2c4fb8..00000000 --- a/xcode/Shared/Shared.swift +++ /dev/null @@ -1,101 +0,0 @@ -import Foundation -import SafariServices -import os - -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) - -struct SharedDefaults { - // https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups - static let suiteName = Bundle.main.infoDictionary?["US_SHARED_GID"] as? String - #if os(iOS) - static let keyName = "iosReadLocation" - #elseif os(macOS) - static let keyName = "hostSelectedSaveLocation" - #endif -} - -func getDocumentsDirectory() -> URL { - let fm = FileManager.default - let paths = fm.urls(for: .documentDirectory, in: .userDomainMask) - let documentsDirectory = paths[0] - return documentsDirectory -} - -func directoryExists(path: String) -> Bool { - var isDirectory = ObjCBool(true) - let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) - let inTrash = path.contains(".Trash") ? false : true - return exists && inTrash && isDirectory.boolValue -} - -func getPlatform() -> String { - var platform:String - #if os(iOS) - if UIDevice.current.userInterfaceIdiom == .pad { - platform = "ipados" - } - else { - platform = "ios" - } - #elseif os(macOS) - platform = "macos" - #endif - return platform -} - -func saveBookmark(url: URL, isShared: Bool, keyName: String, isSecure: Bool) -> Bool { - #if os(iOS) - let options:URL.BookmarkCreationOptions = [] - #elseif os(macOS) - let options:URL.BookmarkCreationOptions = isSecure ? [.withSecurityScope] : [] - #endif - do { - let bookmark = try url.bookmarkData( - options: options, - includingResourceValuesForKeys: nil, - relativeTo: nil - ) - #if os(iOS) - UserDefaults(suiteName: SharedDefaults.suiteName)?.set(bookmark, forKey: keyName) - #elseif os(macOS) - if isShared { - UserDefaults(suiteName: SharedDefaults.suiteName)?.set(bookmark, forKey: keyName) - } else { - UserDefaults.standard.set(bookmark, forKey: keyName) - } - #endif - return true - } catch let error { - logger.error("\(#function, privacy: .public) - \(error, privacy: .public)") - return false - } -} - -func readBookmark(data: Data, isSecure: Bool) -> URL? { - #if os(iOS) - let options:URL.BookmarkResolutionOptions = [] - #elseif os(macOS) - let options:URL.BookmarkResolutionOptions = isSecure ? [.withSecurityScope] : [] - #endif - do { - var bookmarkIsStale = false - let url = try URL( - resolvingBookmarkData: data, - options: options, - relativeTo: nil, - bookmarkDataIsStale: &bookmarkIsStale - ) - if bookmarkIsStale { - NSLog("Stale bookmark, renewing it \(url)") - if saveBookmark(url: url, isShared: true, keyName: SharedDefaults.keyName, isSecure: false) { - NSLog("Successfully renewed stale bookmark - \(url)") - } else { - NSLog("Could not renew stale bookmark - \(url)") - } - } - return url - } catch let error { - logger.error("\(#function, privacy: .public) - \(error, privacy: .public)") - return nil - } -} diff --git a/xcode/Shared/UrlPolyfill.swift b/xcode/Shared/UrlPolyfill.swift new file mode 100644 index 00000000..dc76e2b3 --- /dev/null +++ b/xcode/Shared/UrlPolyfill.swift @@ -0,0 +1,96 @@ +import Foundation + +extension CharacterSet { + // https://developer.apple.com/documentation/foundation/characterset#2902136 + public static let urlAllowed_ = CharacterSet(charactersIn: "#") + .union(.urlFragmentAllowed) + .union(.urlHostAllowed) + .union(.urlPasswordAllowed) + .union(.urlPathAllowed) + .union(.urlQueryAllowed) + .union(.urlUserAllowed) +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI +func encodeURI(_ uri: String) -> String { + // https://developer.apple.com/documentation/foundation/characterset#2902136 +// var urlAllowed = CharacterSet(charactersIn: "#") +// urlAllowed.formUnion(.urlFragmentAllowed) +// urlAllowed.formUnion(.urlHostAllowed) +// urlAllowed.formUnion(.urlPasswordAllowed) +// urlAllowed.formUnion(.urlPathAllowed) +// urlAllowed.formUnion(.urlQueryAllowed) +// urlAllowed.formUnion(.urlUserAllowed) + return uri.addingPercentEncoding(withAllowedCharacters: .urlAllowed_) ?? uri +} + +func fixedURL(string urlString: String) -> URL? { + let rawUrlString = urlString.removingPercentEncoding ?? urlString + var url: URL? + if #available(macOS 14.0, iOS 17.0, *) { + url = URL(string: rawUrlString, encodingInvalidCharacters: true) + } else { + url = URL(string: encodeURI(rawUrlString)) + } + return url +} + +// https://developer.mozilla.org/en-US/docs/Web/API/URL +func jsLikeURL(_ urlString: String, baseString: String? = nil) -> [String: String]? { + var _url: URL? + if let baseString = baseString { + guard let baseUrl = fixedURL(string: baseString) else { + return nil + } + _url = URL(string: urlString, relativeTo: baseUrl) + } else { + _url = fixedURL(string: urlString) + } + guard let url = _url else { + return nil + } + + guard let scheme = url.scheme else { + return nil + } + var port = (url.port == nil) ? "" : String(url.port!) + if (scheme == "http" && port == "80") { port = "" } + if (scheme == "https" && port == "443") { port = "" } + if #available(macOS 13.0, iOS 16.0, *) { + let hostname = url.host(percentEncoded: true) ?? "" + let host = (port == "") ? hostname : "\(hostname):\(port)" + let query = url.query(percentEncoded: true) ?? "" + let fragment = url.fragment(percentEncoded: true) ?? "" + return [ + "hash": fragment == "" ? "" : "#\(fragment)", + "host": host, + "hostname": hostname, + // "href": url.absoluteString, + "origin": "\(scheme)://\(host)", + "password": url.password(percentEncoded: true) ?? "", + "pathname": url.path(percentEncoded: true), + "port": port, + "protocol": "\(scheme):", + "search": query == "" ? "" : "?\(query)", + "username": url.user(percentEncoded: true) ?? "" + ] + } else { + let hostname = url.host ?? "" + let host = (port == "") ? hostname : "\(hostname):\(port)" + let query = url.query ?? "" + let fragment = url.fragment ?? "" + return [ + "hash": fragment == "" ? "" : "#\(fragment)", + "host": host, + "hostname": hostname, + // "href": url.absoluteString, + "origin": "\(scheme)://\(host)", + "password": url.password ?? "", + "pathname": url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? url.path, + "port": port, + "protocol": "\(scheme):", + "search": query == "" ? "" : "?\(query)", + "username": url.user?.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? "" + ] + } +} diff --git a/xcode/Shared/Utilities.swift b/xcode/Shared/Utilities.swift new file mode 100644 index 00000000..8e3f0949 --- /dev/null +++ b/xcode/Shared/Utilities.swift @@ -0,0 +1,52 @@ +import SwiftUI +import os.log + +func USLogger(_ category: String) -> Logger? { + let subsystem = Bundle.main.bundleIdentifier! +#if DEBUG // Always enable logger in DEBUG builds + return Logger(subsystem: subsystem, category: category) +#else + if Preferences.enableLogger { + return Logger(subsystem: subsystem, category: category) + } + return nil +#endif +} + +func getDocumentsDirectory() -> URL { + let fm = FileManager.default + let paths = fm.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectory = paths[0] + return documentsDirectory +} + +func directoryExists(path: String) -> Bool { + var isDirectory = ObjCBool(true) + let exists = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) + let inTrash = path.contains(".Trash") ? false : true + return exists && inTrash && isDirectory.boolValue +} + +func getPlatform() -> String { +#if os(macOS) + return "macos" +#elseif os(iOS) + // https://developer.apple.com/documentation/uikit/uiuserinterfaceidiom + switch UIDevice.current.userInterfaceIdiom { + case .phone: + return "ios" + case .pad, .mac: + return "ipados" + case .vision: + return "visionos" + case .tv: + return "tvos" + case .carPlay: + return "carplay" + case .unspecified: + return "unspecified" + @unknown default: + return "unknown" + } +#endif +} diff --git a/xcode/Tests-Mac/UrlCodecTests.swift b/xcode/Tests-Mac/UrlCodecTests.swift new file mode 100644 index 00000000..0188cd00 --- /dev/null +++ b/xcode/Tests-Mac/UrlCodecTests.swift @@ -0,0 +1,199 @@ +import XCTest + +final class UrlCodecTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + + func testEncodeURI() throws { + + func check(_ urlString: String, _ res: String) -> Bool { + let url = encodeURI(urlString) + if (url != res) { + print(urlString, url) + return false + } + return true + } + + XCTAssert(check("http://user:password@host:80/path?q=1#id", + "http://user:password@host:80/path?q=1#id")) + + XCTAssert(check("https://用户名:密码@中.文:80/path/中文/?a=中文#中文", + "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@%E4%B8%AD.%E6%96%87:80/path/%E4%B8%AD%E6%96%87/?a=%E4%B8%AD%E6%96%87#%E4%B8%AD%E6%96%87")) + + XCTAssert(check("https://用户名:密码@中.文:80/path/中%E6%96%87/?a=中&b=%E6%96%87&c#中文", + "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@%E4%B8%AD.%E6%96%87:80/path/%E4%B8%AD%25E6%2596%2587/?a=%E4%B8%AD&b=%25E6%2596%2587&c#%E4%B8%AD%E6%96%87")) + + } + + func testJsLikeURL() throws { + + func diffPrint(_ dA: Dictionary?, _ dB: Dictionary) { + guard let dA = dA else { + return print("dA is nil") + } + for k in dA.keys { + if dA[k] != dB[k] { + print(k, dA[k] ?? "nil", dB[k] ?? "nil") + } + } + } + + func check(_ urlString: String, _ res: Dictionary) -> Bool { + let url = jsLikeURL(urlString) + if (url != res) { + print(urlString) + diffPrint(url, res) + return false + } + return true + } + + /** javascript get res + + url = new URL("http://user:password@host:80/path?q=1#id") + res = {} + for(let k of ["hash","host","hostname","href","origin","password","pathname","port","protocol","search","username"]){ + res[k] = url[k]; + } + JSON.stringify(res) + */ + + XCTAssert(check("http://user:password@host:80/path?q=1#id", [ + "hash": "#id", + "host": "host", + "hostname": "host", + // "href": "http://user:password@host/path?q=1#id", + "origin": "http://host", + "password": "password", + "pathname": "/path", + "port": "", + "protocol": "http:", + "search": "?q=1", + "username": "user" + ])) + + XCTAssert(check("http://host.test:8080/path?#id", [ + "hash": "#id", + "host": "host.test:8080", + "hostname": "host.test", + // "href": "http://host.test:8080/path?#id", + "origin": "http://host.test:8080", + "password": "", + "pathname": "/path", + "port": "8080", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("http://host.test:8080/path?", [ + "hash": "", + "host": "host.test:8080", + "hostname": "host.test", + // "href": "http://host.test:8080/path?", + "origin": "http://host.test:8080", + "password": "", + "pathname": "/path", + "port": "8080", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("http://host.test/path#id", [ + "hash": "#id", + "host": "host.test", + "hostname": "host.test", + // "href": "http://host.test/path#id", + "origin": "http://host.test", + "password": "", + "pathname": "/path", + "port": "", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("http://host.test/path", [ + "hash": "", + "host": "host.test", + "hostname": "host.test", + // "href": "http://host.test/path", + "origin": "http://host.test", + "password": "", + "pathname": "/path", + "port": "", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("http://host.test/", [ + "hash": "", + "host": "host.test", + "hostname": "host.test", + // "href": "http://host.test/", + "origin": "http://host.test", + "password": "", + "pathname": "/", + "port": "", + "protocol": "http:", + "search": "", + "username": "" + ])) + + XCTAssert(check("https://用户名:密码@host.test:80/path/中%E6%96%87/?a=中&b=%E6%96%87&c#中文", [ + "hash": "#%E4%B8%AD%E6%96%87", + "host": "host.test:80", + "hostname": "host.test", + // "href": "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@host.test:80/path/%E4%B8%AD%E6%96%87/?a=%E4%B8%AD&b=%E6%96%87&c#%E4%B8%AD%E6%96%87", + "origin": "https://host.test:80", + "password": "%E5%AF%86%E7%A0%81", + "pathname": "/path/%E4%B8%AD%E6%96%87/", + "port": "80", + "protocol": "https:", + "search": "?a=%E4%B8%AD&b=%E6%96%87&c", + "username": "%E7%94%A8%E6%88%B7%E5%90%8D" + ])) + + if #available(macOS 14.0, iOS 17.0, *) { + XCTAssert(check("https://用户名:密码@中.文:80/path/中%E6%96%87/?a=中&b=%E6%96%87&c#中文", [ + "hash": "#%E4%B8%AD%E6%96%87", + "host": "xn--fiq.xn--7dv:80", + "hostname": "xn--fiq.xn--7dv", + // "href": "https://%E7%94%A8%E6%88%B7%E5%90%8D:%E5%AF%86%E7%A0%81@xn--fiq.xn--7dv:80/path/%E4%B8%AD%E6%96%87/?a=%E4%B8%AD&b=%E6%96%87&c#%E4%B8%AD%E6%96%87", + "origin": "https://xn--fiq.xn--7dv:80", + "password": "%E5%AF%86%E7%A0%81", + "pathname": "/path/%E4%B8%AD%E6%96%87/", + "port": "80", + "protocol": "https:", + "search": "?a=%E4%B8%AD&b=%E6%96%87&c", + "username": "%E7%94%A8%E6%88%B7%E5%90%8D" + ])) + } + + } // testJsLikeURL() -> END + +} diff --git a/xcode/Tests-Mac/UserscriptsTests.swift b/xcode/Tests-Mac/UserscriptsTests.swift new file mode 100644 index 00000000..451f9e99 --- /dev/null +++ b/xcode/Tests-Mac/UserscriptsTests.swift @@ -0,0 +1,206 @@ +// +// UserscriptsTests.swift +// UserscriptsTests +// +// Created by Justin Wasack on 1/23/22. +// Copyright © 2022 Justin Wasack. All rights reserved. +// + +import XCTest +@testable import Userscripts_Mac_Safari_Extension + +class UserscriptsTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testStringSanitization() throws { + // given + let strs = [ + "String", + "https://something.com/?foo=12", + "I have backslashes \\\\", + ".....ok", + ":Akneh.,><>dkie:lm", + "..解锁B站大会员番剧、", + "解锁B站大会员番剧、B站视频解析下载;全网VIP视频免费破解去广告;全网音乐直接下载;油管、Facebook等国外视频解析下载;网盘搜索引擎破解无限下载等", + "5CLksm3AAbb2F2F2f----___--+87363&^#%o%3O3", + "Example Userscript Name" + ] + + // when + var result = [String]() + for str in strs { + let sanitizedString = sanitize(str) + let unsanitizedString = unsanitize(sanitizedString) + result.append(unsanitizedString) + } + + // then + XCTAssert(result.elementsEqual(strs)) + } + + func testEncodedCheck() throws { + let urls = [ + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect%20外链跳转.user.js", + "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", + "https://cdn.frankerfacez.com/static/ffz_injector.user.js", + "http://www.k21p.com/example.user.js", // add http protocol + "https://example.test/%%%test.user.js", // removingPercentEncoding -> nil + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js" + ] + var result = [String]() + for url in urls { + if isEncoded(url) { + result.append(url) + } + } + // 2 urls already percent encoded + XCTAssert(result.count == 2) + } + + func testGetRemoteFileContents() throws { + var urls:[String] = [ + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect%20外链跳转.user.js", + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js", + "https://update.greasyfork.org/scripts/460897/1277476/gbCookies.js#sha256-Sv+EuBerch8z/6LvAU0m/ufvjmqB1Q/kbQrX7zAvOPk=", + "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", + "https://cdn.frankerfacez.com/static/ffz_injector.user.js", + "http://www.k21p.com/example.user.js" // add http protocol + ] + if #available(macOS 14.0, iOS 17.0, *) { + urls += [ + "https://☁️.com/", // IDN / Non-Punycode-encoded domain name + ] + } + for url in urls { + if getRemoteFileContents(url) == nil { + print(#function, url) + XCTAssert(false) + } + } + } + + func testFileRemoteUpdate() throws { + let urls = [ + "https://www.k21p.com/example.user.js", + "https://www.k21p.com/example.user.js?foo=bar", // query string + "http://www.k21p.com/example.user.js", // http protocol + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js", // non latin chars + "https://www.k21p.com/example.user.jsx" // should fail + + ] + var result = [Int]() + for url in urls { + let content = """ + // ==UserScript== + // @name test + // @match *://*/* + // @version 0.1 + // @updateURL http://www.k21p.com/example.user.js + // @downloadURL \(url) + // ==/UserScript== + """; + let response = getFileRemoteUpdate(content) + if !response.keys.contains("error") { + result.append(1) + } + } + XCTAssert(result.count == (urls.count - 1)) + } + + func testMatching() throws { + let patternDict = [ + "*://*/*": [ + "https://www.bing.com/", + "https://example.org/foo/bar.html", + "https://a.org/some/path/", + "https://☁️.com/" + ], + "*://*.mozilla.org/*": [ + "http://mozilla.org/", + "https://mozilla.org/", + "https://b.mozilla.org/path/" + ], + "*://www.google.com/*": [ + "https://www.google.com/://aa", + "https://www.google.com/preferences?prev=https://www.google.com/", + "https://www.google.com/preferences?prev=", + "https://www.google.com/" + ], + "*://localhost/*": [ + "http://localhost:8000/", + "https://localhost:3000/foo.html" + ], + "http://127.0.0.1/*": [ + "http://127.0.0.1/", + "http://127.0.0.1/foo/bar.html" + ], + "*://*.example.com/*?a=1*": [ + "http://example.com/?a=1", + "https://www.example.com/index?a=1&b=2" + ] + ] + let patternDictFails = [ + "https://www.example.com/*": [ + "file://www.example.com/", + "ftp://www.example.com/", + "ws://www.example.com/", + "http://www.example.com/" + ], + "http://www.example.com/index.html": [ + "http://www.example.com/", + "https://www.example.com/index.html" + ], + "*://localhost/*": [ + "https://localhost.com/", + "ftp://localhost:8080/" + ], + "https://www.example*/*": [ + "https://www.example.com/" + ], + "*://*.example.com/*?a=1*": [ + "http://example.com/", + "https://www.example.com/?a=2" + ] + ] + for (pattern, urls) in patternDict { + for url in urls { + if !match(url, pattern) { + print(#function, "patternDict", url, pattern) + XCTAssert(false) + } + } + } + for (pattern, urls) in patternDictFails { + for url in urls { + if match(url, pattern) { + print(#function, "patternDictFails", url, pattern) + XCTAssert(false) + } + } + } + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index 8286d541..f5944419 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -3,13 +3,25 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 030C64F62AC62CC900548FBD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 030C64F52AC62CC900548FBD /* Settings.bundle */; }; + 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030C64FB2AD0B24C00548FBD /* Initialization.swift */; }; + 030FCAD42B182008004D13CD /* UrlCodecTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FCAD32B182008004D13CD /* UrlCodecTests.swift */; }; + 030FCADC2B189AAB004D13CD /* UrlPolyfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */; }; + 030FCADD2B18BF70004D13CD /* UrlPolyfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */; }; + 030FCADF2B18BF72004D13CD /* UrlPolyfill.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */; }; 0336619F294DF7C900CFE179 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336619E294DF7C900CFE179 /* Functions.swift */; }; 033661A529510B7900CFE179 /* View.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033661A329510B7900CFE179 /* View.storyboard */; }; - 0376905B29808EEA00F3474D /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = 0376905A29808EEA00F3474D /* background.js */; }; + 039F59532AD9591C002E9977 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 039F59522AD9591C002E9977 /* dist */; }; + 039F59562ADA2FE2002E9977 /* USchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 039F59552ADA2FE2002E9977 /* USchemeHandler.swift */; }; + 03C24F972ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; + 03C24F982ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; + 03C24F992ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; + 03C24F9A2ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; + 03C24F9B2ABD2CBB00F130F9 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C24F962ABD2CBB00F130F9 /* Preferences.swift */; }; 03E20D06291E170000B4F692 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 03E20D05291E170000B4F692 /* dist */; }; 03E20D07291E170000B4F692 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 03E20D05291E170000B4F692 /* dist */; }; 4A143AAC279DE6FF0029BFD0 /* UserscriptsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */; }; @@ -17,14 +29,10 @@ 4A301B2A270A474400C7E9E1 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; 4A301B2B270A474F00C7E9E1 /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A617268266B30018536B /* _locales */; }; 4A301B2C270A475200C7E9E1 /* images in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A616268266B30018536B /* images */; }; - 4A301B2E270A475D00C7E9E1 /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 4AB22357268B9B9200D551C6 /* content.js */; }; - 4A301B2F270A476000C7E9E1 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 4AD66D2026826D8100B004E8 /* manifest.json */; }; 4A301B32270A49F200C7E9E1 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED6491268CDB58001794BF /* Functions.swift */; }; 4A36A61A268266B30018536B /* images in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A616268266B30018536B /* images */; }; 4A36A61B268266B30018536B /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A617268266B30018536B /* _locales */; }; 4A4CF6DE270A38BD00111584 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4CF6DD270A38BD00111584 /* AppDelegate.swift */; }; - 4A4CF6E2270A38BD00111584 /* Main.html in Resources */ = {isa = PBXBuildFile; fileRef = 4A4CF6E0270A38BD00111584 /* Main.html */; }; - 4A4CF6E4270A38BD00111584 /* Icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 4A4CF6E3270A38BD00111584 /* Icon.png */; }; 4A4CF6E8270A38BD00111584 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4CF6E7270A38BD00111584 /* SceneDelegate.swift */; }; 4A4CF6EA270A38BD00111584 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4CF6E9270A38BD00111584 /* ViewController.swift */; }; 4A4CF6ED270A38BD00111584 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4A4CF6EB270A38BD00111584 /* LaunchScreen.storyboard */; }; @@ -37,16 +45,13 @@ 4A57B9F9227235CE008A9763 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4A57B9F8227235CE008A9763 /* Assets.xcassets */; }; 4A57BA00227235CE008A9763 /* Userscripts-Mac-Safari-Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4A57B9FF227235CE008A9763 /* Userscripts-Mac-Safari-Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4A57BA08227235CE008A9763 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; - 4AB22358268B9B9200D551C6 /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 4AB22357268B9B9200D551C6 /* content.js */; }; - 4AD66D2326826D8100B004E8 /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = 4AD66D2026826D8100B004E8 /* manifest.json */; }; 4AED6492268CDB58001794BF /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED6491268CDB58001794BF /* Functions.swift */; }; 7878ED24299BA27B00E36A24 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; - 7878ED2B299BA36800E36A24 /* background.js in Resources */ = {isa = PBXBuildFile; fileRef = 0376905A29808EEA00F3474D /* background.js */; }; - 78B25583299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; - 78B25584299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; - 78B25585299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; - 78B25586299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; - 78B25587299D4C27000B2E9B /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Shared.swift */; }; + 78B25583299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; + 78B25584299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; + 78B25585299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; + 78B25586299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; + 78B25587299D4C27000B2E9B /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B25582299D4C27000B2E9B /* Utilities.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -99,9 +104,16 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 030C64F52AC62CC900548FBD /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + 030C64FB2AD0B24C00548FBD /* Initialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initialization.swift; sourceTree = ""; }; + 030FCAD32B182008004D13CD /* UrlCodecTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlCodecTests.swift; sourceTree = ""; }; + 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlPolyfill.swift; sourceTree = ""; }; 0336619E294DF7C900CFE179 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 033661A429510B7900CFE179 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/View.storyboard; sourceTree = ""; }; - 0376905A29808EEA00F3474D /* background.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = background.js; sourceTree = ""; }; + 039F59522AD9591C002E9977 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; + 039F59552ADA2FE2002E9977 /* USchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = USchemeHandler.swift; sourceTree = ""; }; + 039F59662B03AE5A002E9977 /* Userscripts-Vite.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Userscripts-Vite.xcconfig"; sourceTree = ""; }; + 03C24F962ABD2CBB00F130F9 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 03E20D05291E170000B4F692 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; 4A143AA9279DE6FF0029BFD0 /* Mac-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Mac-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserscriptsTests.swift; sourceTree = ""; }; @@ -109,8 +121,6 @@ 4A36A617268266B30018536B /* _locales */ = {isa = PBXFileReference; lastKnownFileType = folder; path = _locales; sourceTree = ""; }; 4A4CF6DB270A38BD00111584 /* Userscripts-iOS-App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Userscripts-iOS-App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 4A4CF6DD270A38BD00111584 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 4A4CF6E1270A38BD00111584 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = ../Base.lproj/Main.html; sourceTree = ""; }; - 4A4CF6E3270A38BD00111584 /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Icon.png; sourceTree = ""; }; 4A4CF6E7270A38BD00111584 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 4A4CF6E9270A38BD00111584 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 4A4CF6EC270A38BD00111584 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -130,7 +140,6 @@ 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = ""; }; 4A57BA0E227235CE008A9763 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4A57BA1D227235E6008A9763 /* Mac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Mac.entitlements; sourceTree = ""; }; - 4AB22357268B9B9200D551C6 /* content.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = content.js; sourceTree = ""; }; 4AD66D2026826D8100B004E8 /* manifest.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = manifest.json; sourceTree = ""; }; 4AED6491268CDB58001794BF /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 78391D4629A4CB3C00979E34 /* Userscripts-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Userscripts-Debug.xcconfig"; sourceTree = ""; }; @@ -141,7 +150,7 @@ 7878ED32299BBEBF00E36A24 /* Mac-Safari-Extension.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Mac-Safari-Extension.xcconfig"; sourceTree = ""; }; 7878ED34299BBEBF00E36A24 /* iOS-App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "iOS-App.xcconfig"; sourceTree = ""; }; 7878ED38299BBEBF00E36A24 /* Mac-App.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Mac-App.xcconfig"; sourceTree = ""; }; - 78B25582299D4C27000B2E9B /* Shared.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; + 78B25582299D4C27000B2E9B /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -183,37 +192,47 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 4A143AAA279DE6FF0029BFD0 /* Mac-Tests */ = { + 0316A9992AD7709600AAEC8A /* App-Shared */ = { + isa = PBXGroup; + children = ( + 039F59512AD95485002E9977 /* Resources */, + 4A4CF6E9270A38BD00111584 /* ViewController.swift */, + 039F59552ADA2FE2002E9977 /* USchemeHandler.swift */, + ); + path = "App-Shared"; + sourceTree = ""; + }; + 039F59512AD95485002E9977 /* Resources */ = { + isa = PBXGroup; + children = ( + 039F59522AD9591C002E9977 /* dist */, + ); + path = Resources; + sourceTree = ""; + }; + 4A143AAA279DE6FF0029BFD0 /* Tests-Mac */ = { isa = PBXGroup; children = ( 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */, + 030FCAD32B182008004D13CD /* UrlCodecTests.swift */, ); - path = "Mac-Tests"; + path = "Tests-Mac"; sourceTree = ""; }; - 4A4CF6DC270A38BD00111584 /* iOS-App */ = { + 4A4CF6DC270A38BD00111584 /* App-iOS */ = { isa = PBXGroup; children = ( 4A4CF6F2270A38BF00111584 /* Info.plist */, 4A4CF718270A38C700111584 /* App.entitlements */, - 4A4CF6DF270A38BD00111584 /* Resources */, + 030C64F52AC62CC900548FBD /* Settings.bundle */, 4A4CF6F1270A38BF00111584 /* Assets.xcassets */, 4A4CF6DD270A38BD00111584 /* AppDelegate.swift */, 4A4CF6E7270A38BD00111584 /* SceneDelegate.swift */, - 4A4CF6E9270A38BD00111584 /* ViewController.swift */, + 030C64FB2AD0B24C00548FBD /* Initialization.swift */, 4A4CF6EB270A38BD00111584 /* LaunchScreen.storyboard */, 4A4CF6EE270A38BD00111584 /* Main.storyboard */, ); - path = "iOS-App"; - sourceTree = ""; - }; - 4A4CF6DF270A38BD00111584 /* Resources */ = { - isa = PBXGroup; - children = ( - 4A4CF6E3270A38BD00111584 /* Icon.png */, - 4A4CF6E0270A38BD00111584 /* Main.html */, - ); - path = Resources; + path = "App-iOS"; sourceTree = ""; }; 4A57B9E3227235CD008A9763 = { @@ -221,13 +240,15 @@ children = ( 7878ED2E299BBEBF00E36A24 /* xcconfig */, 78B25581299D4C27000B2E9B /* Shared */, - 4A57BA06227235CE008A9763 /* Safari-Extension */, - 4A57B9EE227235CD008A9763 /* Mac-App */, - 4A4CF6DC270A38BD00111584 /* iOS-App */, - 4A143AAA279DE6FF0029BFD0 /* Mac-Tests */, + 4A57BA06227235CE008A9763 /* Ext-Safari */, + 0316A9992AD7709600AAEC8A /* App-Shared */, + 4A57B9EE227235CD008A9763 /* App-Mac */, + 4A4CF6DC270A38BD00111584 /* App-iOS */, + 4A143AAA279DE6FF0029BFD0 /* Tests-Mac */, 4A57B9ED227235CD008A9763 /* Products */, ); sourceTree = ""; + usesTabs = 1; }; 4A57B9ED227235CD008A9763 /* Products */ = { isa = PBXGroup; @@ -241,7 +262,7 @@ name = Products; sourceTree = ""; }; - 4A57B9EE227235CD008A9763 /* Mac-App */ = { + 4A57B9EE227235CD008A9763 /* App-Mac */ = { isa = PBXGroup; children = ( 4A57B9FA227235CE008A9763 /* Info.plist */, @@ -253,20 +274,20 @@ 4A57B9F3227235CD008A9763 /* Main.storyboard */, 033661A329510B7900CFE179 /* View.storyboard */, ); - path = "Mac-App"; + path = "App-Mac"; sourceTree = ""; }; - 4A57BA06227235CE008A9763 /* Safari-Extension */ = { + 4A57BA06227235CE008A9763 /* Ext-Safari */ = { isa = PBXGroup; children = ( - 4AF1DD0D268265DB00086E88 /* Resources */, - 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */, - 4AED6491268CDB58001794BF /* Functions.swift */, 4A57BA0E227235CE008A9763 /* Info.plist */, 4A57BA1D227235E6008A9763 /* Mac.entitlements */, 7878ED2D299BA40800E36A24 /* iOS.entitlements */, + 4AF1DD0D268265DB00086E88 /* Resources */, + 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */, + 4AED6491268CDB58001794BF /* Functions.swift */, ); - path = "Safari-Extension"; + path = "Ext-Safari"; sourceTree = ""; }; 4AF1DD0D268265DB00086E88 /* Resources */ = { @@ -276,8 +297,6 @@ 4A36A616268266B30018536B /* images */, 03E20D05291E170000B4F692 /* dist */, 4AD66D2026826D8100B004E8 /* manifest.json */, - 0376905A29808EEA00F3474D /* background.js */, - 4AB22357268B9B9200D551C6 /* content.js */, ); path = Resources; sourceTree = ""; @@ -285,6 +304,7 @@ 7878ED2E299BBEBF00E36A24 /* xcconfig */ = { isa = PBXGroup; children = ( + 039F59662B03AE5A002E9977 /* Userscripts-Vite.xcconfig */, 78391D4629A4CB3C00979E34 /* Userscripts-Debug.xcconfig */, 78391D4729A4CB3C00979E34 /* Userscripts-Release.xcconfig */, 7878ED38299BBEBF00E36A24 /* Mac-App.xcconfig */, @@ -299,7 +319,9 @@ 78B25581299D4C27000B2E9B /* Shared */ = { isa = PBXGroup; children = ( - 78B25582299D4C27000B2E9B /* Shared.swift */, + 78B25582299D4C27000B2E9B /* Utilities.swift */, + 03C24F962ABD2CBB00F130F9 /* Preferences.swift */, + 030FCAD52B1851F8004D13CD /* UrlPolyfill.swift */, ); path = Shared; sourceTree = ""; @@ -333,6 +355,7 @@ 4A4CF6D8270A38BD00111584 /* Frameworks */, 4A4CF6D9270A38BD00111584 /* Resources */, 4A4CF713270A38BF00111584 /* Embed Foundation Extensions */, + 030C64F72AC713A600548FBD /* UScript: Introduce values to app Settings.bundle */, ); buildRules = ( ); @@ -351,6 +374,7 @@ 4A4CF6F3270A38BF00111584 /* Sources */, 4A4CF6F4270A38BF00111584 /* Frameworks */, 4A4CF6F5270A38BF00111584 /* Resources */, + 039F59592ADFA415002E9977 /* UScript: Produce ext manifest.json */, ); buildRules = ( ); @@ -387,6 +411,7 @@ 4A57B9FB227235CE008A9763 /* Sources */, 4A57B9FC227235CE008A9763 /* Frameworks */, 4A57B9FD227235CE008A9763 /* Resources */, + 039F59572ADF9558002E9977 /* UScript: Produce ext manifest.json */, ); buildRules = ( ); @@ -403,6 +428,7 @@ 4A57B9E4227235CD008A9763 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1320; LastUpgradeCheck = 1420; ORGANIZATIONNAME = "Justin Wasack"; @@ -464,11 +490,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4A4CF6E4270A38BD00111584 /* Icon.png in Resources */, 4A4CF6F0270A38BD00111584 /* Main.storyboard in Resources */, 4A4CF6ED270A38BD00111584 /* LaunchScreen.storyboard in Resources */, - 4A4CF6E2270A38BD00111584 /* Main.html in Resources */, + 030C64F62AC62CC900548FBD /* Settings.bundle in Resources */, 4A4CF710270A38BF00111584 /* Assets.xcassets in Resources */, + 039F59532AD9591C002E9977 /* dist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -476,11 +502,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4A301B2E270A475D00C7E9E1 /* content.js in Resources */, 4A301B2C270A475200C7E9E1 /* images in Resources */, - 7878ED2B299BA36800E36A24 /* background.js in Resources */, 03E20D07291E170000B4F692 /* dist in Resources */, - 4A301B2F270A476000C7E9E1 /* manifest.json in Resources */, 4A301B2B270A474F00C7E9E1 /* _locales in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -499,25 +522,93 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4AD66D2326826D8100B004E8 /* manifest.json in Resources */, - 4AB22358268B9B9200D551C6 /* content.js in Resources */, 03E20D06291E170000B4F692 /* dist in Resources */, 4A36A61A268266B30018536B /* images in Resources */, - 0376905B29808EEA00F3474D /* background.js in Resources */, 4A36A61B268266B30018536B /* _locales in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 030C64F72AC713A600548FBD /* UScript: Introduce values to app Settings.bundle */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/$(SRC_DIR)/Settings.bundle/Root.plist", + "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", + ); + name = "UScript: Introduce values to app Settings.bundle"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)/Settings.bundle/Root.plist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Dynamically introduce US_SHARED_GID in xcconfig for Settings.bundle\nUS_SHARED_GID=$(/usr/libexec/PlistBuddy -c \"Print US_SHARED_GID\" \"$TARGET_BUILD_DIR/$INFOPLIST_PATH\")\nSETTINGS_BUNDLE=$TARGET_BUILD_DIR/$EXECUTABLE_FOLDER_PATH/Settings.bundle/Root.plist\n/usr/libexec/PlistBuddy -c \"Set ApplicationGroupContainerIdentifier $US_SHARED_GID\" \"$SETTINGS_BUNDLE\"\n"; + }; + 039F59572ADF9558002E9977 /* UScript: Produce ext manifest.json */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/$(MANIFEST_FILE)", + "$(SRCROOT)/$(SRC_DIR)/Resources/_locales/en/messages.json", + "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", + ); + name = "UScript: Produce ext manifest.json"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/manifest.json", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "sed \"s/_VERSION_/$MAC_APP_VERSION/\" $SRCROOT/$MANIFEST_FILE > $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/manifest.json\nsed -i \"\" \"s/_NAME_/$MANIFEST_NAME/\" $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/_locales/en/messages.json\n"; + }; + 039F59592ADFA415002E9977 /* UScript: Produce ext manifest.json */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/$(MANIFEST_FILE)", + "$(SRCROOT)/$(SRC_DIR)/Resources/_locales/en/messages.json", + "$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)", + ); + name = "UScript: Produce ext manifest.json"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/manifest.json", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "sed \"s/_VERSION_/$IOS_APP_VERSION/\" $SRCROOT/$MANIFEST_FILE > $TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/manifest.json\nsed -i \"\" \"s/_NAME_/$MANIFEST_NAME/\" $TARGET_BUILD_DIR/$CONTENTS_FOLDER_PATH/_locales/en/messages.json\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 4A143AA5279DE6FF0029BFD0 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 7878ED24299BA27B00E36A24 /* SafariWebExtensionHandler.swift in Sources */, + 030FCADC2B189AAB004D13CD /* UrlPolyfill.swift in Sources */, + 030FCAD42B182008004D13CD /* UrlCodecTests.swift in Sources */, 4A143AB2279DEA170029BFD0 /* Functions.swift in Sources */, - 78B25587299D4C27000B2E9B /* Shared.swift in Sources */, + 03C24F9B2ABD2CBB00F130F9 /* Preferences.swift in Sources */, + 78B25587299D4C27000B2E9B /* Utilities.swift in Sources */, 4A143AAC279DE6FF0029BFD0 /* UserscriptsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -528,8 +619,11 @@ files = ( 4A4CF6EA270A38BD00111584 /* ViewController.swift in Sources */, 4A4CF6DE270A38BD00111584 /* AppDelegate.swift in Sources */, - 78B25585299D4C27000B2E9B /* Shared.swift in Sources */, + 03C24F992ABD2CBB00F130F9 /* Preferences.swift in Sources */, + 030C64FC2AD0B24C00548FBD /* Initialization.swift in Sources */, + 78B25585299D4C27000B2E9B /* Utilities.swift in Sources */, 4A4CF6E8270A38BD00111584 /* SceneDelegate.swift in Sources */, + 039F59562ADA2FE2002E9977 /* USchemeHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -537,8 +631,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78B25586299D4C27000B2E9B /* Shared.swift in Sources */, + 78B25586299D4C27000B2E9B /* Utilities.swift in Sources */, 4A301B2A270A474400C7E9E1 /* SafariWebExtensionHandler.swift in Sources */, + 030FCADD2B18BF70004D13CD /* UrlPolyfill.swift in Sources */, + 03C24F9A2ABD2CBB00F130F9 /* Preferences.swift in Sources */, 4A301B32270A49F200C7E9E1 /* Functions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -549,7 +645,8 @@ files = ( 4A57B9F7227235CD008A9763 /* ViewController.swift in Sources */, 4A57B9F2227235CD008A9763 /* AppDelegate.swift in Sources */, - 78B25583299D4C27000B2E9B /* Shared.swift in Sources */, + 03C24F972ABD2CBB00F130F9 /* Preferences.swift in Sources */, + 78B25583299D4C27000B2E9B /* Utilities.swift in Sources */, 0336619F294DF7C900CFE179 /* Functions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -558,8 +655,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78B25584299D4C27000B2E9B /* Shared.swift in Sources */, + 78B25584299D4C27000B2E9B /* Utilities.swift in Sources */, 4A57BA08227235CE008A9763 /* SafariWebExtensionHandler.swift in Sources */, + 030FCADF2B18BF72004D13CD /* UrlPolyfill.swift in Sources */, + 03C24F982ABD2CBB00F130F9 /* Preferences.swift in Sources */, 4AED6492268CDB58001794BF /* Functions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -593,14 +692,6 @@ name = View.storyboard; sourceTree = ""; }; - 4A4CF6E0270A38BD00111584 /* Main.html */ = { - isa = PBXVariantGroup; - children = ( - 4A4CF6E1270A38BD00111584 /* Base */, - ); - name = Main.html; - sourceTree = ""; - }; 4A4CF6EB270A38BD00111584 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -628,6 +719,48 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 039F59602B03A48A002E9977 /* Vite */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 039F59662B03AE5A002E9977 /* Userscripts-Vite.xcconfig */; + buildSettings = { + }; + name = Vite; + }; + 039F59612B03A48A002E9977 /* Vite */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7878ED38299BBEBF00E36A24 /* Mac-App.xcconfig */; + buildSettings = { + }; + name = Vite; + }; + 039F59622B03A48A002E9977 /* Vite */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7878ED32299BBEBF00E36A24 /* Mac-Safari-Extension.xcconfig */; + buildSettings = { + }; + name = Vite; + }; + 039F59632B03A48A002E9977 /* Vite */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7878ED34299BBEBF00E36A24 /* iOS-App.xcconfig */; + buildSettings = { + }; + name = Vite; + }; + 039F59642B03A48A002E9977 /* Vite */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7878ED2F299BBEBF00E36A24 /* iOS-Safari-Extension.xcconfig */; + buildSettings = { + }; + name = Vite; + }; + 039F59652B03A48A002E9977 /* Vite */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7878ED31299BBEBF00E36A24 /* Mac-Tests.xcconfig */; + buildSettings = { + }; + name = Vite; + }; 4A143AAF279DE6FF0029BFD0 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7878ED31299BBEBF00E36A24 /* Mac-Tests.xcconfig */; @@ -718,56 +851,62 @@ 4A143AB1279DE6FF0029BFD0 /* Build configuration list for PBXNativeTarget "Mac-Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( + 039F59652B03A48A002E9977 /* Vite */, 4A143AAF279DE6FF0029BFD0 /* Debug */, 4A143AB0279DE6FF0029BFD0 /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; 4A4CF716270A38BF00111584 /* Build configuration list for PBXNativeTarget "iOS-Safari-Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( + 039F59642B03A48A002E9977 /* Vite */, 4A4CF714270A38BF00111584 /* Debug */, 4A4CF715270A38BF00111584 /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; 4A4CF717270A38BF00111584 /* Build configuration list for PBXNativeTarget "iOS-App" */ = { isa = XCConfigurationList; buildConfigurations = ( + 039F59632B03A48A002E9977 /* Vite */, 4A4CF711270A38BF00111584 /* Debug */, 4A4CF712270A38BF00111584 /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; 4A57B9E7227235CD008A9763 /* Build configuration list for PBXProject "Userscripts" */ = { isa = XCConfigurationList; buildConfigurations = ( + 039F59602B03A48A002E9977 /* Vite */, 4A57BA14227235CE008A9763 /* Debug */, 4A57BA15227235CE008A9763 /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; 4A57BA16227235CE008A9763 /* Build configuration list for PBXNativeTarget "Mac-Safari-Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( + 039F59622B03A48A002E9977 /* Vite */, 4A57BA17227235CE008A9763 /* Debug */, 4A57BA18227235CE008A9763 /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; 4A57BA1A227235CE008A9763 /* Build configuration list for PBXNativeTarget "Mac-App" */ = { isa = XCConfigurationList; buildConfigurations = ( + 039F59612B03A48A002E9977 /* Vite */, 4A57BA1B227235CE008A9763 /* Debug */, 4A57BA1C227235CE008A9763 /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ }; diff --git a/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts Extension.xcscheme b/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Mac-Vite-Dev.xcscheme similarity index 53% rename from xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts Extension.xcscheme rename to xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Mac-Vite-Dev.xcscheme index bf2b16e1..0a3c056b 100644 --- a/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts Extension.xcscheme +++ b/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Mac-Vite-Dev.xcscheme @@ -1,26 +1,11 @@ + LastUpgradeVersion = "1500" + version = "1.7"> - - - - @@ -38,19 +23,11 @@ - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> @@ -65,63 +42,48 @@ - - - + allowLocationSimulation = "YES"> + - - - - - + + debugDocumentVersioning = "YES"> + buildConfiguration = "Vite"> diff --git a/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts.xcscheme b/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Mac.xcscheme similarity index 83% rename from xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts.xcscheme rename to xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Mac.xcscheme index e3d1c38e..d78f492e 100644 --- a/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts.xcscheme +++ b/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/Mac.xcscheme @@ -1,7 +1,7 @@ + LastUpgradeVersion = "1500" + version = "1.7"> @@ -15,7 +15,7 @@ @@ -26,16 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> @@ -64,7 +56,7 @@ @@ -81,7 +73,7 @@ diff --git a/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/iOS-Vite-Dev.xcscheme b/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/iOS-Vite-Dev.xcscheme new file mode 100644 index 00000000..c87e739c --- /dev/null +++ b/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/iOS-Vite-Dev.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme b/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme new file mode 100644 index 00000000..9c0ceed5 --- /dev/null +++ b/xcode/Userscripts.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xcode/iOS-App/AppDelegate.swift b/xcode/iOS-App/AppDelegate.swift deleted file mode 100644 index e1fe4a8a..00000000 --- a/xcode/iOS-App/AppDelegate.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// AppDelegate.swift -// Userscripts-iOS -// -// Created by Justin Wasack on 10/3/21. -// Copyright © 2021 Justin Wasack. All rights reserved. -// - -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - return true - } - - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - -} diff --git a/xcode/iOS-App/Base.lproj/Main.html b/xcode/iOS-App/Base.lproj/Main.html deleted file mode 100644 index e718ab5a..00000000 --- a/xcode/iOS-App/Base.lproj/Main.html +++ /dev/null @@ -1 +0,0 @@ -
    Userscripts App Icon

    You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

    CURRENT DIRECTORY:
    init
    diff --git a/xcode/iOS-App/ViewController.swift b/xcode/iOS-App/ViewController.swift deleted file mode 100644 index f1a3347c..00000000 --- a/xcode/iOS-App/ViewController.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// ViewController.swift -// Userscripts-iOS -// -// Created by Justin Wasack on 10/3/21. -// Copyright © 2021 Justin Wasack. All rights reserved. -// - -import UIKit -import WebKit -import UniformTypeIdentifiers -import os - -fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: #fileID) - -class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler, UIDocumentPickerDelegate { - - @IBOutlet var webView: WKWebView! - - override func viewDidLoad() { - super.viewDidLoad() - let backgroundColor = UIColor.init(red: (47/255.0), green: (51/255.0), blue: (55/255.0), alpha: 1.0) - view.setValue(backgroundColor, forKey: "backgroundColor") - self.webView.isOpaque = false - self.webView.backgroundColor = backgroundColor - self.webView.navigationDelegate = self - self.webView.scrollView.isScrollEnabled = false - self.webView.configuration.userContentController.add(self, name: "controller") - - self.webView.loadFileURL(Bundle.main.url(forResource: "Main", withExtension: "html")!, allowingReadAccessTo: Bundle.main.resourceURL!) - } - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" - let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" - var readLocation:String - if let sharedBookmarkData = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName) { - if let bookmarkUrl = readBookmark(data: sharedBookmarkData, isSecure: true) { - readLocation = bookmarkUrl.absoluteString - } else { - readLocation = "Failed to get read directory" - } - } else { - readLocation = "Select a directory to load userscripts" - } - webView.evaluateJavaScript("printDirectory('\(readLocation)')") - webView.evaluateJavaScript("printVersion('v\(appVersion)', '(\(buildNumber))')") - } - - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if navigationAction.navigationType == .linkActivated { - guard - let url = navigationAction.request.url, - UIApplication.shared.canOpenURL(url) - else { - return - } - UIApplication.shared.open(url) - decisionHandler(.cancel) - } else { - decisionHandler(.allow) - } - } - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - guard let name = message.body as? String else { - logger.error("\(#function, privacy: .public) - Userscripts iOS received a message without a name") - return - } - if name == "SET_READ_LOCATION" { - logger.info("\(#function, privacy: .public) - Userscripts iOS has requested to set the readLocation") - let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) - documentPicker.delegate = self - present(documentPicker, animated: true, completion: nil) - } - } - - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { - // https://developer.apple.com/videos/play/wwdc2018/216 - do { - let shouldStopAccessing = url.startAccessingSecurityScopedResource() - defer { - if shouldStopAccessing {url.stopAccessingSecurityScopedResource()} - } - if saveBookmark(url: url, isShared: true, keyName: SharedDefaults.keyName, isSecure: false) { - webView.evaluateJavaScript("printDirectory('\(url.absoluteString)')") - } else { - throw NSError(domain: "Failed to saved bookmark", code: 0, userInfo: [:]) - } - } catch let error { - logger.error("\(#function, privacy: .public) - \(error, privacy: .public)") - return - } - } -} diff --git a/xcode/xcconfig/Mac-App.xcconfig b/xcode/xcconfig/Mac-App.xcconfig index 76526d70..5bda7200 100644 --- a/xcode/xcconfig/Mac-App.xcconfig +++ b/xcode/xcconfig/Mac-App.xcconfig @@ -1,18 +1,24 @@ +SRC_DIR = App-Mac ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon -CODE_SIGN_ENTITLEMENTS = Mac-App/App.entitlements +CODE_SIGN_ENTITLEMENTS = $(SRC_DIR)/App.entitlements COMBINE_HIDPI_IMAGES = YES CURRENT_PROJECT_VERSION = $(MAC_APP_BUILD) ENABLE_HARDENED_RUNTIME = YES GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = Mac-App/Info.plist +INFOPLIST_FILE = $(SRC_DIR)/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PROJECT_NAME)-$(TARGET_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-$(TARGET_NAME)-Debug +INFOPLIST_KEY_CFBundleDisplayName[config=Vite] = $(PROJECT_NAME)-$(TARGET_NAME)-Vite LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks MARKETING_VERSION = $(MAC_APP_VERSION) PRODUCT_BUNDLE_IDENTIFIER = $(APP_IDENTIFIER) PRODUCT_NAME = $(PROJECT_NAME) PRODUCT_NAME[config=Debug] = $(PROJECT_NAME)-Debug +PRODUCT_NAME[config=Vite] = $(PROJECT_NAME)-Vite + +// Provisioning Profile +PROVISIONING_PROFILE_SPECIFIER[config=Release] = match AppStore $(APP_IDENTIFIER) macos // Override this file -#include? "Mac-App.dev.xcconfig" \ No newline at end of file +#include? "Mac-App.dev.xcconfig" diff --git a/xcode/xcconfig/Mac-Safari-Extension.xcconfig b/xcode/xcconfig/Mac-Safari-Extension.xcconfig index 13f53620..276b9c4f 100644 --- a/xcode/xcconfig/Mac-Safari-Extension.xcconfig +++ b/xcode/xcconfig/Mac-Safari-Extension.xcconfig @@ -1,8 +1,11 @@ -CODE_SIGN_ENTITLEMENTS = Safari-Extension/Mac.entitlements +// Shared config file +#include? "Safari-Extension.xcconfig" + +CODE_SIGN_ENTITLEMENTS = $(SRC_DIR)/Mac.entitlements CURRENT_PROJECT_VERSION = $(MAC_APP_BUILD) ENABLE_HARDENED_RUNTIME = YES GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = Safari-Extension/Info.plist +INFOPLIST_FILE = $(SRC_DIR)/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PRODUCT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PRODUCT_NAME)-Debug LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks @@ -11,5 +14,8 @@ PRODUCT_BUNDLE_IDENTIFIER = $(EXT_IDENTIFIER) PRODUCT_NAME = $(PROJECT_NAME)-$(TARGET_NAME) SKIP_INSTALL = YES +// Provisioning Profile +PROVISIONING_PROFILE_SPECIFIER[config=Release] = match AppStore $(EXT_IDENTIFIER) macos + // Override this file -#include? "Mac-Safari-Extension.dev.xcconfig" \ No newline at end of file +#include? "Mac-Safari-Extension.dev.xcconfig" diff --git a/xcode/xcconfig/Mac-Tests.xcconfig b/xcode/xcconfig/Mac-Tests.xcconfig index 998554bc..01a14d09 100644 --- a/xcode/xcconfig/Mac-Tests.xcconfig +++ b/xcode/xcconfig/Mac-Tests.xcconfig @@ -8,4 +8,4 @@ SWIFT_EMIT_LOC_STRINGS = NO TEST_HOST = $(BUILT_PRODUCTS_DIR)/Userscripts-Debug.app/Contents/MacOS/Userscripts-Debug // Override this file -#include? "Mac-Tests.dev.xcconfig" \ No newline at end of file +#include? "Mac-Tests.dev.xcconfig" diff --git a/xcode/xcconfig/Safari-Extension.xcconfig b/xcode/xcconfig/Safari-Extension.xcconfig new file mode 100644 index 00000000..d2724321 --- /dev/null +++ b/xcode/xcconfig/Safari-Extension.xcconfig @@ -0,0 +1,5 @@ +SRC_DIR = Ext-Safari +MANIFEST_FILE = $(SRC_DIR)/Resources/manifest.json +MANIFEST_NAME = $(PROJECT_NAME) +MANIFEST_NAME[config=Debug] = $(PROJECT_NAME)-Debug +MANIFEST_NAME[config=Vite] = $(PROJECT_NAME)-Vite \ No newline at end of file diff --git a/xcode/xcconfig/Userscripts-Base.xcconfig b/xcode/xcconfig/Userscripts-Base.xcconfig index 7f35556a..6ad773d0 100644 --- a/xcode/xcconfig/Userscripts-Base.xcconfig +++ b/xcode/xcconfig/Userscripts-Base.xcconfig @@ -55,10 +55,11 @@ MTL_FAST_MATH = YES ONLY_ACTIVE_ARCH[config=Debug] = YES SDKROOT = macosx SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Debug] = DEBUG +SWIFT_ACTIVE_COMPILATION_CONDITIONS[config=Vite] = DEBUG SWIFT_COMPILATION_MODE[config=Release] = wholemodule SWIFT_OPTIMIZATION_LEVEL[config=Debug] = -Onone SWIFT_OPTIMIZATION_LEVEL[config=Release] = -O SWIFT_VERSION = 5.0 // Override this file -#include? "Userscripts-Base.dev.xcconfig" \ No newline at end of file +#include? "Userscripts-Base.dev.xcconfig" diff --git a/xcode/xcconfig/Userscripts-Debug.xcconfig b/xcode/xcconfig/Userscripts-Debug.xcconfig index f7b4cc7f..c596ca04 100644 --- a/xcode/xcconfig/Userscripts-Debug.xcconfig +++ b/xcode/xcconfig/Userscripts-Debug.xcconfig @@ -9,7 +9,7 @@ MAC_APP_VERSION = 1.0.0 // Sign to Run Locally CODE_SIGN_IDENTITY = - // Organization Identifier -ORG_IDENTIFIER = debug +ORG_IDENTIFIER = dev.debug // Application Identifier APP_IDENTIFIER = $(ORG_IDENTIFIER).userscripts // Safari Extension Identifier @@ -21,4 +21,4 @@ MAC_SHARED_GID = 00000.$(ORG_IDENTIFIER).userscripts APP_URL_SCHEME = userscriptsdebug // Override this file -#include? "Userscripts-Debug.dev.xcconfig" \ No newline at end of file +#include? "Userscripts-Debug.dev.xcconfig" diff --git a/xcode/xcconfig/Userscripts-Release.xcconfig b/xcode/xcconfig/Userscripts-Release.xcconfig index 516d42eb..9f8d06cd 100644 --- a/xcode/xcconfig/Userscripts-Release.xcconfig +++ b/xcode/xcconfig/Userscripts-Release.xcconfig @@ -1,22 +1,26 @@ #include "Userscripts-Base.xcconfig" -INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2018–2023 Justin Wasack. All rights reserved. -IOS_APP_BUILD = 53 -IOS_APP_VERSION = 1.4.4 -MAC_APP_BUILD = 80 -MAC_APP_VERSION = 4.4.4 +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © 2018–2024 Justin Wasack. All rights reserved. +IOS_APP_BUILD = 57 +IOS_APP_VERSION = 1.5.0 +MAC_APP_BUILD = 83 +MAC_APP_VERSION = 4.5.0 +// Distribution +CODE_SIGN_STYLE = Manual +CODE_SIGN_IDENTITY = Apple Distribution // Developer Team ID DEVELOPMENT_TEAM = J74Q8V8V8N // Organization Identifier ORG_IDENTIFIER = com.userscripts // Application Identifier -APP_IDENTIFIER = com.userscripts.macos +APP_IDENTIFIER = $(ORG_IDENTIFIER).macos // Safari Extension Identifier EXT_IDENTIFIER = $(APP_IDENTIFIER).Userscripts-Extension // App Groups and Group Containers -IOS_SHARED_GID = group.com.userscripts.ios -MAC_SHARED_GID = $(DEVELOPMENT_TEAM).com.userscripts.macos +// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups +IOS_SHARED_GID = group.$(ORG_IDENTIFIER).ios +MAC_SHARED_GID = $(DEVELOPMENT_TEAM).$(ORG_IDENTIFIER).macos // App URL Scheme APP_URL_SCHEME = userscriptsurlscheme diff --git a/xcode/xcconfig/Userscripts-Vite.xcconfig b/xcode/xcconfig/Userscripts-Vite.xcconfig new file mode 100644 index 00000000..1635bab4 --- /dev/null +++ b/xcode/xcconfig/Userscripts-Vite.xcconfig @@ -0,0 +1,24 @@ +#include "Userscripts-Base.xcconfig" + +INFOPLIST_KEY_NSHumanReadableCopyright = Copyright © Userscripts +IOS_APP_BUILD = 2 +IOS_APP_VERSION = 2.0.0 +MAC_APP_BUILD = 5 +MAC_APP_VERSION = 5.0.0 + +// Sign to Run Locally +CODE_SIGN_IDENTITY = - +// Organization Identifier +ORG_IDENTIFIER = dev.vite +// Application Identifier +APP_IDENTIFIER = $(ORG_IDENTIFIER).userscripts +// Safari Extension Identifier +EXT_IDENTIFIER = $(APP_IDENTIFIER).ext.safari +// App Groups and Group Containers +IOS_SHARED_GID = group.$(ORG_IDENTIFIER).userscripts +MAC_SHARED_GID = 00000.$(ORG_IDENTIFIER).userscripts +// App URL Scheme +APP_URL_SCHEME = userscripts-vite + +// Override this file +#include? "Userscripts-Vite.dev.xcconfig" diff --git a/xcode/xcconfig/iOS-App.xcconfig b/xcode/xcconfig/iOS-App.xcconfig index f52d56d6..ba364700 100644 --- a/xcode/xcconfig/iOS-App.xcconfig +++ b/xcode/xcconfig/iOS-App.xcconfig @@ -1,12 +1,14 @@ +SRC_DIR = App-iOS ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor -CODE_SIGN_ENTITLEMENTS = iOS-App/App.entitlements +CODE_SIGN_ENTITLEMENTS = $(SRC_DIR)/App.entitlements CURRENT_PROJECT_VERSION = $(IOS_APP_BUILD) GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = iOS-App/Info.plist +INFOPLIST_FILE = $(SRC_DIR)/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PROJECT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-D +INFOPLIST_KEY_CFBundleDisplayName[config=Vite] = $(PROJECT_NAME)-V INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen INFOPLIST_KEY_UIMainStoryboardFile = Main @@ -26,5 +28,8 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO TARGETED_DEVICE_FAMILY = 1,2 VALIDATE_PRODUCT[config=Release] = YES +// Provisioning Profile +PROVISIONING_PROFILE_SPECIFIER[config=Release] = match AppStore $(APP_IDENTIFIER) + // Override this file -#include? "iOS-App.dev.xcconfig" \ No newline at end of file +#include? "iOS-App.dev.xcconfig" diff --git a/xcode/xcconfig/iOS-Safari-Extension.xcconfig b/xcode/xcconfig/iOS-Safari-Extension.xcconfig index 0bdd357f..93e2e2a3 100644 --- a/xcode/xcconfig/iOS-Safari-Extension.xcconfig +++ b/xcode/xcconfig/iOS-Safari-Extension.xcconfig @@ -1,7 +1,10 @@ -CODE_SIGN_ENTITLEMENTS = Safari-Extension/iOS.entitlements +// Shared config file +#include? "Safari-Extension.xcconfig" + +CODE_SIGN_ENTITLEMENTS = $(SRC_DIR)/iOS.entitlements CURRENT_PROJECT_VERSION = $(IOS_APP_BUILD) GENERATE_INFOPLIST_FILE = YES -INFOPLIST_FILE = Safari-Extension/Info.plist +INFOPLIST_FILE = $(SRC_DIR)/Info.plist INFOPLIST_KEY_CFBundleDisplayName = $(PRODUCT_NAME) INFOPLIST_KEY_CFBundleDisplayName[config=Debug] = $(PROJECT_NAME)-D LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks @@ -15,5 +18,8 @@ SWIFT_EMIT_LOC_STRINGS = YES TARGETED_DEVICE_FAMILY = 1,2 VALIDATE_PRODUCT[config=Release] = YES +// Provisioning Profile +PROVISIONING_PROFILE_SPECIFIER[config=Release] = match AppStore $(EXT_IDENTIFIER) + // Override this file -#include? "iOS-Safari-Extension.dev.xcconfig" \ No newline at end of file +#include? "iOS-Safari-Extension.dev.xcconfig"