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,39 @@
1
+ import type { Page } from '@playwright/test';
2
+ import { type PreparedScript } from './prepare.js';
3
+ /** How a userscript is named in a saved file's header: its @name and @version. */
4
+ export declare function userscriptLabel(script: PreparedScript): string;
5
+ /**
6
+ * The page's current DOM as a standalone HTML document.
7
+ *
8
+ * A `<base>` keeps relative URLs pointing at the site. The page's own
9
+ * JavaScript is disabled (`type="text/plain"`, which keeps it readable
10
+ * while the browser skips it): served again it would re-fetch,
11
+ * redirect to a login, or rebuild the DOM the file was saved to
12
+ * preserve. What a clone cannot carry (typed form values, canvas
13
+ * pixels, shadow roots) is not in the file either.
14
+ *
15
+ * The header records the page's URL, for `--from-saved` to serve it
16
+ * there, and each of `userscripts` (see `userscriptLabel`), the ones
17
+ * running in the page, whose changes are in the file.
18
+ */
19
+ export declare function pageHtml(page: Page, userscripts?: string[]): Promise<string>;
20
+ /**
21
+ * The header comment lines for a page saved from `url` with
22
+ * `userscripts` running. Only `-->` (or `--!>`) ends a comment, and a
23
+ * URL the browser reports has any `>` percent-encoded, so the URL goes
24
+ * in as it is (`xn--` hosts included). A name gets its `>` escaped
25
+ * after `--`, which no real name has.
26
+ */
27
+ export declare function savedHeaderLines(url: string, userscripts: string[]): string[];
28
+ /** `pageHtml`, written to `file`. Returns the HTML. */
29
+ export declare function savePageHtml(page: Page, file: string, userscripts?: string[]): Promise<string>;
30
+ /** What a saved file's header says: its URL, if any, and the userscripts that were active. */
31
+ export interface SavedPageHeader {
32
+ url: string | null;
33
+ userscripts: string[];
34
+ }
35
+ /**
36
+ * Reads a saved file's header. Only what comes before `<html` is
37
+ * looked at, so a comment in the page's own content never counts.
38
+ */
39
+ export declare function readSavedHeader(html: string): SavedPageHeader;
@@ -0,0 +1,89 @@
1
+ // Saving a page's DOM as a file, with a header saying where it came
2
+ // from; and reading that header back (see saved-pages.ts for serving
3
+ // the file).
4
+ //
5
+ // Shared by the `savehtml` run command (page-commands.ts) and the
6
+ // fixture's `saveHtml`, so a developer recording a page by hand and a
7
+ // test recording one produce the same file.
8
+ import { writeFile } from 'node:fs/promises';
9
+ import { preparedScriptName } from './prepare.js';
10
+ // The header lines a saved file starts with, after its doctype. The
11
+ // URL line is the "saved from" comment Internet Explorer's Mark of the
12
+ // Web introduced, and Chrome's Save Page As still writes, so a page
13
+ // saved from the browser's menu is read the same way. The number is
14
+ // the URL's length, which nothing here relies on.
15
+ const SAVED_FROM = /<!--\s*saved from url=\(\d+\)(\S+?)\s*-->/i;
16
+ const USERSCRIPT_PREFIX = 'SourceMonkey: userscript active: ';
17
+ const USERSCRIPT_LINE = new RegExp(`<!-- ${USERSCRIPT_PREFIX}(.*?) -->`, 'g');
18
+ /** How a userscript is named in a saved file's header: its @name and @version. */
19
+ export function userscriptLabel(script) {
20
+ const version = (script.script.header?.tags ?? []).find((t) => t.tag === 'version')?.value;
21
+ return version ? `${preparedScriptName(script)} ${version}` : preparedScriptName(script);
22
+ }
23
+ /**
24
+ * The page's current DOM as a standalone HTML document.
25
+ *
26
+ * A `<base>` keeps relative URLs pointing at the site. The page's own
27
+ * JavaScript is disabled (`type="text/plain"`, which keeps it readable
28
+ * while the browser skips it): served again it would re-fetch,
29
+ * redirect to a login, or rebuild the DOM the file was saved to
30
+ * preserve. What a clone cannot carry (typed form values, canvas
31
+ * pixels, shadow roots) is not in the file either.
32
+ *
33
+ * The header records the page's URL, for `--from-saved` to serve it
34
+ * there, and each of `userscripts` (see `userscriptLabel`), the ones
35
+ * running in the page, whose changes are in the file.
36
+ */
37
+ export async function pageHtml(page, userscripts = []) {
38
+ const url = page.url();
39
+ const { doctype, body } = await page.evaluate((base) => {
40
+ const doc = document.documentElement.cloneNode(true);
41
+ const head = doc.querySelector('head');
42
+ if (head && !head.querySelector('base')) {
43
+ const el = document.createElement('base');
44
+ el.href = base;
45
+ head.prepend(el);
46
+ }
47
+ for (const script of doc.querySelectorAll('script'))
48
+ script.setAttribute('type', 'text/plain');
49
+ const dt = document.doctype;
50
+ return {
51
+ doctype: dt
52
+ ? `<!DOCTYPE ${dt.name}${dt.publicId ? ` PUBLIC "${dt.publicId}"` : ''}${dt.systemId ? ` "${dt.systemId}"` : ''}>`
53
+ : '',
54
+ body: doc.outerHTML,
55
+ };
56
+ }, url);
57
+ return [...(doctype ? [doctype] : []), ...savedHeaderLines(url, userscripts), body].join('\n');
58
+ }
59
+ /**
60
+ * The header comment lines for a page saved from `url` with
61
+ * `userscripts` running. Only `-->` (or `--!>`) ends a comment, and a
62
+ * URL the browser reports has any `>` percent-encoded, so the URL goes
63
+ * in as it is (`xn--` hosts included). A name gets its `>` escaped
64
+ * after `--`, which no real name has.
65
+ */
66
+ export function savedHeaderLines(url, userscripts) {
67
+ return [
68
+ `<!-- saved from url=(${String(url.length).padStart(4, '0')})${url} -->`,
69
+ ...userscripts.map((name) => `<!-- ${USERSCRIPT_PREFIX}${name.replace(/--(!?)>/g, '--$1&gt;')} -->`),
70
+ ];
71
+ }
72
+ /** `pageHtml`, written to `file`. Returns the HTML. */
73
+ export async function savePageHtml(page, file, userscripts = []) {
74
+ const html = await pageHtml(page, userscripts);
75
+ await writeFile(file, html);
76
+ return html;
77
+ }
78
+ /**
79
+ * Reads a saved file's header. Only what comes before `<html` is
80
+ * looked at, so a comment in the page's own content never counts.
81
+ */
82
+ export function readSavedHeader(html) {
83
+ const end = html.search(/<html[\s>]/i);
84
+ const head = end >= 0 ? html.slice(0, end) : html;
85
+ return {
86
+ url: SAVED_FROM.exec(head)?.[1] ?? null,
87
+ userscripts: [...head.matchAll(USERSCRIPT_LINE)].map((m) => m[1]),
88
+ };
89
+ }
@@ -0,0 +1,120 @@
1
+ import type { Page, Request } from '@playwright/test';
2
+ import { type SavedPageHeader } from './save-html.js';
3
+ /**
4
+ * The browser reports `https://example.test` as `https://example.test/`
5
+ * and never reports a fragment, so both sides are normalized the same
6
+ * way before comparing.
7
+ */
8
+ export declare function comparableUrl(raw: string): string;
9
+ /** What `routeSavedPage` serves, as it stands when a request is made. */
10
+ export interface SavedPageServe {
11
+ /** The URL the page answers, matched exactly, query string included, fragment ignored. */
12
+ url: string;
13
+ /** The page's HTML. Read per request, so an edited file is served on the next load. */
14
+ html: () => Promise<string>;
15
+ /** Other requests to abort, rather than send on. */
16
+ block?: (request: Request) => boolean;
17
+ /** How the file is named when it can't be read. */
18
+ name: string;
19
+ /** Told why, when the file can't be read; the page shows it too. */
20
+ onError?: (message: string) => void;
21
+ /** The files saved beside the page, served from disk (see `companionFiles`). */
22
+ companion?: CompanionFiles;
23
+ }
24
+ /**
25
+ * The folder a browser's "Webpage, Complete" save puts beside the page
26
+ * (Chrome and Firefox both name it `<name>_files`), and the URL its
27
+ * files are asked for at: the page links them relatively
28
+ * (`./<name>_files/app.css`), so they resolve against the page's URL.
29
+ */
30
+ export interface CompanionFiles {
31
+ /** The folder's URL path, decoded, on `origin`. */
32
+ origin: string;
33
+ path: string;
34
+ dir: string;
35
+ }
36
+ /** The companion folder for a saved `file` served at `url`, whether or not it exists. */
37
+ export declare function companionFiles(file: string, url: string): CompanionFiles | undefined;
38
+ /**
39
+ * Serves `current()` on `page`: its URL gets its HTML, and every other
40
+ * request falls through to the page's other routes and the network,
41
+ * unless `block` says to abort it. Undefined serves nothing, so a
42
+ * caller can change or end what is served without routing again.
43
+ */
44
+ export declare function routeSavedPage(page: Page, current: () => SavedPageServe | undefined): Promise<void>;
45
+ /**
46
+ * For `--offline`: blocks the requests a page makes while it shows the
47
+ * saved page at `url`. A load of a new page is never blocked, so
48
+ * following a link off it still works.
49
+ */
50
+ export declare function offlineBlock(page: Page, url: string): (request: Request) => boolean;
51
+ /**
52
+ * A saved page as sm-dev serves it in a tab: `file` at `url`, `offline`
53
+ * blocking the rest. `given`: the file as it was named, for messages.
54
+ */
55
+ export interface SavedPageFile {
56
+ file: string;
57
+ given: string;
58
+ url: string;
59
+ offline: boolean;
60
+ }
61
+ /**
62
+ * What `routeSavedPage` serves for `saved` in `page`, for `--from-saved`:
63
+ * the file read fresh on each load, and a read failure told to `out` as
64
+ * well as shown.
65
+ */
66
+ export declare function savedPageServe(page: Page, saved: SavedPageFile, out: (line: string) => void): SavedPageServe;
67
+ /** How `start`, `open` and `newtab` say a tab is loading a saved page. */
68
+ export declare function openingSavedLine(given: string, url: string, tab: string): string;
69
+ /** `--offline` without `--from-saved`. */
70
+ export declare const OFFLINE_NEEDS_SAVED = "The --offline flag goes with --from-saved";
71
+ /** `--url` without `--from-saved` and exactly one saved page. */
72
+ export declare const URL_NEEDS_ONE_SAVED = "The --url flag gives the URL for one saved page, so it goes with --from-saved and a single .html file";
73
+ /** `--from-saved` with no page to open from a file. */
74
+ export declare const NO_SAVED_FILE = "The --from-saved flag opens .html files, and none was given";
75
+ /** How sm-dev's commands say to give a URL for a saved page. */
76
+ export declare const GIVE_SAVED_URL = "Open it by itself, with --url.";
77
+ /**
78
+ * The URL to serve a saved page at: `url`, else the one its header
79
+ * records. Or why there is none: no URL anywhere, or one a route
80
+ * can't answer (`about:blank`, `file://`). `remedy` is a sentence
81
+ * saying how to give one where it was named.
82
+ */
83
+ export declare function savedPageUrl(given: string, header: SavedPageHeader, url: string | undefined, remedy: string): {
84
+ url: string;
85
+ } | {
86
+ problem: string;
87
+ };
88
+ /** A saved page to open (`--from-saved`), with the userscripts its header lists. */
89
+ export type SavedPageStart = SavedPageFile & {
90
+ userscripts: string[];
91
+ };
92
+ /** One page a command opens: a URL, or with `saved`, a saved page served at it. */
93
+ export interface PageToOpen {
94
+ url: string;
95
+ saved?: SavedPageStart;
96
+ }
97
+ /** A page a command was given: the word as typed, and the URL it stands for. */
98
+ export interface PageWord {
99
+ word: string;
100
+ url: string;
101
+ }
102
+ /**
103
+ * With `--from-saved` (`fromSaved`): each word naming a local `.html`
104
+ * file is a saved page, at the URL it records or `--url`; the others
105
+ * stay URLs. Without, `--url` and `--offline` are refused. Throws a
106
+ * `UsageError` naming the file for any saved page that can't be opened,
107
+ * before anything opens.
108
+ */
109
+ export declare function pagesToOpen(words: PageWord[], opts: {
110
+ fromSaved: boolean;
111
+ url?: string;
112
+ offline: boolean;
113
+ }, cwd?: string): Promise<PageToOpen[]>;
114
+ /** Reads a saved file for serving, with its header. */
115
+ export declare function loadSavedPage(file: string): Promise<{
116
+ html: string;
117
+ header: SavedPageHeader;
118
+ }>;
119
+ /** Why a saved page named with `--from-saved` could not be read. */
120
+ export declare function savedReadMessage(given: string, err: NodeJS.ErrnoException): string;
@@ -0,0 +1,275 @@
1
+ // Serving a saved page (from save-html.ts, or a browser's own Save Page
2
+ // As) back at the URL it was saved from, so scripts match and run on it
3
+ // as on the live site.
4
+ //
5
+ // Shared by `--from-saved` on `start`, `open` and `newtab`
6
+ // (console-commands.ts, run.ts, cli.ts) and the fixture's `serveHtml`,
7
+ // so both serve one the same way.
8
+ import { readFile } from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { UsageError } from './checks.js';
12
+ import { isDirectoryMessage, noSuchFileMessage } from './prepare.js';
13
+ import { readSavedHeader } from './save-html.js';
14
+ /**
15
+ * The browser reports `https://example.test` as `https://example.test/`
16
+ * and never reports a fragment, so both sides are normalized the same
17
+ * way before comparing.
18
+ */
19
+ export function comparableUrl(raw) {
20
+ try {
21
+ const u = new URL(raw);
22
+ u.hash = '';
23
+ return u.href;
24
+ }
25
+ catch {
26
+ return raw;
27
+ }
28
+ }
29
+ /** The companion folder for a saved `file` served at `url`, whether or not it exists. */
30
+ export function companionFiles(file, url) {
31
+ const stem = path.basename(file).replace(/\.x?html?$/i, '');
32
+ let at;
33
+ try {
34
+ // Built by the URL parser, so it's encoded as the browser encodes
35
+ // the page's own relative links.
36
+ at = new URL(`./${stem}_files/`, url);
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ // Resolved, so the check that a request stays inside it compares like with like.
42
+ return { origin: at.origin, path: decodedPath(at) ?? at.pathname, dir: path.resolve(path.dirname(file), `${stem}_files`) };
43
+ }
44
+ // Where on disk a request for a companion file is, or null when it
45
+ // isn't one. Never outside the folder, whatever `..` the URL has.
46
+ function companionPath(companion, raw) {
47
+ let url;
48
+ try {
49
+ url = new URL(raw);
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ const wanted = decodedPath(url);
55
+ if (url.origin !== companion.origin || !wanted?.startsWith(companion.path))
56
+ return null;
57
+ const file = path.resolve(companion.dir, wanted.slice(companion.path.length));
58
+ return file.startsWith(companion.dir + path.sep) ? file : null;
59
+ }
60
+ function decodedPath(url) {
61
+ try {
62
+ return decodeURIComponent(url.pathname);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ // By extension: a stylesheet or module script served with the wrong
69
+ // type is refused. Anything else is left for the browser to sniff.
70
+ const CONTENT_TYPES = {
71
+ '.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json',
72
+ '.html': 'text/html', '.htm': 'text/html', '.svg': 'image/svg+xml', '.png': 'image/png',
73
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.avif': 'image/avif',
74
+ '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
75
+ };
76
+ /**
77
+ * Serves `current()` on `page`: its URL gets its HTML, and every other
78
+ * request falls through to the page's other routes and the network,
79
+ * unless `block` says to abort it. Undefined serves nothing, so a
80
+ * caller can change or end what is served without routing again.
81
+ */
82
+ export async function routeSavedPage(page, current) {
83
+ await bypassServiceWorker(page);
84
+ await page.route('**/*', async (route) => {
85
+ const saved = current();
86
+ const request = route.request();
87
+ if (saved && comparableUrl(request.url()) === comparableUrl(saved.url)) {
88
+ let body;
89
+ try {
90
+ body = await saved.html();
91
+ }
92
+ catch (err) {
93
+ // Shown as the page too, where the person is looking.
94
+ const message = savedReadMessage(saved.name, err);
95
+ saved.onError?.(message);
96
+ await route.fulfill({ status: 500, contentType: 'text/plain; charset=utf-8', body: message });
97
+ return;
98
+ }
99
+ await route.fulfill({ status: 200, contentType: 'text/html; charset=utf-8', body });
100
+ return;
101
+ }
102
+ // A file saved beside the page, while it shows, even with
103
+ // `--offline`. One that isn't there is a 404, not the live site's:
104
+ // the site never had it at that URL.
105
+ const showing = saved !== undefined && comparableUrl(page.url()) === comparableUrl(saved.url);
106
+ const local = showing && saved.companion ? companionPath(saved.companion, request.url()) : null;
107
+ if (local) {
108
+ const body = await readFile(local).catch(() => null);
109
+ const type = CONTENT_TYPES[path.extname(local).toLowerCase()];
110
+ await route.fulfill(body ? { status: 200, body, ...(type ? { contentType: type } : {}) } : { status: 404, body: '' });
111
+ return;
112
+ }
113
+ if (saved?.block?.(request)) {
114
+ await route.abort();
115
+ return;
116
+ }
117
+ // `fallback`, not `continue`: Playwright runs route handlers newest
118
+ // first, and only `fallback` hands a request to the next one. That
119
+ // lets a second saved page, or a HAR, sit beside this route.
120
+ await route.fallback();
121
+ });
122
+ }
123
+ /**
124
+ * For `--offline`: blocks the requests a page makes while it shows the
125
+ * saved page at `url`. A load of a new page is never blocked, so
126
+ * following a link off it still works.
127
+ */
128
+ export function offlineBlock(page, url) {
129
+ return (request) => {
130
+ if (comparableUrl(page.url()) !== comparableUrl(url))
131
+ return false;
132
+ try {
133
+ return !(request.isNavigationRequest() && request.frame() === page.mainFrame());
134
+ }
135
+ catch {
136
+ // A service worker's request has no frame.
137
+ return true;
138
+ }
139
+ };
140
+ }
141
+ /**
142
+ * What `routeSavedPage` serves for `saved` in `page`, for `--from-saved`:
143
+ * the file read fresh on each load, and a read failure told to `out` as
144
+ * well as shown.
145
+ */
146
+ export function savedPageServe(page, saved, out) {
147
+ return {
148
+ url: saved.url,
149
+ html: () => readFile(saved.file, 'utf8'),
150
+ block: saved.offline ? offlineBlock(page, saved.url) : undefined,
151
+ name: saved.given,
152
+ onError: (message) => out(`[sm-dev] ${message}`),
153
+ companion: companionFiles(saved.file, saved.url),
154
+ };
155
+ }
156
+ /** How `start`, `open` and `newtab` say a tab is loading a saved page. */
157
+ export function openingSavedLine(given, url, tab) {
158
+ return `[sm-dev] Opening ${given} as ${url} in ${tab}`;
159
+ }
160
+ /** `--offline` without `--from-saved`. */
161
+ export const OFFLINE_NEEDS_SAVED = 'The --offline flag goes with --from-saved';
162
+ /** `--url` without `--from-saved` and exactly one saved page. */
163
+ export const URL_NEEDS_ONE_SAVED = 'The --url flag gives the URL for one saved page, so it goes with --from-saved and a single .html file';
164
+ /** `--from-saved` with no page to open from a file. */
165
+ export const NO_SAVED_FILE = 'The --from-saved flag opens .html files, and none was given';
166
+ /** How sm-dev's commands say to give a URL for a saved page. */
167
+ export const GIVE_SAVED_URL = 'Open it by itself, with --url.';
168
+ /**
169
+ * The URL to serve a saved page at: `url`, else the one its header
170
+ * records. Or why there is none: no URL anywhere, or one a route
171
+ * can't answer (`about:blank`, `file://`). `remedy` is a sentence
172
+ * saying how to give one where it was named.
173
+ */
174
+ export function savedPageUrl(given, header, url, remedy) {
175
+ const at = url ?? header.url;
176
+ if (!at)
177
+ return { problem: `${given} does not say what URL it was saved from. ${remedy}` };
178
+ if (!/^https?:/i.test(at))
179
+ return { problem: `${given} can only be served at an http or https URL, not ${at}. ${remedy}` };
180
+ return { url: at };
181
+ }
182
+ /**
183
+ * With `--from-saved` (`fromSaved`): each word naming a local `.html`
184
+ * file is a saved page, at the URL it records or `--url`; the others
185
+ * stay URLs. Without, `--url` and `--offline` are refused. Throws a
186
+ * `UsageError` naming the file for any saved page that can't be opened,
187
+ * before anything opens.
188
+ */
189
+ export async function pagesToOpen(words, opts, cwd = process.cwd()) {
190
+ if (!opts.fromSaved) {
191
+ if (opts.url !== undefined)
192
+ throw new UsageError(URL_NEEDS_ONE_SAVED);
193
+ if (opts.offline)
194
+ throw new UsageError(OFFLINE_NEEDS_SAVED);
195
+ return words.map(({ url }) => ({ url }));
196
+ }
197
+ const files = words.map((w) => savedPageFile(w, cwd));
198
+ const count = files.filter(Boolean).length;
199
+ if (count === 0)
200
+ throw new UsageError(NO_SAVED_FILE);
201
+ if (opts.url !== undefined && count > 1)
202
+ throw new UsageError(URL_NEEDS_ONE_SAVED);
203
+ return Promise.all(words.map(async ({ word, url }, i) => {
204
+ const file = files[i];
205
+ if (!file)
206
+ return { url };
207
+ let header;
208
+ try {
209
+ ({ header } = await loadSavedPage(file));
210
+ }
211
+ catch (err) {
212
+ // A name that's no file is a mistake in the command; one that
213
+ // can't be read for another reason isn't.
214
+ const code = err.code;
215
+ const message = savedReadMessage(word, err);
216
+ throw code === 'ENOENT' || code === 'EISDIR' ? new UsageError(message) : new Error(message);
217
+ }
218
+ const found = savedPageUrl(word, header, opts.url, GIVE_SAVED_URL);
219
+ if ('problem' in found)
220
+ throw new UsageError(found.problem);
221
+ return { url: found.url, saved: { file, given: word, url: found.url, offline: opts.offline, userscripts: header.userscripts } };
222
+ }));
223
+ }
224
+ // The file a page names, when it's a local `.html` file: what
225
+ // `--from-saved` serves. A word ending `.html` with no `://` is one
226
+ // even where it would read as a host (`pgae.html`), so a typo is
227
+ // said to be no file rather than opened live; `localhost:8080/x.html`
228
+ // keeps its port, and so its host.
229
+ function savedPageFile({ word, url }, cwd) {
230
+ if (url.startsWith('file:'))
231
+ return /\.x?html?$/i.test(url) ? fileURLToPath(url) : null;
232
+ const first = word.split('/')[0];
233
+ if (word.includes('://') || /:\d+$/.test(first) || !/\.x?html?$/i.test(word))
234
+ return null;
235
+ return path.resolve(cwd, word);
236
+ }
237
+ // A site's service worker answers its requests before Playwright's
238
+ // routes see them, so a site that has one would get its live page
239
+ // instead of the saved one. Bypassing it takes the network domain
240
+ // enabled on a session of our own, kept for the page's life. Best
241
+ // effort: a browser that refuses still serves pages without one.
242
+ const bypassed = new WeakMap();
243
+ function bypassServiceWorker(page) {
244
+ let session = bypassed.get(page);
245
+ if (!session) {
246
+ session = (async () => {
247
+ try {
248
+ const cdp = await page.context().newCDPSession(page);
249
+ await cdp.send('Network.enable');
250
+ await cdp.send('Network.setBypassServiceWorker', { bypass: true });
251
+ return cdp;
252
+ }
253
+ catch {
254
+ return null;
255
+ }
256
+ })();
257
+ bypassed.set(page, session);
258
+ }
259
+ return session;
260
+ }
261
+ /** Reads a saved file for serving, with its header. */
262
+ export async function loadSavedPage(file) {
263
+ const html = await readFile(file, 'utf8');
264
+ return { html, header: readSavedHeader(html) };
265
+ }
266
+ /** Why a saved page named with `--from-saved` could not be read. */
267
+ export function savedReadMessage(given, err) {
268
+ if (err.code === 'ENOENT')
269
+ return noSuchFileMessage(given);
270
+ if (err.code === 'EISDIR')
271
+ return isDirectoryMessage(given, 'Name a saved .html file.');
272
+ if (err.code === 'EACCES')
273
+ return `The saved page ${given} could not be read: Permission denied`;
274
+ return `The saved page ${given} could not be read: ${err.message}`;
275
+ }
@@ -0,0 +1,17 @@
1
+ /** What the comment says. */
2
+ export interface ScriptMark {
3
+ /** A run's script: the run's id (`D1`). */
4
+ run?: string;
5
+ /** A run's script: when the run started, which tells it from an older run with the same id. */
6
+ started?: string;
7
+ /** The file it was installed from. */
8
+ file?: string;
9
+ }
10
+ /** `text` with `mark` as its last line, in place of any it had. */
11
+ export declare function markedText(text: string, mark: ScriptMark): string;
12
+ /** What the comment in `text` says, or null when it has none. */
13
+ export declare function readMark(text: string): ScriptMark | null;
14
+ /** `text` without the comment, as it was before it was added. */
15
+ export declare function withoutMark(text: string): string;
16
+ /** Whether two texts are the same script, the comment and trailing white space aside. */
17
+ export declare function sameScript(a: string, b: string): boolean;
@@ -0,0 +1,56 @@
1
+ // The comment sm-dev adds as the last line of a script it installs in
2
+ // another manager (Violentmonkey, Tampermonkey), saying where
3
+ // it came from, and for a run's script, which run:
4
+ //
5
+ // // sm-dev: run=D1 started=2026-10-04T12:00:00.000Z file=/home/me/foo.user.js
6
+ //
7
+ // In the script's own text, since that's what every manager keeps as
8
+ // given, so no manager's internals are relied on. Last, so it moves no
9
+ // line numbers. See docs/sm-dev-violentmonkey.md.
10
+ const PREFIX = '// sm-dev: ';
11
+ /** `text` with `mark` as its last line, in place of any it had. */
12
+ export function markedText(text, mark) {
13
+ const body = withoutMark(text);
14
+ // The file last, since a path can hold spaces.
15
+ const fields = [
16
+ ...(mark.run !== undefined ? [`run=${mark.run}`] : []),
17
+ ...(mark.started !== undefined ? [`started=${mark.started}`] : []),
18
+ ...(mark.file !== undefined ? [`file=${mark.file}`] : []),
19
+ ];
20
+ if (fields.length === 0)
21
+ return body;
22
+ return `${body}${body.endsWith('\n') || body === '' ? '' : '\n'}${PREFIX}${fields.join(' ')}\n`;
23
+ }
24
+ // The last line that has anything on it, and where it starts.
25
+ function lastLine(text) {
26
+ const trimmed = text.replace(/\s+$/, '');
27
+ const start = trimmed.lastIndexOf('\n') + 1;
28
+ return { line: trimmed.slice(start), start };
29
+ }
30
+ /** What the comment in `text` says, or null when it has none. */
31
+ export function readMark(text) {
32
+ const { line } = lastLine(text);
33
+ if (!line.startsWith(PREFIX))
34
+ return null;
35
+ const mark = {};
36
+ const rest = line.slice(PREFIX.length);
37
+ const file = /(?:^| )file=(.*)$/.exec(rest);
38
+ if (file)
39
+ mark.file = file[1];
40
+ for (const [, key, value] of (file ? rest.slice(0, file.index) : rest).matchAll(/(\w+)=(\S*)/g)) {
41
+ if (key === 'run')
42
+ mark.run = value;
43
+ else if (key === 'started')
44
+ mark.started = value;
45
+ }
46
+ return mark;
47
+ }
48
+ /** `text` without the comment, as it was before it was added. */
49
+ export function withoutMark(text) {
50
+ const { line, start } = lastLine(text);
51
+ return line.startsWith(PREFIX) ? text.slice(0, start) : text;
52
+ }
53
+ /** Whether two texts are the same script, the comment and trailing white space aside. */
54
+ export function sameScript(a, b) {
55
+ return withoutMark(a).trimEnd() === withoutMark(b).trimEnd();
56
+ }