sourcemonkey 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// Display helpers shared by the collections page, the userscript viewer,
|
|
2
|
+
// the script preview, and the service worker.
|
|
3
|
+
//
|
|
4
|
+
// Two groups live here: the script-status row formatting (size +
|
|
5
|
+
// status label) both pages render, and the name/path formatting that
|
|
6
|
+
// turns a stored URL into something a human wants to read. Pure
|
|
7
|
+
// functions — no DOM, no chrome.* calls. (`version-probe.js` is the
|
|
8
|
+
// pure half of the probe module; its fetching half lives in
|
|
9
|
+
// `version-probe-fetch.js` and is deliberately not reachable here.)
|
|
10
|
+
import { getValue, joinFullSource } from './userscript-header.js';
|
|
11
|
+
import { outdatedProbe, probeLabel } from './version-probe.js';
|
|
12
|
+
import { describeDiagnostic } from './diagnostic-location.js';
|
|
13
|
+
// ---------- Names and paths ----------
|
|
14
|
+
// Anything of the form `<scheme>://…`. Used to decide whether
|
|
15
|
+
// percent-decoding is safe: URL path segments are encoded, on-disk
|
|
16
|
+
// names are literal.
|
|
17
|
+
const ABSOLUTE_URL_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
18
|
+
// Percent-decode for display. Never throws: a lone `%` (a literal
|
|
19
|
+
// percent in a filename) would make decodeURIComponent raise URIError,
|
|
20
|
+
// and losing the whole string is far worse than leaving one escape
|
|
21
|
+
// undecoded.
|
|
22
|
+
// Tooltip on a script's enable checkbox, wherever one is drawn: the
|
|
23
|
+
// collections page's per-row toggle, the viewer's controls row, the
|
|
24
|
+
// "Other versions" panel, and the toolbar popup's rows. The viewer and
|
|
25
|
+
// the popup append a multi-enable hint to it where other copies exist
|
|
26
|
+
// (see `enableTitle` in solo-click.ts); the collections page shows it
|
|
27
|
+
// bare.
|
|
28
|
+
export const ENABLE_TITLE = 'Enable this script';
|
|
29
|
+
export function decodeForDisplay(text) {
|
|
30
|
+
try {
|
|
31
|
+
return decodeURIComponent(text);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Last `/`-separated segment of a path. No decoding — callers that
|
|
38
|
+
// want a display form pass the result through decodeForDisplay.
|
|
39
|
+
export function leafName(path) {
|
|
40
|
+
const slash = path.lastIndexOf('/');
|
|
41
|
+
return slash < 0 ? path : path.slice(slash + 1);
|
|
42
|
+
}
|
|
43
|
+
// Readable form of a stored location — the string a heading, tooltip,
|
|
44
|
+
// or title shows. Scripts installed from the web (GreasyFork above
|
|
45
|
+
// all) carry their human name inside the URL path, so the stored URL
|
|
46
|
+
// is a wall of `%20` / `%F0%9F…`; Chrome's own omnibox shows those
|
|
47
|
+
// decoded and so do we.
|
|
48
|
+
//
|
|
49
|
+
// Only absolute URLs are decoded. A relativePath from a local directory
|
|
50
|
+
// collection is a literal on-disk name, where a `%20` really is a
|
|
51
|
+
// percent followed by two digits.
|
|
52
|
+
//
|
|
53
|
+
// Display only: hrefs, clipboard copies, fetches, and stored values
|
|
54
|
+
// all keep the encoded form.
|
|
55
|
+
export function displayLocation(urlOrPath) {
|
|
56
|
+
return ABSOLUTE_URL_RE.test(urlOrPath) ? decodeForDisplay(urlOrPath) : urlOrPath;
|
|
57
|
+
}
|
|
58
|
+
// Short display name for a script's location: the URL's filename (no
|
|
59
|
+
// query string, decoded), or the last path segment for an on-disk
|
|
60
|
+
// relativePath. Falls back to the whole input when a URL-shaped string
|
|
61
|
+
// won't parse or has an empty path, so the caller always has something
|
|
62
|
+
// to render.
|
|
63
|
+
//
|
|
64
|
+
// The second `leafName` is not redundant: an escaped `%2F` decodes to
|
|
65
|
+
// a real slash, and this feeds log IDs — a "basename" carrying a path
|
|
66
|
+
// separator would read as a directory in every log line.
|
|
67
|
+
export function scriptBasename(urlOrPath) {
|
|
68
|
+
if (!ABSOLUTE_URL_RE.test(urlOrPath))
|
|
69
|
+
return leafName(urlOrPath);
|
|
70
|
+
try {
|
|
71
|
+
return leafName(decodeForDisplay(leafName(new URL(urlOrPath).pathname))) || urlOrPath;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return leafName(urlOrPath);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// The trailing suffix a log ID carries: the script's number (`#E4`),
|
|
78
|
+
// or `#N` for an unnumbered script sharing its basename; '' for none.
|
|
79
|
+
// Anchored to that shape rather than the first `#` in the string: log
|
|
80
|
+
// IDs are decoded filenames, so a script whose URL carries a `%23` has
|
|
81
|
+
// a literal `#` mid-name and would otherwise render half its own name
|
|
82
|
+
// as a suffix.
|
|
83
|
+
export function logIdSuffix(logId) {
|
|
84
|
+
return /#E?\d+$/.exec(logId)?.[0] ?? '';
|
|
85
|
+
}
|
|
86
|
+
/** The script number a log ID ends with (`foo.user.js#E4` → 4), or null for none. */
|
|
87
|
+
export function logIdScriptNumber(logId) {
|
|
88
|
+
const n = /#E(\d+)$/.exec(logId)?.[1];
|
|
89
|
+
return n === undefined ? null : Number(n);
|
|
90
|
+
}
|
|
91
|
+
// The name to show for a script in badges, menus, the collections list,
|
|
92
|
+
// and match listings: its `@name` when it declares a usable one, else
|
|
93
|
+
// its location in readable form. Shared so every surface labels the
|
|
94
|
+
// same script the same way.
|
|
95
|
+
//
|
|
96
|
+
// A whitespace-only `@name` counts as no name — a header that parsed
|
|
97
|
+
// but declared `// @name ` must not paint a blank row where a name
|
|
98
|
+
// belongs.
|
|
99
|
+
//
|
|
100
|
+
// `fallback` lets a caller substitute its own location text (the
|
|
101
|
+
// collections page shortens a container script's URL to its basename);
|
|
102
|
+
// omitted, the whole location is used.
|
|
103
|
+
export function scriptDisplayName(script, fallback) {
|
|
104
|
+
return headerDisplayName(script.header) || fallback || displayLocation(script.relativePath);
|
|
105
|
+
}
|
|
106
|
+
// The order scripts are listed in wherever names are shown in a list:
|
|
107
|
+
// case-insensitive, with numbers compared by value ("Item 9" before
|
|
108
|
+
// "Item 10"). Shared so every list orders the same scripts alike.
|
|
109
|
+
const nameCollator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
|
|
110
|
+
export function compareScriptNames(a, b) {
|
|
111
|
+
return nameCollator.compare(a, b);
|
|
112
|
+
}
|
|
113
|
+
// The `@name` a header declares, or '' when it declares none usable.
|
|
114
|
+
// Split out so the viewer's heading label and every other surface agree
|
|
115
|
+
// on what counts as a name — a future change to the rule (a localized
|
|
116
|
+
// `@name:en`, different trimming) must not land in only one of them.
|
|
117
|
+
export function headerDisplayName(header) {
|
|
118
|
+
return (header ? getValue(header, 'name')?.trim() : undefined) || '';
|
|
119
|
+
}
|
|
120
|
+
// The script's `@name`, or its location when it declares no usable one.
|
|
121
|
+
// The location is NOT appended to a name that exists — a heading
|
|
122
|
+
// carrying both runs the width of the window, and the details panel's
|
|
123
|
+
// `Location` row is where a reader goes for the full path.
|
|
124
|
+
//
|
|
125
|
+
// `headerDisplayName`, not a local read of `@name`: the viewer must not
|
|
126
|
+
// disagree with the collections page about what a script is called.
|
|
127
|
+
export function scriptLabel(header, location) {
|
|
128
|
+
return headerDisplayName(header) || displayLocation(location);
|
|
129
|
+
}
|
|
130
|
+
// Lives here rather than in collection-display.ts, its first caller: this
|
|
131
|
+
// module is a leaf, and page-heading.ts needs the helper on the early
|
|
132
|
+
// path where dragging in collection-display's scan/network graph would
|
|
133
|
+
// undo the point of loading early.
|
|
134
|
+
// Append a path / URL string to an element, inserting a `<wbr>` after
|
|
135
|
+
// each `/` so the browser prefers slash boundaries when wrapping a
|
|
136
|
+
// long value. Combined with `overflow-wrap: anywhere` on the element,
|
|
137
|
+
// the wrap order becomes: spaces first, then slashes, then anywhere
|
|
138
|
+
// inside a word only when nothing else fits. The slash itself stays
|
|
139
|
+
// attached to the preceding segment so it reads as `foo/` then a
|
|
140
|
+
// break, not a `/bar` continuation.
|
|
141
|
+
//
|
|
142
|
+
// Use this for any field that may render a long path or URL: the
|
|
143
|
+
// collections page display name, the viewer's Location row, the Match /
|
|
144
|
+
// Exclude pattern rows, etc.
|
|
145
|
+
export function appendWithSlashBreaks(el, text) {
|
|
146
|
+
const doc = el.ownerDocument;
|
|
147
|
+
const parts = text.split('/');
|
|
148
|
+
parts.forEach((part, i) => {
|
|
149
|
+
if (i > 0) {
|
|
150
|
+
el.appendChild(doc.createTextNode('/'));
|
|
151
|
+
el.appendChild(doc.createElement('wbr'));
|
|
152
|
+
}
|
|
153
|
+
if (part)
|
|
154
|
+
el.appendChild(doc.createTextNode(part));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// Format a byte count for the size column. Returns "12 KB", "950 B",
|
|
158
|
+
// "1.4 MB", etc. — short enough to fit comfortably alongside filename +
|
|
159
|
+
// status. Whole KB: a tenth of a KB is never the interesting part. The
|
|
160
|
+
// MB switch is on the ROUNDED KB, so 1023.6 KB reads "1.0 MB", not
|
|
161
|
+
// "1024 KB".
|
|
162
|
+
export function formatBytes(size) {
|
|
163
|
+
if (size < 1024)
|
|
164
|
+
return `${size} B`;
|
|
165
|
+
const kb = Math.round(size / 1024);
|
|
166
|
+
if (kb < 1024)
|
|
167
|
+
return `${kb} KB`;
|
|
168
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
169
|
+
}
|
|
170
|
+
// Storage use against a quota, both in MB: `9.9 MB of 10 MB (99%)`.
|
|
171
|
+
// One unit on both sides, since `10,106 KB of 10 MB` reads as over the
|
|
172
|
+
// limit at a glance. Rounded DOWN, so a store that still has room never
|
|
173
|
+
// reads as `10.0 MB of 10 MB (100%)`.
|
|
174
|
+
export function formatStorageUsage(used, quota) {
|
|
175
|
+
const tenths = Math.floor((used / (1024 * 1024)) * 10) / 10;
|
|
176
|
+
const percent = Math.floor((used / quota) * 100);
|
|
177
|
+
return `${tenths.toFixed(1)} MB of ${formatQuota(quota)} (${percent}%)`;
|
|
178
|
+
}
|
|
179
|
+
// A storage quota: `10 MB` for a whole number of MB (Chrome's is), not
|
|
180
|
+
// `10.0 MB`.
|
|
181
|
+
export function formatQuota(bytes) {
|
|
182
|
+
const mb = bytes / (1024 * 1024);
|
|
183
|
+
return Number.isInteger(mb) ? `${mb} MB` : `${mb.toFixed(1)} MB`;
|
|
184
|
+
}
|
|
185
|
+
// Text shown in the "Size" cell. Size is independent of status:
|
|
186
|
+
// `invalid` rows that retain prior content still show its byte count,
|
|
187
|
+
// and a 0-byte file shows "0 B" (distinct from unknown). Only rows
|
|
188
|
+
// that have never successfully loaded — `size == null` — render as
|
|
189
|
+
// "—" to signal "unknown".
|
|
190
|
+
export function formatScriptSize(script) {
|
|
191
|
+
if (script.size == null)
|
|
192
|
+
return '—';
|
|
193
|
+
return formatBytes(script.size);
|
|
194
|
+
}
|
|
195
|
+
export const LOAD_STATES = ['ok', 'invalid', 'rejected', 'refresh-failed', 'outdated'];
|
|
196
|
+
export function isLoadState(value) {
|
|
197
|
+
return LOAD_STATES.includes(value);
|
|
198
|
+
}
|
|
199
|
+
// `outdated` and `rejected` come in as caller-computed flags rather
|
|
200
|
+
// than being derived here: neither lives on the script row (one is a
|
|
201
|
+
// version probe, the other is session state the injector writes), and
|
|
202
|
+
// the collections page's repaint path rebuilds status cells from DOM
|
|
203
|
+
// datasets with no script row in hand. Both callers read `outdated`
|
|
204
|
+
// from `scriptOutdatedProbe` below and `rejected` from the shared
|
|
205
|
+
// rejection map, so the two surfaces can't drift.
|
|
206
|
+
export function effectiveScriptStatus(script, maxLogLevel, outdated = false, rejected = false) {
|
|
207
|
+
if (script.status === 'invalid')
|
|
208
|
+
return 'invalid';
|
|
209
|
+
if (rejected)
|
|
210
|
+
return 'rejected';
|
|
211
|
+
if (script.refreshError)
|
|
212
|
+
return 'refresh-failed';
|
|
213
|
+
if (outdated)
|
|
214
|
+
return 'outdated';
|
|
215
|
+
if (maxLogLevel === 'error')
|
|
216
|
+
return 'error';
|
|
217
|
+
if (maxLogLevel === 'warning')
|
|
218
|
+
return 'warning';
|
|
219
|
+
return 'ok';
|
|
220
|
+
}
|
|
221
|
+
// The probe (if any) saying an upstream copy is ahead of the version we
|
|
222
|
+
// hold. Null whenever nothing is ahead, nothing was probed, or either
|
|
223
|
+
// side has no `@version` to compare.
|
|
224
|
+
export function scriptOutdatedProbe(script) {
|
|
225
|
+
const loadedVersion = script.header ? getValue(script.header, 'version') : undefined;
|
|
226
|
+
return outdatedProbe(loadedVersion, script.versionProbes);
|
|
227
|
+
}
|
|
228
|
+
// Text shown in the "Status" cell. REJECTED means the script is fine but Chrome refused to register it. REFRESH
|
|
229
|
+
// FAILED means the script still runs from its cached copy but the last
|
|
230
|
+
// update attempt failed. OUTDATED means a newer version exists at a URL
|
|
231
|
+
// we deliberately don't update from. WARNING / ERROR mean the script
|
|
232
|
+
// loaded fine but its Log carries entries at that level (see
|
|
233
|
+
// `effectiveScriptStatus`).
|
|
234
|
+
export function formatScriptStatusLabel(status) {
|
|
235
|
+
switch (status) {
|
|
236
|
+
case 'ok': return 'OK';
|
|
237
|
+
case 'invalid': return 'INVALID';
|
|
238
|
+
case 'rejected': return 'REJECTED';
|
|
239
|
+
case 'refresh-failed': return 'REFRESH FAILED';
|
|
240
|
+
case 'outdated': return 'OUTDATED';
|
|
241
|
+
case 'warning': return 'WARNING';
|
|
242
|
+
case 'error': return 'ERROR';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// The viewer URL for a stored script, optionally landing on its Log
|
|
246
|
+
// pane. Shared by every in-page surface that links to or navigates to
|
|
247
|
+
// a script, so none of them can drift on how the tab is addressed.
|
|
248
|
+
//
|
|
249
|
+
// Page-relative, not the absolute `chrome.runtime.getURL(...)` form:
|
|
250
|
+
// every extension page lives at the extension root, so the browser
|
|
251
|
+
// resolves it against the current page, and the long
|
|
252
|
+
// `chrome-extension://<id>/` prefix stays out of a table that can hold
|
|
253
|
+
// hundreds of rows with two links each. The service worker builds the
|
|
254
|
+
// absolute form instead (`viewerUrlForScript` in background.ts)
|
|
255
|
+
// because it has no page to be relative to.
|
|
256
|
+
export function viewerUrl(scriptId, tab) {
|
|
257
|
+
const params = new URLSearchParams({ script: scriptId });
|
|
258
|
+
// The viewer parses `#Log` via parseHash and lands on the Log pane.
|
|
259
|
+
return `userscript.html?${params.toString()}${tab === 'log' ? '#Log' : ''}`;
|
|
260
|
+
}
|
|
261
|
+
// The compare page for two copies of a script (src/extension/compare.ts). The
|
|
262
|
+
// right-hand side is the base: the copy on screen, or the download a
|
|
263
|
+
// preview is showing; `other` is the copy to hold up against it.
|
|
264
|
+
export function compareUrl(base, other) {
|
|
265
|
+
const params = new URLSearchParams('script' in base ? { script: base.script } : { preview: base.preview });
|
|
266
|
+
params.set('other', other);
|
|
267
|
+
return `compare.html?${params.toString()}`;
|
|
268
|
+
}
|
|
269
|
+
// The install-preview URL for a location the user typed or pasted.
|
|
270
|
+
// Page-relative for the same reason `viewerUrl` is; callers that need
|
|
271
|
+
// the absolute form wrap it in `chrome.runtime.getURL`.
|
|
272
|
+
export function previewUrl(location) {
|
|
273
|
+
return `userscript.html?preview=${encodeURIComponent(location)}`;
|
|
274
|
+
}
|
|
275
|
+
// What a script's Status cell should say. The label follows
|
|
276
|
+
// `effectiveScriptStatus`; the hover text follows whatever the cell is
|
|
277
|
+
// actually showing — Chrome's rejection reason, the refresh-failure
|
|
278
|
+
// reason, the newer version found upstream, or the load error.
|
|
279
|
+
//
|
|
280
|
+
// Shared by the viewer's own Status row and the "Other versions"
|
|
281
|
+
// panel's Status column, which would otherwise explain the same
|
|
282
|
+
// verdict two ways. The collections page keeps its own copy of this
|
|
283
|
+
// logic: its repaint path rebuilds status cells from DOM datasets with
|
|
284
|
+
// no script row in hand, which is exactly what this takes.
|
|
285
|
+
export function scriptStatusView(script, maxLogLevel, rejection) {
|
|
286
|
+
const outdated = scriptOutdatedProbe(script);
|
|
287
|
+
const status = effectiveScriptStatus(script, maxLogLevel, outdated !== null, rejection !== undefined);
|
|
288
|
+
return { status, label: formatScriptStatusLabel(status), title: statusTitle() };
|
|
289
|
+
function statusTitle() {
|
|
290
|
+
if (status === 'rejected' && rejection)
|
|
291
|
+
return rejection;
|
|
292
|
+
if (status === 'refresh-failed')
|
|
293
|
+
return script.refreshError;
|
|
294
|
+
if (status === 'outdated' && outdated) {
|
|
295
|
+
return `Version ${outdated.version} at ${probeLabel(outdated.kinds)}: ${outdated.url}`;
|
|
296
|
+
}
|
|
297
|
+
if (script.errorMessage)
|
|
298
|
+
return describeDiagnostic(script.errorMessage, script.errorLocation);
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Drop a Subresource Integrity fragment from a `@require` / `@resource`
|
|
303
|
+
// reference, for DISPLAY only.
|
|
304
|
+
//
|
|
305
|
+
// Lives here rather than in `file-loader.ts` (which owns resolution and
|
|
306
|
+
// fetching) precisely because loading must never use it — it sits
|
|
307
|
+
// beside `displayLocation`, the other transform every human-facing URL
|
|
308
|
+
// goes through.
|
|
309
|
+
//
|
|
310
|
+
// Tampermonkey carries the expected hash in the URL fragment, as a
|
|
311
|
+
// comma-separated list of `<algo>=<hex>` or `<algo>-<base64>` parts:
|
|
312
|
+
//
|
|
313
|
+
// @require https://example.com/lib.js#sha256=e3b0c44298fc1c149…
|
|
314
|
+
// @require https://example.com/lib.js#md5=abc,sha256=def
|
|
315
|
+
//
|
|
316
|
+
// A 64-hex-digit tail is most of the width of a tooltip or a details
|
|
317
|
+
// row and tells the reader nothing they can act on, so every surface
|
|
318
|
+
// that shows a ref to a human strips it. Fetching, resolution, dedup,
|
|
319
|
+
// storage, and error messages all keep the verbatim form — an error
|
|
320
|
+
// should name what the author actually wrote.
|
|
321
|
+
//
|
|
322
|
+
// Only stripped when EVERY comma-separated part looks like a hash
|
|
323
|
+
// declaration. An ordinary fragment (`#section`) is content the author
|
|
324
|
+
// meant, so it survives; a malformed hash tail survives too, which is
|
|
325
|
+
// the right way round — a reader wondering why validation didn't happen
|
|
326
|
+
// needs to see what they typed.
|
|
327
|
+
//
|
|
328
|
+
// SourceMonkey does not yet validate these hashes; see TODO.md.
|
|
329
|
+
const SRI_PART_RE = /^(?:md5|sha1|sha256|sha384|sha512)[=-]\S+$/i;
|
|
330
|
+
export function stripSriFragment(urlOrRef) {
|
|
331
|
+
const hash = urlOrRef.indexOf('#');
|
|
332
|
+
if (hash < 0)
|
|
333
|
+
return urlOrRef;
|
|
334
|
+
const fragment = urlOrRef.slice(hash + 1);
|
|
335
|
+
if (!fragment)
|
|
336
|
+
return urlOrRef;
|
|
337
|
+
if (!fragment.split(',').every((part) => SRI_PART_RE.test(part)))
|
|
338
|
+
return urlOrRef;
|
|
339
|
+
return urlOrRef.slice(0, hash);
|
|
340
|
+
}
|
|
341
|
+
// A stored script's text as it was on disk, rebuilt from the stored
|
|
342
|
+
// split: what is above the header, the header, the body. The same
|
|
343
|
+
// string the injector splices into the registered code, so the viewer's
|
|
344
|
+
// main tab line N matches the file's line N AND the injected line N
|
|
345
|
+
// (modulo any @require sections that sit before the main script). The
|
|
346
|
+
// compare page diffs this too.
|
|
347
|
+
export function fullSource(script) {
|
|
348
|
+
return joinFullSource(script.leadingText ?? '', script.headerText ?? '', script.bodyText);
|
|
349
|
+
}
|
|
350
|
+
// Pull a display basename off a require's resolved URL. Falls back to
|
|
351
|
+
// the verbatim ref if URL parsing fails — won't happen for entries we
|
|
352
|
+
// stored (the resolver wouldn't have accepted the URL otherwise), but
|
|
353
|
+
// the fallback keeps a future shape change from breaking the tab UI.
|
|
354
|
+
// Not `scriptBasename`: that one falls back to the whole URL, and a
|
|
355
|
+
// require tab labeled with a full CDN URL would wreck the tab strip.
|
|
356
|
+
export function requireBasename(url, ref) {
|
|
357
|
+
let name;
|
|
358
|
+
try {
|
|
359
|
+
name = leafName(new URL(url).pathname);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return ref;
|
|
363
|
+
}
|
|
364
|
+
return name ? decodeForDisplay(name) : ref;
|
|
365
|
+
}
|
|
366
|
+
// The counts in words, per kind, since each is a different thing to go
|
|
367
|
+
// and check: `1 file modified, 2 new files`. Empty when nothing differs.
|
|
368
|
+
export function pendingSummary(counts) {
|
|
369
|
+
const files = (n) => `${n} ${n === 1 ? 'file' : 'files'}`;
|
|
370
|
+
const parts = [];
|
|
371
|
+
if (counts.manifest)
|
|
372
|
+
parts.push('the manifest changed');
|
|
373
|
+
if (counts.modified > 0)
|
|
374
|
+
parts.push(`${files(counts.modified)} modified`);
|
|
375
|
+
if (counts.deleted > 0)
|
|
376
|
+
parts.push(`${files(counts.deleted)} deleted`);
|
|
377
|
+
if (counts.added > 0)
|
|
378
|
+
parts.push(`${counts.added} new ${counts.added === 1 ? 'file' : 'files'}`);
|
|
379
|
+
if (parts.length === 0)
|
|
380
|
+
return '';
|
|
381
|
+
const text = parts.join(', ');
|
|
382
|
+
return `${text[0].toUpperCase()}${text.slice(1)}`;
|
|
383
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// A script's own icon (`@icon` and its aliases): which header tag names
|
|
2
|
+
// it, and the rules an icon file has to meet to be shown. Shared by the
|
|
3
|
+
// pages that paint icons (`extension/site-icon.ts`) and the load-time
|
|
4
|
+
// check that warns about an icon that can't be shown
|
|
5
|
+
// (`checkScriptIcon` in `external-files.ts`), so both apply one rule.
|
|
6
|
+
// Tag priority for a script-declared icon. `@icon` is the only form
|
|
7
|
+
// documented by all three managers (TM/VM/GM, i.e. Tampermonkey,
|
|
8
|
+
// Violentmonkey, Greasemonkey); the rest are TM-only aliases. The
|
|
9
|
+
// smaller variants come first so a script that declares both a regular
|
|
10
|
+
// and a 64×64 icon picks the one closer to our 16×16 display size.
|
|
11
|
+
const ICON_HEADER_TAGS = ['icon', 'iconurl', 'defaulticon', 'icon64url', 'icon64'];
|
|
12
|
+
// Largest icon file we will show. An icon is drawn at 16px, so anything
|
|
13
|
+
// near this is far bigger than it needs to be.
|
|
14
|
+
export const MAX_ICON_BYTES = 128 * 1024;
|
|
15
|
+
// Whether a response's Content-Type is one an icon can have.
|
|
16
|
+
export function isIconContentType(contentType) {
|
|
17
|
+
return contentType.startsWith('image/');
|
|
18
|
+
}
|
|
19
|
+
// The icon declaration that wins: its reference, raw (just trimmed and
|
|
20
|
+
// non-empty), and the header line it is on. Null when none is set.
|
|
21
|
+
export function iconDeclFromHeader(header) {
|
|
22
|
+
if (!header)
|
|
23
|
+
return null;
|
|
24
|
+
for (const tag of ICON_HEADER_TAGS) {
|
|
25
|
+
for (const e of header.tags) {
|
|
26
|
+
if (e.tag !== tag || e.locale !== undefined)
|
|
27
|
+
continue;
|
|
28
|
+
const trimmed = e.value.trim();
|
|
29
|
+
if (trimmed)
|
|
30
|
+
return e.line === undefined ? { ref: trimmed } : { ref: trimmed, line: e.line };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
// Read the highest-priority icon reference declared in the parsed
|
|
36
|
+
// metadata, or null when none is set. The reference comes back raw
|
|
37
|
+
// (just trimmed + non-empty); resolving it into an absolute URL —
|
|
38
|
+
// including the relative-path-against-script-parent rules and `..`
|
|
39
|
+
// rejection — is the file-loader's job and happens at render time
|
|
40
|
+
// when the script's `fileUrl` is in scope.
|
|
41
|
+
export function iconRefFromHeader(header) {
|
|
42
|
+
return iconDeclFromHeader(header)?.ref ?? null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Pure helpers for the userscript viewer's link rendering. Kept
|
|
2
|
+
// DOM-free so the URL-detection + tag-classification rules can be
|
|
3
|
+
// unit tested in Node without a Prism-shaped DOM.
|
|
4
|
+
//
|
|
5
|
+
// Two consumers in src/extension/userscript.ts:
|
|
6
|
+
// - The Details panel renderer (Match / Location / etc.).
|
|
7
|
+
// - The post-Prism walk over the highlighted code box.
|
|
8
|
+
// Both call the same classifiers so a header line in the body is
|
|
9
|
+
// linkified by the same rules as the corresponding details row.
|
|
10
|
+
// Tags whose value is a single resource reference. The body-linkifier
|
|
11
|
+
// passes the value through file-loader's `resolveScriptResource`,
|
|
12
|
+
// which both linkifies absolute http(s) URLs and resolves relative
|
|
13
|
+
// paths against the script's own URL (the same rule the `@icon`
|
|
14
|
+
// favicon path uses). Anything the resolver rejects falls
|
|
15
|
+
// back to plain text.
|
|
16
|
+
//
|
|
17
|
+
// `@resource` is *not* in this set even though its second token is a
|
|
18
|
+
// URL — its grammar is two args (`<name> <url>`), so it gets its own
|
|
19
|
+
// carve-out in `linkForHeaderTag` (src/extension/userscript.ts) that linkifies
|
|
20
|
+
// only the URL portion. Treating it here would mis-resolve
|
|
21
|
+
// `myCSS https://example.com/x.css` as a single relative path
|
|
22
|
+
// `<parent>/myCSS%20https://...`.
|
|
23
|
+
export const RESOLVABLE_HEADER_TAGS = new Set([
|
|
24
|
+
'icon', 'iconurl', 'defaulticon', 'icon64', 'icon64url',
|
|
25
|
+
'require',
|
|
26
|
+
'updateurl', 'downloadurl',
|
|
27
|
+
'homepage', 'homepageurl', 'website', 'source',
|
|
28
|
+
'supporturl',
|
|
29
|
+
]);
|
|
30
|
+
// Tags whose value is shaped like a Chrome `@match` pattern. Formally
|
|
31
|
+
// `@include` and `@exclude` are free-form globs over the whole URL
|
|
32
|
+
// (or `/regex/` literals), but in practice almost every excluded /
|
|
33
|
+
// included pattern is written as a match pattern, and giving the
|
|
34
|
+
// user a click target is more useful than rendering plain text.
|
|
35
|
+
// `matchTargetInfo` returns null for shapes it can't dewildcard
|
|
36
|
+
// (regex literals, `<all_urls>`, host wildcards) and those still
|
|
37
|
+
// fall back to plain text.
|
|
38
|
+
export const MATCH_HEADER_TAGS = new Set([
|
|
39
|
+
'match', 'exclude', 'include',
|
|
40
|
+
]);
|
|
41
|
+
// Word-boundary anchor keeps `https://…` inside a longer identifier
|
|
42
|
+
// (rare but possible inside a string literal) from being picked up
|
|
43
|
+
// mid-token. The character class excludes whitespace, quotes, angle
|
|
44
|
+
// brackets, and backticks so a URL inside `"…"` or `<…>` stops at
|
|
45
|
+
// the delimiter. `*` is also excluded so a `@match` / `@exclude`
|
|
46
|
+
// pattern like `https://example.com/athletic/*` yields a clickable
|
|
47
|
+
// `https://example.com/athletic/` followed by a literal `*` — `*` is
|
|
48
|
+
// technically a legal URL character but in userscript headers it's
|
|
49
|
+
// always a glob wildcard, and including it in the href produces a
|
|
50
|
+
// link that doesn't navigate anywhere useful.
|
|
51
|
+
const URL_RE = /\b(?:https?|file):\/\/[^\s'"<>`*]+/g;
|
|
52
|
+
// Trailing punctuation that's commonly adjacent to URLs in prose.
|
|
53
|
+
// `)` and `]` are conditional (see peelTrailingPunctuation) so a
|
|
54
|
+
// URL with balanced brackets like `…/Foo_(disambig)` keeps them.
|
|
55
|
+
const ALWAYS_PEEL_RE = /[.,;:!?]+$/;
|
|
56
|
+
export function splitTextByUrls(text) {
|
|
57
|
+
const out = [];
|
|
58
|
+
let cursor = 0;
|
|
59
|
+
for (const m of text.matchAll(URL_RE)) {
|
|
60
|
+
const start = m.index ?? 0;
|
|
61
|
+
if (start > cursor) {
|
|
62
|
+
out.push({ kind: 'text', value: text.slice(cursor, start) });
|
|
63
|
+
}
|
|
64
|
+
const { url, trailing } = peelTrailingPunctuation(m[0]);
|
|
65
|
+
out.push({ kind: 'url', value: url });
|
|
66
|
+
if (trailing)
|
|
67
|
+
out.push({ kind: 'text', value: trailing });
|
|
68
|
+
cursor = start + m[0].length;
|
|
69
|
+
}
|
|
70
|
+
if (cursor < text.length) {
|
|
71
|
+
out.push({ kind: 'text', value: text.slice(cursor) });
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
// Peel trailing prose punctuation off a candidate URL. Closing
|
|
76
|
+
// brackets `)` and `]` only peel when the URL has no matching open
|
|
77
|
+
// bracket — so `(https://example.com/x)` peels the `)` (likely
|
|
78
|
+
// surrounding parens in prose) but `https://en.wikipedia.org/wiki/Foo_(disambig)`
|
|
79
|
+
// keeps it (URL contains its own `(`). Always-peelable chars
|
|
80
|
+
// (`.,;:!?`) come off first.
|
|
81
|
+
function peelTrailingPunctuation(raw) {
|
|
82
|
+
let url = raw;
|
|
83
|
+
let trailing = '';
|
|
84
|
+
// Always-peelable run first; close-brackets one-at-a-time after,
|
|
85
|
+
// since a single URL might end in `…)?` and we want both gone.
|
|
86
|
+
let progressed = true;
|
|
87
|
+
while (progressed) {
|
|
88
|
+
progressed = false;
|
|
89
|
+
const m = ALWAYS_PEEL_RE.exec(url);
|
|
90
|
+
if (m) {
|
|
91
|
+
trailing = m[0] + trailing;
|
|
92
|
+
url = url.slice(0, url.length - m[0].length);
|
|
93
|
+
progressed = true;
|
|
94
|
+
}
|
|
95
|
+
const last = url.charAt(url.length - 1);
|
|
96
|
+
if ((last === ')' && !url.includes('(')) ||
|
|
97
|
+
(last === ']' && !url.includes('['))) {
|
|
98
|
+
trailing = last + trailing;
|
|
99
|
+
url = url.slice(0, url.length - 1);
|
|
100
|
+
progressed = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { url, trailing };
|
|
104
|
+
}
|
|
105
|
+
// Trailing-whitespace class includes `\r` so a CRLF-encoded line
|
|
106
|
+
// that reaches us with a stray `\r` at the end still matches. The
|
|
107
|
+
// rest of the SourceMonkey pipeline appears LF-normalised but the
|
|
108
|
+
// regex stays cheap, and the `\r` fallback keeps `parseTagLine`
|
|
109
|
+
// robust against any future input that bypasses normalisation.
|
|
110
|
+
const TAG_LINE_RE = /^([ \t]*\/\/[ \t]*@([A-Za-z][\w-]*)(?::([\w-]+))?[ \t]+)(\S.*?)([ \t\r]*)$/;
|
|
111
|
+
export function parseTagLine(line) {
|
|
112
|
+
const m = TAG_LINE_RE.exec(line);
|
|
113
|
+
if (!m)
|
|
114
|
+
return null;
|
|
115
|
+
const [, before, tag, locale, value, after] = m;
|
|
116
|
+
const out = {
|
|
117
|
+
before,
|
|
118
|
+
tag: tag.toLowerCase(),
|
|
119
|
+
value,
|
|
120
|
+
after,
|
|
121
|
+
};
|
|
122
|
+
if (locale)
|
|
123
|
+
out.locale = locale;
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
export function splitResourceValue(value) {
|
|
127
|
+
const gap = value.search(/[ \t]/);
|
|
128
|
+
if (gap < 0)
|
|
129
|
+
return null;
|
|
130
|
+
const afterGap = value.slice(gap);
|
|
131
|
+
const gapLen = afterGap.match(/^[ \t]+/)[0].length;
|
|
132
|
+
const urlStart = gap + gapLen;
|
|
133
|
+
const rest = value.slice(urlStart);
|
|
134
|
+
const trail = rest.search(/[ \t]/);
|
|
135
|
+
const urlEnd = trail < 0 ? value.length : urlStart + trail;
|
|
136
|
+
const url = value.slice(urlStart, urlEnd);
|
|
137
|
+
if (!url)
|
|
138
|
+
return null;
|
|
139
|
+
return {
|
|
140
|
+
prefix: value.slice(0, urlStart),
|
|
141
|
+
url,
|
|
142
|
+
suffix: value.slice(urlEnd),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// Build a click-through URL for one `@connect` value. The
|
|
146
|
+
// body-linkifier wraps the verbatim value text in an anchor pointing
|
|
147
|
+
// here; the anchor's `href` is normalised via the URL parser so the
|
|
148
|
+
// click goes somewhere navigable even when the header value was just
|
|
149
|
+
// a bare host.
|
|
150
|
+
//
|
|
151
|
+
// Rules:
|
|
152
|
+
// - `*` and `self` → null (no single site to point at).
|
|
153
|
+
// - `<scheme>://…` → verbatim URL (only http/https accepted).
|
|
154
|
+
// - `*.example.com` → `https://example.com/` (dewildcarded apex,
|
|
155
|
+
// matching the same rule `matchTargetInfo` applies for @match
|
|
156
|
+
// subdomain wildcards).
|
|
157
|
+
// - bare host (`example.com`, `localhost`, `1.2.3.4`) →
|
|
158
|
+
// `https://<host>/`.
|
|
159
|
+
// - Anything that doesn't parse as a URL after that → null.
|
|
160
|
+
//
|
|
161
|
+
// HTTPS is the assumed default for bare hosts even though the
|
|
162
|
+
// userscript may issue HTTP requests via GM_xmlhttpRequest — the
|
|
163
|
+
// click-through is just "visit this site to look it up", and
|
|
164
|
+
// HTTPS is the right default for that.
|
|
165
|
+
export function connectLinkUrl(raw) {
|
|
166
|
+
const t = raw.trim();
|
|
167
|
+
if (!t || t === '*')
|
|
168
|
+
return null;
|
|
169
|
+
if (t.toLowerCase() === 'self')
|
|
170
|
+
return null;
|
|
171
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(t)) {
|
|
172
|
+
try {
|
|
173
|
+
const u = new URL(t);
|
|
174
|
+
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
175
|
+
return u.toString();
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
let host = t;
|
|
184
|
+
if (host.startsWith('*.'))
|
|
185
|
+
host = host.slice(2);
|
|
186
|
+
if (!host || host.includes('*'))
|
|
187
|
+
return null;
|
|
188
|
+
try {
|
|
189
|
+
return new URL(`https://${host}/`).toString();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Sentinel line shapes that bracket the metadata block. Used by the
|
|
196
|
+
// body-linkifier to flip its "inside header" state.
|
|
197
|
+
const HEADER_OPEN_RE = /^[ \t]*\/\/[ \t]*==UserScript==[ \t]*$/;
|
|
198
|
+
const HEADER_CLOSE_RE = /^[ \t]*\/\/[ \t]*==\/UserScript==[ \t]*$/;
|
|
199
|
+
export function isHeaderOpenLine(line) {
|
|
200
|
+
return HEADER_OPEN_RE.test(line);
|
|
201
|
+
}
|
|
202
|
+
export function isHeaderCloseLine(line) {
|
|
203
|
+
return HEADER_CLOSE_RE.test(line);
|
|
204
|
+
}
|