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,867 @@
|
|
|
1
|
+
// The compare page (`compare.html`): two copies of one script side by
|
|
2
|
+
// side — their header fields in one table, then a diff of each file
|
|
3
|
+
// they run, tab by tab.
|
|
4
|
+
//
|
|
5
|
+
// Reached from the versions panel's Compare column: a row's DIFF
|
|
6
|
+
// button opens it. One side is the copy that column compared against
|
|
7
|
+
// — the script on screen (`?script=<id>`), or the download an install
|
|
8
|
+
// preview is showing (`?preview=<location>`); `?other=<id>` is the row
|
|
9
|
+
// whose button was pressed. The diff reads left to right, old to new,
|
|
10
|
+
// and which side is which follows from that:
|
|
11
|
+
// - from the viewer, the script on screen is the BASE and goes on
|
|
12
|
+
// the left: the diff says what the other copy changed;
|
|
13
|
+
// - from a preview, the download is the NEW copy and goes on the
|
|
14
|
+
// right: the diff says what installing it would change.
|
|
15
|
+
// A swap button between the two sides' Collection and Location cells
|
|
16
|
+
// turns it around.
|
|
17
|
+
//
|
|
18
|
+
// What is compared is what runs: the script's own text and every
|
|
19
|
+
// `@require` it declares, which is the same set the Compare column's
|
|
20
|
+
// verdict was computed from (`contentFingerprint`, script-identity.ts).
|
|
21
|
+
// A preview side is fetched afresh here — the preview page holds its
|
|
22
|
+
// download in memory, and nothing is stored until Install — through the
|
|
23
|
+
// same pipeline that page uses, so the `@require`s it resolves are the
|
|
24
|
+
// ones an install would store.
|
|
25
|
+
import { buildScriptFromText, describeError, getCollection, getScript, localFileScope, } from './script-store.js';
|
|
26
|
+
import { attachFiles } from '../external-files.js';
|
|
27
|
+
import { canonicalCollectionInput, collectionCanonicalUrl, fetchSingleFileText, isSingleFileLocation, LOCATION_SHAPES, normalizeCollectionInput, scriptAbsolutePath, } from './directories.js';
|
|
28
|
+
import { appendWithSlashBreaks, displayLocation, formatScriptSize, fullSource, previewUrl, requireBasename, scriptBasename, scriptLabel, viewerUrl, } from '../script-display.js';
|
|
29
|
+
import { collectionCell, declaredHomepage } from './script-details.js';
|
|
30
|
+
import { getValue, getValues } from '../userscript-header.js';
|
|
31
|
+
import { headingPrefixEl, HEADING_SEPARATOR, INSTALLED_HEADING, MISSING_LABEL, setPageTitle, } from './page-heading.js';
|
|
32
|
+
import { installHomeLinkHandler } from './page-links.js';
|
|
33
|
+
import { installGithubAuthListener, loadGithubAuthToken } from '../github-auth.js';
|
|
34
|
+
import { installLocalRootsListener, localRootsReady, localSearchRoots } from '../local-file-overrides.js';
|
|
35
|
+
import { setGlyphButton } from './glyph.js';
|
|
36
|
+
import { HEADING_SITE_ICONS, renderSiteIconInto } from './site-icon.js';
|
|
37
|
+
import { installSelectAll } from './select-all.js';
|
|
38
|
+
import { ignoreResizeObserverLoopErrors } from './resize-observer-loop.js';
|
|
39
|
+
import { countChanges, diffLines, splitLines, toHunks } from './line-diff.js';
|
|
40
|
+
// The heading's page name, between the product's and the script's.
|
|
41
|
+
const COMPARE_HEADING = `${INSTALLED_HEADING}Compare${HEADING_SEPARATOR}`;
|
|
42
|
+
const params = new URLSearchParams(location.search);
|
|
43
|
+
const baseScriptId = params.get('script');
|
|
44
|
+
const previewInput = params.get('preview');
|
|
45
|
+
const otherScriptId = params.get('other');
|
|
46
|
+
const headingEl = document.getElementById('heading');
|
|
47
|
+
const messageEl = document.getElementById('message');
|
|
48
|
+
const fieldsEl = document.getElementById('fields');
|
|
49
|
+
const viewRowEl = document.getElementById('view-row');
|
|
50
|
+
const tabsEl = document.getElementById('tabs');
|
|
51
|
+
const diffEl = document.getElementById('diff');
|
|
52
|
+
// The two sides as they are currently shown: `left` is diffed against
|
|
53
|
+
// `right`. Filled by `load`, swapped by the swap button.
|
|
54
|
+
let left = null;
|
|
55
|
+
let right = null;
|
|
56
|
+
// The copy the page was opened for, whichever side it is on now: the
|
|
57
|
+
// script on screen, or the preview's download. The script's own tab is
|
|
58
|
+
// named from it and the `@require` tabs follow its declaration order,
|
|
59
|
+
// so neither shifts on a swap. (The heading is the LEFT side's — see
|
|
60
|
+
// `renderHeading`.)
|
|
61
|
+
let base = null;
|
|
62
|
+
// Which file's diff is on screen, by index into `filePairs()`. Kept
|
|
63
|
+
// across a swap or a view change, which repaint the same tabs.
|
|
64
|
+
let activeTab = 0;
|
|
65
|
+
const VIEW_KEY = 'compare.view';
|
|
66
|
+
const SCOPE_KEY = 'compare.scope';
|
|
67
|
+
let view = readSetting(VIEW_KEY, 'unified', ['unified', 'split']);
|
|
68
|
+
let scope = readSetting(SCOPE_KEY, 'changes', ['changes', 'whole']);
|
|
69
|
+
function readSetting(key, fallback, allowed) {
|
|
70
|
+
try {
|
|
71
|
+
const got = localStorage.getItem(key);
|
|
72
|
+
return allowed.includes(got) ? got : fallback;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return fallback;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function writeSetting(key, value) {
|
|
79
|
+
try {
|
|
80
|
+
localStorage.setItem(key, value);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Storage that refuses a write (a locked-down profile) costs only
|
|
84
|
+
// the memory of the choice; the page itself is unaffected.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
installHomeLinkHandler();
|
|
88
|
+
ignoreResizeObserverLoopErrors();
|
|
89
|
+
// Ctrl/Cmd-A selects the diff on screen rather than the page: the one
|
|
90
|
+
// pane there is, or, side by side, the pane focus is in — the left one
|
|
91
|
+
// when it is nowhere in particular. The line numbers and signs are
|
|
92
|
+
// `user-select: none`, so what is copied is the lines.
|
|
93
|
+
installSelectAll(() => {
|
|
94
|
+
const focused = document.activeElement?.closest('.diff-body .pane');
|
|
95
|
+
const pane = focused ?? diffEl.querySelector('.diff-body .pane.left') ?? diffEl.querySelector('.diff-body');
|
|
96
|
+
return pane?.querySelector('table') ?? null;
|
|
97
|
+
});
|
|
98
|
+
installLocalRootsListener();
|
|
99
|
+
installGithubAuthListener();
|
|
100
|
+
const githubTokenReady = loadGithubAuthToken();
|
|
101
|
+
void load();
|
|
102
|
+
async function load() {
|
|
103
|
+
if (!otherScriptId || (!baseScriptId && !previewInput)) {
|
|
104
|
+
fail('This page needs two copies to compare: a script and another copy of it');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
setMessage('Loading…');
|
|
108
|
+
try {
|
|
109
|
+
// The preview side reads local stand-ins for its `@require`s the
|
|
110
|
+
// way the preview page does, and that needs the configured roots.
|
|
111
|
+
await localRootsReady();
|
|
112
|
+
const [loadedBase, other] = await Promise.all([
|
|
113
|
+
baseScriptId ? loadInstalled(baseScriptId, 'The script') : loadPreview(previewInput),
|
|
114
|
+
loadInstalled(otherScriptId, 'The other copy'),
|
|
115
|
+
]);
|
|
116
|
+
base = loadedBase;
|
|
117
|
+
[left, right] = baseScriptId ? [loadedBase, other] : [other, loadedBase];
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
fail(describeError(err));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setMessage('');
|
|
124
|
+
renderViewRow();
|
|
125
|
+
renderAll();
|
|
126
|
+
}
|
|
127
|
+
// A stored copy. The collection row may be gone — a copy whose
|
|
128
|
+
// collection was removed while this page was opening — and the side
|
|
129
|
+
// still renders, with no Collection cell to show. `what` names the side
|
|
130
|
+
// in the failure, so the message says which copy went missing.
|
|
131
|
+
async function loadInstalled(id, what) {
|
|
132
|
+
const script = await getScript(id);
|
|
133
|
+
if (!script)
|
|
134
|
+
throw new Error(`${what} is no longer installed`);
|
|
135
|
+
const collection = (await getCollection(script.collectionId)) ?? null;
|
|
136
|
+
return {
|
|
137
|
+
kind: 'installed',
|
|
138
|
+
label: scriptLabel(script.header, script.relativePath),
|
|
139
|
+
collection,
|
|
140
|
+
location: displayLocation(collection ? scriptAbsolutePath(collection.path, script.relativePath) : script.relativePath),
|
|
141
|
+
pageUrl: viewerUrl(script.id),
|
|
142
|
+
header: script.header,
|
|
143
|
+
icons: {
|
|
144
|
+
matching: script.matching,
|
|
145
|
+
fileUrl: script.fileUrl,
|
|
146
|
+
// A missing collection row reads as ENABLED, as the viewer's
|
|
147
|
+
// heading has it.
|
|
148
|
+
localRoots: localSearchRoots(script.fileUrl, collection ? localFileScope(collection) : { enabled: true }),
|
|
149
|
+
},
|
|
150
|
+
text: fullSource(script),
|
|
151
|
+
requires: script.requires ?? [],
|
|
152
|
+
size: formatScriptSize(script),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// The download an install preview is showing, fetched again here: the
|
|
156
|
+
// preview holds it in memory only. The same normalization and the same
|
|
157
|
+
// `@require` resolution the preview runs, so what this side shows is
|
|
158
|
+
// what installing would store.
|
|
159
|
+
async function loadPreview(input) {
|
|
160
|
+
if (!isSingleFileLocation(input)) {
|
|
161
|
+
throw new Error(`This is not a script location. It ${LOCATION_SHAPES}.`);
|
|
162
|
+
}
|
|
163
|
+
await githubTokenReady;
|
|
164
|
+
const previewLocation = canonicalCollectionInput(await normalizeCollectionInput(input));
|
|
165
|
+
const fileUrl = collectionCanonicalUrl(previewLocation);
|
|
166
|
+
const text = await fetchSingleFileText(fileUrl);
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const script = await buildScriptFromText({
|
|
169
|
+
id: 'preview',
|
|
170
|
+
collectionId: 'preview',
|
|
171
|
+
relativePath: previewLocation,
|
|
172
|
+
fileUrl,
|
|
173
|
+
text,
|
|
174
|
+
now,
|
|
175
|
+
});
|
|
176
|
+
// The preview page refuses to install this; a diff of a file that
|
|
177
|
+
// cannot be installed would only be misread as one that can.
|
|
178
|
+
if (script.status !== 'ok') {
|
|
179
|
+
throw new Error(`The script being previewed cannot be compared: ${script.errorMessage ?? 'It is not a valid userscript'}`);
|
|
180
|
+
}
|
|
181
|
+
const fetchCache = new Map();
|
|
182
|
+
const localRoots = localSearchRoots(fileUrl, { enabled: true });
|
|
183
|
+
const files = await attachFiles({
|
|
184
|
+
scriptFileUrl: fileUrl,
|
|
185
|
+
header: script.header,
|
|
186
|
+
priorRequires: new Map(),
|
|
187
|
+
priorResources: new Map(),
|
|
188
|
+
recheck: 'all',
|
|
189
|
+
fetchCache,
|
|
190
|
+
now,
|
|
191
|
+
localRoots,
|
|
192
|
+
});
|
|
193
|
+
if (files.errorMessage) {
|
|
194
|
+
throw new Error(`The script being previewed cannot be compared: ${files.errorMessage}`);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
kind: 'preview',
|
|
198
|
+
label: scriptLabel(script.header, previewLocation),
|
|
199
|
+
collection: null,
|
|
200
|
+
location: displayLocation(previewLocation),
|
|
201
|
+
pageUrl: previewUrl(previewLocation),
|
|
202
|
+
header: script.header,
|
|
203
|
+
icons: { matching: script.matching, fileUrl, localRoots },
|
|
204
|
+
text,
|
|
205
|
+
requires: files.requires,
|
|
206
|
+
size: formatScriptSize(script),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function fail(message) {
|
|
210
|
+
setMessage(message, 'error');
|
|
211
|
+
setPageTitle('Compare: ', MISSING_LABEL);
|
|
212
|
+
}
|
|
213
|
+
function setMessage(text, kind = '') {
|
|
214
|
+
messageEl.textContent = text;
|
|
215
|
+
messageEl.className = `message ${kind}`.trim();
|
|
216
|
+
}
|
|
217
|
+
// `SourceMonkey | Compare | <icons> <name>`, as the viewer's heading
|
|
218
|
+
// reads — the favicons and the name of the LEFT side, so a swap
|
|
219
|
+
// repaints it from the other copy. (The names are usually the same;
|
|
220
|
+
// the icons can differ, when one copy's `@match` or `@icon` does.)
|
|
221
|
+
function renderHeading() {
|
|
222
|
+
const side = left;
|
|
223
|
+
headingEl.replaceChildren(headingPrefixEl(COMPARE_HEADING));
|
|
224
|
+
const iconSlot = document.createElement('span');
|
|
225
|
+
iconSlot.className = 'site-icon';
|
|
226
|
+
headingEl.appendChild(iconSlot);
|
|
227
|
+
appendWithSlashBreaks(headingEl, side.label);
|
|
228
|
+
setPageTitle('Compare: ', side.label);
|
|
229
|
+
void renderSiteIconInto(iconSlot, { matching: side.icons.matching, fileUrl: side.icons.fileUrl, header: side.header }, side.icons.localRoots, HEADING_SITE_ICONS);
|
|
230
|
+
}
|
|
231
|
+
// Everything that depends on which side is which.
|
|
232
|
+
function renderAll() {
|
|
233
|
+
renderHeading();
|
|
234
|
+
renderFields();
|
|
235
|
+
renderTabs();
|
|
236
|
+
renderDiff();
|
|
237
|
+
}
|
|
238
|
+
function renderFields() {
|
|
239
|
+
const l = left;
|
|
240
|
+
const r = right;
|
|
241
|
+
const table = document.createElement('table');
|
|
242
|
+
table.className = 'fields';
|
|
243
|
+
table.appendChild(fieldsHead(l, r));
|
|
244
|
+
const tbody = document.createElement('tbody');
|
|
245
|
+
for (const row of fieldRows(l, r))
|
|
246
|
+
tbody.appendChild(fieldRow(row));
|
|
247
|
+
table.appendChild(tbody);
|
|
248
|
+
fieldsEl.replaceChildren(table);
|
|
249
|
+
}
|
|
250
|
+
// The two rows that say which copy is which — Collection and Location,
|
|
251
|
+
// labelled like the rows under them — with the swap button between
|
|
252
|
+
// the sides, spanning both.
|
|
253
|
+
function fieldsHead(l, r) {
|
|
254
|
+
const thead = document.createElement('thead');
|
|
255
|
+
// Tinted like the rows below when the two differ, which for the
|
|
256
|
+
// collection means two different collections, not two spellings.
|
|
257
|
+
const collectionRow = document.createElement('tr');
|
|
258
|
+
const sameCollection = l.collection !== null && r.collection !== null
|
|
259
|
+
&& l.collection.id === r.collection.id;
|
|
260
|
+
collectionRow.className = `side-collection ${sameCollection ? 'same' : 'differs'}`;
|
|
261
|
+
collectionRow.appendChild(label('Collection'));
|
|
262
|
+
collectionRow.appendChild(sideCell(collectionValue(l), 'left'));
|
|
263
|
+
const swapCell = document.createElement('td');
|
|
264
|
+
swapCell.className = 'swap-col';
|
|
265
|
+
swapCell.rowSpan = 2;
|
|
266
|
+
const swapBtn = document.createElement('button');
|
|
267
|
+
swapBtn.type = 'button';
|
|
268
|
+
swapBtn.id = 'swap';
|
|
269
|
+
setGlyphButton(swapBtn, 'sm-swap', 'Swap the two sides');
|
|
270
|
+
swapBtn.addEventListener('click', () => {
|
|
271
|
+
[left, right] = [right, left];
|
|
272
|
+
renderAll();
|
|
273
|
+
});
|
|
274
|
+
swapCell.appendChild(swapBtn);
|
|
275
|
+
collectionRow.appendChild(swapCell);
|
|
276
|
+
collectionRow.appendChild(sideCell(collectionValue(r), 'right'));
|
|
277
|
+
thead.appendChild(collectionRow);
|
|
278
|
+
const locationRow = document.createElement('tr');
|
|
279
|
+
locationRow.className = 'side-location ' + (l.location === r.location ? 'same' : 'differs');
|
|
280
|
+
locationRow.appendChild(label('Location'));
|
|
281
|
+
locationRow.appendChild(sideCell(locationValue(l), 'left'));
|
|
282
|
+
locationRow.appendChild(sideCell(locationValue(r), 'right'));
|
|
283
|
+
thead.appendChild(locationRow);
|
|
284
|
+
return thead;
|
|
285
|
+
}
|
|
286
|
+
function label(text) {
|
|
287
|
+
const th = document.createElement('th');
|
|
288
|
+
th.textContent = text;
|
|
289
|
+
return th;
|
|
290
|
+
}
|
|
291
|
+
function sideCell(content, side) {
|
|
292
|
+
const td = document.createElement('td');
|
|
293
|
+
td.className = `value ${side}`;
|
|
294
|
+
td.appendChild(content);
|
|
295
|
+
return td;
|
|
296
|
+
}
|
|
297
|
+
// A side's collection: its type bubble and name, linked to its section
|
|
298
|
+
// on the collections page — or the reason there is none.
|
|
299
|
+
function collectionValue(side) {
|
|
300
|
+
if (side.collection)
|
|
301
|
+
return collectionCell(side.collection);
|
|
302
|
+
const span = document.createElement('span');
|
|
303
|
+
span.className = side.kind === 'preview' ? 'side-preview' : 'none';
|
|
304
|
+
span.textContent = side.kind === 'preview' ? 'Not installed' : 'Collection removed';
|
|
305
|
+
return span;
|
|
306
|
+
}
|
|
307
|
+
// A side's location, linked to that copy's own page: the viewer, or
|
|
308
|
+
// the install preview.
|
|
309
|
+
function locationValue(side) {
|
|
310
|
+
const a = document.createElement('a');
|
|
311
|
+
a.href = side.pageUrl;
|
|
312
|
+
a.target = '_blank';
|
|
313
|
+
a.rel = 'noopener';
|
|
314
|
+
a.title = side.kind === 'preview' ? 'Open the install preview' : 'Open this copy in the viewer';
|
|
315
|
+
appendWithSlashBreaks(a, side.location);
|
|
316
|
+
return a;
|
|
317
|
+
}
|
|
318
|
+
// The rows, in the viewer's details-panel order. A row appears when
|
|
319
|
+
// either side has something for it; a tag neither declares gets none.
|
|
320
|
+
function fieldRows(l, r) {
|
|
321
|
+
const rows = [];
|
|
322
|
+
const single = (label, read) => {
|
|
323
|
+
const a = l.header ? read(l.header) : undefined;
|
|
324
|
+
const b = r.header ? read(r.header) : undefined;
|
|
325
|
+
if (a === undefined && b === undefined)
|
|
326
|
+
return;
|
|
327
|
+
rows.push({ label, values: [{ items: a ? [a] : [] }, { items: b ? [b] : [] }] });
|
|
328
|
+
};
|
|
329
|
+
const list = (label, tag) => {
|
|
330
|
+
const a = l.header ? getValues(l.header, tag) : [];
|
|
331
|
+
const b = r.header ? getValues(r.header, tag) : [];
|
|
332
|
+
if (a.length === 0 && b.length === 0)
|
|
333
|
+
return;
|
|
334
|
+
rows.push({ label, values: [{ items: [...a] }, { items: [...b] }] });
|
|
335
|
+
};
|
|
336
|
+
single('Name', (h) => getValue(h, 'name'));
|
|
337
|
+
single('Description', (h) => getValue(h, 'description'));
|
|
338
|
+
single('Version', (h) => getValue(h, 'version'));
|
|
339
|
+
single('Homepage', declaredHomepage);
|
|
340
|
+
list('Match', 'match');
|
|
341
|
+
list('Include', 'include');
|
|
342
|
+
list('Exclude', 'exclude');
|
|
343
|
+
list('Network access', 'connect');
|
|
344
|
+
list('Require', 'require');
|
|
345
|
+
list('Resource', 'resource');
|
|
346
|
+
rows.push({
|
|
347
|
+
label: 'Size',
|
|
348
|
+
values: [{ items: [l.size] }, { items: [r.size] }],
|
|
349
|
+
});
|
|
350
|
+
return rows;
|
|
351
|
+
}
|
|
352
|
+
function fieldRow(row) {
|
|
353
|
+
const tr = document.createElement('tr');
|
|
354
|
+
const [a, b] = row.values;
|
|
355
|
+
const differs = !sameItems(a.items, b.items);
|
|
356
|
+
tr.className = differs ? 'differs' : 'same';
|
|
357
|
+
tr.appendChild(label(row.label));
|
|
358
|
+
tr.appendChild(valueCell(a, b, 'left'));
|
|
359
|
+
const gap = document.createElement('td');
|
|
360
|
+
gap.className = 'swap-col';
|
|
361
|
+
tr.appendChild(gap);
|
|
362
|
+
tr.appendChild(valueCell(b, a, 'right'));
|
|
363
|
+
return tr;
|
|
364
|
+
}
|
|
365
|
+
function sameItems(a, b) {
|
|
366
|
+
return a.length === b.length && a.every((item, i) => item === b[i]);
|
|
367
|
+
}
|
|
368
|
+
// A value cell. A list is one item per line, with the items the OTHER
|
|
369
|
+
// side lacks marked, so a Match row with one new pattern shows which
|
|
370
|
+
// one; a single value with no counterpart reads "none".
|
|
371
|
+
function valueCell(value, other, side) {
|
|
372
|
+
const td = document.createElement('td');
|
|
373
|
+
td.className = `value ${side}`;
|
|
374
|
+
if (value.items.length === 0) {
|
|
375
|
+
const none = document.createElement('span');
|
|
376
|
+
none.className = 'none';
|
|
377
|
+
none.textContent = 'none';
|
|
378
|
+
td.appendChild(none);
|
|
379
|
+
}
|
|
380
|
+
else if (value.items.length === 1 && other.items.length <= 1) {
|
|
381
|
+
td.textContent = value.items[0];
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
const ul = document.createElement('ul');
|
|
385
|
+
for (const item of value.items) {
|
|
386
|
+
const li = document.createElement('li');
|
|
387
|
+
li.textContent = item;
|
|
388
|
+
if (!other.items.includes(item))
|
|
389
|
+
li.className = 'only';
|
|
390
|
+
ul.appendChild(li);
|
|
391
|
+
}
|
|
392
|
+
td.appendChild(ul);
|
|
393
|
+
}
|
|
394
|
+
return td;
|
|
395
|
+
}
|
|
396
|
+
function filePairs() {
|
|
397
|
+
const b = base;
|
|
398
|
+
const other = b === left ? right : left;
|
|
399
|
+
// A pair in base / other terms, turned into left / right below.
|
|
400
|
+
const pair = (label, ofBase, ofOther) => ({
|
|
401
|
+
label,
|
|
402
|
+
leftText: b === left ? ofBase : ofOther,
|
|
403
|
+
rightText: b === left ? ofOther : ofBase,
|
|
404
|
+
});
|
|
405
|
+
const pairs = [pair(scriptBasename(b.location), b.text, other.text)];
|
|
406
|
+
const name = (req) => requireBasename(req.url, req.ref);
|
|
407
|
+
const ambiguous = new Set([...repeatedNames(b.requires), ...repeatedNames(other.requires)]);
|
|
408
|
+
// Base's requires first, in declaration order — the same tabs in the
|
|
409
|
+
// same places after a swap — each taking the first unclaimed match on
|
|
410
|
+
// the other side: by ref, then by basename.
|
|
411
|
+
const unclaimed = [...other.requires];
|
|
412
|
+
const claim = (pick) => {
|
|
413
|
+
const at = unclaimed.findIndex(pick);
|
|
414
|
+
return at < 0 ? undefined : unclaimed.splice(at, 1)[0];
|
|
415
|
+
};
|
|
416
|
+
for (const req of b.requires) {
|
|
417
|
+
const match = claim((o) => o.ref === req.ref)
|
|
418
|
+
?? (ambiguous.has(name(req)) ? undefined : claim((o) => name(o) === name(req)));
|
|
419
|
+
pairs.push(pair(name(req), req.source, match?.source ?? null));
|
|
420
|
+
}
|
|
421
|
+
// Then whatever the other side declares that nothing above claimed.
|
|
422
|
+
for (const req of unclaimed)
|
|
423
|
+
pairs.push(pair(name(req), null, req.source));
|
|
424
|
+
return pairs;
|
|
425
|
+
}
|
|
426
|
+
// The basenames declared more than once on one side.
|
|
427
|
+
function repeatedNames(requires) {
|
|
428
|
+
const seen = new Set();
|
|
429
|
+
const repeated = new Set();
|
|
430
|
+
for (const req of requires) {
|
|
431
|
+
const n = requireBasename(req.url, req.ref);
|
|
432
|
+
if (seen.has(n))
|
|
433
|
+
repeated.add(n);
|
|
434
|
+
seen.add(n);
|
|
435
|
+
}
|
|
436
|
+
return [...repeated];
|
|
437
|
+
}
|
|
438
|
+
// Same means the diff would show nothing: compared line by line, the
|
|
439
|
+
// way the diff reads the texts, so a copy that only gained or lost a
|
|
440
|
+
// final newline is not called different by the chip and then shown
|
|
441
|
+
// with no differences below it.
|
|
442
|
+
function verdict(pair) {
|
|
443
|
+
if (pair.leftText === null)
|
|
444
|
+
return 'added';
|
|
445
|
+
if (pair.rightText === null)
|
|
446
|
+
return 'removed';
|
|
447
|
+
return sameItems(splitLines(pair.leftText), splitLines(pair.rightText)) ? 'same' : 'different';
|
|
448
|
+
}
|
|
449
|
+
function renderTabs() {
|
|
450
|
+
const pairs = filePairs();
|
|
451
|
+
if (activeTab >= pairs.length)
|
|
452
|
+
activeTab = 0;
|
|
453
|
+
tabsEl.replaceChildren();
|
|
454
|
+
pairs.forEach((pair, i) => {
|
|
455
|
+
const btn = document.createElement('button');
|
|
456
|
+
btn.type = 'button';
|
|
457
|
+
btn.className = 'tab' + (i === activeTab ? ' active' : '');
|
|
458
|
+
btn.setAttribute('role', 'tab');
|
|
459
|
+
btn.setAttribute('aria-selected', String(i === activeTab));
|
|
460
|
+
btn.appendChild(document.createTextNode(pair.label));
|
|
461
|
+
const v = verdict(pair);
|
|
462
|
+
const chip = document.createElement('span');
|
|
463
|
+
chip.className = `chip chip-${v}`;
|
|
464
|
+
chip.textContent = v;
|
|
465
|
+
btn.appendChild(chip);
|
|
466
|
+
btn.addEventListener('click', () => {
|
|
467
|
+
activeTab = i;
|
|
468
|
+
renderTabs();
|
|
469
|
+
renderDiff();
|
|
470
|
+
});
|
|
471
|
+
tabsEl.appendChild(btn);
|
|
472
|
+
});
|
|
473
|
+
tabsEl.hidden = false;
|
|
474
|
+
}
|
|
475
|
+
// ---------- The view controls ----------
|
|
476
|
+
function renderViewRow() {
|
|
477
|
+
viewRowEl.replaceChildren(segmented([['unified', 'Unified'], ['split', 'Side by side']], () => view, (v) => { view = v; writeSetting(VIEW_KEY, v); }), segmented([['changes', 'Changes only'], ['whole', 'Whole file']], () => scope, (v) => { scope = v; writeSetting(SCOPE_KEY, v); }));
|
|
478
|
+
}
|
|
479
|
+
// A segmented control: one button per choice, the current one pressed.
|
|
480
|
+
function segmented(choices, current, choose) {
|
|
481
|
+
const seg = document.createElement('span');
|
|
482
|
+
seg.className = 'seg';
|
|
483
|
+
seg.setAttribute('role', 'group');
|
|
484
|
+
for (const [value, label] of choices) {
|
|
485
|
+
const btn = document.createElement('button');
|
|
486
|
+
btn.type = 'button';
|
|
487
|
+
btn.textContent = label;
|
|
488
|
+
btn.dataset.value = value;
|
|
489
|
+
btn.setAttribute('aria-pressed', String(value === current()));
|
|
490
|
+
btn.classList.toggle('on', value === current());
|
|
491
|
+
btn.addEventListener('click', () => {
|
|
492
|
+
choose(value);
|
|
493
|
+
for (const b of seg.querySelectorAll('button')) {
|
|
494
|
+
b.classList.toggle('on', b === btn);
|
|
495
|
+
b.setAttribute('aria-pressed', String(b === btn));
|
|
496
|
+
}
|
|
497
|
+
renderDiff();
|
|
498
|
+
});
|
|
499
|
+
seg.appendChild(btn);
|
|
500
|
+
}
|
|
501
|
+
return seg;
|
|
502
|
+
}
|
|
503
|
+
// ---------- The diff ----------
|
|
504
|
+
function renderDiff() {
|
|
505
|
+
const pair = filePairs()[activeTab];
|
|
506
|
+
if (!pair) {
|
|
507
|
+
diffEl.hidden = true;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const a = splitLines(pair.leftText ?? '');
|
|
511
|
+
const b = splitLines(pair.rightText ?? '');
|
|
512
|
+
const ops = diffLines(a, b);
|
|
513
|
+
const counts = countChanges(ops);
|
|
514
|
+
// The lines scroll inside the body, under a summary strip that stays
|
|
515
|
+
// put — the box fills what is left of the window (compare.html), so
|
|
516
|
+
// the scroll bars are at the window's edge rather than under a
|
|
517
|
+
// page's worth of code.
|
|
518
|
+
const body = document.createElement('div');
|
|
519
|
+
body.className = 'diff-body';
|
|
520
|
+
// A file both sides have in full and agree on is shown once, whatever
|
|
521
|
+
// the view: two identical columns say nothing the one doesn't.
|
|
522
|
+
const identical = counts.added === 0 && counts.removed === 0;
|
|
523
|
+
if (view === 'split' && !identical) {
|
|
524
|
+
body.classList.add('split');
|
|
525
|
+
body.append(...splitPanes(ops));
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
// The one scroller, focusable like the viewer's code box (side by
|
|
529
|
+
// side, the panes are the scrollers and the focus targets).
|
|
530
|
+
body.tabIndex = 0;
|
|
531
|
+
body.appendChild(unifiedTable(ops, identical));
|
|
532
|
+
}
|
|
533
|
+
diffEl.replaceChildren(summaryRow(counts, body), body);
|
|
534
|
+
diffEl.hidden = false;
|
|
535
|
+
}
|
|
536
|
+
// The summary strip: the counts, then the Up/Down arrows that jump
|
|
537
|
+
// between change blocks (the collections page's move arrows, by glyph).
|
|
538
|
+
// `body` is the scroller the arrows drive.
|
|
539
|
+
function summaryRow(counts, body) {
|
|
540
|
+
const row = document.createElement('div');
|
|
541
|
+
row.className = 'diff-summary';
|
|
542
|
+
const text = document.createElement('span');
|
|
543
|
+
text.className = 'diff-counts';
|
|
544
|
+
row.appendChild(text);
|
|
545
|
+
if (counts.removed === 0 && counts.added === 0) {
|
|
546
|
+
text.textContent = 'No differences';
|
|
547
|
+
return row;
|
|
548
|
+
}
|
|
549
|
+
const parts = [];
|
|
550
|
+
if (counts.removed > 0) {
|
|
551
|
+
const del = document.createElement('span');
|
|
552
|
+
del.className = 'removed';
|
|
553
|
+
del.textContent = `−${counts.removed}`;
|
|
554
|
+
parts.push(del);
|
|
555
|
+
}
|
|
556
|
+
if (counts.added > 0) {
|
|
557
|
+
const add = document.createElement('span');
|
|
558
|
+
add.className = 'added';
|
|
559
|
+
add.textContent = `+${counts.added}`;
|
|
560
|
+
parts.push(add);
|
|
561
|
+
}
|
|
562
|
+
parts.forEach((part, i) => {
|
|
563
|
+
if (i > 0)
|
|
564
|
+
text.appendChild(document.createTextNode(' '));
|
|
565
|
+
text.appendChild(part);
|
|
566
|
+
});
|
|
567
|
+
text.appendChild(document.createTextNode(counts.removed + counts.added === 1 ? ' line' : ' lines'));
|
|
568
|
+
row.append(...blockNav(body));
|
|
569
|
+
return row;
|
|
570
|
+
}
|
|
571
|
+
// ---------- Jumping between change blocks ----------
|
|
572
|
+
// A change block is a run of changed rows — removals, additions, and
|
|
573
|
+
// (side by side) the blanks across from them — between two unchanged
|
|
574
|
+
// rows or seams. Its first row wears `.block-start`; the arrows step
|
|
575
|
+
// between those rows in whichever element scrolls: the unified body,
|
|
576
|
+
// or the left pane, whose scroll the right pane follows.
|
|
577
|
+
//
|
|
578
|
+
// A jump lands a few lines of context ABOVE the block at the top of
|
|
579
|
+
// the box, not the block's first row: a changed line flush with the
|
|
580
|
+
// top edge does not read as the start of anything, and the lines
|
|
581
|
+
// before it are what say what the change is to. The rest of the box
|
|
582
|
+
// is then the block and what follows it.
|
|
583
|
+
//
|
|
584
|
+
// Which block an arrow steps from: the marked one (the last jumped
|
|
585
|
+
// to, `.current`) when any of it is on screen — so the blocks at the
|
|
586
|
+
// ends of the file, whose landing spots are past the scroll range,
|
|
587
|
+
// can still be stepped through by the mark alone, with the box
|
|
588
|
+
// scrolled as far as it goes. When no marked block is on screen
|
|
589
|
+
// (none yet, or the user has scrolled away by hand) the step is from
|
|
590
|
+
// what is on screen instead: Down to the first block whose first row
|
|
591
|
+
// is at or below the top edge, Up to the last one above it. A block
|
|
592
|
+
// index alone would send the click somewhere unrelated to the view.
|
|
593
|
+
// (After a hand scroll, Down can then land a block already near the
|
|
594
|
+
// top by moving the box UP a few rows — the jump's context above —
|
|
595
|
+
// which is the block's own place, not a wrong direction.)
|
|
596
|
+
const CONTEXT_ABOVE = 5;
|
|
597
|
+
function blockNav(body) {
|
|
598
|
+
const up = arrowButton('up', 'Previous change');
|
|
599
|
+
const down = arrowButton('down', 'Next change');
|
|
600
|
+
const scroller = () => body.querySelector('.pane.left') ?? body;
|
|
601
|
+
// Each block's landing spot is the row CONTEXT_ABOVE rows above its
|
|
602
|
+
// first — found by walking, not by row height, since a `@@` seam is
|
|
603
|
+
// not as tall as a line. A row's offsetTop is from its table, which
|
|
604
|
+
// sits at the top of the scroller, so it is the scrollTop that puts
|
|
605
|
+
// that row at the top. A block within reach of the top of the file
|
|
606
|
+
// lands at the top.
|
|
607
|
+
const blocks = () => [...scroller().querySelectorAll('tr.block-start')].map((tr) => {
|
|
608
|
+
let row = tr;
|
|
609
|
+
for (let n = 0; n < CONTEXT_ABOVE && row.previousElementSibling; n++) {
|
|
610
|
+
row = row.previousElementSibling;
|
|
611
|
+
}
|
|
612
|
+
return { tr, top: row.offsetTop };
|
|
613
|
+
});
|
|
614
|
+
// The index to step from, or -1 for "what is on screen": the marked
|
|
615
|
+
// block, when any of its rows is in view — its first row above the
|
|
616
|
+
// top edge with the rest still showing is still the block in hand.
|
|
617
|
+
const current = (all) => {
|
|
618
|
+
const el = scroller();
|
|
619
|
+
const i = all.findIndex((b) => b.tr.classList.contains('current'));
|
|
620
|
+
if (i < 0)
|
|
621
|
+
return -1;
|
|
622
|
+
let last = all[i].tr;
|
|
623
|
+
while (last.nextElementSibling?.classList.contains('current'))
|
|
624
|
+
last = last.nextElementSibling;
|
|
625
|
+
const top = all[i].tr.offsetTop;
|
|
626
|
+
const bottom = last.offsetTop + last.offsetHeight;
|
|
627
|
+
return bottom > el.scrollTop && top < el.scrollTop + el.clientHeight ? i : -1;
|
|
628
|
+
};
|
|
629
|
+
const target = (direction) => {
|
|
630
|
+
const all = blocks();
|
|
631
|
+
const i = current(all);
|
|
632
|
+
if (i >= 0)
|
|
633
|
+
return all[direction === 'up' ? i - 1 : i + 1];
|
|
634
|
+
const top = scroller().scrollTop;
|
|
635
|
+
return direction === 'up'
|
|
636
|
+
? all.filter((b) => b.tr.offsetTop < top).pop()
|
|
637
|
+
: all.find((b) => b.tr.offsetTop >= top);
|
|
638
|
+
};
|
|
639
|
+
const refresh = () => {
|
|
640
|
+
up.disabled = !target('up');
|
|
641
|
+
down.disabled = !target('down');
|
|
642
|
+
};
|
|
643
|
+
const step = (direction) => {
|
|
644
|
+
const block = target(direction);
|
|
645
|
+
if (!block)
|
|
646
|
+
return;
|
|
647
|
+
// Past the scroll range this is clamped: the mark alone moves.
|
|
648
|
+
scroller().scrollTop = block.top;
|
|
649
|
+
markCurrent(body, block.tr);
|
|
650
|
+
refresh();
|
|
651
|
+
};
|
|
652
|
+
up.addEventListener('click', () => step('up'));
|
|
653
|
+
down.addEventListener('click', () => step('down'));
|
|
654
|
+
for (const el of body.classList.contains('split') ? body.querySelectorAll('.pane') : [body]) {
|
|
655
|
+
el.addEventListener('scroll', refresh);
|
|
656
|
+
}
|
|
657
|
+
// The body is not in the layout yet — it is appended after this
|
|
658
|
+
// returns — so the first refresh waits for it: the observer reports
|
|
659
|
+
// once on observe, after layout. A resize also changes what is on
|
|
660
|
+
// screen, which the arrows' states depend on.
|
|
661
|
+
new ResizeObserver(refresh).observe(body);
|
|
662
|
+
return [up, down];
|
|
663
|
+
}
|
|
664
|
+
// Mark the block just jumped to, on every row of it and in both panes:
|
|
665
|
+
// the landing spot has lines of context above the block, and often
|
|
666
|
+
// another block in view, so nothing else says which one the arrow
|
|
667
|
+
// went to. `.current` on the rows; the gutter is what wears it (the
|
|
668
|
+
// line numbers, compare.html). The mark stays until the next jump, or
|
|
669
|
+
// a redraw of the diff (view, scope, tab) — a stale one off screen
|
|
670
|
+
// after a hand scroll costs nothing.
|
|
671
|
+
//
|
|
672
|
+
// The panes hold the same rows at the same indexes (splitPanes), so
|
|
673
|
+
// the block is found by index in the given table and applied to each.
|
|
674
|
+
function markCurrent(body, start) {
|
|
675
|
+
for (const tr of body.querySelectorAll('tr.current'))
|
|
676
|
+
tr.classList.remove('current');
|
|
677
|
+
const table = start.closest('table');
|
|
678
|
+
if (!table)
|
|
679
|
+
return;
|
|
680
|
+
const from = [...table.rows].indexOf(start);
|
|
681
|
+
for (const t of body.querySelectorAll('table')) {
|
|
682
|
+
const rows = [...t.rows];
|
|
683
|
+
for (let i = from; i < rows.length && rows[i].matches('.del, .add, .filler'); i++) {
|
|
684
|
+
rows[i].classList.add('current');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// An Up or Down arrow, in the same character and box as the collections
|
|
689
|
+
// page's move arrows — `.icon-btn` so it lines up with every other icon
|
|
690
|
+
// button, sized down by the strip to sit in its line.
|
|
691
|
+
function arrowButton(direction, label) {
|
|
692
|
+
const btn = document.createElement('button');
|
|
693
|
+
btn.type = 'button';
|
|
694
|
+
btn.className = `icon-btn block-${direction === 'up' ? 'prev' : 'next'}`;
|
|
695
|
+
btn.textContent = direction === 'up' ? '↑' : '↓';
|
|
696
|
+
btn.title = label;
|
|
697
|
+
btn.setAttribute('aria-label', label);
|
|
698
|
+
return btn;
|
|
699
|
+
}
|
|
700
|
+
// Which ops to draw: every line, or the hunks with `@@` seams between
|
|
701
|
+
// them. `whole` is forced for an identical file, where hunks would be
|
|
702
|
+
// nothing at all.
|
|
703
|
+
function visibleOps(ops, whole) {
|
|
704
|
+
if (whole || scope === 'whole')
|
|
705
|
+
return [...ops];
|
|
706
|
+
const out = [];
|
|
707
|
+
for (const hunk of toHunks(ops)) {
|
|
708
|
+
out.push({
|
|
709
|
+
kind: 'hunk',
|
|
710
|
+
text: `@@ -${range(hunk.aStart, hunk.aCount)} +${range(hunk.bStart, hunk.bCount)} @@`,
|
|
711
|
+
});
|
|
712
|
+
out.push(...hunk.ops);
|
|
713
|
+
}
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
// A hunk header's range, as git prints it: the count is left off when
|
|
717
|
+
// it is one.
|
|
718
|
+
function range(start, count) {
|
|
719
|
+
return count === 1 ? String(start) : `${start},${count}`;
|
|
720
|
+
}
|
|
721
|
+
function unifiedTable(ops, whole) {
|
|
722
|
+
const table = document.createElement('table');
|
|
723
|
+
table.className = 'diff unified';
|
|
724
|
+
// The first changed row after an unchanged row (or a seam, or the
|
|
725
|
+
// top) starts a block — see blockNav.
|
|
726
|
+
let inBlock = false;
|
|
727
|
+
for (const op of visibleOps(ops, whole)) {
|
|
728
|
+
const tr = document.createElement('tr');
|
|
729
|
+
if (op.kind === 'hunk') {
|
|
730
|
+
tr.className = 'hunk';
|
|
731
|
+
tr.appendChild(td('num', ''));
|
|
732
|
+
tr.appendChild(td('sign', ''));
|
|
733
|
+
tr.appendChild(td('code', op.text));
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
tr.className = op.kind === 'same' ? '' : op.kind;
|
|
737
|
+
// The number from whichever side has the line; a shared line's
|
|
738
|
+
// two numbers can differ, and the right side's is the base's.
|
|
739
|
+
tr.appendChild(td('num', String(op.bLine ?? op.aLine ?? '')));
|
|
740
|
+
tr.appendChild(td('sign', op.kind === 'del' ? '−' : op.kind === 'add' ? '+' : ' '));
|
|
741
|
+
tr.appendChild(td('code', op.text));
|
|
742
|
+
}
|
|
743
|
+
const changed = op.kind === 'del' || op.kind === 'add';
|
|
744
|
+
if (changed && !inBlock)
|
|
745
|
+
tr.classList.add('block-start');
|
|
746
|
+
inBlock = changed;
|
|
747
|
+
table.appendChild(tr);
|
|
748
|
+
}
|
|
749
|
+
return table;
|
|
750
|
+
}
|
|
751
|
+
// Side by side: a pane per side, each scrolling sideways on its own so
|
|
752
|
+
// a long line in one never pushes the other off screen. The two hold
|
|
753
|
+
// the same rows — an unchanged line on both, a removed line beside the
|
|
754
|
+
// added line that replaced it (a run of removals zipped with the run of
|
|
755
|
+
// additions after it), a blank beside a line with no counterpart — so
|
|
756
|
+
// they line up, and their vertical scroll is kept in step.
|
|
757
|
+
function splitPanes(ops) {
|
|
758
|
+
const leftTable = document.createElement('table');
|
|
759
|
+
leftTable.className = 'diff';
|
|
760
|
+
const rightTable = document.createElement('table');
|
|
761
|
+
rightTable.className = 'diff';
|
|
762
|
+
const rows = visibleOps(ops, false);
|
|
763
|
+
let i = 0;
|
|
764
|
+
while (i < rows.length) {
|
|
765
|
+
const op = rows[i];
|
|
766
|
+
if (op.kind === 'hunk') {
|
|
767
|
+
leftTable.appendChild(paneRow(op, 'left'));
|
|
768
|
+
rightTable.appendChild(paneRow(op, 'right'));
|
|
769
|
+
i++;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (op.kind === 'same') {
|
|
773
|
+
leftTable.appendChild(paneRow(op, 'left'));
|
|
774
|
+
rightTable.appendChild(paneRow(op, 'right'));
|
|
775
|
+
i++;
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
// The whole changed run, whatever order the diff emits the two
|
|
779
|
+
// kinds in, so a block here is the block unified view has.
|
|
780
|
+
const dels = [];
|
|
781
|
+
const adds = [];
|
|
782
|
+
for (let next = rows[i]; next && (next.kind === 'del' || next.kind === 'add'); next = rows[++i]) {
|
|
783
|
+
(next.kind === 'del' ? dels : adds).push(next);
|
|
784
|
+
}
|
|
785
|
+
const n = Math.max(dels.length, adds.length);
|
|
786
|
+
for (let k = 0; k < n; k++) {
|
|
787
|
+
const left = paneRow(dels[k], 'left');
|
|
788
|
+
const right = paneRow(adds[k], 'right');
|
|
789
|
+
// The run is one block (blockNav); its first row starts it, on
|
|
790
|
+
// both sides so either pane can be measured.
|
|
791
|
+
if (k === 0) {
|
|
792
|
+
left.classList.add('block-start');
|
|
793
|
+
right.classList.add('block-start');
|
|
794
|
+
}
|
|
795
|
+
leftTable.appendChild(left);
|
|
796
|
+
rightTable.appendChild(right);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const leftPane = pane(leftTable, 'left');
|
|
800
|
+
const rightPane = pane(rightTable, 'right');
|
|
801
|
+
linkScroll(leftPane, rightPane);
|
|
802
|
+
return [leftPane, rightPane];
|
|
803
|
+
}
|
|
804
|
+
// Keep two panes' vertical scroll in step. The scroll one pane is
|
|
805
|
+
// given by the other is remembered and its own event for it ignored:
|
|
806
|
+
// otherwise that event would set the first pane in turn, and the two
|
|
807
|
+
// would fight whenever the second could not scroll quite as far — a
|
|
808
|
+
// drag of one scrollbar then bounces between the two positions.
|
|
809
|
+
//
|
|
810
|
+
// The record is one-shot — consumed by the event it was made for —
|
|
811
|
+
// or a later scroll of the user's own that happens to land on the
|
|
812
|
+
// same value (PageDown then PageUp) would be swallowed too and leave
|
|
813
|
+
// the panes out of step until the next scroll.
|
|
814
|
+
function linkScroll(a, b) {
|
|
815
|
+
const given = new Map();
|
|
816
|
+
const follow = (from, to) => {
|
|
817
|
+
if (given.get(from) === from.scrollTop) {
|
|
818
|
+
given.delete(from);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
// Already level: setting the same value fires no event, so a
|
|
822
|
+
// record left here would swallow a real scroll later.
|
|
823
|
+
if (to.scrollTop === from.scrollTop)
|
|
824
|
+
return;
|
|
825
|
+
to.scrollTop = from.scrollTop;
|
|
826
|
+
// What `to` actually landed on, which is what its event will say.
|
|
827
|
+
given.set(to, to.scrollTop);
|
|
828
|
+
};
|
|
829
|
+
a.addEventListener('scroll', () => follow(a, b));
|
|
830
|
+
b.addEventListener('scroll', () => follow(b, a));
|
|
831
|
+
}
|
|
832
|
+
// Focusable, like the viewer's code box: a pane that can be stood in
|
|
833
|
+
// scrolls with the arrow keys, and is what Ctrl-A selects.
|
|
834
|
+
function pane(table, side) {
|
|
835
|
+
const el = document.createElement('div');
|
|
836
|
+
el.className = `pane ${side}`;
|
|
837
|
+
el.tabIndex = 0;
|
|
838
|
+
el.appendChild(table);
|
|
839
|
+
return el;
|
|
840
|
+
}
|
|
841
|
+
// One row of one pane. No op is the blank across from a line the other
|
|
842
|
+
// side has alone.
|
|
843
|
+
function paneRow(op, side) {
|
|
844
|
+
const tr = document.createElement('tr');
|
|
845
|
+
if (!op) {
|
|
846
|
+
tr.className = 'filler';
|
|
847
|
+
tr.appendChild(td('num', ''));
|
|
848
|
+
tr.appendChild(td('code', ''));
|
|
849
|
+
return tr;
|
|
850
|
+
}
|
|
851
|
+
if (op.kind === 'hunk') {
|
|
852
|
+
tr.className = 'hunk';
|
|
853
|
+
tr.appendChild(td('num', ''));
|
|
854
|
+
tr.appendChild(td('code', op.text));
|
|
855
|
+
return tr;
|
|
856
|
+
}
|
|
857
|
+
tr.className = op.kind === 'same' ? '' : op.kind;
|
|
858
|
+
tr.appendChild(td('num', String(side === 'left' ? op.aLine : op.bLine)));
|
|
859
|
+
tr.appendChild(td('code', op.text));
|
|
860
|
+
return tr;
|
|
861
|
+
}
|
|
862
|
+
function td(className, text) {
|
|
863
|
+
const cell = document.createElement('td');
|
|
864
|
+
cell.className = className;
|
|
865
|
+
cell.textContent = text;
|
|
866
|
+
return cell;
|
|
867
|
+
}
|