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.
Files changed (367) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/bytes.js +37 -0
  4. package/dist/channel-placeholder.js +5 -0
  5. package/dist/collection-scan.js +17 -0
  6. package/dist/collections.html +353 -0
  7. package/dist/compare.html +358 -0
  8. package/dist/composed-source.js +205 -0
  9. package/dist/connect-prompt.html +47 -0
  10. package/dist/debug/icons.html +211 -0
  11. package/dist/debug/storage.html +163 -0
  12. package/dist/debug-log.js +75 -0
  13. package/dist/diagnostic-location.js +64 -0
  14. package/dist/eval-hook.js +308 -0
  15. package/dist/extension/background.js +5263 -0
  16. package/dist/extension/channel.js +133 -0
  17. package/dist/extension/code-links.js +187 -0
  18. package/dist/extension/code-tabs.js +460 -0
  19. package/dist/extension/collection-dialog.js +619 -0
  20. package/dist/extension/collection-display.js +322 -0
  21. package/dist/extension/collections.js +2905 -0
  22. package/dist/extension/compare.js +867 -0
  23. package/dist/extension/concurrency.js +186 -0
  24. package/dist/extension/connect-grants.js +109 -0
  25. package/dist/extension/connect-prompt-page.js +70 -0
  26. package/dist/extension/connect-prompt.js +266 -0
  27. package/dist/extension/copy-button.js +63 -0
  28. package/dist/extension/copy-dialog.js +502 -0
  29. package/dist/extension/copy-preference.js +66 -0
  30. package/dist/extension/debug/icons.js +203 -0
  31. package/dist/extension/debug/storage.js +365 -0
  32. package/dist/extension/deep-link.js +77 -0
  33. package/dist/extension/delete-dialog.js +432 -0
  34. package/dist/extension/delete-icon.js +40 -0
  35. package/dist/extension/details-tooltips.js +66 -0
  36. package/dist/extension/directories.js +679 -0
  37. package/dist/extension/export-backup.js +407 -0
  38. package/dist/extension/export-dialog.js +142 -0
  39. package/dist/extension/file-access.js +74 -0
  40. package/dist/extension/file-changes.js +346 -0
  41. package/dist/extension/frame-info.js +85 -0
  42. package/dist/extension/github-provider.js +336 -0
  43. package/dist/extension/glyph.js +84 -0
  44. package/dist/extension/gm-bindings.js +601 -0
  45. package/dist/extension/gm-download.js +419 -0
  46. package/dist/extension/gm-menu.js +228 -0
  47. package/dist/extension/gm-notification.js +141 -0
  48. package/dist/extension/gm-originator-map.js +23 -0
  49. package/dist/extension/gm-tab-data.js +121 -0
  50. package/dist/extension/gm-tab.js +160 -0
  51. package/dist/extension/gm-url-change.js +111 -0
  52. package/dist/extension/gm-url-scheme.js +27 -0
  53. package/dist/extension/gm-values.js +176 -0
  54. package/dist/extension/gm-window-control.js +104 -0
  55. package/dist/extension/gm-xhr-headers.js +333 -0
  56. package/dist/extension/gm-xhr.js +584 -0
  57. package/dist/extension/injector.js +3145 -0
  58. package/dist/extension/install-capture.js +277 -0
  59. package/dist/extension/line-diff.js +233 -0
  60. package/dist/extension/local-copy.js +560 -0
  61. package/dist/extension/local-delete.js +276 -0
  62. package/dist/extension/location-kinds.js +108 -0
  63. package/dist/extension/log-format.js +158 -0
  64. package/dist/extension/managed-changes.js +361 -0
  65. package/dist/extension/managed-items.js +57 -0
  66. package/dist/extension/match-index.js +375 -0
  67. package/dist/extension/missing-scripts.js +66 -0
  68. package/dist/extension/more-menu.js +109 -0
  69. package/dist/extension/move-dialog.js +197 -0
  70. package/dist/extension/offscreen.js +37 -0
  71. package/dist/extension/options.js +504 -0
  72. package/dist/extension/page-heading.js +170 -0
  73. package/dist/extension/page-links.js +199 -0
  74. package/dist/extension/parse-check-client.js +70 -0
  75. package/dist/extension/parse-check.js +161 -0
  76. package/dist/extension/parse-sandbox-host.js +172 -0
  77. package/dist/extension/parse-sandbox.js +175 -0
  78. package/dist/extension/popup-commands-open.js +41 -0
  79. package/dist/extension/popup-view.js +387 -0
  80. package/dist/extension/popup.js +703 -0
  81. package/dist/extension/prism-userscript.js +53 -0
  82. package/dist/extension/refresh-error.js +136 -0
  83. package/dist/extension/refresh-targets.js +160 -0
  84. package/dist/extension/related-versions.js +1144 -0
  85. package/dist/extension/resize-observer-loop.js +19 -0
  86. package/dist/extension/row-cache.js +387 -0
  87. package/dist/extension/script-blobs.js +413 -0
  88. package/dist/extension/script-details.js +658 -0
  89. package/dist/extension/script-enablement.js +285 -0
  90. package/dist/extension/script-identity.js +310 -0
  91. package/dist/extension/script-keys.js +54 -0
  92. package/dist/extension/script-log-id.js +155 -0
  93. package/dist/extension/script-log.js +290 -0
  94. package/dist/extension/script-rejection.js +104 -0
  95. package/dist/extension/script-sites.js +158 -0
  96. package/dist/extension/script-store.js +3507 -0
  97. package/dist/extension/script-updater.js +991 -0
  98. package/dist/extension/script-url-hash.js +61 -0
  99. package/dist/extension/select-all.js +37 -0
  100. package/dist/extension/select-open-guard.js +90 -0
  101. package/dist/extension/site-icon.js +1261 -0
  102. package/dist/extension/sm-dev-link.js +302 -0
  103. package/dist/extension/solo-click.js +58 -0
  104. package/dist/extension/sticky-numbers.js +188 -0
  105. package/dist/extension/storage-breakdown.js +253 -0
  106. package/dist/extension/storage-scan.js +86 -0
  107. package/dist/extension/storage-usage.js +194 -0
  108. package/dist/extension/structural-keys.js +29 -0
  109. package/dist/extension/sw-teardown.js +36 -0
  110. package/dist/extension/tab-status.js +403 -0
  111. package/dist/extension/toolbar-summary.js +105 -0
  112. package/dist/extension/user-scripts-access.js +43 -0
  113. package/dist/extension/userscript.js +3672 -0
  114. package/dist/extension/value-store.js +204 -0
  115. package/dist/extension/version-probe-fetch.js +68 -0
  116. package/dist/extension/worker-client.js +109 -0
  117. package/dist/extension/worker-ops.js +171 -0
  118. package/dist/extension/zip-writer.js +24 -0
  119. package/dist/external-files.js +878 -0
  120. package/dist/file-loader.js +436 -0
  121. package/dist/file-locations.js +439 -0
  122. package/dist/generated-wrappers.js +5 -0
  123. package/dist/github-auth.js +148 -0
  124. package/dist/github-urls.js +46 -0
  125. package/dist/gm-api-catalog.js +217 -0
  126. package/dist/gm-connect.js +273 -0
  127. package/dist/gm-xhr-types.js +5 -0
  128. package/dist/http-read.js +142 -0
  129. package/dist/icons/icon-128.png +0 -0
  130. package/dist/icons/icon-16.png +0 -0
  131. package/dist/icons/icon-48.png +0 -0
  132. package/dist/local-file-overrides.js +558 -0
  133. package/dist/manifest.json +38 -0
  134. package/dist/markup-response.js +126 -0
  135. package/dist/match-targets.js +305 -0
  136. package/dist/menu-captions.js +22 -0
  137. package/dist/offscreen.html +39 -0
  138. package/dist/options.html +197 -0
  139. package/dist/parse-check-protocol.js +51 -0
  140. package/dist/parse-location.js +171 -0
  141. package/dist/parse-sandbox.html +56 -0
  142. package/dist/phase-timing.js +155 -0
  143. package/dist/popup.html +197 -0
  144. package/dist/public-suffixes.js +6957 -0
  145. package/dist/run-context.js +31 -0
  146. package/dist/runtime-messages.js +34 -0
  147. package/dist/sandboxed-page.js +35 -0
  148. package/dist/script-build.js +787 -0
  149. package/dist/script-display.js +383 -0
  150. package/dist/script-icon.js +43 -0
  151. package/dist/script-links.js +204 -0
  152. package/dist/script-source-url.js +214 -0
  153. package/dist/script-world.js +65 -0
  154. package/dist/sm-dev-protocol.js +84 -0
  155. package/dist/source-map.js +177 -0
  156. package/dist/stack-link.js +138 -0
  157. package/dist/styles.css +825 -0
  158. package/dist/unsafe-window-use.js +229 -0
  159. package/dist/url-match.js +734 -0
  160. package/dist/userscript-header.js +248 -0
  161. package/dist/userscript.html +1165 -0
  162. package/dist/vendor/fflate.LICENSE +21 -0
  163. package/dist/vendor/fflate.js +2692 -0
  164. package/dist/vendor/prism/prism-line-numbers.css +1 -0
  165. package/dist/vendor/prism/prism-line-numbers.js +1 -0
  166. package/dist/vendor/prism/prism.css +1 -0
  167. package/dist/vendor/prism/prism.js +1946 -0
  168. package/dist/version-compare.js +141 -0
  169. package/dist/version-probe.js +120 -0
  170. package/lib/build-info.json +1 -0
  171. package/lib/bytes.d.ts +3 -0
  172. package/lib/bytes.js +37 -0
  173. package/lib/channel-placeholder.d.ts +1 -0
  174. package/lib/channel-placeholder.js +5 -0
  175. package/lib/collection-scan.d.ts +8 -0
  176. package/lib/collection-scan.js +17 -0
  177. package/lib/composed-source.d.ts +28 -0
  178. package/lib/composed-source.js +205 -0
  179. package/lib/debug-log.d.ts +9 -0
  180. package/lib/debug-log.js +75 -0
  181. package/lib/diagnostic-location.d.ts +8 -0
  182. package/lib/diagnostic-location.js +64 -0
  183. package/lib/eval-hook.d.ts +34 -0
  184. package/lib/eval-hook.js +308 -0
  185. package/lib/external-files.d.ts +111 -0
  186. package/lib/external-files.js +878 -0
  187. package/lib/file-loader.d.ts +48 -0
  188. package/lib/file-loader.js +436 -0
  189. package/lib/file-locations.d.ts +44 -0
  190. package/lib/file-locations.js +439 -0
  191. package/lib/generated-wrappers.d.ts +11 -0
  192. package/lib/generated-wrappers.js +5 -0
  193. package/lib/github-auth.d.ts +12 -0
  194. package/lib/github-auth.js +148 -0
  195. package/lib/github-urls.d.ts +1 -0
  196. package/lib/github-urls.js +46 -0
  197. package/lib/gm-api-catalog.d.ts +24 -0
  198. package/lib/gm-api-catalog.js +217 -0
  199. package/lib/gm-connect.d.ts +34 -0
  200. package/lib/gm-connect.js +273 -0
  201. package/lib/gm-xhr-types.d.ts +68 -0
  202. package/lib/gm-xhr-types.js +5 -0
  203. package/lib/harness/browser-extensions.d.ts +73 -0
  204. package/lib/harness/browser-extensions.js +285 -0
  205. package/lib/harness/browser-lang.d.ts +20 -0
  206. package/lib/harness/browser-lang.js +86 -0
  207. package/lib/harness/browser-launch.d.ts +14 -0
  208. package/lib/harness/browser-launch.js +184 -0
  209. package/lib/harness/checks.d.ts +110 -0
  210. package/lib/harness/checks.js +321 -0
  211. package/lib/harness/cli.d.ts +2 -0
  212. package/lib/harness/cli.js +2259 -0
  213. package/lib/harness/command-args.d.ts +91 -0
  214. package/lib/harness/command-args.js +369 -0
  215. package/lib/harness/command-lines.d.ts +80 -0
  216. package/lib/harness/command-lines.js +180 -0
  217. package/lib/harness/command-list.d.ts +206 -0
  218. package/lib/harness/command-list.js +1475 -0
  219. package/lib/harness/console-commands.d.ts +463 -0
  220. package/lib/harness/console-commands.js +3216 -0
  221. package/lib/harness/download-watch.d.ts +21 -0
  222. package/lib/harness/download-watch.js +156 -0
  223. package/lib/harness/extension-backend.d.ts +41 -0
  224. package/lib/harness/extension-backend.js +100 -0
  225. package/lib/harness/extension-host.d.ts +112 -0
  226. package/lib/harness/extension-host.js +552 -0
  227. package/lib/harness/extension-launch.d.ts +67 -0
  228. package/lib/harness/extension-launch.js +285 -0
  229. package/lib/harness/extension-link.d.ts +315 -0
  230. package/lib/harness/extension-link.js +607 -0
  231. package/lib/harness/external-manager.d.ts +146 -0
  232. package/lib/harness/external-manager.js +414 -0
  233. package/lib/harness/fixture.d.ts +165 -0
  234. package/lib/harness/fixture.js +461 -0
  235. package/lib/harness/frame-sessions.d.ts +71 -0
  236. package/lib/harness/frame-sessions.js +248 -0
  237. package/lib/harness/frames.d.ts +68 -0
  238. package/lib/harness/frames.js +216 -0
  239. package/lib/harness/help-format.d.ts +17 -0
  240. package/lib/harness/help-format.js +127 -0
  241. package/lib/harness/host.d.ts +352 -0
  242. package/lib/harness/host.js +423 -0
  243. package/lib/harness/index.d.ts +8 -0
  244. package/lib/harness/index.js +10 -0
  245. package/lib/harness/input-file.d.ts +18 -0
  246. package/lib/harness/input-file.js +61 -0
  247. package/lib/harness/json-output.d.ts +565 -0
  248. package/lib/harness/json-output.js +71 -0
  249. package/lib/harness/manager-actions.d.ts +21 -0
  250. package/lib/harness/manager-actions.js +129 -0
  251. package/lib/harness/manager-commands.d.ts +142 -0
  252. package/lib/harness/manager-commands.js +1196 -0
  253. package/lib/harness/manager-fetch.d.ts +33 -0
  254. package/lib/harness/manager-fetch.js +239 -0
  255. package/lib/harness/managers.d.ts +24 -0
  256. package/lib/harness/managers.js +27 -0
  257. package/lib/harness/output-style.d.ts +125 -0
  258. package/lib/harness/output-style.js +322 -0
  259. package/lib/harness/page-commands.d.ts +134 -0
  260. package/lib/harness/page-commands.js +1141 -0
  261. package/lib/harness/page-output.d.ts +67 -0
  262. package/lib/harness/page-output.js +139 -0
  263. package/lib/harness/page-watch.d.ts +171 -0
  264. package/lib/harness/page-watch.js +607 -0
  265. package/lib/harness/parse-check-node.d.ts +3 -0
  266. package/lib/harness/parse-check-node.js +132 -0
  267. package/lib/harness/path-args.d.ts +22 -0
  268. package/lib/harness/path-args.js +148 -0
  269. package/lib/harness/playwright-chromium.d.ts +16 -0
  270. package/lib/harness/playwright-chromium.js +86 -0
  271. package/lib/harness/prepare.d.ts +168 -0
  272. package/lib/harness/prepare.js +364 -0
  273. package/lib/harness/prompt-editor.d.ts +17 -0
  274. package/lib/harness/prompt-editor.js +354 -0
  275. package/lib/harness/prompt-history.d.ts +13 -0
  276. package/lib/harness/prompt-history.js +89 -0
  277. package/lib/harness/rebuild-advice.d.ts +6 -0
  278. package/lib/harness/rebuild-advice.js +39 -0
  279. package/lib/harness/run-log.d.ts +47 -0
  280. package/lib/harness/run-log.js +140 -0
  281. package/lib/harness/run-logs.d.ts +132 -0
  282. package/lib/harness/run-logs.js +241 -0
  283. package/lib/harness/run.d.ts +194 -0
  284. package/lib/harness/run.js +1973 -0
  285. package/lib/harness/runs.d.ts +229 -0
  286. package/lib/harness/runs.js +726 -0
  287. package/lib/harness/save-html.d.ts +39 -0
  288. package/lib/harness/save-html.js +89 -0
  289. package/lib/harness/saved-pages.d.ts +120 -0
  290. package/lib/harness/saved-pages.js +275 -0
  291. package/lib/harness/script-mark.d.ts +17 -0
  292. package/lib/harness/script-mark.js +56 -0
  293. package/lib/harness/snapshot.d.ts +143 -0
  294. package/lib/harness/snapshot.js +474 -0
  295. package/lib/harness/stuck-tabs.d.ts +19 -0
  296. package/lib/harness/stuck-tabs.js +131 -0
  297. package/lib/harness/tab-refs.d.ts +20 -0
  298. package/lib/harness/tab-refs.js +80 -0
  299. package/lib/harness/tampermonkey.d.ts +71 -0
  300. package/lib/harness/tampermonkey.js +356 -0
  301. package/lib/harness/tm-ids.d.ts +18 -0
  302. package/lib/harness/tm-ids.js +88 -0
  303. package/lib/harness/violentmonkey.d.ts +34 -0
  304. package/lib/harness/violentmonkey.js +162 -0
  305. package/lib/harness/wait.d.ts +77 -0
  306. package/lib/harness/wait.js +458 -0
  307. package/lib/http-read.d.ts +30 -0
  308. package/lib/http-read.js +142 -0
  309. package/lib/local-file-overrides.d.ts +35 -0
  310. package/lib/local-file-overrides.js +558 -0
  311. package/lib/manifest.json +38 -0
  312. package/lib/markup-response.d.ts +11 -0
  313. package/lib/markup-response.js +126 -0
  314. package/lib/match-targets.d.ts +18 -0
  315. package/lib/match-targets.js +305 -0
  316. package/lib/menu-captions.d.ts +1 -0
  317. package/lib/menu-captions.js +22 -0
  318. package/lib/parse-check-protocol.d.ts +27 -0
  319. package/lib/parse-check-protocol.js +51 -0
  320. package/lib/parse-location.d.ts +12 -0
  321. package/lib/parse-location.js +171 -0
  322. package/lib/phase-timing.d.ts +12 -0
  323. package/lib/phase-timing.js +155 -0
  324. package/lib/public-suffixes.d.ts +1 -0
  325. package/lib/public-suffixes.js +6957 -0
  326. package/lib/run-context.d.ts +2 -0
  327. package/lib/run-context.js +31 -0
  328. package/lib/runtime-messages.d.ts +10 -0
  329. package/lib/runtime-messages.js +34 -0
  330. package/lib/sandboxed-page.d.ts +5 -0
  331. package/lib/sandboxed-page.js +35 -0
  332. package/lib/script-build.d.ts +111 -0
  333. package/lib/script-build.js +787 -0
  334. package/lib/script-display.d.ts +51 -0
  335. package/lib/script-display.js +383 -0
  336. package/lib/script-icon.d.ts +16 -0
  337. package/lib/script-icon.js +43 -0
  338. package/lib/script-links.d.ts +24 -0
  339. package/lib/script-links.js +204 -0
  340. package/lib/script-source-url.d.ts +8 -0
  341. package/lib/script-source-url.js +214 -0
  342. package/lib/script-world.d.ts +6 -0
  343. package/lib/script-world.js +65 -0
  344. package/lib/sm-dev-protocol.d.ts +251 -0
  345. package/lib/sm-dev-protocol.js +84 -0
  346. package/lib/source-map.d.ts +32 -0
  347. package/lib/source-map.js +177 -0
  348. package/lib/stack-link.d.ts +15 -0
  349. package/lib/stack-link.js +138 -0
  350. package/lib/unsafe-window-use.d.ts +1 -0
  351. package/lib/unsafe-window-use.js +229 -0
  352. package/lib/url-match.d.ts +40 -0
  353. package/lib/url-match.js +734 -0
  354. package/lib/userscript-header.d.ts +33 -0
  355. package/lib/userscript-header.js +248 -0
  356. package/lib/version-compare.d.ts +6 -0
  357. package/lib/version-compare.js +141 -0
  358. package/lib/version-probe.d.ts +17 -0
  359. package/lib/version-probe.js +120 -0
  360. package/package.json +55 -3
  361. package/skills/SourceMonkey-dev/SKILL.md +868 -0
  362. package/skills/SourceMonkey-dev/references/cdp.md +293 -0
  363. package/skills/SourceMonkey-dev/references/commands.md +297 -0
  364. package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
  365. package/skills/SourceMonkey-dev/references/specs.md +46 -0
  366. package/skills/SourceMonkey-friction/SKILL.md +103 -0
  367. package/skills/SourceMonkey-friction/references/template.md +59 -0
@@ -0,0 +1,658 @@
1
+ // The details panel — the `<dl>` of parsed-header fields plus the
2
+ // where-did-this-come-from rows — shared by the installed-script
3
+ // viewer and the not-yet-installed preview.
4
+ //
5
+ // The two modes render the same rows from the same code; rows whose
6
+ // data a mode doesn't have simply aren't passed in (a preview has no
7
+ // Collection or Status; an unparseable file has no header). Row order
8
+ // is fixed here so the panel doesn't reshuffle between modes.
9
+ //
10
+ // What earns a row: the panel is NOT a mirror of the header block.
11
+ // The header block is on screen already — the first code tab opens on
12
+ // it — so a row that only repeats a tag verbatim is asking the reader
13
+ // to look at the same string twice. A row earns its place when it is
14
+ // - identity a reader scans for (Name, Description, Version),
15
+ // - something we resolved or computed that the file doesn't say
16
+ // (Collection, Location, hosting-site page, Size, Status, the
17
+ // upstream version probes), or
18
+ // - a claim worth acting on (Homepage, Match / Include / Exclude,
19
+ // Network access).
20
+ // Tags that fail all three — `@author`, `@namespace`, `@run-at`,
21
+ // `@icon`, … — stay in the code view where they were written.
22
+ //
23
+ // `@grant` is the deliberate omission, and the reasoning is specific
24
+ // to what our GM surface can actually do:
25
+ // - `GM_xmlhttpRequest` is the API that reaches other origins, and
26
+ // only the hosts its `@connect` lines name, or that the user allowed
27
+ // when asked (`connect-prompt.ts`), get past the page's own origin.
28
+ // The Network access row lists both, so it tells that story more
29
+ // precisely than a grant name would.
30
+ // - Most of the rest act within reach the page already has: styles,
31
+ // elements, menu commands, resources, per-script storage.
32
+ // - `GM_download` and `GM_openInTab` genuinely do reach outside the
33
+ // page and are NOT `@connect`-gated. They are the case this
34
+ // omission trades away; see the TODO note in docs/script-viewer.md.
35
+ import { openInNewTabAnchor } from './code-links.js';
36
+ import { makeCopyButton } from './copy-button.js';
37
+ import { standInTag, tagLocalFile } from './file-changes.js';
38
+ import { rowTooltip } from './details-tooltips.js';
39
+ import { matchTargetInfo, sameDirLinkUrl } from '../match-targets.js';
40
+ import { connectLinkUrl } from '../script-links.js';
41
+ import { renderPatternIconInto } from './site-icon.js';
42
+ import { isContainerCollection, locationFsPath } from '../file-locations.js';
43
+ import { appendWithSlashBreaks, displayLocation, formatBytes, logIdScriptNumber, stripSriFragment } from '../script-display.js';
44
+ import { sitePageRow } from './script-sites.js';
45
+ import { effectiveDisplayName, makeCollectionTypeBubble, collectionAnchorId, } from './collection-display.js';
46
+ import { wireNamedTabLink } from './page-links.js';
47
+ import { getValue, getValues } from '../userscript-header.js';
48
+ import { versionRelation } from '../version-compare.js';
49
+ import { probeLabel } from '../version-probe.js';
50
+ export function renderDetails(container, model) {
51
+ // Set before any row is built and read only by `appendRow`. Threading
52
+ // it through every add* helper would touch a dozen signatures to
53
+ // deliver one value that can't vary within a single render.
54
+ currentMode = model.mode ?? 'installed';
55
+ container.replaceChildren();
56
+ const dl = document.createElement('dl');
57
+ const h = model.header;
58
+ if (h) {
59
+ addText(dl, 'Name', getValue(h, 'name'));
60
+ addText(dl, 'Description', getValue(h, 'description'));
61
+ const version = getValue(h, 'version');
62
+ const probes = probesWorthShowing(version, model.versionProbes);
63
+ // A script with no `@version` normally gets no Version row at all.
64
+ // It gets one reading `<none>` when a probe DID find a version
65
+ // upstream: that absence is one half of the comparison, and the
66
+ // probe row directly below is meaningless without it.
67
+ if (version)
68
+ addText(dl, 'Version', version);
69
+ else if (probes.length > 0)
70
+ appendRow(dl, 'Version', document.createTextNode(NO_VERSION));
71
+ addVersionProbes(dl, version, probes);
72
+ }
73
+ // Collection row: the colored bubble + display name combo the
74
+ // collections page uses, with the display name doubling as a
75
+ // back-link to the collections page (anchored to this collection's
76
+ // section). Above Location so the "where did this come from" rows are
77
+ // grouped.
78
+ if (model.collection) {
79
+ appendRow(dl, 'Collection', collectionCell(model.collection, model.collectionTrailing));
80
+ }
81
+ if (model.location) {
82
+ appendRow(dl, 'Location', locationCell(model.location));
83
+ addSitePage(dl, model.location.href, h);
84
+ }
85
+ // Straight after the hosting-site row: both name a page about this
86
+ // script, one derived from where it lives and one claimed by the file
87
+ // itself, and reading them together is what shows when they disagree.
88
+ //
89
+ // Outside the `location` block on purpose, even though it reads as
90
+ // part of that group: `@homepage` comes from the header alone, so a
91
+ // script whose parent collection row has vanished (no Location row,
92
+ // see `currentFilenamePath`) must not lose it too.
93
+ if (h)
94
+ addLink(dl, 'Homepage', declaredHomepage(h));
95
+ // Always, for an installed script: its `#E4` is written nowhere in
96
+ // the file, and it is what a console line, a stack trace's
97
+ // `//# sourceURL=` and sm-dev (`E4`) name the script by.
98
+ if (model.logId) {
99
+ const dd = appendRow(dl, 'Log ID', logIdCell(model.logId));
100
+ // The tooltip's third line names this script's own number.
101
+ const number = logIdScriptNumber(model.logId);
102
+ const dt = dd.previousElementSibling;
103
+ if (number !== null && dt)
104
+ dt.title = `${dt.title ? `${dt.title}\n` : ''}E${number} is the script's ID in sm-dev.`;
105
+ }
106
+ if (h) {
107
+ addPatterns(dl, 'Match', getValues(h, 'match'), model.scriptUrl);
108
+ addPatterns(dl, 'Include', getValues(h, 'include'), model.scriptUrl);
109
+ addPatterns(dl, 'Exclude', getValues(h, 'exclude'), model.scriptUrl);
110
+ // Directly under the match targets, because it is the same
111
+ // question asked outward: those rows say which pages the script
112
+ // reads, this one says where it may send what it read. `@grant`
113
+ // gets no row beside it — the rest of the GM surface acts within
114
+ // the reach the page already has, so `@connect` is where the
115
+ // interesting part of an install decision lives. See
116
+ // `docs/script-viewer.md`.
117
+ addConnect(dl, getValues(h, 'connect'), model.connect);
118
+ }
119
+ // Last of the "what does this actually pull in" group, under the
120
+ // reach rows and above the byte total those files contribute to.
121
+ addLocalFiles(dl, model.files);
122
+ if (model.size) {
123
+ appendRow(dl, 'Size', document.createTextNode(model.size + dependencySizeSuffix(model.files)));
124
+ }
125
+ if (model.status) {
126
+ appendRow(dl, 'Status', model.status);
127
+ }
128
+ container.appendChild(dl);
129
+ }
130
+ // What the Version row shows when the script declares no `@version`.
131
+ // Only rendered when a probe DID find one upstream — see
132
+ // `renderDetails`. Angle brackets mark it as our word for an absence
133
+ // rather than a version string a file could actually carry.
134
+ export const NO_VERSION = '<none>';
135
+ // Which probes get a row. A probe earns one when it has something the
136
+ // reader can act on:
137
+ //
138
+ // - its version differs from ours (`newer` / `older`);
139
+ // - exactly one side declares a `@version` at all (`unknown`) — we
140
+ // can't say which is ahead, but "they publish 2.0 and we declare
141
+ // nothing" is still a difference worth seeing;
142
+ // - the latest check FAILED, whatever the version says. This row is
143
+ // the only surface a probe failure has: probes run downstream of
144
+ // the refresh and deliberately can't set a status or a
145
+ // `refreshError` (see `runVersionProbes` in `script-updater.ts`),
146
+ // and nothing logs them. Dropping the row would let a permanently
147
+ // broken `@updateURL` go silent, and the script would read as up to
148
+ // date when it had never once been checked.
149
+ //
150
+ // Only one case is dropped: the version matches ours AND the check
151
+ // succeeded — the expected state of an up-to-date script, and a line
152
+ // the reader would learn nothing from.
153
+ //
154
+ // Exported so the caller can ask "will any probe render?" before
155
+ // deciding how to draw the Version row above them.
156
+ export function probesWorthShowing(loadedVersion, probes) {
157
+ return (probes ?? []).filter((probe) => {
158
+ if (probe.error)
159
+ return true;
160
+ if (probe.version === undefined)
161
+ return true;
162
+ return versionRelation(loadedVersion, probe.version) !== 'same';
163
+ });
164
+ }
165
+ // One row per probe worth showing, labeled by the header that named it
166
+ // — "Version at update URL" or "Version at download URL".
167
+ //
168
+ // The value says how that copy relates to ours ("2.1 (Newer)") because
169
+ // the relation is the part a user acts on; the bare number alone would
170
+ // make them do the comparison themselves. Where there is no relation to
171
+ // state, the bare number is all we can honestly report. The URL rides
172
+ // along as the row's tooltip rather than a third column of text.
173
+ function addVersionProbes(dl, loadedVersion, probes) {
174
+ for (const probe of probes) {
175
+ const relation = versionRelation(loadedVersion, probe.version);
176
+ const dd = appendRow(dl, `Version at ${probeLabel(probe.kinds)}`, document.createTextNode(probeValueText(probe, relation)));
177
+ dd.title = probeTitleText(probe, relation);
178
+ if (relation === 'newer')
179
+ dd.className = 'version-newer';
180
+ }
181
+ }
182
+ // What the row reads as. Whichever of the version and the error is the
183
+ // news goes in the value; the other one goes in the tooltip.
184
+ function probeValueText(probe, relation) {
185
+ // No version ever resolved, so the error IS the finding. The `unknown`
186
+ // fallback covers a probe recorded before any attempt completed —
187
+ // shouldn't outlive a check, but must not render blank if it does.
188
+ if (probe.version === undefined)
189
+ return probe.error ?? 'unknown';
190
+ if (relation === 'newer' || relation === 'older') {
191
+ return `${probe.version} (${relation === 'newer' ? 'Newer' : 'Older'})`;
192
+ }
193
+ // Our `@version` is theirs plus a `-local` tail — a copy made for
194
+ // editing. Neither newer nor older: the release we started from.
195
+ if (relation === 'edited')
196
+ return `${probe.version} (Base of this local edit)`;
197
+ // `unknown`: we declare no `@version` and they do. No qualifier — the
198
+ // number is all we can say, and the Version row above reads `<none>`,
199
+ // so the contrast is on screen without us guessing at a direction.
200
+ if (relation === 'unknown')
201
+ return probe.version;
202
+ // `same` — reachable only when the check errored (see
203
+ // `probesWorthShowing`). The version is old news and matches ours
204
+ // anyway, so the failed re-check is what the row is here to say.
205
+ return probe.error ?? probe.version;
206
+ }
207
+ function probeTitleText(probe, relation) {
208
+ if (!probe.error)
209
+ return probe.url;
210
+ // The value is showing a version, so the tooltip carries the failure
211
+ // — otherwise the row reads as freshly confirmed when it isn't.
212
+ if (relation !== 'same') {
213
+ return `${probe.url} (last check failed: ${probe.error})`;
214
+ }
215
+ // The value is showing the error, so the tooltip carries the version
216
+ // we last saw there, which is the part it displaced.
217
+ return probe.version !== undefined
218
+ ? `${probe.url} (last known version ${probe.version})`
219
+ : probe.url;
220
+ }
221
+ // The publishing site's page for the script, derived from the Location
222
+ // URL — the location is a raw-code URL, this is the page a person can
223
+ // read. `sitePageRow` owns which sites are recognised and when the row
224
+ // would only repeat something already on screen; see
225
+ // `script-sites.ts`.
226
+ //
227
+ // Labeled with the site's own name ("Greasy Fork page") rather than a
228
+ // generic "Home page": the panel already has a Homepage row fed by the
229
+ // script's `@homepage`, and two rows with the same label saying
230
+ // different things would be worse than either alone.
231
+ function addSitePage(dl, locationUrl, h) {
232
+ const row = sitePageRow(locationUrl, h ? declaredHomepage(h) : undefined);
233
+ if (!row)
234
+ return;
235
+ appendRow(dl, row.label, openInNewTabAnchor(row.url, row.url));
236
+ }
237
+ // The header's own claim about where the script lives: the two
238
+ // spellings of the tag, first one wins. Read in three places — the
239
+ // Homepage row here and on the compare page, and the hosting-site
240
+ // row's duplicate check — which have to agree on what counts, or the
241
+ // check silently stops matching when this grows a tag.
242
+ export function declaredHomepage(h) {
243
+ return getValue(h, 'homepage') ?? getValue(h, 'homepageURL');
244
+ }
245
+ function addText(dl, label, value) {
246
+ if (!value)
247
+ return;
248
+ appendRow(dl, label, document.createTextNode(value));
249
+ }
250
+ // `@connect` row: hosts and the wildcard/`self` forms, rendered like
251
+ // the Match patterns above it (always bullets, each with its site's
252
+ // icon and a link to it; see `connectValueNode`).
253
+ //
254
+ // The user's prompt answers follow in their own list, each with a
255
+ // Reset button: a grant made in a hurry has to be as easy to find and
256
+ // undo as the header lines are.
257
+ function addConnect(dl, values, answers) {
258
+ const items = [];
259
+ if (answers) {
260
+ if (answers.grants.allHosts)
261
+ items.push({ host: null, allowed: true, reset: () => answers.revoke(null) });
262
+ for (const host of answers.grants.hosts)
263
+ items.push({ host, allowed: true, reset: () => answers.revoke(host) });
264
+ for (const host of answers.grants.denied ?? [])
265
+ items.push({ host, allowed: false, reset: () => answers.forgetDenied(host) });
266
+ }
267
+ if (values.length === 0 && items.length === 0)
268
+ return;
269
+ // Two lists, each under a heading and present only when non-empty:
270
+ // what the header declares, then what the user answered at the
271
+ // prompt (which only asks about hosts `@connect` doesn't cover).
272
+ const cell = document.createElement('div');
273
+ const addGroup = (heading, list, make) => {
274
+ if (list.length === 0)
275
+ return;
276
+ const h = document.createElement('div');
277
+ h.className = 'list-heading';
278
+ h.textContent = heading;
279
+ cell.append(h, bulletList(list, make));
280
+ };
281
+ addGroup('Declared in @connect:', values, (v) => {
282
+ if (v.trim() !== '*')
283
+ return connectValueNode(v);
284
+ // Worded like the "All sites" prompt answer below, rather than a
285
+ // bare `*` beside a globe.
286
+ return allSitesNode();
287
+ });
288
+ addGroup('Undeclared in @connect:', items, (item) => {
289
+ const span = document.createElement('span');
290
+ span.className = 'connect-answer';
291
+ // One element for the label and its colon: the span is a flex row,
292
+ // so separate pieces would each get the gap, putting a space before
293
+ // the colon.
294
+ const label = document.createElement('span');
295
+ if (item.host === null) {
296
+ label.append(allSitesNode(), ':');
297
+ }
298
+ else {
299
+ // A denied host gets no favicon fetch: opening this page must not
300
+ // contact a site the user refused.
301
+ label.append(connectValueNode(item.host, item.allowed), ':');
302
+ }
303
+ span.append(label);
304
+ const verdict = document.createElement('b');
305
+ verdict.textContent = item.allowed ? 'Allowed' : 'Denied';
306
+ const btn = document.createElement('button');
307
+ btn.type = 'button';
308
+ btn.textContent = 'Reset';
309
+ btn.title = `Clear this ${item.allowed ? 'approval' : 'denial'}. The next request will prompt again.`;
310
+ const reset = item.reset;
311
+ btn.addEventListener('click', () => {
312
+ btn.disabled = true;
313
+ reset();
314
+ });
315
+ span.append(verdict, btn);
316
+ return span;
317
+ });
318
+ appendRow(dl, 'Network access', cell);
319
+ }
320
+ // The bold "All sites" both lists use: for `@connect *`, and for the
321
+ // "Allow all sites" prompt answer.
322
+ function allSitesNode() {
323
+ const all = document.createElement('b');
324
+ all.textContent = 'All sites';
325
+ all.title = 'The script can access any site and act as you.';
326
+ return all;
327
+ }
328
+ function addLink(dl, label, value) {
329
+ if (!value)
330
+ return;
331
+ const info = safeUrl(value);
332
+ appendRow(dl, label, info ? openInNewTabAnchor(info, value) : document.createTextNode(value));
333
+ }
334
+ // Every `@require` / `@resource` whose body was read from a local file
335
+ // instead of the URL it declares, with the file we actually read.
336
+ //
337
+ // This row is a disclosure, not a listing: it exists because running
338
+ // something other than what a declaration names must never be silent
339
+ // (see docs/local-file-overrides.md). So it is absent for the
340
+ // overwhelming majority of scripts, and its presence is the whole
341
+ // signal. That also keeps it inside the panel's "earns a row" rule —
342
+ // it reports what we resolved, never what the header said.
343
+ //
344
+ // `@require` also wears a `local` chip on its own code tab, but
345
+ // `@resource` gets no tab at all, and the install preview has no Log
346
+ // tab to fall back on — so for a resource stand-in before install this
347
+ // row is the only surface there is.
348
+ function addLocalFiles(dl, files) {
349
+ const entries = [];
350
+ const groups = [
351
+ ['@require', files?.requires],
352
+ ['@resource', files?.resources],
353
+ ];
354
+ for (const [kind, list] of groups) {
355
+ for (const f of list ?? []) {
356
+ if (f.localUrl)
357
+ entries.push({ kind, declared: f.url, file: { ...f, localUrl: f.localUrl } });
358
+ }
359
+ }
360
+ if (entries.length === 0)
361
+ return;
362
+ appendRow(dl, 'Local overrides', listNode(entries, (e) => {
363
+ const span = document.createElement('span');
364
+ span.className = 'local-override-cell';
365
+ const kind = document.createElement('span');
366
+ kind.className = 'local-override-kind';
367
+ kind.textContent = e.kind;
368
+ span.appendChild(kind);
369
+ // The local path is the value, because it's what the row promises
370
+ // and what the reader needs in order to go open the file.
371
+ //
372
+ // Rendered as a BARE filesystem path, not the `file://` URL we
373
+ // hold: the Location row above resolves its own `file://` location
374
+ // the same way (`locationFsPath`, then `displayLocation`), and two
375
+ // path styles in one panel would read as two different kinds of
376
+ // thing. The code tab's tooltip keeps the URL form deliberately —
377
+ // there it sits directly under the declared URL, and the pair reads
378
+ // as "asked for this, got that".
379
+ const localPath = locationFsPath(e.file.localUrl);
380
+ const path = document.createElement('span');
381
+ path.className = 'local-override-path';
382
+ appendWithSlashBreaks(path, displayLocation(localPath));
383
+ // Which declaration it stood in for rides along as the tooltip,
384
+ // in the URL display form every other declared ref uses.
385
+ path.title = `Stands in for ${displayLocation(stripSriFragment(e.declared))}`
386
+ + (files?.pushed ? '\n\nSnapshot of this file, pushed by sm-dev' : '');
387
+ // A stand-in the user has since deleted or edited gets a chip
388
+ // saying so: this row still claims to load the bytes it holds,
389
+ // until the next refresh (or, for a pushed snapshot, the next push).
390
+ tagLocalFile(path, standInTag(localPath, e.file, files?.pushed));
391
+ span.appendChild(path);
392
+ // Copies the same path the row shows — for a local file that IS the
393
+ // useful thing to paste, and it matches what the Location row's
394
+ // button gives you for a `file://` script.
395
+ span.appendChild(makeCopyButton({
396
+ label: 'Copy local file path to clipboard',
397
+ text: () => localPath,
398
+ }));
399
+ return span;
400
+ }));
401
+ }
402
+ // Tail on the Size row accounting for what the script loads alongside
403
+ // itself: ` (plus 2 @require files: 125 KB, 1 @resource file: 4 KB)`,
404
+ // carrying only the halves that exist and '' when neither does.
405
+ //
406
+ // Counts entries that actually loaded and reported a size, NOT the
407
+ // declarations in the header. A `@require` that failed to fetch
408
+ // contributes no bytes, so counting it would promise a total the
409
+ // number beside it doesn't include.
410
+ //
411
+ // A failure therefore shows up as the tail going missing entirely, not
412
+ // as a short count: `attachFiles` reports a failure with EMPTY lists,
413
+ // so one broken dependency drops the whole suffix. The declarations
414
+ // themselves stay visible in the metadata block either way, and the
415
+ // error itself is what the message bar above the panel is for.
416
+ function dependencySizeSuffix(files) {
417
+ const parts = [];
418
+ const groups = [
419
+ ['@require', files?.requires],
420
+ ['@resource', files?.resources],
421
+ ];
422
+ for (const [tag, entries] of groups) {
423
+ const sizes = (entries ?? []).map((e) => e.size).filter((s) => s != null);
424
+ if (sizes.length === 0)
425
+ continue;
426
+ const total = sizes.reduce((a, b) => a + b, 0);
427
+ const noun = sizes.length === 1 ? 'file' : 'files';
428
+ parts.push(`${sizes.length} ${tag} ${noun}: ${formatBytes(total)}`);
429
+ }
430
+ return parts.length === 0 ? '' : ` (plus ${parts.join(', ')})`;
431
+ }
432
+ // `@match` / `@include` / `@exclude` row: each pattern that resolves to
433
+ // a usable linkUrl via the same wildcard-trim rule as the site favicon
434
+ // becomes a clickable open-in-new-tab anchor, led by that site's
435
+ // favicon (the globe when the pattern names no site). A pattern naming
436
+ // no site gets a second chance against the script's own directory.
437
+ // Patterns we can't linkify either way (`<all_urls>`, regex literals,
438
+ // a host wildcard with nothing to anchor on) render as plain text so
439
+ // the row still shows every entry.
440
+ //
441
+ // Always a bulleted list, even for one pattern: these rows sit
442
+ // together and an icon-led inline value beside an icon-led list read
443
+ // as two different things.
444
+ function addPatterns(dl, label, values, scriptUrl) {
445
+ if (values.length === 0)
446
+ return;
447
+ appendRow(dl, label, bulletList(values, (p) => patternValueNode(p, scriptUrl)));
448
+ }
449
+ // One value renders inline; several render as a `<ul>` (folded when
450
+ // long — see `bulletList`). Generic in the item type so the rows whose
451
+ // items aren't plain strings (Local overrides) share the same
452
+ // one-vs-many rule rather than reimplementing it.
453
+ function listNode(values, make) {
454
+ if (values.length === 1)
455
+ return make(values[0]);
456
+ return bulletList(values, make);
457
+ }
458
+ // How many items a details-panel list shows before folding the rest
459
+ // behind a "(N more)" link. Long enough that ordinary scripts never
460
+ // fold; short enough that a fifty-site `@match` block doesn't push the
461
+ // code box off the screen.
462
+ const LIST_FOLD_AT = 10;
463
+ // A `<ul>` of `values`, folded past `LIST_FOLD_AT`: the tail is hidden
464
+ // behind a last bullet reading "(N more)", which swaps the whole list
465
+ // in and turns into "(show fewer)". All items are rendered up front —
466
+ // the fold is display only — so a per-item side effect (a favicon
467
+ // fetch) happens once, whether or not the reader ever expands.
468
+ function bulletList(values, make) {
469
+ const ul = document.createElement('ul');
470
+ const items = [];
471
+ for (const v of values) {
472
+ const li = document.createElement('li');
473
+ li.appendChild(make(v));
474
+ ul.appendChild(li);
475
+ items.push(li);
476
+ }
477
+ // A "(1 more)" bullet takes the line it would save; show the item.
478
+ const hidden = items.slice(LIST_FOLD_AT);
479
+ if (hidden.length < 2)
480
+ return ul;
481
+ const toggleLi = document.createElement('li');
482
+ toggleLi.className = 'list-fold';
483
+ const toggle = document.createElement('button');
484
+ toggle.type = 'button';
485
+ toggle.className = 'list-fold-toggle';
486
+ toggleLi.appendChild(toggle);
487
+ ul.appendChild(toggleLi);
488
+ const setFolded = (folded) => {
489
+ for (const li of hidden)
490
+ li.hidden = folded;
491
+ toggle.textContent = folded ? `(${hidden.length} more)` : '(show fewer)';
492
+ toggle.setAttribute('aria-expanded', String(!folded));
493
+ };
494
+ toggle.addEventListener('click', () => setFolded(!hidden[0].hidden));
495
+ setFolded(true);
496
+ return ul;
497
+ }
498
+ function patternValueNode(pattern, scriptUrl) {
499
+ const target = matchTargetInfo(pattern);
500
+ return siteCell(pattern, target, target?.linkUrl ?? sameDirLinkUrl(pattern, scriptUrl));
501
+ }
502
+ // A `@connect` value (or a prompt-answer host) in the same icon + link
503
+ // cell as a Match pattern. `connectLinkUrl` picks the site: a host
504
+ // links to its https root, `*.host` to the bare host, and `self` / `*`
505
+ // name no single site, so they stay plain text beside the globe.
506
+ // `fetchIcon` false keeps the globe (still linked) instead of looking
507
+ // up the site's favicon.
508
+ function connectValueNode(value, fetchIcon = true) {
509
+ const linkUrl = connectLinkUrl(value);
510
+ const target = linkUrl && fetchIcon ? { origin: new URL(linkUrl).origin, linkUrl } : null;
511
+ return siteCell(value, target, linkUrl, connectNoLinkTitle(value));
512
+ }
513
+ // The globe's tooltip for `self`, which names no single site to link
514
+ // to. The icon's generic "Unable to extract link" would read as a
515
+ // fault for it, when it is just a broader rule. Any other unlinked
516
+ // value (`foo*.com`) is read as a literal host that matches nothing, so
517
+ // it keeps the generic tooltip. (`*` shows as "All sites", with no
518
+ // icon; see `addConnect`.)
519
+ function connectNoLinkTitle(value) {
520
+ const v = value.trim().toLowerCase();
521
+ if (v === 'self')
522
+ return 'The sites in this script\'s @match and @include rules, and the page it is running on';
523
+ return undefined;
524
+ }
525
+ // One value with its site icon: a Match pattern or a `@connect` value.
526
+ // `noLinkTitle` replaces the icon's default tooltip when there's no
527
+ // link.
528
+ function siteCell(value, target, linkUrl, noLinkTitle) {
529
+ let text;
530
+ if (!linkUrl) {
531
+ // Non-linkable value (`<all_urls>`, regex literal, `*`, etc.);
532
+ // render as plain text but still allow slash-aware wrapping in case
533
+ // it is long.
534
+ const span = document.createElement('span');
535
+ appendWithSlashBreaks(span, value);
536
+ text = span;
537
+ }
538
+ else {
539
+ text = openInNewTabAnchor(linkUrl, value);
540
+ }
541
+ // Icon first, then the value; both open the same URL.
542
+ const cell = document.createElement('span');
543
+ cell.className = 'pattern-cell';
544
+ const slot = document.createElement('span');
545
+ slot.className = 'site-icon pattern-icon';
546
+ renderPatternIconInto(slot, target, linkUrl, noLinkTitle);
547
+ cell.append(slot, text);
548
+ return cell;
549
+ }
550
+ function logIdCell(logId) {
551
+ const span = document.createElement('span');
552
+ span.className = 'log-id-cell';
553
+ span.textContent = logId;
554
+ return span;
555
+ }
556
+ // Collection-row cell: colored type bubble on the left (linking out to
557
+ // the underlying file:// / http(s):// URL), followed by the
558
+ // collection's display name as a back-link to the collections page
559
+ // anchored at this collection's section. Also each side's Collection
560
+ // cell on the compare page.
561
+ export function collectionCell(collection, trailing) {
562
+ const cell = document.createElement('span');
563
+ cell.className = 'collection-cell';
564
+ cell.appendChild(makeCollectionTypeBubble(collection.path));
565
+ const nameLink = document.createElement('a');
566
+ nameLink.className = 'collection-display-name';
567
+ appendWithSlashBreaks(nameLink, effectiveDisplayName(collection));
568
+ // The collections page is a named tab: this opens it beside the
569
+ // viewer rather than replacing it, reusing the one that is already
570
+ // open and scrolling it to this collection. See src/extension/page-links.ts.
571
+ wireNamedTabLink(nameLink, 'collections', collectionAnchorId(collection.id));
572
+ // Tooltip surfaces the raw stored path; the anchor itself jumps to
573
+ // the section on the collections page. Container collections have a
574
+ // synthetic `container:…` path which would leak — skip then.
575
+ if (!isContainerCollection(collection.path))
576
+ nameLink.title = collection.path;
577
+ cell.appendChild(nameLink);
578
+ if (trailing)
579
+ cell.appendChild(trailing);
580
+ return cell;
581
+ }
582
+ function locationCell(row) {
583
+ const cell = document.createElement('span');
584
+ cell.className = 'filename-cell';
585
+ if (row.unopenable) {
586
+ const textEl = document.createElement('span');
587
+ textEl.className = 'filename-text';
588
+ textEl.title = row.text;
589
+ appendWithSlashBreaks(textEl, row.text);
590
+ cell.appendChild(textEl);
591
+ if (row.trailing)
592
+ cell.appendChild(row.trailing);
593
+ return cell;
594
+ }
595
+ // Wrap the path in an anchor so it doubles as an open-in-new-tab
596
+ // link. http(s) URLs navigate normally; file:// links are accepted
597
+ // by the anchor but Chromium typically refuses to navigate to them
598
+ // from a chrome-extension page — the path is still readable, still
599
+ // selectable, and the Copy button beside it is the reliable
600
+ // "send this to another app" affordance.
601
+ const pathEl = document.createElement('a');
602
+ pathEl.className = 'filename-text';
603
+ pathEl.href = row.href;
604
+ pathEl.target = '_blank';
605
+ pathEl.rel = 'noopener noreferrer';
606
+ // Display text is the bare fs path for file:// scripts, and the
607
+ // percent-decoded URL for the rest; the tooltip surfaces the actual
608
+ // href, so the encoded form the browser will request is one hover
609
+ // away and the user can see what would open (file://... vs
610
+ // http://...) without clicking through. Slash-aware appender lets
611
+ // long paths wrap at `/` boundaries before breaking mid-segment.
612
+ pathEl.title = row.href;
613
+ appendWithSlashBreaks(pathEl, row.text);
614
+ if (row.onActivate) {
615
+ const handler = row.onActivate;
616
+ for (const type of ['click', 'auxclick']) {
617
+ pathEl.addEventListener(type, (ev) => {
618
+ ev.preventDefault();
619
+ handler();
620
+ });
621
+ }
622
+ }
623
+ if (row.localFile)
624
+ tagLocalFile(pathEl, row.localFile);
625
+ cell.appendChild(pathEl);
626
+ // The caller's button is a single long-lived element; appendChild
627
+ // moves it into the new cell on every render, so any in-flight
628
+ // "Copied" flash carries across.
629
+ if (row.trailing)
630
+ cell.appendChild(row.trailing);
631
+ return cell;
632
+ }
633
+ // See `renderDetails` — the mode the rows currently being built belong
634
+ // to, for their label tooltips.
635
+ let currentMode = 'installed';
636
+ function appendRow(dl, label, value) {
637
+ const dt = document.createElement('dt');
638
+ dt.textContent = label;
639
+ const tip = rowTooltip(label, currentMode);
640
+ if (tip)
641
+ dt.title = tip;
642
+ const dd = document.createElement('dd');
643
+ dd.appendChild(value);
644
+ dl.append(dt, dd);
645
+ return dd;
646
+ }
647
+ // Header values are file content, so a `@homepage` can be anything.
648
+ // Only linkify what parses as an absolute http(s) URL — a relative or
649
+ // javascript: value stays plain text.
650
+ function safeUrl(value) {
651
+ try {
652
+ const u = new URL(value);
653
+ return u.protocol === 'http:' || u.protocol === 'https:' ? u.href : null;
654
+ }
655
+ catch {
656
+ return null;
657
+ }
658
+ }