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,277 @@
|
|
|
1
|
+
// Captures navigations to `*.user.js` URLs — http(s):// and file://
|
|
2
|
+
// alike — and routes them to the userscript viewer's preview mode
|
|
3
|
+
// instead.
|
|
4
|
+
//
|
|
5
|
+
// Why this is needed at all: Chrome never renders a `.user.js` URL as
|
|
6
|
+
// a page you can click "install" on. It is treated like any other
|
|
7
|
+
// resource, so a `text/javascript` response just renders as source
|
|
8
|
+
// text and a local file opens as text. The user never gets an install
|
|
9
|
+
// prompt, so a script manager has to intercept the navigation itself.
|
|
10
|
+
// Tampermonkey and Violentmonkey do the same thing.
|
|
11
|
+
//
|
|
12
|
+
// (Chrome ≤ 151 special-cased the suffix and forced the navigation
|
|
13
|
+
// into a download that was then handed to the MV2 extension
|
|
14
|
+
// installer. `minimum_chrome_version` in the manifest puts the floor
|
|
15
|
+
// at 152, where crrev 38b617ca removed that; nothing here works
|
|
16
|
+
// around it any more.)
|
|
17
|
+
//
|
|
18
|
+
// The capture itself is `webNavigation.onBeforeNavigate`, which fires
|
|
19
|
+
// before the request goes out: we rewrite the tab to
|
|
20
|
+
// `userscript.html?preview=…`.
|
|
21
|
+
//
|
|
22
|
+
// `file://` capture additionally depends on the user's per-extension
|
|
23
|
+
// "Allow access to file URLs" toggle:
|
|
24
|
+
// - `webNavigation` delivery is gated on host access, so with the
|
|
25
|
+
// toggle off the event is expected never to arrive.
|
|
26
|
+
// - If one somehow did, the preview couldn't read the file anyway
|
|
27
|
+
// and would show its fetch error.
|
|
28
|
+
// - Either way the feature is absent rather than broken, so nothing
|
|
29
|
+
// here tests for the toggle.
|
|
30
|
+
// - Not verified automatically: Chrome grants file access
|
|
31
|
+
// unconditionally to `--load-extension` builds, so the e2e suite
|
|
32
|
+
// only reaches the toggle-on case. See TODO.md.
|
|
33
|
+
//
|
|
34
|
+
// Redirecting via `chrome.tabs.update` (rather than a DNR `redirect`
|
|
35
|
+
// action) is deliberate: DNR redirects to an extension page require
|
|
36
|
+
// the page in `web_accessible_resources`, which hands our extension ID
|
|
37
|
+
// to every site for fingerprinting. An extension-initiated navigation
|
|
38
|
+
// has no such requirement.
|
|
39
|
+
//
|
|
40
|
+
// See [docs/install-capture.md].
|
|
41
|
+
import { dlog } from '../debug-log.js';
|
|
42
|
+
// Storage key for the user-facing "capture .user.js links" toggle
|
|
43
|
+
// (options page). Absent means enabled — capture is the default.
|
|
44
|
+
export const INSTALL_CAPTURE_KEY = 'smCaptureUserJsLinks';
|
|
45
|
+
// The viewer page, which renders a not-yet-installed script from
|
|
46
|
+
// `?preview=<url>` and offers Install / Update. Same page an installed
|
|
47
|
+
// script opens in — see [docs/script-viewer.md].
|
|
48
|
+
const PREVIEW_PAGE = 'userscript.html';
|
|
49
|
+
// The one spelling of the suffix every matcher here keys off.
|
|
50
|
+
const USER_JS_SUFFIX = '.user.js';
|
|
51
|
+
// Session-storage prefix for one-shot capture bypasses. The preview
|
|
52
|
+
// page writes one of these right before navigating to the raw URL so
|
|
53
|
+
// its own navigation isn't captured straight back into a preview.
|
|
54
|
+
// Session storage (not an in-memory set) because the writer is an
|
|
55
|
+
// extension page and the reader is the SW, which may restart in
|
|
56
|
+
// between.
|
|
57
|
+
const BYPASS_PREFIX = 'installBypass:';
|
|
58
|
+
// How long a bypass stays valid. Long enough to cover a slow
|
|
59
|
+
// navigation start, short enough that a later manual visit to the same
|
|
60
|
+
// URL is captured normally again.
|
|
61
|
+
const BYPASS_TTL_MS = 30_000;
|
|
62
|
+
// Schemes the capture acts on. `file:` is here so that opening a
|
|
63
|
+
// local `.user.js` offers the same install prompt a remote one does;
|
|
64
|
+
// without it a script on disk is the one way in with no prompt at
|
|
65
|
+
// all. The cost (a local file opened *to read* gets hijacked) is
|
|
66
|
+
// accepted and mitigated rather than avoided; see
|
|
67
|
+
// [docs/install-capture.md] "The cost, and what pays for it".
|
|
68
|
+
//
|
|
69
|
+
// Anything else (`chrome-extension:`, `ftp:`, `data:`, …) stays out:
|
|
70
|
+
// those aren't collection shapes `addCollection` can register.
|
|
71
|
+
const CAPTURED_PROTOCOLS = new Set(['http:', 'https:', 'file:']);
|
|
72
|
+
// Code-hosting sites that serve an HTML *page* — a file browser with
|
|
73
|
+
// syntax highlighting, line numbers, and navigation — at a URL whose
|
|
74
|
+
// path still ends in `.user.js`. Those pages are something the user
|
|
75
|
+
// asked to read and Chrome renders them correctly on its own, so the
|
|
76
|
+
// capture stays out of the way.
|
|
77
|
+
//
|
|
78
|
+
// Only the page-view path is listed. Each of these hosts serves the
|
|
79
|
+
// real bytes from a different path (noted per entry), and those stay
|
|
80
|
+
// captured — the exclusion is about the rendering, not the host.
|
|
81
|
+
//
|
|
82
|
+
// Not a completeness claim, and it doesn't need to be: an unlisted
|
|
83
|
+
// forge's blob view is still captured, but the preview then sees a
|
|
84
|
+
// markup response and continues to the page by itself. This list only
|
|
85
|
+
// saves the round trip for the hosts worth knowing in advance.
|
|
86
|
+
//
|
|
87
|
+
// Pasting any of these URLs into the collections page's Add field
|
|
88
|
+
// still installs the script — that path rewrites a blob URL to its raw
|
|
89
|
+
// form (`normalizeCollectionInput`).
|
|
90
|
+
const CODE_HOST_PAGE_VIEWS = [
|
|
91
|
+
// bytes at raw.githubusercontent.com, or /<o>/<r>/raw/…
|
|
92
|
+
{ host: 'github.com', path: /^\/[^\/]+\/[^\/]+\/blob\// },
|
|
93
|
+
// bytes at /-/raw/…; the group segment count varies, so no anchor
|
|
94
|
+
{ host: 'gitlab.com', path: /\/-\/blob\// },
|
|
95
|
+
// bytes at /<w>/<r>/raw/…
|
|
96
|
+
{ host: 'bitbucket.org', path: /^\/[^\/]+\/[^\/]+\/src\// },
|
|
97
|
+
// Gitea: page at /src/branch/<b>/…, bytes at /raw/branch/<b>/…
|
|
98
|
+
{ host: 'codeberg.org', path: /^\/[^\/]+\/[^\/]+\/src\// },
|
|
99
|
+
];
|
|
100
|
+
function isCodeHostPageView(u) {
|
|
101
|
+
// `?raw=true` on a blob URL is GitHub's own "give me the bytes"
|
|
102
|
+
// switch — it 302s to raw.githubusercontent.com. Excluding it would
|
|
103
|
+
// lose the capture entirely, since `onBeforeNavigate` does not fire
|
|
104
|
+
// again for a server redirect.
|
|
105
|
+
if (u.searchParams.get('raw') === 'true' || u.searchParams.get('raw') === '1')
|
|
106
|
+
return false;
|
|
107
|
+
// `hostname` is already lowercased by the URL parser. `www.` is
|
|
108
|
+
// stripped so the same rule covers the redirecting alias.
|
|
109
|
+
const host = u.hostname.replace(/^www\./, '');
|
|
110
|
+
return CODE_HOST_PAGE_VIEWS.some((e) => e.host === host && e.path.test(u.pathname));
|
|
111
|
+
}
|
|
112
|
+
// True for URLs the capture should act on: an http(s) or file:// URL
|
|
113
|
+
// whose path ends in `.user.js`, excluding the forge page views above.
|
|
114
|
+
// Query strings and fragments are ignored (they live outside
|
|
115
|
+
// `pathname`), so `…/foo.user.js?v=3#top` matches.
|
|
116
|
+
//
|
|
117
|
+
// The suffix match is case-SENSITIVE, matching both the
|
|
118
|
+
// `webNavigation` pre-filter below and `isSingleFileInput` in
|
|
119
|
+
// directories.ts (which decides, post-install, whether the URL is a
|
|
120
|
+
// single script or a directory to scan). Chrome's own legacy check is
|
|
121
|
+
// case-insensitive, so the three ways SourceMonkey can see one URL
|
|
122
|
+
// have to agree or a `FOO.USER.JS` ends up captured but registered as
|
|
123
|
+
// a directory collection. Lowercase is the universal convention; a
|
|
124
|
+
// mixed-case suffix is simply not ours to handle.
|
|
125
|
+
export function isUserJsUrl(url) {
|
|
126
|
+
let u;
|
|
127
|
+
try {
|
|
128
|
+
u = new URL(url);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
if (!CAPTURED_PROTOCOLS.has(u.protocol))
|
|
134
|
+
return false;
|
|
135
|
+
if (!u.pathname.endsWith(USER_JS_SUFFIX))
|
|
136
|
+
return false;
|
|
137
|
+
return !isCodeHostPageView(u);
|
|
138
|
+
}
|
|
139
|
+
// Marks a preview the capture opened, as opposed to one the user asked
|
|
140
|
+
// for from the collections page. Only the captured kind may bail out
|
|
141
|
+
// on its own (see the viewer's markup handling): a capture is us
|
|
142
|
+
// guessing that a URL was a script, so being wrong means getting out
|
|
143
|
+
// of the way. A preview the user explicitly asked for is a request we
|
|
144
|
+
// owe an answer to, and silently navigating them off SourceMonkey to
|
|
145
|
+
// some web page would be the wrong one.
|
|
146
|
+
//
|
|
147
|
+
// Nothing on the web can set this, and it has to stay that way: a page
|
|
148
|
+
// that could forge it could make the viewer navigate the tab and plant
|
|
149
|
+
// a capture bypass. `deep-link.ts` strips it for that reason. The
|
|
150
|
+
// whole flow — who adds it, who strips it, who reads it — is written
|
|
151
|
+
// up in [docs/install-capture.md], "The `captured=1` marker".
|
|
152
|
+
export const CAPTURED_PARAM = 'captured';
|
|
153
|
+
// Build the preview URL for a captured script URL. The script URL is
|
|
154
|
+
// carried as a normal query param — the viewer is the only consumer and
|
|
155
|
+
// it validates the value before fetching anything.
|
|
156
|
+
export function buildPreviewPageUrl(scriptUrl) {
|
|
157
|
+
const qs = new URLSearchParams({ preview: scriptUrl, [CAPTURED_PARAM]: '1' });
|
|
158
|
+
return `${chrome.runtime.getURL(PREVIEW_PAGE)}?${qs.toString()}`;
|
|
159
|
+
}
|
|
160
|
+
function bypassKey(url) {
|
|
161
|
+
return BYPASS_PREFIX + url;
|
|
162
|
+
}
|
|
163
|
+
// Record a one-shot "don't capture this URL" marker. Called from the
|
|
164
|
+
// preview page's View collection action.
|
|
165
|
+
export async function markInstallBypass(url) {
|
|
166
|
+
await chrome.storage.session.set({ [bypassKey(url)]: Date.now() + BYPASS_TTL_MS });
|
|
167
|
+
}
|
|
168
|
+
// Read-and-clear a bypass marker. Returns true when the caller should
|
|
169
|
+
// let the navigation through untouched.
|
|
170
|
+
export async function consumeInstallBypass(url) {
|
|
171
|
+
const key = bypassKey(url);
|
|
172
|
+
const got = await chrome.storage.session.get(key);
|
|
173
|
+
const expiresAt = got[key];
|
|
174
|
+
if (typeof expiresAt !== 'number')
|
|
175
|
+
return false;
|
|
176
|
+
await chrome.storage.session.remove(key);
|
|
177
|
+
return expiresAt > Date.now();
|
|
178
|
+
}
|
|
179
|
+
// In-SW mirror of the capture toggle. Read once at startup and kept
|
|
180
|
+
// current by the storage listener, so the navigation handler never has
|
|
181
|
+
// to await storage on the critical path — every millisecond there is a
|
|
182
|
+
// millisecond the response can use to win the race.
|
|
183
|
+
//
|
|
184
|
+
// It starts at the default (on), which is a lie for a user who turned
|
|
185
|
+
// capture off: an MV3 service worker is woken *by* the navigation
|
|
186
|
+
// event, so the first navigation after every wake can arrive before
|
|
187
|
+
// the initial read lands. `captureFlagReady` is awaited on the async
|
|
188
|
+
// side of the handler — off the sync fast path — so that navigation
|
|
189
|
+
// gets the real answer too.
|
|
190
|
+
let captureEnabled = true;
|
|
191
|
+
let captureFlagReady = Promise.resolve();
|
|
192
|
+
// Bumped by every write. The initial read applies its result only if
|
|
193
|
+
// nothing newer landed while it was in flight — otherwise a toggle made
|
|
194
|
+
// during startup would be clobbered by the stale value, leaving the
|
|
195
|
+
// mirror wrong until the next toggle.
|
|
196
|
+
let captureFlagGeneration = 0;
|
|
197
|
+
// The live mirror, not a settled answer: it reads the optimistic
|
|
198
|
+
// default (on) until the startup read lands, so only a poll for
|
|
199
|
+
// `false` means anything. Exposed for the e2e suite, where a test that
|
|
200
|
+
// turns capture off needs to know the SW has seen the change before it
|
|
201
|
+
// navigates — the storage write resolving is not the same as the
|
|
202
|
+
// `onChanged` listener having run.
|
|
203
|
+
export function isCaptureEnabled() {
|
|
204
|
+
return captureEnabled;
|
|
205
|
+
}
|
|
206
|
+
async function loadCaptureFlag() {
|
|
207
|
+
const generation = captureFlagGeneration;
|
|
208
|
+
try {
|
|
209
|
+
const got = await chrome.storage.local.get(INSTALL_CAPTURE_KEY);
|
|
210
|
+
if (generation !== captureFlagGeneration)
|
|
211
|
+
return;
|
|
212
|
+
captureEnabled = got[INSTALL_CAPTURE_KEY] !== false;
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
// Leave the mirror at its default rather than taking the SW's
|
|
216
|
+
// startup down; the next toggle re-syncs everything.
|
|
217
|
+
console.info('[SourceMonkey] could not read the .user.js capture setting', err);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Wire the capture. Safe to call when webNavigation is unavailable —
|
|
221
|
+
// the feature just stays off.
|
|
222
|
+
export function installUserJsCapture() {
|
|
223
|
+
const wn = chrome.webNavigation;
|
|
224
|
+
if (!wn) {
|
|
225
|
+
console.info('[SourceMonkey] chrome.webNavigation not available; .user.js capture disabled');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
captureFlagReady = loadCaptureFlag();
|
|
229
|
+
chrome.storage.onChanged.addListener((changes, areaName) => {
|
|
230
|
+
if (areaName !== 'local')
|
|
231
|
+
return;
|
|
232
|
+
const change = changes[INSTALL_CAPTURE_KEY];
|
|
233
|
+
if (!change)
|
|
234
|
+
return;
|
|
235
|
+
captureFlagGeneration++;
|
|
236
|
+
// The bump above makes an in-flight initial read discard its
|
|
237
|
+
// result, so the toggle can't be clobbered by a stale value. The
|
|
238
|
+
// flag itself lands synchronously; `captureFlagReady` is reset so
|
|
239
|
+
// that a LATER navigation isn't parked behind the superseded
|
|
240
|
+
// startup read.
|
|
241
|
+
captureEnabled = change.newValue !== false;
|
|
242
|
+
captureFlagReady = Promise.resolve();
|
|
243
|
+
});
|
|
244
|
+
wn.onBeforeNavigate.addListener((d) => {
|
|
245
|
+
if (d.frameId !== 0)
|
|
246
|
+
return;
|
|
247
|
+
if (!captureEnabled)
|
|
248
|
+
return;
|
|
249
|
+
if (!isUserJsUrl(d.url))
|
|
250
|
+
return;
|
|
251
|
+
void captureNavigation(d.tabId, d.url);
|
|
252
|
+
},
|
|
253
|
+
// Cheap pre-filter so the listener isn't woken for every
|
|
254
|
+
// navigation. `pathSuffix` matches the path only, so query strings
|
|
255
|
+
// don't defeat it, and it is case-sensitive — the same rule
|
|
256
|
+
// `isUserJsUrl` applies, so the two agree on `FOO.USER.JS` (not
|
|
257
|
+
// captured). No scheme condition: the filter is scheme-agnostic,
|
|
258
|
+
// so `file://` navigations arrive here too and `isUserJsUrl` is
|
|
259
|
+
// the only thing deciding which schemes are in scope.
|
|
260
|
+
{ url: [{ pathSuffix: '.user.js' }] });
|
|
261
|
+
}
|
|
262
|
+
async function captureNavigation(tabId, url) {
|
|
263
|
+
// Re-check against the settled flag: the sync check upstream may have
|
|
264
|
+
// run against the startup default. Cheap — resolved for every
|
|
265
|
+
// navigation but the first after an SW wake.
|
|
266
|
+
await captureFlagReady;
|
|
267
|
+
if (!captureEnabled)
|
|
268
|
+
return;
|
|
269
|
+
if (await consumeInstallBypass(url)) {
|
|
270
|
+
dlog(`[SourceMonkey] .user.js capture bypassed for ${url}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.log(`[SourceMonkey] capturing .user.js navigation: ${url}`);
|
|
274
|
+
// The tab may have closed (or navigated on) between the event firing
|
|
275
|
+
// and this update landing; ignore the resulting rejection.
|
|
276
|
+
await chrome.tabs.update(tabId, { url: buildPreviewPageUrl(url) }).catch(() => { });
|
|
277
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Line diff for the compare page: which lines two texts share, which
|
|
2
|
+
// each has alone, and how to fold the shared runs away into hunks.
|
|
3
|
+
//
|
|
4
|
+
// Pure — no DOM, no store — so the compare page can be tested apart
|
|
5
|
+
// from the pages that show its output. A line is the unit: a changed
|
|
6
|
+
// character marks its whole line, and nothing here knows what a
|
|
7
|
+
// function or a header tag is. Grouping changes into hunks with a few
|
|
8
|
+
// lines of context on either side is what makes the output read as
|
|
9
|
+
// "just the parts that changed", and it is a separate pass over the
|
|
10
|
+
// line ops rather than anything the diff itself does.
|
|
11
|
+
// Split into lines without inventing a trailing empty one: a text that
|
|
12
|
+
// ends in a newline has as many lines as newlines, not one more.
|
|
13
|
+
export function splitLines(text) {
|
|
14
|
+
if (text === '')
|
|
15
|
+
return [];
|
|
16
|
+
const lines = text.split('\n');
|
|
17
|
+
if (lines[lines.length - 1] === '')
|
|
18
|
+
lines.pop();
|
|
19
|
+
return lines;
|
|
20
|
+
}
|
|
21
|
+
// Past this many edits the search is abandoned and the two texts are
|
|
22
|
+
// shown as a whole replacement. Myers' algorithm costs O((N+M)·D) time
|
|
23
|
+
// and its backtrack trace grows as D², so a pair of large, unrelated
|
|
24
|
+
// files (one minified library against a different one) would otherwise
|
|
25
|
+
// spend seconds and tens of megabytes to print a diff no one can read
|
|
26
|
+
// anyway. A few thousand edits is far past what anyone reads line by
|
|
27
|
+
// line.
|
|
28
|
+
const MAX_EDITS = 3000;
|
|
29
|
+
// Myers' O(ND) shortest-edit-script diff over two line arrays, the same
|
|
30
|
+
// algorithm `diff` and git use. The common prefix and suffix are peeled
|
|
31
|
+
// off first: they are the bulk of any real comparison, and they cost
|
|
32
|
+
// the search nothing that way.
|
|
33
|
+
export function diffLines(a, b) {
|
|
34
|
+
let prefix = 0;
|
|
35
|
+
while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix])
|
|
36
|
+
prefix++;
|
|
37
|
+
let suffix = 0;
|
|
38
|
+
while (suffix < a.length - prefix && suffix < b.length - prefix
|
|
39
|
+
&& a[a.length - 1 - suffix] === b[b.length - 1 - suffix])
|
|
40
|
+
suffix++;
|
|
41
|
+
const ops = [];
|
|
42
|
+
for (let i = 0; i < prefix; i++) {
|
|
43
|
+
ops.push({ kind: 'same', text: a[i], aLine: i + 1, bLine: i + 1 });
|
|
44
|
+
}
|
|
45
|
+
const midA = a.slice(prefix, a.length - suffix);
|
|
46
|
+
const midB = b.slice(prefix, b.length - suffix);
|
|
47
|
+
const middle = myers(midA, midB) ?? replaceAll(midA, midB);
|
|
48
|
+
for (const op of middle) {
|
|
49
|
+
if (op.aLine !== undefined)
|
|
50
|
+
op.aLine += prefix;
|
|
51
|
+
if (op.bLine !== undefined)
|
|
52
|
+
op.bLine += prefix;
|
|
53
|
+
ops.push(op);
|
|
54
|
+
}
|
|
55
|
+
for (let i = 0; i < suffix; i++) {
|
|
56
|
+
const ai = a.length - suffix + i;
|
|
57
|
+
const bi = b.length - suffix + i;
|
|
58
|
+
ops.push({ kind: 'same', text: a[ai], aLine: ai + 1, bLine: bi + 1 });
|
|
59
|
+
}
|
|
60
|
+
return ops;
|
|
61
|
+
}
|
|
62
|
+
// The fallback past `MAX_EDITS`: everything in A removed, everything in
|
|
63
|
+
// B added. Honest, if unhelpful — the counts are right, and the reader
|
|
64
|
+
// sees both texts in full.
|
|
65
|
+
function replaceAll(a, b) {
|
|
66
|
+
const ops = [];
|
|
67
|
+
a.forEach((text, i) => ops.push({ kind: 'del', text, aLine: i + 1 }));
|
|
68
|
+
b.forEach((text, i) => ops.push({ kind: 'add', text, bLine: i + 1 }));
|
|
69
|
+
return ops;
|
|
70
|
+
}
|
|
71
|
+
// The forward search from "An O(ND) Difference Algorithm and Its
|
|
72
|
+
// Variations" (Myers, 1986). Line numbers in the result are 1-based
|
|
73
|
+
// within the arrays given. Returns null when the edit count passes
|
|
74
|
+
// `MAX_EDITS`.
|
|
75
|
+
//
|
|
76
|
+
// `v[k]` is the furthest x reached on diagonal k (where x − y = k)
|
|
77
|
+
// with the current number of edits. Diagonals run from −d to d after d
|
|
78
|
+
// edits, so each step's array is sized to just that range (plus the
|
|
79
|
+
// neighbour on either side it reads) rather than to the worst case,
|
|
80
|
+
// and the trace kept for the backtrack costs D² rather than D·(N+M).
|
|
81
|
+
// The array for step d is indexed as `v[k + d + 1]`.
|
|
82
|
+
function myers(a, b) {
|
|
83
|
+
const n = a.length;
|
|
84
|
+
const m = b.length;
|
|
85
|
+
if (n === 0 && m === 0)
|
|
86
|
+
return [];
|
|
87
|
+
const max = Math.min(n + m, MAX_EDITS);
|
|
88
|
+
// Before step 0: k ∈ [−1, 1], and the seed on k = 1 is what makes
|
|
89
|
+
// the first move a step down onto k = 0 at x = 0.
|
|
90
|
+
let v = new Int32Array(3).fill(-1);
|
|
91
|
+
v[2] = 0;
|
|
92
|
+
const trace = [];
|
|
93
|
+
for (let d = 0; d <= max; d++) {
|
|
94
|
+
trace.push(v);
|
|
95
|
+
const offset = d + 1;
|
|
96
|
+
// Step d + 1 reads one diagonal further out on each side.
|
|
97
|
+
const next = new Int32Array(v.length + 2).fill(-1);
|
|
98
|
+
next.set(v, 1);
|
|
99
|
+
for (let k = -d; k <= d; k += 2) {
|
|
100
|
+
// Step down (an added line) or right (a removed line), whichever
|
|
101
|
+
// reached further — preferring right on a tie, the conventional
|
|
102
|
+
// choice that lists a deletion before the insertion beside it.
|
|
103
|
+
let x;
|
|
104
|
+
if (k === -d || (k !== d && v[offset + k - 1] < v[offset + k + 1])) {
|
|
105
|
+
x = v[offset + k + 1];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
x = v[offset + k - 1] + 1;
|
|
109
|
+
}
|
|
110
|
+
let y = x - k;
|
|
111
|
+
// Then along the diagonal for as long as the lines agree.
|
|
112
|
+
while (x < n && y < m && a[x] === b[y]) {
|
|
113
|
+
x++;
|
|
114
|
+
y++;
|
|
115
|
+
}
|
|
116
|
+
next[offset + 1 + k] = x;
|
|
117
|
+
if (x >= n && y >= m)
|
|
118
|
+
return backtrack(a, b, trace, d);
|
|
119
|
+
}
|
|
120
|
+
v = next;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Walk the trace back from the end to recover the edit script, then
|
|
125
|
+
// reverse it into reading order. `trace[d]` is the state before step
|
|
126
|
+
// d, which is what decided where that step went.
|
|
127
|
+
function backtrack(a, b, trace, edits) {
|
|
128
|
+
const ops = [];
|
|
129
|
+
let x = a.length;
|
|
130
|
+
let y = b.length;
|
|
131
|
+
for (let d = edits; d > 0; d--) {
|
|
132
|
+
const v = trace[d];
|
|
133
|
+
const offset = d + 1;
|
|
134
|
+
const k = x - y;
|
|
135
|
+
const prevK = (k === -d || (k !== d && v[offset + k - 1] < v[offset + k + 1]))
|
|
136
|
+
? k + 1
|
|
137
|
+
: k - 1;
|
|
138
|
+
const prevX = v[offset + prevK];
|
|
139
|
+
const prevY = prevX - prevK;
|
|
140
|
+
// The diagonal run that followed the edit, walked backwards.
|
|
141
|
+
while (x > prevX && y > prevY) {
|
|
142
|
+
x--;
|
|
143
|
+
y--;
|
|
144
|
+
ops.push({ kind: 'same', text: a[x], aLine: x + 1, bLine: y + 1 });
|
|
145
|
+
}
|
|
146
|
+
if (x === prevX) {
|
|
147
|
+
y--;
|
|
148
|
+
ops.push({ kind: 'add', text: b[y], bLine: y + 1 });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
x--;
|
|
152
|
+
ops.push({ kind: 'del', text: a[x], aLine: x + 1 });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Whatever is left is the diagonal run before the first edit.
|
|
156
|
+
while (x > 0 && y > 0) {
|
|
157
|
+
x--;
|
|
158
|
+
y--;
|
|
159
|
+
ops.push({ kind: 'same', text: a[x], aLine: x + 1, bLine: y + 1 });
|
|
160
|
+
}
|
|
161
|
+
return ops.reverse();
|
|
162
|
+
}
|
|
163
|
+
// Group the changed lines into hunks, each padded with `context` lines
|
|
164
|
+
// of unchanged text on either side; two changes closer together than
|
|
165
|
+
// twice that share a hunk. An op list with no changes yields no hunks.
|
|
166
|
+
export function toHunks(ops, context = 3) {
|
|
167
|
+
const hunks = [];
|
|
168
|
+
let i = 0;
|
|
169
|
+
while (i < ops.length) {
|
|
170
|
+
if (ops[i].kind === 'same') {
|
|
171
|
+
i++;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// The hunk starts `context` lines before this change…
|
|
175
|
+
const start = Math.max(0, i - context);
|
|
176
|
+
// …and runs until a stretch of more than 2·context unchanged lines
|
|
177
|
+
// separates it from the next change, or the ops run out.
|
|
178
|
+
let end = i;
|
|
179
|
+
let sameRun = 0;
|
|
180
|
+
let j = i;
|
|
181
|
+
while (j < ops.length) {
|
|
182
|
+
if (ops[j].kind === 'same') {
|
|
183
|
+
sameRun++;
|
|
184
|
+
if (sameRun > 2 * context)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
sameRun = 0;
|
|
189
|
+
end = j;
|
|
190
|
+
}
|
|
191
|
+
j++;
|
|
192
|
+
}
|
|
193
|
+
const stop = Math.min(ops.length, end + 1 + context);
|
|
194
|
+
hunks.push(makeHunk(ops.slice(start, stop)));
|
|
195
|
+
i = stop;
|
|
196
|
+
}
|
|
197
|
+
return hunks;
|
|
198
|
+
}
|
|
199
|
+
function makeHunk(ops) {
|
|
200
|
+
let aStart = 0;
|
|
201
|
+
let bStart = 0;
|
|
202
|
+
let aCount = 0;
|
|
203
|
+
let bCount = 0;
|
|
204
|
+
for (const op of ops) {
|
|
205
|
+
if (op.aLine !== undefined) {
|
|
206
|
+
if (!aStart)
|
|
207
|
+
aStart = op.aLine;
|
|
208
|
+
aCount++;
|
|
209
|
+
}
|
|
210
|
+
if (op.bLine !== undefined) {
|
|
211
|
+
if (!bStart)
|
|
212
|
+
bStart = op.bLine;
|
|
213
|
+
bCount++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// A hunk with nothing on one side (a pure insertion at the top of an
|
|
217
|
+
// empty file) starts where that side's next line would be, which is
|
|
218
|
+
// what `@@ -0,0 +1,3 @@` means.
|
|
219
|
+
return { ops, aStart, aCount, bStart, bCount };
|
|
220
|
+
}
|
|
221
|
+
// How many lines each side has alone — the `−4 +9` a diff is summed
|
|
222
|
+
// up as.
|
|
223
|
+
export function countChanges(ops) {
|
|
224
|
+
let removed = 0;
|
|
225
|
+
let added = 0;
|
|
226
|
+
for (const op of ops) {
|
|
227
|
+
if (op.kind === 'del')
|
|
228
|
+
removed++;
|
|
229
|
+
else if (op.kind === 'add')
|
|
230
|
+
added++;
|
|
231
|
+
}
|
|
232
|
+
return { removed, added };
|
|
233
|
+
}
|