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,734 @@
1
+ // URL matching for userscripts.
2
+ //
3
+ // Implements Tampermonkey-style three flavours of pattern:
4
+ //
5
+ // @match Chrome match-pattern grammar: <scheme>://<host>/<path>
6
+ // - scheme: *, http, https, file, ftp
7
+ // - host: *, *.domain, exact domain, or empty (for file://)
8
+ // - path: glob with `*` (matches any chars including `/`)
9
+ // Plus the special literal `<all_urls>` (any http/https/file/ftp URL).
10
+ // Some shapes Chrome's parser refuses are read leniently
11
+ // first — see `canonicalMatchPatterns`.
12
+ //
13
+ // @include Free-form glob over the *whole* URL string. `*` matches any
14
+ // run of characters. If the pattern is wrapped in `/.../` it
15
+ // is treated as a JavaScript regex.
16
+ //
17
+ // @exclude Same syntax as @include. If any @exclude matches, the
18
+ // script does not match — excludes win over both @match and
19
+ // @include.
20
+ //
21
+ // All three also take Tampermonkey's `.tld` host suffix, meaning "any
22
+ // public suffix" — see `.tld` hosts below.
23
+ //
24
+ // `urlMatchesScript(url, matching)` is the entry point used by the
25
+ // background SW; the per-pattern helpers are exported for tests.
26
+ import { ICANN_SUFFIX_RULES } from './public-suffixes.js';
27
+ const HTTP_LIKE_SCHEMES = /^(https?|file|ftp):/i;
28
+ // Escape every regex metacharacter except `*`, which we want to keep so
29
+ // the caller can substitute it in. Used by both pattern flavours.
30
+ function escapeForRegexExceptStar(s) {
31
+ return s.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
32
+ }
33
+ // Convert a `*`-glob (where `*` matches any chars, including `/`) to an
34
+ // anchored RegExp.
35
+ function globToRegExp(glob, flags = '') {
36
+ const body = escapeForRegexExceptStar(glob).replace(/\*/g, '.*');
37
+ return new RegExp('^' + body + '$', flags);
38
+ }
39
+ // ---------- `.tld` hosts ----------
40
+ //
41
+ // Tampermonkey and Violentmonkey read a host ending in `.tld` as "this
42
+ // name under any public suffix": `*://www.google.tld/*` runs on
43
+ // www.google.com, www.google.de and www.google.co.uk. They take it in
44
+ // `@match`, `@include` and `@exclude` alike.
45
+ //
46
+ // A public suffix is one under which anyone can register a name. We
47
+ // take them from the ICANN section of the Public Suffix List (PSL,
48
+ // publicsuffix.org), generated into public-suffixes.ts:
49
+ // - That covers multi-level ones (`co.uk`, `com.au`) where a
50
+ // registry keeps the second level, and not `com.de`, an ordinary
51
+ // name someone owns.
52
+ // - The PSL's private section (`blogspot.com`, `github.io`) is left
53
+ // out: `www.google.blogspot.com` belongs to whoever registered it
54
+ // there, not to Google. (Violentmonkey includes it.)
55
+ // - So are names no registry hands out (`.local`, `.lan`): the PSL's
56
+ // implicit "any unknown label" rule isn't applied.
57
+ //
58
+ // Matching tries each public suffix the URL's own host ends in, with
59
+ // `.tld` replaced by it, as an ordinary pattern (see `tldSuffixesOf`).
60
+ //
61
+ // The list stays out of the page: the wrapper's in-page matcher reads
62
+ // `.tld` loosely, and the service worker's start check (the reply to
63
+ // `sm:check-pending`) applies this exact reading. See docs/matching.md.
64
+ // PSL rules, parsed on first use: plain suffixes, `*.x` wildcards
65
+ // (every label under x is a suffix), and `!y` exceptions to them.
66
+ let suffixRules = null;
67
+ function rules() {
68
+ if (suffixRules)
69
+ return suffixRules;
70
+ const r = { plain: new Set(), wild: new Set(), except: new Set() };
71
+ for (const line of ICANN_SUFFIX_RULES.split('\n')) {
72
+ if (!line)
73
+ continue;
74
+ if (line.startsWith('!'))
75
+ r.except.add(line.slice(1));
76
+ else if (line.startsWith('*.'))
77
+ r.wild.add(line.slice(2));
78
+ else
79
+ r.plain.add(line);
80
+ }
81
+ suffixRules = r;
82
+ return r;
83
+ }
84
+ // Is this (lowercase, punycode) name exactly an ICANN public suffix?
85
+ export function isPublicSuffix(name) {
86
+ const r = rules();
87
+ if (r.except.has(name))
88
+ return false;
89
+ if (r.plain.has(name))
90
+ return true;
91
+ const dot = name.indexOf('.');
92
+ return dot > 0 && r.wild.has(name.slice(dot + 1));
93
+ }
94
+ // The public suffixes a URL's host ends in, shortest first: for
95
+ // `www.google.co.uk`, `uk` and `co.uk`. Never the whole host, since a
96
+ // `.tld` pattern always names something in front of it.
97
+ function tldSuffixesOf(url) {
98
+ let hostname;
99
+ try {
100
+ hostname = new URL(url).hostname.toLowerCase();
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ const labels = hostname.split('.');
106
+ const out = [];
107
+ for (let k = 1; k < labels.length; k++) {
108
+ const suffix = labels.slice(-k).join('.');
109
+ if (isPublicSuffix(suffix))
110
+ out.push(suffix);
111
+ }
112
+ return out;
113
+ }
114
+ // A `.tld` pattern tested as each of its readings for this URL, with
115
+ // `.tld` replaced by one of the host's public suffixes. Null when the
116
+ // pattern has no `.tld` host, for the caller to test it as it is.
117
+ function testTld(pattern, url, globStar, test) {
118
+ const at = tldIndex(pattern, globStar);
119
+ if (at < 0)
120
+ return null;
121
+ return tldSuffixesOf(url).some((suffix) => test(pattern.slice(0, at) + '.' + suffix + pattern.slice(at + 4)));
122
+ }
123
+ // Where the `.tld` that ends a pattern's host starts, or -1 when its
124
+ // host doesn't end in one. The host is what follows `://` (the start,
125
+ // for a pattern naming no scheme) up to the next `/`, less a `:port`.
126
+ // Only a host's LAST label counts: `.tld` elsewhere is literal text.
127
+ //
128
+ // `globStar`: an `@include` glob may run the host straight into a `*`
129
+ // with no `/` (`http://www.google.tld*`), as older scripts do. The
130
+ // `.tld` still ends the host, and the `*` stays a glob star. Not for a
131
+ // `@match`, whose grammar has no `*` after the host.
132
+ //
133
+ // A `.tld` with no name in front (`*.tld`, a bare `.tld`) stays
134
+ // literal: "any host under any TLD" is not what anybody means by it.
135
+ function tldIndex(pattern, globStar = false) {
136
+ const sep = pattern.indexOf('://');
137
+ const start = sep < 0 ? 0 : sep + 3;
138
+ const slash = pattern.indexOf('/', start);
139
+ let host = pattern.slice(start, slash < 0 ? pattern.length : slash).replace(/:(\d+|\*)$/, '');
140
+ if (globStar && host.endsWith('*'))
141
+ host = host.slice(0, -1);
142
+ if (!host.toLowerCase().endsWith('.tld'))
143
+ return -1;
144
+ const base = host.slice(0, -4);
145
+ if (base === '' || base === '*')
146
+ return -1;
147
+ return start + host.length - 4;
148
+ }
149
+ // Does this glob or `@match` pattern have a `.tld` host — one Chrome
150
+ // can't match for us? Regex patterns never do: `.tld` in a regex is
151
+ // the author's own regex text.
152
+ export function hostEndsInTld(pattern) {
153
+ if (isRegexPattern(pattern))
154
+ return false;
155
+ if (pattern === '<all_urls>')
156
+ return false;
157
+ return tldIndex(pattern, true) >= 0;
158
+ }
159
+ // A glob Chrome can register for a `.tld` `@match` or `@include`, as a
160
+ // coarse first pass: the `.tld` becomes `.*`, so `*://www.google.tld/*`
161
+ // registers as `*://www.google.*/*`. It reaches more than `.tld` means
162
+ // (`www.google.evil.com`, and a glob `*` also crosses `/`), so the
163
+ // wrapper's in-page matcher and the worker's `.tld` check must still
164
+ // narrow it. A `@match` host's leading `*.` also covers the bare name
165
+ // (`*.imdb.tld` runs on `imdb.com`), which as a glob needs `*imdb.*`.
166
+ // Only for patterns where `hostEndsInTld` is true.
167
+ export function tldCoarseGlob(pattern, isMatch) {
168
+ const at = tldIndex(pattern, !isMatch);
169
+ let head = pattern.slice(0, at);
170
+ if (isMatch) {
171
+ const sep = head.indexOf('://');
172
+ if (sep >= 0 && head.startsWith('*.', sep + 3))
173
+ head = head.slice(0, sep + 3) + '*' + head.slice(sep + 5);
174
+ }
175
+ return head + '.*' + pattern.slice(at + 4);
176
+ }
177
+ function isRegexPattern(pattern) {
178
+ return pattern.length >= 2 && pattern.startsWith('/') && pattern.endsWith('/');
179
+ }
180
+ // ---------- lenient readings ----------
181
+ //
182
+ // The concrete schemes a `@match` may name — `MATCH_SCHEMES` minus the
183
+ // `*` that stands for http-or-https. Also what a scheme glob is
184
+ // expanded against.
185
+ const CONCRETE_SCHEMES = ['http', 'https', 'file', 'ftp'];
186
+ // Rewrite the `@match` shapes Chrome's parser refuses but the other
187
+ // managers run, into the patterns Chrome's grammar spells them with.
188
+ // Returns one pattern per concrete scheme the original names — usually
189
+ // one, and the input verbatim when there is nothing to rewrite.
190
+ //
191
+ // *.example.com/* → *://*.example.com/* (scheme omitted)
192
+ // http*://example.com/* → *://example.com/* (scheme glob)
193
+ // *s://example.com/* → https://example.com/*
194
+ // https://example.com → https://example.com/ (no path)
195
+ // *://*example.com/* → *://*.example.com/* (`*` stuck to a label)
196
+ //
197
+ // The last two are host-and-path readings, in `readHostAndPath`.
198
+ //
199
+ // Why: an omitted scheme and `http*` together account for nearly every
200
+ // `@match` in the Greasy Fork corpus that Chrome refuses — the rewrite
201
+ // turns 877 of those 1,035 patterns into ones it takes, across the
202
+ // 1,991 most-installed scripts, which are working scripts under the
203
+ // managers their authors use (see docs/matching.md). Both shapes have
204
+ // one obvious reading; Chrome's parser is the only thing that can't
205
+ // spell it, so we spell it for it.
206
+ //
207
+ // A scheme glob is ENUMERATED, not approximated: `*` comes back only
208
+ // when the glob names exactly the pair `*` stands for, and every other
209
+ // expansion comes back as concrete schemes (`*s://` → `https://`). So
210
+ // no caller has to re-check the scheme afterwards — a rounded-up `*`
211
+ // would have to be filtered somewhere, and Chrome, which does the
212
+ // matching, has nowhere to filter before injecting.
213
+ //
214
+ // A scheme glob matching no supported scheme (`chrome-*://`) is left
215
+ // alone, so `matchPatternProblem` still names it as unsupported rather
216
+ // than silently widening it.
217
+ //
218
+ // This runs ahead of BOTH the matcher and the validator, so what we
219
+ // register with Chrome and what our own matcher says stay the same
220
+ // thing, and `pageMatcher` builds the wrapper's regexes from it too.
221
+ export function canonicalMatchPatterns(pattern) {
222
+ if (pattern === '<all_urls>')
223
+ return [pattern];
224
+ // A broken scheme (`//host/…`, `*//host/…`, `://host/…`) is left as
225
+ // written, for `matchPatternProblem` to flag. Supplying `*://` would
226
+ // turn `*//host/…` into "any host, path beginning `//host/`".
227
+ if (BROKEN_SCHEME.test(pattern))
228
+ return [pattern];
229
+ // Supply the scheme the author left out.
230
+ let p = pattern;
231
+ if (!p.includes('://'))
232
+ p = '*://' + p;
233
+ p = readHostAndPath(p);
234
+ const sep = p.indexOf('://');
235
+ const scheme = p.slice(0, sep).toLowerCase();
236
+ const rest = p.slice(sep); // keeps the `://`
237
+ if (scheme === '*')
238
+ return anyScheme(rest);
239
+ if (!scheme.includes('*'))
240
+ return [p];
241
+ let schemeRx;
242
+ try {
243
+ schemeRx = globToRegExp(scheme);
244
+ }
245
+ catch {
246
+ return [pattern];
247
+ }
248
+ const expanded = CONCRETE_SCHEMES.filter((s) => schemeRx.test(s));
249
+ if (expanded.length === 0)
250
+ return [pattern];
251
+ // Exactly the pair `*` already means, so say it the way `*` is said.
252
+ if (expanded.join() === 'http,https')
253
+ return anyScheme(rest);
254
+ return expanded.map((s) => s + rest);
255
+ }
256
+ // A pattern starting with `//`, `*//` or `:`: slashes or a colon with
257
+ // no scheme in front of them. Too odd to guess at; see
258
+ // docs/match-rules.md.
259
+ const BROKEN_SCHEME = /^(\*?\/\/|:)/;
260
+ // The host-and-path half of the lenient readings, for a pattern that
261
+ // has its `://`:
262
+ // - No path reads as the root page, `/`, as Tampermonkey reads it.
263
+ // Violentmonkey never runs one, so this takes nothing away there.
264
+ // - A `*` stuck to the front of a host (`*example.com`) reads as
265
+ // `*.example.com`: the name and its subdomains, which is what such
266
+ // authors mean. Tampermonkey runs it on `example.com` alone, and
267
+ // Violentmonkey as a glob that also takes `fooexample.com`; this
268
+ // covers the first and the sensible part of the second.
269
+ // Any other `*` inside a host is left for `matchPatternProblem`.
270
+ function readHostAndPath(p) {
271
+ const start = p.indexOf('://') + 3;
272
+ const slash = p.indexOf('/', start);
273
+ let host = slash < 0 ? p.slice(start) : p.slice(start, slash);
274
+ const path = slash < 0 ? '/' : p.slice(slash);
275
+ // The port's own `*` (`:*`) doesn't count as a second one.
276
+ const bareHost = host.replace(/:(\d+|\*)$/, '');
277
+ if (/^\*[^.*:]/.test(bareHost) && !bareHost.includes('*', 1))
278
+ host = '*.' + host.slice(1);
279
+ return p.slice(0, start) + host + path;
280
+ }
281
+ // `*://<rest>` — http-or-https — spelled the way Chrome will take it.
282
+ //
283
+ // Chrome's parser refuses `*` beside a numeric port ("Invalid port"),
284
+ // though it takes the same port after a named scheme and takes
285
+ // `*://host:*/…` as well. Probed against Chrome 148; the e2e case in
286
+ // `tests/e2e/injection.spec.ts` is the standing check.
287
+ // Writing the pair out is the only spelling of those patterns Chrome
288
+ // will register, and means exactly what `*` does.
289
+ function anyScheme(rest) {
290
+ const host = rest.slice(3).split('/')[0];
291
+ if (!/:\d+$/.test(host))
292
+ return ['*' + rest];
293
+ return ['http' + rest, 'https' + rest];
294
+ }
295
+ // Test a single Chrome @match pattern against a URL. Returns false for
296
+ // any malformed input rather than throwing — callers loop over many
297
+ // patterns and a typo in one shouldn't break the others.
298
+ export function matchPattern(pattern, url) {
299
+ if (pattern === '<all_urls>')
300
+ return HTTP_LIKE_SCHEMES.test(url);
301
+ // `.tld` is found in each canonical form, so it's judged on the
302
+ // pattern as read, the same text that is registered.
303
+ return canonicalMatchPatterns(pattern).some((p) => testTld(p, url, false, (q) => matchCanonical(q, url)) ?? matchCanonical(p, url));
304
+ }
305
+ // `matchPattern` for a pattern already in Chrome's spelling.
306
+ function matchCanonical(pattern, url) {
307
+ const parts = /^(\*|https?|file|ftp):\/\/([^/]*)(\/.*)$/i.exec(pattern);
308
+ if (!parts)
309
+ return false;
310
+ const [, scheme, host, path] = parts;
311
+ let parsed;
312
+ try {
313
+ parsed = new URL(url);
314
+ }
315
+ catch {
316
+ return false;
317
+ }
318
+ // Scheme: `*` only covers http(s), per Chrome's match-pattern rules.
319
+ if (scheme === '*') {
320
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
321
+ return false;
322
+ }
323
+ else if (parsed.protocol !== `${scheme.toLowerCase()}:`) {
324
+ return false;
325
+ }
326
+ // Host. file:// URLs typically have an empty host; we allow `*` to
327
+ // match that too so `file:///*` style patterns work.
328
+ //
329
+ // Chrome's documented match-pattern grammar disallows ports, but
330
+ // Chrome's actual matcher accepts them — `http://host:port/*` will
331
+ // be registered and Chrome will match it against URLs on that port.
332
+ // (Not beside a `*` scheme, which is why `anyScheme` above writes
333
+ // that pair out.)
334
+ // Honoring that here keeps OUR matcher in step with Chrome's, which
335
+ // matters because the badge classification uses our matcher and would
336
+ // otherwise show UNEXPECTED for every wrapper Chrome happily injected.
337
+ // Strategy: split off a trailing `:port` if present and check
338
+ // hostname + port separately. Pattern hosts without a port match any
339
+ // port (the user opted not to constrain it).
340
+ const colon = host.lastIndexOf(':');
341
+ // Must look like host:digits — IPv6 literals are bracketed so a colon
342
+ // outside `[…]` is unambiguous; we don't support bracketed v6 here.
343
+ // `:*` is any port, as in Chrome.
344
+ const hasPort = colon > 0 && /^(\d+|\*)$/.test(host.slice(colon + 1));
345
+ const patternHost = hasPort ? host.slice(0, colon) : host;
346
+ const patternPort = hasPort ? host.slice(colon + 1) : '';
347
+ if (patternHost === '*') {
348
+ // any host (including empty)
349
+ }
350
+ else if (patternHost.startsWith('*.')) {
351
+ const suffix = patternHost.slice(2).toLowerCase();
352
+ const h = parsed.hostname.toLowerCase();
353
+ if (h !== suffix && !h.endsWith('.' + suffix))
354
+ return false;
355
+ }
356
+ else if (patternHost === '') {
357
+ if (parsed.hostname !== '')
358
+ return false;
359
+ }
360
+ else if (parsed.hostname.toLowerCase() !== patternHost.toLowerCase()) {
361
+ return false;
362
+ }
363
+ // If the pattern pinned a port, the URL must use exactly that port
364
+ // (after URL parsing normalises default ports — http defaults port to
365
+ // '', so a pattern of `http://host:80/*` won't match `http://host/`
366
+ // because parsed.port is ''. That mirrors Chrome's literal compare.)
367
+ if (patternPort !== '' && patternPort !== '*' && parsed.port !== patternPort)
368
+ return false;
369
+ // Path glob runs against pathname + query, so authors can target
370
+ // ?id=... etc. Not the fragment: Chrome's matcher ignores it, so a
371
+ // `#` in the pattern matches nothing and a URL's `#hash` never stops
372
+ // a match.
373
+ // A bare `?` (an empty query) counts, as Chrome reads it, though
374
+ // `search` drops it.
375
+ const bareQuery = !parsed.search && parsed.href.split('#')[0].endsWith('?');
376
+ const tail = parsed.pathname + (bareQuery ? '?' : parsed.search);
377
+ try {
378
+ return globToRegExp(path).test(tail);
379
+ }
380
+ catch {
381
+ return false;
382
+ }
383
+ }
384
+ // ---------- @match validation ----------
385
+ //
386
+ // Chrome re-parses every `@match` we hand `chrome.userScripts.register`
387
+ // with its own match-pattern grammar, and refuses the WHOLE
388
+ // registration — every other pattern in it, and the script with them —
389
+ // over a single malformed one. Its complaint comes back in Chrome's
390
+ // coordinates ("matches[1]: Empty path"), with no way back to the
391
+ // header line that caused it.
392
+ //
393
+ // So we check the same grammar here, at parse time, where the line IS
394
+ // still in hand: `script-store.ts` turns a problem into a located
395
+ // warning (or into the script's error, when it leaves nothing to match
396
+ // on) and `injector.ts` drops the pattern before Chrome sees it.
397
+ //
398
+ // Deliberately narrower than Chrome's parser. It flags only shapes that
399
+ // are certainly wrong, because a false positive would stop a working
400
+ // script from running for no reason. Subtler refusals (a malformed
401
+ // port, a host Chrome's canonicalizer rejects) still reach Chrome and
402
+ // still come back as REJECTED — see docs/diagnostics.md.
403
+ //
404
+ // The invariant that makes dropping safe: every pattern flagged here is
405
+ // one `matchPattern` above can never match, so no page loses a script
406
+ // it was getting. `tests/unit/url-match.test.mjs` pins it.
407
+ // The schemes a `@match` may name, which is also exactly the set
408
+ // `matchPattern` implements. `*` covers http and https only, per
409
+ // Chrome's grammar.
410
+ export const MATCH_SCHEMES = ['*', 'http', 'https', 'file', 'ftp'];
411
+ // The problem with a `@match` pattern, or null when Chrome's grammar
412
+ // accepts it — after the lenient rewrites in `canonicalMatchPatterns`,
413
+ // which is the same text `buildRegistrationSpec` registers.
414
+ //
415
+ // The problem names the pattern the author WROTE, not its rewritten
416
+ // form: that is the text they have to go and find in the header.
417
+ //
418
+ // A pattern is a problem only when EVERY canonical form is one. A
419
+ // scheme glob can expand into a usable form and an unusable one
420
+ // (`f*:///*` → `file:///*`, legal, plus `ftp:///*`, an empty host for a
421
+ // scheme that needs one), and `buildRegistrationSpec` registers the
422
+ // forms that survive — so reporting on the worst of them would warn
423
+ // about a pattern that is running, or store a script INVALID for
424
+ // having no usable `@match` while one of its forms was registered.
425
+ export function matchPatternProblem(pattern) {
426
+ if (pattern === '<all_urls>')
427
+ return null;
428
+ let first = null;
429
+ for (const canonical of canonicalMatchPatterns(pattern)) {
430
+ const problem = canonicalPatternProblem(canonical);
431
+ if (!problem)
432
+ return null;
433
+ first ??= { ...problem, pattern };
434
+ }
435
+ return first;
436
+ }
437
+ // The check itself, over a pattern already in Chrome's spelling. Every
438
+ // shape that reaches here has a `://` — `canonicalMatchPatterns`
439
+ // supplies one when the author didn't — except a broken scheme, which
440
+ // it leaves as written.
441
+ function canonicalPatternProblem(pattern) {
442
+ if (BROKEN_SCHEME.test(pattern)) {
443
+ return {
444
+ pattern,
445
+ what: 'doesn\'t start with a scheme and "://"',
446
+ fix: 'start it with "*://" for http and https',
447
+ };
448
+ }
449
+ const sep = pattern.indexOf('://');
450
+ const scheme = pattern.slice(0, sep);
451
+ if (!MATCH_SCHEMES.includes(scheme.toLowerCase())) {
452
+ return {
453
+ pattern,
454
+ what: `names the unsupported scheme "${scheme}"`,
455
+ fix: `use one of ${MATCH_SCHEMES.join(', ')} ("*" covers http and https)`,
456
+ };
457
+ }
458
+ const rest = pattern.slice(sep + 3);
459
+ // Always found: `readHostAndPath` supplies a path where there was none.
460
+ const slash = rest.indexOf('/');
461
+ const host = rest.slice(0, slash);
462
+ // Chrome takes a `:port` off the host before judging it, so
463
+ // `https://:8080/*` is an empty host rather than a host named
464
+ // ":8080". Strip the same suffix — the port itself is left to Chrome
465
+ // (a malformed one is exactly the kind of subtler refusal this
466
+ // function doesn't try to reproduce).
467
+ const bareHost = host.replace(/:(\d+|\*)$/, '');
468
+ // `file://` URLs have no host, so an empty one is only wrong for the
469
+ // schemes that do.
470
+ if (bareHost === '' && scheme.toLowerCase() !== 'file') {
471
+ return {
472
+ pattern,
473
+ what: 'has no host',
474
+ fix: 'name a host, or "*" for any host',
475
+ };
476
+ }
477
+ // Characters no hostname can hold. Checked because the scheme-less
478
+ // reading supplies a scheme for ANY text: a regex someone pasted into
479
+ // an `@match` (`https?:\/\/weibo.com/p/*`, a real one) reads as a
480
+ // pattern whose host is `https?:\`, which would otherwise pass every
481
+ // check below and be handed to Chrome — and Chrome refusing it takes
482
+ // the whole registration down. A reject-list rather than a list of
483
+ // legal characters, so an internationalized host still passes.
484
+ //
485
+ // No `:` in the list, deliberately. A colon that survived the port
486
+ // strip means a malformed port (`https://host:notaport/*`), and ports
487
+ // are the part of the grammar this function leaves to Chrome — see
488
+ // the narrowness note above, and `tests/e2e/rejected-registration.spec.ts`,
489
+ // which provokes REJECTED with exactly that shape.
490
+ if (/[\s\\?#@%"<>{}|^`]/.test(bareHost)) {
491
+ return {
492
+ pattern,
493
+ what: `names the invalid host "${bareHost}"`,
494
+ fix: 'use a host name like example.com, "*" for any host, '
495
+ + 'or "*.example.com" for a domain and its subdomains',
496
+ };
497
+ }
498
+ // A host wildcard is either the whole host or the leading label —
499
+ // anywhere else Chrome answers "Invalid host wildcard".
500
+ const afterWildcard = bareHost === '*' ? '' : bareHost.startsWith('*.') ? bareHost.slice(2) : bareHost;
501
+ if (afterWildcard.includes('*')) {
502
+ return {
503
+ pattern,
504
+ what: 'uses "*" inside its host',
505
+ fix: '"*" has to be the whole host or its first label, as in *.example.com',
506
+ };
507
+ }
508
+ // Chrome takes this one, but matches it against URLs without their
509
+ // fragment, so it can never match. Violentmonkey does match it, so a
510
+ // script written there would otherwise just never run here.
511
+ if (rest.slice(slash).includes('#')) {
512
+ return {
513
+ pattern,
514
+ what: 'names a #hash that @match can\'t see',
515
+ fix: 'use @include to match on the hash',
516
+ };
517
+ }
518
+ return null;
519
+ }
520
+ // The warning for a pattern we dropped from a script that still has
521
+ // others to run on. Parallel to the unknown-`@grant` wording: name what
522
+ // is being ignored first, since that is the fact the reader is
523
+ // scanning the Log for.
524
+ export function ignoredMatchMessage(problem) {
525
+ const fix = problem.fix ? `; ${problem.fix}` : '';
526
+ return `Ignoring @match ${problem.pattern}, which ${problem.what}${fix}`;
527
+ }
528
+ // The pieces `unusableMatchMessage` is built from, kept separate so the
529
+ // recognizer below can be derived from the same strings rather than
530
+ // from a copy of the wording.
531
+ //
532
+ // The clause deliberately ends the way `NO_MATCH_OR_INCLUDE`
533
+ // (`script-store.ts`) does: the two failures are the same outcome
534
+ // reached two ways — no pattern at all, versus none Chrome will take —
535
+ // and reading alike is the point.
536
+ const MATCH_TAG = '@match ';
537
+ const NEVER_RUNS = ', so this script would never run on any page';
538
+ // The error for a script whose `@match` patterns are ALL unusable and
539
+ // which has no `@include` to fall back on. Same problem, different
540
+ // consequence — the script cannot run at all, so this is the row's
541
+ // `errorMessage` rather than a warning beside a working script.
542
+ //
543
+ // Starts with the tag rather than a capitalized word, like the
544
+ // `@require` failures do: the tag names what the reader has to go and
545
+ // fix, and a `@match` isn't a word we get to recase.
546
+ export function unusableMatchMessage(problem) {
547
+ const fix = problem.fix ? `; ${problem.fix}` : '';
548
+ return `${MATCH_TAG}${problem.pattern} ${problem.what}${NEVER_RUNS}${fix}`;
549
+ }
550
+ // One of those messages with the offending pattern elided, or null when
551
+ // the message is something else. What a bulk survey groups on — dozens
552
+ // of scripts making the same mistake with different URLs are one row,
553
+ // not dozens (`tests/greasyfork-examples/validate-corpus.mjs`).
554
+ //
555
+ // Derived from the constants above rather than matching the wording: a
556
+ // recognizer that carries its own copy of the sentence goes stale the
557
+ // first time the sentence is reworded, and does it silently. See
558
+ // docs/diagnostics.md, "One string, one place".
559
+ export function unusableMatchGroupKey(message) {
560
+ if (!message.startsWith(MATCH_TAG) || !message.includes(NEVER_RUNS))
561
+ return null;
562
+ const afterPattern = message.indexOf(' ', MATCH_TAG.length);
563
+ if (afterPattern < 0)
564
+ return null;
565
+ return `${MATCH_TAG}… ${message.slice(afterPattern + 1)}`;
566
+ }
567
+ // Test a single @include / @exclude pattern against a URL. Patterns
568
+ // wrapped in `/.../` are regex; everything else is a `*`-glob over the
569
+ // whole URL string. Both ignore case, as Tampermonkey's and
570
+ // Violentmonkey's do. Chrome tests globs case-sensitively, so what we
571
+ // register is widened to cover that (`chromeIncludeGlob`), and the
572
+ // wrapper's matcher holds the script to this reading.
573
+ export function includePattern(pattern, url) {
574
+ if (isRegexPattern(pattern)) {
575
+ try {
576
+ return new RegExp(pattern.slice(1, -1), 'i').test(url);
577
+ }
578
+ catch {
579
+ return false;
580
+ }
581
+ }
582
+ const tld = testTld(pattern, url, true, (p) => includePattern(p, url));
583
+ if (tld !== null)
584
+ return tld;
585
+ try {
586
+ return globToRegExp(pattern, 'i').test(url);
587
+ }
588
+ catch {
589
+ return false;
590
+ }
591
+ }
592
+ // The glob to register with Chrome for a glob `@include`, which we read
593
+ // ignoring case and Chrome tests case-sensitively. It must cover every
594
+ // URL the `@include` does, and the wrapper's matcher narrows it.
595
+ // - Chrome lowercases a URL's scheme and host before testing, so a
596
+ // host is lowercased and stays exact.
597
+ // - Every other letter becomes `?`, Chrome's one-character wildcard,
598
+ // which takes it in either case.
599
+ // - The host is the text before the first `/` (after a leading
600
+ // `scheme://`), when it has a dot, or follows `scheme://` with no
601
+ // `*` (`localhost`). `*Foo/bar*` has none: its `Foo` may well be
602
+ // in the path.
603
+ export function chromeIncludeGlob(glob) {
604
+ const sep = glob.indexOf('://');
605
+ const firstSlash = glob.indexOf('/');
606
+ const start = sep >= 0 && sep < firstSlash ? sep + 3 : 0;
607
+ const slash = glob.indexOf('/', start);
608
+ const hostEnd = slash < 0 ? glob.length : slash;
609
+ const host = glob.slice(start, hostEnd);
610
+ const isHost = host.includes('.') || (start > 0 && !host.includes('*'));
611
+ const end = isHost ? hostEnd : start;
612
+ return glob.slice(0, end).toLowerCase() + glob.slice(end).replace(/[a-z]/gi, '?');
613
+ }
614
+ export function pageMatcher(matching) {
615
+ const include = [];
616
+ const exclude = [];
617
+ for (const m of matching.matches) {
618
+ for (const p of canonicalMatchPatterns(m)) {
619
+ const rx = matchRegex(p);
620
+ if (rx)
621
+ include.push(rx);
622
+ }
623
+ }
624
+ for (const inc of matching.includes) {
625
+ const rx = includeRegex(inc);
626
+ if (rx)
627
+ include.push(rx);
628
+ }
629
+ for (const ex of matching.excludes) {
630
+ if (hostEndsInTld(ex))
631
+ continue;
632
+ const rx = includeRegex(ex);
633
+ if (rx)
634
+ exclude.push(rx);
635
+ }
636
+ const tld = matching.includes.some(hostEndsInTld) || matching.excludes.some(hostEndsInTld)
637
+ || matching.matches.some((m) => canonicalMatchPatterns(m).some(hostEndsInTld));
638
+ return tld ? { include, exclude, tld } : { include, exclude };
639
+ }
640
+ // Any run of labels, standing in for a `.tld` in the page's loose reading.
641
+ const LOOSE_TLD = '(?:\\.[a-z0-9-]+)+';
642
+ // The host part of a URL: up to the port, path, query or fragment, past
643
+ // any `user:pass@`. A bracketed IPv6 literal holds colons of its own.
644
+ const URL_HOST = '(?:\\[[^\\]]*\\]|[^/?#:@\\[]*)';
645
+ // An `@include` / `@exclude` as a page regex, ignoring case as
646
+ // `includePattern` does.
647
+ function includeRegex(pattern) {
648
+ if (isRegexPattern(pattern)) {
649
+ const source = pattern.slice(1, -1);
650
+ try {
651
+ new RegExp(source, 'i');
652
+ }
653
+ catch {
654
+ return null;
655
+ }
656
+ return [source, 'i'];
657
+ }
658
+ const glob = (g) => escapeForRegexExceptStar(g).replace(/\*/g, '.*');
659
+ const at = tldIndex(pattern, true);
660
+ if (at < 0)
661
+ return ['^' + glob(pattern) + '$', 'i'];
662
+ return ['^' + glob(pattern.slice(0, at)) + LOOSE_TLD + glob(pattern.slice(at + 4)) + '$', 'i'];
663
+ }
664
+ // A canonical `@match` as a regex over the whole URL, testing what
665
+ // `matchCanonical` does: scheme, host, port, then the path glob against
666
+ // the path and query (a bare `?` included, as in Chrome). Case matters
667
+ // in the path, so no `i` flag: the URL's scheme and host are already
668
+ // lowercase, so the pattern's are lowercased to meet them.
669
+ function matchRegex(pattern) {
670
+ if (pattern === '<all_urls>')
671
+ return ['^(?:https?|file|ftp):', 'i'];
672
+ const parts = /^(\*|https?|file|ftp):\/\/([^/]*)(\/.*)$/i.exec(pattern);
673
+ if (!parts)
674
+ return null;
675
+ const [, scheme, host, path] = parts;
676
+ // The path glob never sees the fragment, so a `#` in it matches nothing.
677
+ if (path.includes('#'))
678
+ return null;
679
+ const colon = host.lastIndexOf(':');
680
+ const hasPort = colon > 0 && /^(\d+|\*)$/.test(host.slice(colon + 1));
681
+ const patternHost = (hasPort ? host.slice(0, colon) : host).toLowerCase();
682
+ const patternPort = hasPort ? host.slice(colon + 1) : '';
683
+ // A `user@` in the pattern's host never matches a URL's host.
684
+ if (patternHost.includes('@'))
685
+ return null;
686
+ const esc = (s) => escapeForRegexExceptStar(s).replace(/\*/g, '\\*');
687
+ const anySub = '(?:' + URL_HOST + '\\.)?';
688
+ let hostRx;
689
+ if (patternHost === '*') {
690
+ hostRx = URL_HOST;
691
+ }
692
+ else if (tldIndex(patternHost) >= 0) {
693
+ const base = patternHost.slice(0, -4);
694
+ hostRx = base.startsWith('*.') ? anySub + esc(base.slice(2)) + LOOSE_TLD : esc(base) + LOOSE_TLD;
695
+ }
696
+ else if (patternHost.startsWith('*.')) {
697
+ hostRx = anySub + esc(patternHost.slice(2));
698
+ }
699
+ else {
700
+ hostRx = esc(patternHost);
701
+ }
702
+ const portRx = patternPort === '' || patternPort === '*' ? '(?::\\d+)?' : ':' + patternPort;
703
+ const schemeRx = scheme === '*' ? 'https?' : scheme.toLowerCase();
704
+ // The path glob covers the path and query, never the fragment.
705
+ const pathRx = escapeForRegexExceptStar(path).replace(/\*/g, '[^#]*');
706
+ return ['^' + schemeRx + '://(?:[^/?#@]*@)?' + hostRx + portRx + pathRx + '(?:#.*)?$', ''];
707
+ }
708
+ // Combined script-level test: a script matches a URL iff
709
+ // - no @exclude pattern matches, AND
710
+ // - at least one @match or @include pattern matches.
711
+ // A script with zero match/include patterns never matches anything,
712
+ // which is intentional — validateHeader already flags those as errors.
713
+ //
714
+ // Stops at the first rule that settles it: this runs for every script
715
+ // on every URL the badge checks. `explainUrlMatch` is the same rule,
716
+ // listing every hit instead; `tests/unit/url-match.test.mjs` pins the
717
+ // two together.
718
+ export function urlMatchesScript(url, matching) {
719
+ if (matching.excludes.some((ex) => includePattern(ex, url)))
720
+ return false;
721
+ return matching.matches.some((m) => matchPattern(m, url)) || matching.includes.some((inc) => includePattern(inc, url));
722
+ }
723
+ // The same test, also naming the rules that answered, for a developer
724
+ // asking why a script did or didn't run. Slower than `urlMatchesScript`,
725
+ // since it tries every rule to list them all.
726
+ export function explainUrlMatch(url, matching) {
727
+ const excludedBy = matching.excludes.filter((ex) => includePattern(ex, url)).map((ex) => `@exclude ${ex}`);
728
+ const matchedBy = [
729
+ ...matching.matches.filter((m) => matchPattern(m, url)).map((m) => `@match ${m}`),
730
+ ...matching.includes.filter((inc) => includePattern(inc, url)).map((inc) => `@include ${inc}`),
731
+ ];
732
+ const matches = excludedBy.length === 0 && matchedBy.length > 0;
733
+ return { matches, rule: excludedBy[0] ?? matchedBy[0] ?? null, matchedBy, excludedBy };
734
+ }