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,403 @@
1
+ // Per-tab script-status model + badge computation.
2
+ //
3
+ // For each tab we track two views of the world:
4
+ // - `matches` (expected) — what the in-memory match index says should
5
+ // be running on this URL. Computed on navigation by background.ts.
6
+ // - `actual` — what injected wrappers have actually reported via
7
+ // sm:start / sm:error. Driven by chrome.runtime.onUserScriptMessage.
8
+ //
9
+ // Each script in the union of those two sets gets a status here. The
10
+ // toolbar badge's color and tooltip are summed up from the popup's
11
+ // view of them (src/extension/toolbar-summary.ts). Keeping the
12
+ // computation here (pure) makes it easy to unit test without the
13
+ // chrome.* surface.
14
+ import { compareScriptNames } from '../script-display.js';
15
+ // Tampermonkey-ish orange (the "things differ" state).
16
+ export const BADGE_ORANGE = '#f57c00';
17
+ // Green = expected and actual agree, no errors.
18
+ export const BADGE_GREEN = '#2e7d32';
19
+ // Red = at least one script reported an error.
20
+ export const BADGE_RED = '#d32f2f';
21
+ // Purple = a reload would change what runs: a running script changed
22
+ // on disk, or a script was enabled, disabled or removed since the page
23
+ // loaded, or the page loaded before the extension started. Loses to
24
+ // RED (errors still matter most) but wins over ORANGE. Picked bright
25
+ // (≈ Material purple-A400) so the badge reads as a vivid "something
26
+ // changed" signal, not a muted/inactive color.
27
+ export const BADGE_PURPLE = '#d500f9';
28
+ // Grey = the user reloaded this page with no userscripts, so nothing
29
+ // ran on purpose. Loses to every other color: an UNEXPECTED report on a
30
+ // skipped document is still a disagreement worth orange.
31
+ export const BADGE_GREY = '#757575';
32
+ // Which color wins is decided in src/extension/toolbar-summary.ts.
33
+ // The note a tab an sm-dev run controls carries, in the badge tooltip
34
+ // and as the popup's banner.
35
+ export const SM_DEV_CONTROL_LABEL = 'Page under sm-dev control';
36
+ // Material cyan-600: the corner marker drawn on the icon in a tab an
37
+ // sm-dev run controls. Used for nothing else, so it can't be read as a
38
+ // status.
39
+ export const SM_DEV_MARKER_CYAN = '#00acc1';
40
+ // Pure: derive each script's status from a tab's combined expected +
41
+ // actual.
42
+ //
43
+ // Rules:
44
+ // - Per script, status is one of:
45
+ // OK — expected AND actually started (no error reported)
46
+ // ERROR — actual reported `error` (regardless of expected),
47
+ // including a script that got no run decision
48
+ // REJECTED — expected, never started, and Chrome is refusing to
49
+ // register it (so it never will) — see
50
+ // src/extension/script-rejection.ts
51
+ // NOT_LOADED — expected, never started, on a document the user
52
+ // loaded with "Reload page with no userscripts", or
53
+ // one loaded while user scripts were disabled, while
54
+ // they still are
55
+ // UNTRACKED — expected, never reported, on a document that
56
+ // loaded before this copy of the extension started,
57
+ // or while user scripts were disabled
58
+ // MISSING — expected but no `actual` entry yet
59
+ // UNEXPECTED — actual `started` but not in our expected set
60
+ // RELOAD — a script that's started here, but its on-disk
61
+ // content has changed since (would be OK otherwise)
62
+ // - The count is the size of the union of expected ∪ actual IDs,
63
+ // with the scripts running in frames.
64
+ // - A script running in frames takes the worst of its runs' statuses
65
+ // (`STATUS_RANK`); one running only in frames is `framesOnly`.
66
+ //
67
+ // REJECTED is a refinement of MISSING, not a new axis: both mean
68
+ // "expected here, never reported". MISSING is provisional — the wrapper
69
+ // may still be on its way, or the user may have loaded the page before
70
+ // installing the script — so it earns amber. A rejected registration is
71
+ // settled: Chrome refused it, so nothing is coming, and red says so.
72
+ // It reads the same word as the collections page and the viewer.
73
+ //
74
+ // `rejected` is passed in rather than stored on TabMatchState because
75
+ // it's global (one refusal applies to every tab) while the state is
76
+ // per-document. Persisting a copy per tab would need a fan-out to keep
77
+ // them all honest.
78
+ export function computeBadge(state, rejected = new Set(),
79
+ // User scripts are disabled right now. Only asked about a document
80
+ // marked `loadedWithoutUserscripts`.
81
+ userScriptsDisabled = false) {
82
+ const expectedById = new Map();
83
+ for (const m of state.matches)
84
+ expectedById.set(m.id, m);
85
+ const stale = new Set(state.staleScriptIds ?? []);
86
+ const frameRuns = state.frameRuns ?? {};
87
+ const allIds = new Set([
88
+ ...expectedById.keys(),
89
+ ...Object.keys(state.actual),
90
+ ...Object.keys(frameRuns),
91
+ ]);
92
+ const rows = [];
93
+ for (const id of allIds) {
94
+ const expected = expectedById.get(id);
95
+ const actual = state.actual[id];
96
+ // Prefer the actual-side name (most recently reported by the
97
+ // wrapper); fall back to the match-index name; finally the id.
98
+ const runs = Object.values(frameRuns[id] ?? {});
99
+ const name = actual?.name ?? expected?.name ?? runs[0]?.name ?? id;
100
+ if (!expected && !actual) {
101
+ rows.push({ id, name, ...frameStatus(runs), framesOnly: true });
102
+ continue;
103
+ }
104
+ let status;
105
+ let message;
106
+ // A manual script runs its current copy each time it's asked, so
107
+ // it's never stale, and it's expected once it has run.
108
+ const manual = state.manualRuns?.[id] !== undefined;
109
+ if (actual && stale.has(id) && !manual) {
110
+ // Whether the wrapper reported started or errored, that report
111
+ // came from the OLD content. RELOAD/purple is more actionable
112
+ // than ERROR/red here — the on-disk fix may already address the
113
+ // failure, and we'd rather nudge the user to reload than to
114
+ // keep showing an outdated error.
115
+ status = 'RELOAD';
116
+ }
117
+ else if (actual?.state === 'error') {
118
+ status = 'ERROR';
119
+ message = actual.message;
120
+ }
121
+ else if (expected && !actual && skippedOnDocument(state, id)) {
122
+ // Settled too, but by the user: the SW told every wrapper on this
123
+ // document to skip, so "never reported" is the intended outcome.
124
+ // Checked ahead of REJECTED — a refused registration didn't get
125
+ // to matter on a document where nothing was going to run.
126
+ status = 'NOT_LOADED';
127
+ }
128
+ else if (expected && !actual && rejected.has(id)) {
129
+ // Settled, not provisional: Chrome refused the registration, so
130
+ // no wrapper is coming. Red, like an error.
131
+ status = 'REJECTED';
132
+ }
133
+ else if (expected && !actual && state.loadedWithoutUserscripts && userScriptsDisabled) {
134
+ // Kept out, like a skipped load, until user scripts are enabled:
135
+ // a reload won't run it yet.
136
+ status = 'NOT_LOADED';
137
+ }
138
+ else if (expected && !actual && (state.loadedBefore || state.loadedWithoutUserscripts)) {
139
+ // Not provisional like MISSING: no report is coming, since none
140
+ // would have reached this copy of the extension, or nothing was
141
+ // injected.
142
+ status = 'UNTRACKED';
143
+ }
144
+ else if (expected && !actual) {
145
+ status = 'MISSING';
146
+ }
147
+ else if (!expected && actual && !manual) {
148
+ status = 'UNEXPECTED';
149
+ }
150
+ else {
151
+ status = 'OK';
152
+ }
153
+ if (runs.length > 0) {
154
+ const inFrames = frameStatus(runs);
155
+ if (STATUS_RANK[inFrames.status] > STATUS_RANK[status])
156
+ ({ status, message } = inFrames);
157
+ }
158
+ rows.push({ id, name, status, message });
159
+ }
160
+ rows.sort((a, b) => compareScriptNames(a.name, b.name));
161
+ const count = [...allIds].filter((id) => countsOnBadge(state, id)).length;
162
+ return { count, rows };
163
+ }
164
+ // Worst first: a row with runs in several documents shows the worst.
165
+ // Errors outrank everything; a run of outdated code (RELOAD) outranks
166
+ // what a reload would also settle (MISSING, UNEXPECTED).
167
+ const STATUS_RANK = {
168
+ ERROR: 7, REJECTED: 7, RELOAD: 5, MISSING: 4, UNEXPECTED: 3, UNTRACKED: 2, NOT_LOADED: 1, OK: 0,
169
+ };
170
+ // A script's status from its runs in frames: outdated code first, as
171
+ // for the top frame's run, then an error, else running.
172
+ function frameStatus(runs) {
173
+ if (runs.some((r) => r.stale))
174
+ return { status: 'RELOAD' };
175
+ const error = runs.find((r) => r.state === 'error');
176
+ return error ? { status: 'ERROR', message: error.message } : { status: 'OK' };
177
+ }
178
+ /** Whether `id` has reported running in this document or any of its frames. */
179
+ export function runsHere(state, id) {
180
+ return state.actual[id] != null || state.frameRuns?.[id] != null;
181
+ }
182
+ /**
183
+ * Whether the badge counts `id`. In a tab an sm-dev run controls, the
184
+ * count is of the scripts its claim let run: the rest were kept out,
185
+ * not left out by a problem.
186
+ */
187
+ export function countsOnBadge(state, id) {
188
+ if (state.smDevAllow === undefined || state.scriptsSkipped)
189
+ return true;
190
+ return !skippedOnDocument(state, id) || runsHere(state, id);
191
+ }
192
+ /**
193
+ * Whether the worker told `id` to skip this document: a reload with no
194
+ * userscripts, with only another script, or without this one; or a tab
195
+ * an sm-dev run controls that doesn't allow it.
196
+ */
197
+ export function skippedOnDocument(state, id) {
198
+ if (state.scriptsSkipped)
199
+ return true;
200
+ if (state.skipOnly !== undefined && state.skipOnly !== id)
201
+ return true;
202
+ if (state.skipWithout === id)
203
+ return true;
204
+ return state.smDevAllow !== undefined && !(state.smDevAllow?.includes(id) ?? false);
205
+ }
206
+ /** Whether the page is still settling (`settleBy`) at `now`. */
207
+ export function isSettling(state, now) {
208
+ return state.settleBy !== undefined && now < state.settleBy;
209
+ }
210
+ // ---------- Per-tab document history ----------
211
+ //
212
+ // chrome.userScripts injects once per document. bfcache restores
213
+ // reuse the original `documentId`, so to recognise "this is the same
214
+ // page you had loaded a moment ago" we have to remember each tab's
215
+ // recent documents — not just the current one. Otherwise navigating
216
+ // A → B → back to A throws away A's expected/actual state, then
217
+ // onCommitted for A lands with no prior state and we incorrectly
218
+ // reset, painting every still-running script as MISSING.
219
+ //
220
+ // Cap kept small (covers normal back/forward chains) so the session-
221
+ // storage value per tab stays cheap and a runaway tab can't bloat
222
+ // indefinitely. LRU order: oldest first; the current doc is always
223
+ // the rightmost entry.
224
+ export const MAX_DOCS_PER_TAB = 5;
225
+ export function getDocState(record, documentId) {
226
+ if (!record || !documentId)
227
+ return null;
228
+ return record.byDoc[documentId] ?? null;
229
+ }
230
+ export function getCurrentState(record) {
231
+ if (!record)
232
+ return null;
233
+ return record.byDoc[record.currentDocId] ?? null;
234
+ }
235
+ function trimLru(record) {
236
+ if (record.docOrder.length <= MAX_DOCS_PER_TAB)
237
+ return record;
238
+ const docOrder = record.docOrder.slice();
239
+ const byDoc = { ...record.byDoc };
240
+ while (docOrder.length > MAX_DOCS_PER_TAB) {
241
+ const evicted = docOrder.shift();
242
+ // Don't evict the current doc even if it's somehow at the head —
243
+ // promote it back to the most-recent slot.
244
+ if (evicted === record.currentDocId) {
245
+ docOrder.push(evicted);
246
+ continue;
247
+ }
248
+ delete byDoc[evicted];
249
+ }
250
+ return { currentDocId: record.currentDocId, docOrder, byDoc };
251
+ }
252
+ // Pure: write `state` for `documentId` and mark this doc as the one
253
+ // the badge should display. Used by navigation events
254
+ // (onCommitted / onHistoryStateUpdated) and by message-handler
255
+ // bootstrap when there's no prior record at all.
256
+ export function setCurrentDoc(record, documentId, state) {
257
+ const docOrder = (record?.docOrder ?? []).filter((d) => d !== documentId);
258
+ docOrder.push(documentId);
259
+ const byDoc = { ...(record?.byDoc ?? {}), [documentId]: state };
260
+ return trimLru({ currentDocId: documentId, docOrder, byDoc });
261
+ }
262
+ // Pure: write `state` for `documentId` WITHOUT promoting it to current.
263
+ // Used by the sm:* message handler: a wrapper firing from a doc that
264
+ // isn't currently displayed (e.g. teardown of the doc we just
265
+ // navigated away from) should still update that doc's recorded state,
266
+ // but the badge stays on whatever the navigation handler set.
267
+ export function writeDocState(record, documentId, state) {
268
+ if (!record) {
269
+ return { currentDocId: documentId, docOrder: [documentId], byDoc: { [documentId]: state } };
270
+ }
271
+ const docOrder = record.docOrder.filter((d) => d !== documentId);
272
+ docOrder.push(documentId);
273
+ const byDoc = { ...record.byDoc, [documentId]: state };
274
+ return trimLru({ currentDocId: record.currentDocId, docOrder, byDoc });
275
+ }
276
+ // Pure: union two expected-match lists by script id. Used when a SPA
277
+ // navigation within the same document brings in matches for a new URL —
278
+ // we accumulate rather than replace, so a script that matched only the
279
+ // initial URL still surfaces as MISSING if its `sm:start` never came.
280
+ // Sorted by name for stable downstream display.
281
+ export function mergeMatches(existing, incoming) {
282
+ const byId = new Map();
283
+ for (const m of existing)
284
+ byId.set(m.id, m);
285
+ // Incoming wins on name updates (the cache is fresher).
286
+ for (const m of incoming)
287
+ byId.set(m.id, m);
288
+ return [...byId.values()].sort((a, b) => compareScriptNames(a.name, b.name));
289
+ }
290
+ // Pure: flag `scriptId` as stale. No-op when the wrapper hasn't
291
+ // reported anything for this script — the SW listener uses the
292
+ // identity-return to skip storage writes for non-running tabs.
293
+ //
294
+ // Its runs in frames are marked each (`FrameRun.stale`), so a frame
295
+ // that loads again afterwards, with the new code, isn't.
296
+ export function markScriptStale(state, scriptId) {
297
+ let next = state;
298
+ const runs = state.frameRuns?.[scriptId];
299
+ if (runs && Object.values(runs).some((r) => !r.stale)) {
300
+ const marked = Object.fromEntries(Object.entries(runs).map(([doc, r]) => [doc, { ...r, stale: true }]));
301
+ next = { ...next, frameRuns: { ...next.frameRuns, [scriptId]: marked } };
302
+ }
303
+ const existing = next.staleScriptIds ?? [];
304
+ if (!state.actual[scriptId] || existing.includes(scriptId))
305
+ return next;
306
+ return { ...next, staleScriptIds: [...existing, scriptId] };
307
+ }
308
+ // Pure: clear the stale flag for `scriptId`. Used after a sm:swapped
309
+ // report — the wrapper just ran the latest content, so any prior
310
+ // staleness marker for this id no longer reflects reality.
311
+ export function clearScriptStale(state, scriptId) {
312
+ const existing = state.staleScriptIds;
313
+ if (!existing || !existing.includes(scriptId))
314
+ return state;
315
+ const next = existing.filter((id) => id !== scriptId);
316
+ if (next.length === 0) {
317
+ const { staleScriptIds: _drop, ...rest } = state;
318
+ return rest;
319
+ }
320
+ return { ...state, staleScriptIds: next };
321
+ }
322
+ // Pure: note that `scriptId` logged a warning from this document.
323
+ // Identity-returns when it already had one, so the caller can skip a
324
+ // storage write.
325
+ export function noteScriptWarning(state, scriptId) {
326
+ const existing = state.warnedIds ?? [];
327
+ if (existing.includes(scriptId))
328
+ return state;
329
+ return { ...state, warnedIds: [...existing, scriptId] };
330
+ }
331
+ // Pure: merge an incoming wrapper report into the tab's `actual` map.
332
+ // Returns a new state object (callers re-write storage with it).
333
+ //
334
+ // Guard: an existing `error` entry is sticky for the same id — a later
335
+ // sm:start (e.g. on retry / re-evaluation) shouldn't silently flip it
336
+ // back to OK and hide the failure.
337
+ //
338
+ // Except for a manual script's `sm:start` (`manual`): each one is a new
339
+ // run the person asked for, so it replaces the last run's outcome.
340
+ export function applyScriptReport(state, id, report, manual = false) {
341
+ if (!manual && report.state === 'started' && state.actual[id]?.state === 'error') {
342
+ return state;
343
+ }
344
+ return {
345
+ ...state,
346
+ actual: { ...state.actual, [id]: report },
347
+ };
348
+ }
349
+ // Pure: count a request to run manual script `id` on this document.
350
+ // Recorded before the run is injected, so its reports find it there.
351
+ export function noteManualRun(state, id) {
352
+ const runs = state.manualRuns ?? {};
353
+ return { ...state, manualRuns: { ...runs, [id]: (runs[id] ?? 0) + 1 } };
354
+ }
355
+ // Pure: note that a wrapper in `world` reported from this document, and
356
+ // whether it saw an opaque origin. Identity-returns when nothing is new,
357
+ // so the caller can skip a storage write.
358
+ export function notePageReport(state, world, opaqueOrigin) {
359
+ const worlds = state.reportedWorlds ?? [];
360
+ const newWorld = !worlds.includes(world);
361
+ const newOpaque = opaqueOrigin && !state.opaqueOrigin;
362
+ if (!newWorld && !newOpaque)
363
+ return state;
364
+ return {
365
+ ...state,
366
+ ...(newWorld ? { reportedWorlds: [...worlds, world] } : {}),
367
+ ...(newOpaque ? { opaqueOrigin: true } : {}),
368
+ };
369
+ }
370
+ // Pure: merge a report from a script in one of the page's iframes, the
371
+ // same way `applyScriptReport` does for the top frame: an error stays
372
+ // for that frame's document.
373
+ export function applyFrameReport(state, id, frameDocId, run) {
374
+ const runs = state.frameRuns?.[id] ?? {};
375
+ if (run.state === 'started' && runs[frameDocId]?.state === 'error')
376
+ return state;
377
+ return {
378
+ ...state,
379
+ frameRuns: { ...state.frameRuns, [id]: { ...runs, [frameDocId]: run } },
380
+ };
381
+ }
382
+ // Pure: drop the frame runs whose document isn't in `liveDocIds` (the
383
+ // tab's frames now): the frame navigated or was removed. Identity-
384
+ // returns when nothing went, so the caller can skip a storage write.
385
+ export function pruneFrameRuns(state, liveDocIds) {
386
+ if (!state.frameRuns)
387
+ return state;
388
+ let changed = false;
389
+ const kept = {};
390
+ for (const [id, runs] of Object.entries(state.frameRuns)) {
391
+ const live = Object.entries(runs).filter(([docId]) => liveDocIds.has(docId));
392
+ if (live.length !== Object.keys(runs).length)
393
+ changed = true;
394
+ if (live.length > 0)
395
+ kept[id] = Object.fromEntries(live);
396
+ }
397
+ if (!changed)
398
+ return state;
399
+ if (Object.keys(kept).length > 0)
400
+ return { ...state, frameRuns: kept };
401
+ const { frameRuns: _drop, ...rest } = state;
402
+ return rest;
403
+ }
@@ -0,0 +1,105 @@
1
+ // Pure: the toolbar icon's badge (count and color) and its hover
2
+ // tooltip, summed up from the popup's view of the tab, so the icon and
3
+ // the popup can't disagree about a script's status. Painted by
4
+ // `renderBadge` in background.ts. Design: docs/matching.md#tooltip.
5
+ import { POPUP_NO_SCRIPTS, POPUP_NOT_INJECTABLE, USER_SCRIPTS_OFF_HEADING, } from './popup-view.js';
6
+ import { BADGE_GREEN, BADGE_GREY, BADGE_ORANGE, BADGE_PURPLE, BADGE_RED, SM_DEV_CONTROL_LABEL, countsOnBadge, runsHere, } from './tab-status.js';
7
+ export const TOOLBAR_CLICK_HINT = 'Click for details. Double-click for collections.';
8
+ const noun = (n) => (n === 1 ? 'script' : 'scripts');
9
+ const scripts = (n) => `${n} ${noun(n)}`;
10
+ const has = (n) => (n === 1 ? 'has' : 'have');
11
+ const is = (n) => (n === 1 ? 'is' : 'are');
12
+ export function summarizeToolbar(view, state) {
13
+ // An invalid script that isn't enabled is listed for reference only:
14
+ // it wasn't going to run, so it's not this page's problem.
15
+ const rows = [...view.enabled, ...view.frames, ...view.manual, ...view.disabled]
16
+ .flatMap((g) => g.rows)
17
+ .filter((r) => r.status !== 'INVALID' || r.active);
18
+ const count = (pred) => rows.filter(pred).length;
19
+ const withStatus = (...statuses) => count((r) => r.status !== null && statuses.includes(r.status));
20
+ const running = count((r) => state != null && runsHere(state, r.id));
21
+ const errors = withStatus('ERROR');
22
+ const rejected = withStatus('REJECTED');
23
+ const invalid = withStatus('INVALID');
24
+ // What a reload would change, each its own line: updated on disk,
25
+ // enabled since the page loaded (or matching only the URL it moved
26
+ // to), disabled or removed since but still running, or not known
27
+ // because the page loaded before the extension started.
28
+ const updated = withStatus('RELOAD');
29
+ const added = withStatus('PENDING');
30
+ const removed = withStatus('STILL_RUNNING');
31
+ const untracked = withStatus('UNTRACKED');
32
+ const missing = withStatus('MISSING');
33
+ const unexpected = withStatus('UNEXPECTED');
34
+ const warned = count((r) => r.warned);
35
+ const skipped = withStatus('NOT_LOADED');
36
+ // Manual scripts not run here yet are the normal state, and can be
37
+ // many (an all-pages bookmarklet), so they get no line and no badge;
38
+ // one counts once it has run. They still keep "No scripts match"
39
+ // away, which would be untrue.
40
+ const anyStatus = rows.some((r) => r.status !== null);
41
+ const anyBadged = rows.some((r) => r.status !== null && r.status !== 'NOT_RUN');
42
+ const lines = [];
43
+ if (!view.injectable)
44
+ lines.push(POPUP_NOT_INJECTABLE);
45
+ // Nothing on the page can run until they're enabled, so that is the
46
+ // whole story: no "skipped" line, and no reload to suggest.
47
+ else if (view.userScriptsDisabled)
48
+ lines.push(USER_SCRIPTS_OFF_HEADING);
49
+ else if (!anyStatus)
50
+ lines.push(POPUP_NO_SCRIPTS);
51
+ else {
52
+ if (running > 0)
53
+ lines.push(`${scripts(running)} running.`);
54
+ if (errors > 0)
55
+ lines.push(`${scripts(errors)} ${has(errors)} errors.`);
56
+ if (rejected > 0)
57
+ lines.push(`Chrome rejected ${scripts(rejected)}.`);
58
+ if (invalid > 0)
59
+ lines.push(`${scripts(invalid)} failed to load.`);
60
+ if (updated > 0)
61
+ lines.push(`Reload needed to update ${scripts(updated)}.`);
62
+ if (added > 0)
63
+ lines.push(`Reload needed to add ${scripts(added)}.`);
64
+ if (removed > 0)
65
+ lines.push(`Reload needed to remove ${scripts(removed)}.`);
66
+ if (untracked > 0) {
67
+ const why = state?.loadedWithoutUserscripts && !state.loadedBefore
68
+ ? 'Page loaded while user scripts were disabled'
69
+ : 'Page loaded before SourceMonkey started';
70
+ lines.push(`${why}. Reload needed for ${scripts(untracked)}.`);
71
+ }
72
+ if (missing > 0)
73
+ lines.push(`${missing} expected ${noun(missing)} ${is(missing)} not running.`);
74
+ if (unexpected > 0)
75
+ lines.push(`${unexpected} unexpected ${noun(unexpected)} ${is(unexpected)} running.`);
76
+ if (warned > 0)
77
+ lines.push(`${scripts(warned)} ${has(warned)} warnings.`);
78
+ if (skipped > 0)
79
+ lines.push(`${scripts(skipped)} skipped on this load.`);
80
+ }
81
+ // Red, purple, orange or grey, the first that applies, else green.
82
+ // Warnings have none: a header warning would otherwise keep the badge
83
+ // off green for good.
84
+ let color = null;
85
+ if (anyBadged) {
86
+ if (errors + rejected + invalid > 0 || view.userScriptsDisabled)
87
+ color = BADGE_RED;
88
+ else if (updated + added + removed + untracked > 0)
89
+ color = BADGE_PURPLE;
90
+ else if (missing + unexpected > 0)
91
+ color = BADGE_ORANGE;
92
+ // Grey says nothing ran: not for a tab where some scripts were kept
93
+ // out (an sm-dev claim) and the rest ran.
94
+ else if (skipped > 0 && running === 0)
95
+ color = BADGE_GREY;
96
+ else
97
+ color = BADGE_GREEN;
98
+ }
99
+ const head = view.smDev ? ['SourceMonkey', SM_DEV_CONTROL_LABEL] : ['SourceMonkey'];
100
+ // The rows the popup gives a status, so the count and the lines
101
+ // agree: a script disabled or deleted since load that never started
102
+ // has none, and one enabled since that hasn't run yet does.
103
+ const counted = count((r) => r.status !== null && r.status !== 'NOT_RUN' && (state === null || countsOnBadge(state, r.id)));
104
+ return { count: counted, color, title: [...head, ...lines, TOOLBAR_CLICK_HINT].join('\n') };
105
+ }
@@ -0,0 +1,43 @@
1
+ // The "Allow User Scripts" probe and the text that explains it being
2
+ // off, shared by the collections page's banner and the popup's bar.
3
+ //
4
+ // The toggle lives on `chrome://extensions`. While it's off nothing
5
+ // injects, though matching still works (the badge counts scripts).
6
+ // Pages ask here, since Chrome tells them nothing when it changes:
7
+ // turning it on or off changes `chrome.userScripts` in open pages at
8
+ // once, with no event.
9
+ import { extensionDetailsLink } from './file-access.js';
10
+ import { USER_SCRIPTS_OFF_HEADING } from './popup-view.js';
11
+ export { USER_SCRIPTS_OFF_HEADING };
12
+ // Whether chrome.userScripts is callable. The namespace's presence only
13
+ // confirms the permission is in the manifest; the toggle still has to be
14
+ // on before a call works.
15
+ //
16
+ // Returns the raw error message so a page can show anything that isn't
17
+ // the routine "toggle is off" case. Asks for no scripts by id, so the
18
+ // call returns none: the popup probes on every redraw.
19
+ export async function probeUserScriptsApi() {
20
+ // Deliberately NOT recased to a message of ours. This value shares a
21
+ // slot with Chrome's verbatim `err.message` below and renders as
22
+ // `Chrome reported: <it>`, so it is written to read as Chrome's own
23
+ // report rather than as SourceMonkey's sentence — the API-flavoured
24
+ // wording is the point. See docs/diagnostics.md#wording.
25
+ if (!chrome.userScripts)
26
+ return { usable: false, error: 'chrome.userScripts is not available' };
27
+ try {
28
+ await chrome.userScripts.getScripts({ ids: [] });
29
+ return { usable: true };
30
+ }
31
+ catch (err) {
32
+ return { usable: false, error: err instanceof Error ? err.message : String(err) };
33
+ }
34
+ }
35
+ // The fix, as a line of the page's own: "Open extension settings and
36
+ // enable "Allow User Scripts" for SourceMonkey."
37
+ export function userScriptsFixLine(tag) {
38
+ const line = document.createElement(tag);
39
+ const toggle = document.createElement('code');
40
+ toggle.textContent = 'Allow User Scripts';
41
+ line.append('Open ', extensionDetailsLink('extension settings'), ' and enable "', toggle, '" for SourceMonkey.');
42
+ return line;
43
+ }