@drscrewdriver/dsh-better-display 0.3.3-fork.1

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 (390) hide show
  1. package/CHANGELOG.md +259 -0
  2. package/DESIGN.md +72 -0
  3. package/LICENSE +24 -0
  4. package/README.en.md +55 -0
  5. package/README.md +55 -0
  6. package/THIRD_PARTY_NOTICES.md +65 -0
  7. package/compat/harness-rc2.json +99 -0
  8. package/cordis.patch.yml +5 -0
  9. package/cordis.yml +4 -0
  10. package/docs/official-rendering-bridge.md +65 -0
  11. package/dshx.yml +5 -0
  12. package/lib/client.js +49292 -0
  13. package/lib/client.js.map +1 -0
  14. package/lib/dsh-better-display.js +101 -0
  15. package/lib/types/client/Blocks.d.ts +49 -0
  16. package/lib/types/client/Blocks.d.ts.map +1 -0
  17. package/lib/types/client/Blocks.js +143 -0
  18. package/lib/types/client/Blocks.js.map +1 -0
  19. package/lib/types/client/ChoreographedFlow.d.ts +22 -0
  20. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -0
  21. package/lib/types/client/ChoreographedFlow.js +218 -0
  22. package/lib/types/client/ChoreographedFlow.js.map +1 -0
  23. package/lib/types/client/ClosedProcessSummary.d.ts +9 -0
  24. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -0
  25. package/lib/types/client/ClosedProcessSummary.js +16 -0
  26. package/lib/types/client/ClosedProcessSummary.js.map +1 -0
  27. package/lib/types/client/DiffPanel.d.ts +13 -0
  28. package/lib/types/client/DiffPanel.d.ts.map +1 -0
  29. package/lib/types/client/DiffPanel.js +115 -0
  30. package/lib/types/client/DiffPanel.js.map +1 -0
  31. package/lib/types/client/LiveFold.d.ts +10 -0
  32. package/lib/types/client/LiveFold.d.ts.map +1 -0
  33. package/lib/types/client/LiveFold.js +39 -0
  34. package/lib/types/client/LiveFold.js.map +1 -0
  35. package/lib/types/client/McpAppFrame.d.ts +32 -0
  36. package/lib/types/client/McpAppFrame.d.ts.map +1 -0
  37. package/lib/types/client/McpAppFrame.js +193 -0
  38. package/lib/types/client/McpAppFrame.js.map +1 -0
  39. package/lib/types/client/OfficialContent.d.ts +25 -0
  40. package/lib/types/client/OfficialContent.d.ts.map +1 -0
  41. package/lib/types/client/OfficialContent.js +53 -0
  42. package/lib/types/client/OfficialContent.js.map +1 -0
  43. package/lib/types/client/Reader.d.ts +3 -0
  44. package/lib/types/client/Reader.d.ts.map +1 -0
  45. package/lib/types/client/Reader.js +603 -0
  46. package/lib/types/client/Reader.js.map +1 -0
  47. package/lib/types/client/ReasoningCard.d.ts +11 -0
  48. package/lib/types/client/ReasoningCard.d.ts.map +1 -0
  49. package/lib/types/client/ReasoningCard.js +322 -0
  50. package/lib/types/client/ReasoningCard.js.map +1 -0
  51. package/lib/types/client/SettingsSection.d.ts +37 -0
  52. package/lib/types/client/SettingsSection.d.ts.map +1 -0
  53. package/lib/types/client/SettingsSection.js +63 -0
  54. package/lib/types/client/SettingsSection.js.map +1 -0
  55. package/lib/types/client/StickyLane.d.ts +8 -0
  56. package/lib/types/client/StickyLane.d.ts.map +1 -0
  57. package/lib/types/client/StickyLane.js +39 -0
  58. package/lib/types/client/StickyLane.js.map +1 -0
  59. package/lib/types/client/TimelineRail.d.ts +10 -0
  60. package/lib/types/client/TimelineRail.d.ts.map +1 -0
  61. package/lib/types/client/TimelineRail.js +168 -0
  62. package/lib/types/client/TimelineRail.js.map +1 -0
  63. package/lib/types/client/ToolActivity.d.ts +17 -0
  64. package/lib/types/client/ToolActivity.d.ts.map +1 -0
  65. package/lib/types/client/ToolActivity.js +244 -0
  66. package/lib/types/client/ToolActivity.js.map +1 -0
  67. package/lib/types/client/TurnMetrics.d.ts +11 -0
  68. package/lib/types/client/TurnMetrics.d.ts.map +1 -0
  69. package/lib/types/client/TurnMetrics.js +44 -0
  70. package/lib/types/client/TurnMetrics.js.map +1 -0
  71. package/lib/types/client/WaitClock.d.ts +16 -0
  72. package/lib/types/client/WaitClock.d.ts.map +1 -0
  73. package/lib/types/client/WaitClock.js +29 -0
  74. package/lib/types/client/WaitClock.js.map +1 -0
  75. package/lib/types/client/WaitingClock.d.ts +15 -0
  76. package/lib/types/client/WaitingClock.d.ts.map +1 -0
  77. package/lib/types/client/WaitingClock.js +30 -0
  78. package/lib/types/client/WaitingClock.js.map +1 -0
  79. package/lib/types/client/WaitingStatus.d.ts +6 -0
  80. package/lib/types/client/WaitingStatus.d.ts.map +1 -0
  81. package/lib/types/client/WaitingStatus.js +9 -0
  82. package/lib/types/client/WaitingStatus.js.map +1 -0
  83. package/lib/types/client/conversation-scroll.d.ts +19 -0
  84. package/lib/types/client/conversation-scroll.d.ts.map +1 -0
  85. package/lib/types/client/conversation-scroll.js +25 -0
  86. package/lib/types/client/conversation-scroll.js.map +1 -0
  87. package/lib/types/client/deliverables.d.ts +24 -0
  88. package/lib/types/client/deliverables.d.ts.map +1 -0
  89. package/lib/types/client/deliverables.js +104 -0
  90. package/lib/types/client/deliverables.js.map +1 -0
  91. package/lib/types/client/entry-policy.d.ts +10 -0
  92. package/lib/types/client/entry-policy.d.ts.map +1 -0
  93. package/lib/types/client/entry-policy.js +24 -0
  94. package/lib/types/client/entry-policy.js.map +1 -0
  95. package/lib/types/client/entry.d.ts +4 -0
  96. package/lib/types/client/entry.d.ts.map +1 -0
  97. package/lib/types/client/entry.js +34 -0
  98. package/lib/types/client/entry.js.map +1 -0
  99. package/lib/types/client/fold-choreography.d.ts +40 -0
  100. package/lib/types/client/fold-choreography.d.ts.map +1 -0
  101. package/lib/types/client/fold-choreography.js +48 -0
  102. package/lib/types/client/fold-choreography.js.map +1 -0
  103. package/lib/types/client/fold-intensity.d.ts +11 -0
  104. package/lib/types/client/fold-intensity.d.ts.map +1 -0
  105. package/lib/types/client/fold-intensity.js +32 -0
  106. package/lib/types/client/fold-intensity.js.map +1 -0
  107. package/lib/types/client/index.d.ts +7 -0
  108. package/lib/types/client/index.d.ts.map +1 -0
  109. package/lib/types/client/index.js +195 -0
  110. package/lib/types/client/index.js.map +1 -0
  111. package/lib/types/client/live-turn.d.ts +58 -0
  112. package/lib/types/client/live-turn.d.ts.map +1 -0
  113. package/lib/types/client/live-turn.js +190 -0
  114. package/lib/types/client/live-turn.js.map +1 -0
  115. package/lib/types/client/markdown/MarkdownText.d.ts +41 -0
  116. package/lib/types/client/markdown/MarkdownText.d.ts.map +1 -0
  117. package/lib/types/client/markdown/MarkdownText.js +178 -0
  118. package/lib/types/client/markdown/MarkdownText.js.map +1 -0
  119. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts +9 -0
  120. package/lib/types/client/markdown/cjkFriendlyStrong.d.ts.map +1 -0
  121. package/lib/types/client/markdown/cjkFriendlyStrong.js +73 -0
  122. package/lib/types/client/markdown/cjkFriendlyStrong.js.map +1 -0
  123. package/lib/types/client/markdown/incremental.d.ts +69 -0
  124. package/lib/types/client/markdown/incremental.d.ts.map +1 -0
  125. package/lib/types/client/markdown/incremental.js +108 -0
  126. package/lib/types/client/markdown/incremental.js.map +1 -0
  127. package/lib/types/client/markdown/katex.d.ts +27 -0
  128. package/lib/types/client/markdown/katex.d.ts.map +1 -0
  129. package/lib/types/client/markdown/katex.js +88 -0
  130. package/lib/types/client/markdown/katex.js.map +1 -0
  131. package/lib/types/client/markdown/mathCompatibility.d.ts +11 -0
  132. package/lib/types/client/markdown/mathCompatibility.d.ts.map +1 -0
  133. package/lib/types/client/markdown/mathCompatibility.js +293 -0
  134. package/lib/types/client/markdown/mathCompatibility.js.map +1 -0
  135. package/lib/types/client/markdown/parse.d.ts +25 -0
  136. package/lib/types/client/markdown/parse.d.ts.map +1 -0
  137. package/lib/types/client/markdown/parse.js +42 -0
  138. package/lib/types/client/markdown/parse.js.map +1 -0
  139. package/lib/types/client/markdown/render.d.ts +121 -0
  140. package/lib/types/client/markdown/render.d.ts.map +1 -0
  141. package/lib/types/client/markdown/render.js +452 -0
  142. package/lib/types/client/markdown/render.js.map +1 -0
  143. package/lib/types/client/mcp-app.d.ts +50 -0
  144. package/lib/types/client/mcp-app.d.ts.map +1 -0
  145. package/lib/types/client/mcp-app.js +328 -0
  146. package/lib/types/client/mcp-app.js.map +1 -0
  147. package/lib/types/client/message-chrome.d.ts +19 -0
  148. package/lib/types/client/message-chrome.d.ts.map +1 -0
  149. package/lib/types/client/message-chrome.js +42 -0
  150. package/lib/types/client/message-chrome.js.map +1 -0
  151. package/lib/types/client/motion.d.ts +38 -0
  152. package/lib/types/client/motion.d.ts.map +1 -0
  153. package/lib/types/client/motion.js +453 -0
  154. package/lib/types/client/motion.js.map +1 -0
  155. package/lib/types/client/native/ContextBody.d.ts +130 -0
  156. package/lib/types/client/native/ContextBody.d.ts.map +1 -0
  157. package/lib/types/client/native/ContextBody.js +398 -0
  158. package/lib/types/client/native/ContextBody.js.map +1 -0
  159. package/lib/types/client/native/ContextInjectionRow.d.ts +35 -0
  160. package/lib/types/client/native/ContextInjectionRow.d.ts.map +1 -0
  161. package/lib/types/client/native/ContextInjectionRow.js +42 -0
  162. package/lib/types/client/native/ContextInjectionRow.js.map +1 -0
  163. package/lib/types/client/native/ReferenceIcon.d.ts +16 -0
  164. package/lib/types/client/native/ReferenceIcon.d.ts.map +1 -0
  165. package/lib/types/client/native/ReferenceIcon.js +16 -0
  166. package/lib/types/client/native/ReferenceIcon.js.map +1 -0
  167. package/lib/types/client/native/tool-call-model.d.ts +58 -0
  168. package/lib/types/client/native/tool-call-model.d.ts.map +1 -0
  169. package/lib/types/client/native/tool-call-model.js +219 -0
  170. package/lib/types/client/native/tool-call-model.js.map +1 -0
  171. package/lib/types/client/native/tool-update.d.ts +23 -0
  172. package/lib/types/client/native/tool-update.d.ts.map +1 -0
  173. package/lib/types/client/native/tool-update.js +22 -0
  174. package/lib/types/client/native/tool-update.js.map +1 -0
  175. package/lib/types/client/official-slots.d.ts +71 -0
  176. package/lib/types/client/official-slots.d.ts.map +1 -0
  177. package/lib/types/client/official-slots.js +183 -0
  178. package/lib/types/client/official-slots.js.map +1 -0
  179. package/lib/types/client/open-file.d.ts +29 -0
  180. package/lib/types/client/open-file.d.ts.map +1 -0
  181. package/lib/types/client/open-file.js +71 -0
  182. package/lib/types/client/open-file.js.map +1 -0
  183. package/lib/types/client/pending-submission.d.ts +28 -0
  184. package/lib/types/client/pending-submission.d.ts.map +1 -0
  185. package/lib/types/client/pending-submission.js +28 -0
  186. package/lib/types/client/pending-submission.js.map +1 -0
  187. package/lib/types/client/platform-media.d.ts +6 -0
  188. package/lib/types/client/platform-media.d.ts.map +1 -0
  189. package/lib/types/client/platform-media.js +18 -0
  190. package/lib/types/client/platform-media.js.map +1 -0
  191. package/lib/types/client/primitive-labels.d.ts +10 -0
  192. package/lib/types/client/primitive-labels.d.ts.map +1 -0
  193. package/lib/types/client/primitive-labels.js +38 -0
  194. package/lib/types/client/primitive-labels.js.map +1 -0
  195. package/lib/types/client/projection.d.ts +41 -0
  196. package/lib/types/client/projection.d.ts.map +1 -0
  197. package/lib/types/client/projection.js +112 -0
  198. package/lib/types/client/projection.js.map +1 -0
  199. package/lib/types/client/reasoning-follow.d.ts +10 -0
  200. package/lib/types/client/reasoning-follow.d.ts.map +1 -0
  201. package/lib/types/client/reasoning-follow.js +21 -0
  202. package/lib/types/client/reasoning-follow.js.map +1 -0
  203. package/lib/types/client/resource-address.d.ts +3 -0
  204. package/lib/types/client/resource-address.d.ts.map +1 -0
  205. package/lib/types/client/resource-address.js +33 -0
  206. package/lib/types/client/resource-address.js.map +1 -0
  207. package/lib/types/client/settings-copy.d.ts +7 -0
  208. package/lib/types/client/settings-copy.d.ts.map +1 -0
  209. package/lib/types/client/settings-copy.js +53 -0
  210. package/lib/types/client/settings-copy.js.map +1 -0
  211. package/lib/types/client/settings-slots.d.ts +23 -0
  212. package/lib/types/client/settings-slots.d.ts.map +1 -0
  213. package/lib/types/client/settings-slots.js +2 -0
  214. package/lib/types/client/settings-slots.js.map +1 -0
  215. package/lib/types/client/settings.d.ts +4 -0
  216. package/lib/types/client/settings.d.ts.map +1 -0
  217. package/lib/types/client/settings.js +60 -0
  218. package/lib/types/client/settings.js.map +1 -0
  219. package/lib/types/client/skill-status.d.ts +25 -0
  220. package/lib/types/client/skill-status.d.ts.map +1 -0
  221. package/lib/types/client/skill-status.js +44 -0
  222. package/lib/types/client/skill-status.js.map +1 -0
  223. package/lib/types/client/store.d.ts +35 -0
  224. package/lib/types/client/store.d.ts.map +1 -0
  225. package/lib/types/client/store.js +37 -0
  226. package/lib/types/client/store.js.map +1 -0
  227. package/lib/types/client/stream-buffer.d.ts +37 -0
  228. package/lib/types/client/stream-buffer.d.ts.map +1 -0
  229. package/lib/types/client/stream-buffer.js +150 -0
  230. package/lib/types/client/stream-buffer.js.map +1 -0
  231. package/lib/types/client/streaming.d.ts +18 -0
  232. package/lib/types/client/streaming.d.ts.map +1 -0
  233. package/lib/types/client/streaming.js +84 -0
  234. package/lib/types/client/streaming.js.map +1 -0
  235. package/lib/types/client/timeline.d.ts +26 -0
  236. package/lib/types/client/timeline.d.ts.map +1 -0
  237. package/lib/types/client/timeline.js +48 -0
  238. package/lib/types/client/timeline.js.map +1 -0
  239. package/lib/types/client/tool-activity.d.ts +98 -0
  240. package/lib/types/client/tool-activity.d.ts.map +1 -0
  241. package/lib/types/client/tool-activity.js +301 -0
  242. package/lib/types/client/tool-activity.js.map +1 -0
  243. package/lib/types/client/types.d.ts +91 -0
  244. package/lib/types/client/types.d.ts.map +1 -0
  245. package/lib/types/client/types.js +2 -0
  246. package/lib/types/client/types.js.map +1 -0
  247. package/lib/types/client/waiting-clock.d.ts +49 -0
  248. package/lib/types/client/waiting-clock.d.ts.map +1 -0
  249. package/lib/types/client/waiting-clock.js +80 -0
  250. package/lib/types/client/waiting-clock.js.map +1 -0
  251. package/lib/types/client/word-motion.d.ts +16 -0
  252. package/lib/types/client/word-motion.d.ts.map +1 -0
  253. package/lib/types/client/word-motion.js +90 -0
  254. package/lib/types/client/word-motion.js.map +1 -0
  255. package/lib/types/client/word-timeline.d.ts +32 -0
  256. package/lib/types/client/word-timeline.d.ts.map +1 -0
  257. package/lib/types/client/word-timeline.js +119 -0
  258. package/lib/types/client/word-timeline.js.map +1 -0
  259. package/lib/types/dsh-better-display.d.ts +17 -0
  260. package/lib/types/dsh-better-display.d.ts.map +1 -0
  261. package/lib/types/dsh-better-display.js +93 -0
  262. package/lib/types/dsh-better-display.js.map +1 -0
  263. package/lib/types/skill-roots.d.ts +22 -0
  264. package/lib/types/skill-roots.d.ts.map +1 -0
  265. package/lib/types/skill-roots.js +126 -0
  266. package/lib/types/skill-roots.js.map +1 -0
  267. package/lib/types/skill-status.d.ts +36 -0
  268. package/lib/types/skill-status.d.ts.map +1 -0
  269. package/lib/types/skill-status.js +51 -0
  270. package/lib/types/skill-status.js.map +1 -0
  271. package/package.json +133 -0
  272. package/scripts/build-motion-fixture.mjs +26 -0
  273. package/scripts/check-harness-compat.mjs +71 -0
  274. package/scripts/link-harness-dependencies.mjs +54 -0
  275. package/scripts/verify-auto-fold.mjs +114 -0
  276. package/scripts/verify-official-bridge.mjs +121 -0
  277. package/scripts/verify-tool-shimmer.mjs +62 -0
  278. package/skills/generative-mcpapps/SKILL.md +47 -0
  279. package/skills/generative-mcpapps/examples/interactive_quiz.html +153 -0
  280. package/skills/generative-mcpapps/references/html_boilerplate.md +122 -0
  281. package/skills/generative-mcpapps/references/sep1865_protocol.md +54 -0
  282. package/src/client/Blocks.tsx +213 -0
  283. package/src/client/ChoreographedFlow.tsx +232 -0
  284. package/src/client/ClosedProcessSummary.tsx +23 -0
  285. package/src/client/DiffPanel.tsx +124 -0
  286. package/src/client/LiveFold.tsx +43 -0
  287. package/src/client/McpAppFrame.module.css +210 -0
  288. package/src/client/McpAppFrame.tsx +317 -0
  289. package/src/client/OfficialContent.tsx +69 -0
  290. package/src/client/Reader.module.css +692 -0
  291. package/src/client/Reader.tsx +878 -0
  292. package/src/client/ReasoningCard.tsx +306 -0
  293. package/src/client/SettingsSection.module.css +184 -0
  294. package/src/client/SettingsSection.tsx +194 -0
  295. package/src/client/StickyLane.tsx +34 -0
  296. package/src/client/TimelineRail.module.css +170 -0
  297. package/src/client/TimelineRail.tsx +262 -0
  298. package/src/client/ToolActivity.tsx +274 -0
  299. package/src/client/TurnMetrics.module.css +139 -0
  300. package/src/client/TurnMetrics.tsx +160 -0
  301. package/src/client/WaitClock.tsx +33 -0
  302. package/src/client/WaitingStatus.tsx +13 -0
  303. package/src/client/conversation-scroll.ts +28 -0
  304. package/src/client/deliverables.ts +122 -0
  305. package/src/client/entry-policy.ts +20 -0
  306. package/src/client/entry.tsx +40 -0
  307. package/src/client/fold-choreography.ts +55 -0
  308. package/src/client/fold-intensity.ts +36 -0
  309. package/src/client/index.tsx +222 -0
  310. package/src/client/live-turn.ts +198 -0
  311. package/src/client/markdown/MarkdownText.module.css +304 -0
  312. package/src/client/markdown/MarkdownText.tsx +195 -0
  313. package/src/client/markdown/cjkFriendlyStrong.ts +84 -0
  314. package/src/client/markdown/incremental.ts +131 -0
  315. package/src/client/markdown/katex.tsx +91 -0
  316. package/src/client/markdown/mathCompatibility.ts +350 -0
  317. package/src/client/markdown/parse.ts +45 -0
  318. package/src/client/markdown/render.tsx +647 -0
  319. package/src/client/markdown/upstream.json +14 -0
  320. package/src/client/mcp-app.ts +323 -0
  321. package/src/client/message-chrome.ts +47 -0
  322. package/src/client/motion.tsx +410 -0
  323. package/src/client/native/ContextBody.module.css +161 -0
  324. package/src/client/native/ContextBody.tsx +591 -0
  325. package/src/client/native/ContextInjectionRow.module.css +66 -0
  326. package/src/client/native/ContextInjectionRow.tsx +106 -0
  327. package/src/client/native/ReferenceIcon.tsx +35 -0
  328. package/src/client/native/tool-call-model.ts +248 -0
  329. package/src/client/native/tool-update.ts +32 -0
  330. package/src/client/native/upstream.json +41 -0
  331. package/src/client/official-slots.tsx +211 -0
  332. package/src/client/open-file.ts +94 -0
  333. package/src/client/pending-submission.ts +49 -0
  334. package/src/client/platform-media.ts +17 -0
  335. package/src/client/primitive-labels.ts +54 -0
  336. package/src/client/projection.ts +118 -0
  337. package/src/client/reasoning-follow.ts +28 -0
  338. package/src/client/settings-copy.ts +80 -0
  339. package/src/client/settings-slots.ts +20 -0
  340. package/src/client/settings.tsx +73 -0
  341. package/src/client/skill-status.ts +78 -0
  342. package/src/client/store.ts +75 -0
  343. package/src/client/stream-buffer.ts +136 -0
  344. package/src/client/streaming.tsx +70 -0
  345. package/src/client/style.d.ts +4 -0
  346. package/src/client/timeline.ts +77 -0
  347. package/src/client/tool-activity.ts +298 -0
  348. package/src/client/types.ts +89 -0
  349. package/src/client/waiting-clock.ts +83 -0
  350. package/src/client/word-motion.tsx +92 -0
  351. package/src/client/word-timeline.ts +113 -0
  352. package/src/dsh-better-display.ts +108 -0
  353. package/src/skill-roots.ts +138 -0
  354. package/src/skill-status.ts +81 -0
  355. package/tests/browser-auto-fold.tsx +64 -0
  356. package/tests/browser-closed-gap.tsx +20 -0
  357. package/tests/browser-motion.tsx +65 -0
  358. package/tests/browser-official-bridge.tsx +152 -0
  359. package/tests/browser-sticky-lanes.tsx +22 -0
  360. package/tests/browser-waiting-clock.tsx +14 -0
  361. package/tests/chat-flow-hook.test.ts +66 -0
  362. package/tests/cloud-gate.test.ts +34 -0
  363. package/tests/context-tool-update.test.ts +33 -0
  364. package/tests/conversation-scroll.test.ts +47 -0
  365. package/tests/deliverables.test.ts +128 -0
  366. package/tests/entry.test.ts +42 -0
  367. package/tests/fixtures/maid-composer-hide.html +31 -0
  368. package/tests/fold-choreography.test.ts +54 -0
  369. package/tests/fold-intensity.test.ts +94 -0
  370. package/tests/footer-precedence.test.ts +129 -0
  371. package/tests/history-word-regression.test.ts +16 -0
  372. package/tests/live-turn.test.ts +218 -0
  373. package/tests/mcp-apps.test.ts +157 -0
  374. package/tests/message-chrome.test.ts +29 -0
  375. package/tests/official-slots.test.ts +112 -0
  376. package/tests/open-file.test.ts +114 -0
  377. package/tests/pending-submission.test.ts +45 -0
  378. package/tests/platform-media.test.ts +23 -0
  379. package/tests/projection.test.ts +184 -0
  380. package/tests/reasoning-follow.test.ts +34 -0
  381. package/tests/settings-copy.test.ts +63 -0
  382. package/tests/skill-status.test.ts +137 -0
  383. package/tests/stock-install.test.ts +58 -0
  384. package/tests/stream-buffer.test.ts +102 -0
  385. package/tests/timeline.test.ts +59 -0
  386. package/tests/tool-diff.test.ts +57 -0
  387. package/tests/waiting-clock.test.ts +83 -0
  388. package/tests/word-timeline.test.ts +146 -0
  389. package/tsconfig.json +11 -0
  390. package/tsdown.config.ts +51 -0
@@ -0,0 +1,45 @@
1
+ // Adapted from DeepSeek Harness b150a551b8d465e31e418e1b2eaf5e79bbb7d28e (MIT). See THIRD_PARTY_NOTICES.md.
2
+ /**
3
+ * The markdown renderer's two mdast grammars, one per rendering arm. Each
4
+ * arm is internally consistent — the incremental tail parses, the one-shot
5
+ * parses, and the plain-text projection of a given grammar always agree on
6
+ * where blocks start and end — and the settled grammar is the streaming one
7
+ * plus the math extensions, so the arms differ only where TeX delimiters
8
+ * begin a math construct (a `$$` block is a paragraph while streaming and a
9
+ * math block once settled, by design).
10
+ */
11
+
12
+ import type { Root } from 'mdast'
13
+ import { fromMarkdown } from 'mdast-util-from-markdown'
14
+ import { gfmFromMarkdown } from 'mdast-util-gfm'
15
+ import { mathFromMarkdown } from 'mdast-util-math'
16
+ import { gfm } from 'micromark-extension-gfm'
17
+ import { math } from 'micromark-extension-math'
18
+ import { cjkFriendlyStrong } from './cjkFriendlyStrong.js'
19
+ import { mathCompatibility } from './mathCompatibility.js'
20
+
21
+ /**
22
+ * Parse GFM markdown (the streaming arm's grammar: no math, so incomplete
23
+ * TeX never flashes KaTeX errors mid-stream).
24
+ * @param text - Markdown source.
25
+ * @returns The mdast root.
26
+ */
27
+ export function parseGfm(text: string): Root {
28
+ return fromMarkdown(text, {
29
+ extensions: [gfm(), cjkFriendlyStrong()],
30
+ mdastExtensions: [gfmFromMarkdown()],
31
+ })
32
+ }
33
+
34
+ /**
35
+ * Parse GFM markdown plus TeX math with the compatibility delimiters
36
+ * (the settled arm's grammar).
37
+ * @param text - Markdown source.
38
+ * @returns The mdast root.
39
+ */
40
+ export function parseGfmWithMath(text: string): Root {
41
+ return fromMarkdown(text, {
42
+ extensions: [gfm(), cjkFriendlyStrong(), mathCompatibility(), math()],
43
+ mdastExtensions: [gfmFromMarkdown(), mathFromMarkdown()],
44
+ })
45
+ }
@@ -0,0 +1,647 @@
1
+ // Adapted from DeepSeek Harness b150a551b8d465e31e418e1b2eaf5e79bbb7d28e (MIT). See THIRD_PARTY_NOTICES.md.
2
+ /**
3
+ * Direct mdast→React markdown renderer. Replaces the react-markdown /
4
+ * remark-rehype pipeline with one switch over parsed nodes so streaming can
5
+ * cache frozen blocks as React elements; the rendered DOM is pinned
6
+ * byte-for-byte by `tests/fixtures/markdown-dom` and must not drift.
7
+ *
8
+ * Untrusted-output policy (unchanged from the replaced pipeline): link and
9
+ * image destinations pass a protocol allowlist, images additionally require
10
+ * absolute HTTP(S), raw HTML renders as literal text (no HTML enters the
11
+ * DOM), and KaTeX runs without trusted commands. Fragment-anchor URLs fail
12
+ * the allowlist, so footnote references and back-references render as plain
13
+ * text rather than in-page links.
14
+ *
15
+ * Merge-extensible node unions fall through the documented default (render
16
+ * nothing) rather than ending in assertNever: grammars registered elsewhere
17
+ * may add node types this renderer has no mapping for.
18
+ */
19
+
20
+ import { Fragment, createElement, useState } from 'react'
21
+ import type { Key, ReactNode } from 'react'
22
+ import clsx from 'clsx'
23
+ import type * as Md from 'mdast'
24
+ import type {} from 'mdast-util-math'
25
+ import { normalizeUri } from 'micromark-util-sanitize-uri'
26
+ import { CodeBlock } from '@deepseek-ai/dsh-client-ui-primitives'
27
+ import { renderTexToReact } from './katex.js'
28
+ import { McpAppCodeBlock, StreamingMcpAppPlaceholder } from '../McpAppFrame.js'
29
+ import { isMcpAppCodeBlock, extractMcpAppTitle, extractMcpAppHeight } from '../mcp-app.js'
30
+ import type { MarkdownPathImages } from '@deepseek-ai/dsh-client-ui-primitives'
31
+ import type { PositionedBlock } from './incremental.js'
32
+ import css from './MarkdownText.module.css'
33
+
34
+ /** Copy-button labels forwarded to fence CodeBlocks (this package is cordis-free, so copy arrives via props). */
35
+ export interface MarkdownCodeLabels {
36
+ /** Copy-button idle label. */
37
+ copyLabel?: string | undefined
38
+ /** Copy-button label during the post-copy confirmation window. */
39
+ copiedLabel?: string | undefined
40
+ }
41
+
42
+ function sanitizeUrl(url: string): string {
43
+ try {
44
+ switch (new URL(url).protocol) {
45
+ case 'http:':
46
+ case 'https:':
47
+ case 'mailto:':
48
+ return url
49
+ default:
50
+ return ''
51
+ }
52
+ } catch {
53
+ // Relative and otherwise unparsable destinations are disallowed alongside
54
+ // disallowed protocols; new URL() has no other failure mode for strings.
55
+ return ''
56
+ }
57
+ }
58
+
59
+ function remoteImageUrl(url: string): string | undefined {
60
+ try {
61
+ const protocol = new URL(url).protocol
62
+ return protocol === 'http:' || protocol === 'https:' ? url : undefined
63
+ } catch {
64
+ // Same single failure mode as above: not an absolute URL.
65
+ return undefined
66
+ }
67
+ }
68
+
69
+ /** Link/image reference targets collected from a document (first definition per identifier wins, as in CommonMark). */
70
+ export interface ReferenceTargets {
71
+ /** Link/image definitions keyed by upper-cased identifier. */
72
+ definitions: Map<string, Md.Definition>
73
+ /** Footnote definitions keyed by upper-cased identifier. */
74
+ footnotes: Map<string, Md.FootnoteDefinition>
75
+ }
76
+
77
+ /**
78
+ * Create an empty {@link ReferenceTargets}.
79
+ * @returns Fresh empty maps.
80
+ */
81
+ export function createReferenceTargets(): ReferenceTargets {
82
+ return { definitions: new Map(), footnotes: new Map() }
83
+ }
84
+
85
+ /**
86
+ * Record every definition and footnote definition under `nodes` into
87
+ * `targets`, depth-first, keeping the first definition per identifier.
88
+ * @param nodes - Subtrees to walk (top-level blocks or any nested children).
89
+ * @param targets - Accumulator, typically shared across incremental segments.
90
+ */
91
+ export function collectReferenceTargets(
92
+ nodes: readonly Md.RootContent[],
93
+ targets: ReferenceTargets,
94
+ ): void {
95
+ for (const node of nodes) {
96
+ if (node.type === 'definition') {
97
+ const id = node.identifier.toUpperCase()
98
+ if (!targets.definitions.has(id)) targets.definitions.set(id, node)
99
+ } else if (node.type === 'footnoteDefinition') {
100
+ const id = node.identifier.toUpperCase()
101
+ if (!targets.footnotes.has(id)) targets.footnotes.set(id, node)
102
+ }
103
+ if ('children' in node) collectReferenceTargets(node.children, targets)
104
+ }
105
+ }
106
+
107
+ /**
108
+ * File-mention affordance for inline code: the owner resolves an authored
109
+ * token to the file it names, using its own vocabulary of real files — the
110
+ * renderer never guesses at what looks like a path.
111
+ */
112
+ export interface MarkdownFileMentions {
113
+ /**
114
+ * Resolve one inline-code token.
115
+ * @param value - The authored token, exactly as written.
116
+ * @returns The opener with its accessible label and full-path title, or
117
+ * undefined when the token names no known file — it then stays inert code.
118
+ */
119
+ resolve(value: string): { open: () => void; label: string; title: string } | undefined
120
+ }
121
+
122
+ /**
123
+ * One render pass's state: immutable options and targets plus the footnote
124
+ * numbering accumulated in document order while references render.
125
+ */
126
+ export interface MarkdownRenderContext {
127
+ /** Reader addition: optional semantic text-leaf renderer with an absolute source offset. */
128
+ readonly renderText?: (value: string, offset: number, inline?: boolean) => ReactNode
129
+ /** Reader addition: keep atomic code content behind the source-order gate. */
130
+ readonly renderAtom?: (children: ReactNode, offset: number) => ReactNode
131
+ readonly sourceOffset?: number
132
+ /** Streaming arm: fences render plain and TeX stays literal. */
133
+ readonly streaming: boolean
134
+ /** Localized fence copy-button labels. */
135
+ readonly codeLabels: MarkdownCodeLabels | undefined
136
+ /** Inside a blockquote's children: tables there always fill the quote's width. */
137
+ readonly inBlockquote?: boolean
138
+ /** Inline-code file mentions; absent wherever no opener vocabulary exists. */
139
+ readonly fileMentions: MarkdownFileMentions | undefined
140
+ readonly pathImages?: MarkdownPathImages
141
+ /** Inside an anchor's children: interactive mentions must not nest there. */
142
+ readonly inLink?: boolean
143
+ /** Reference targets visible to this pass. */
144
+ readonly targets: ReferenceTargets
145
+ /** Footnote identifiers in first-reference order; a footnote's number is its 1-based index here. */
146
+ readonly footnoteOrder: string[]
147
+ /** References rendered per identifier; drives the section's back-reference count. */
148
+ readonly footnoteCounts: Map<string, number>
149
+ }
150
+
151
+ /**
152
+ * Render top-level blocks. Nodes that render nothing (definitions, unmapped
153
+ * types) are dropped rather than kept as null placeholders, matching the
154
+ * replaced pipeline's child lists so separator newlines land identically.
155
+ * @param blocks - Blocks with their stream-stable render keys.
156
+ * @param context - The pass state; footnote numbering mutates in document order.
157
+ * @returns One React node per rendered block.
158
+ */
159
+ export function renderBlocks(
160
+ blocks: readonly PositionedBlock[],
161
+ context: MarkdownRenderContext,
162
+ ): ReactNode[] {
163
+ return blocks
164
+ .map(block => renderNode(block.node, block.key, {
165
+ ...context,
166
+ sourceOffset: block.key - (block.node.position?.start.offset ?? block.key),
167
+ }))
168
+ .filter(element => element !== null)
169
+ }
170
+
171
+ /**
172
+ * Interleave the newline text nodes the replaced pipeline emitted between
173
+ * block-level children. They are invisible between elements but coalesce
174
+ * into adjacent literal raw-HTML text, where the DOM parity fixtures pin
175
+ * them.
176
+ * @param elements - Rendered block children with empty renders already dropped.
177
+ * @param edges - Also emit the leading and trailing newline (hast's loose wrap).
178
+ * @returns The interleaved children.
179
+ */
180
+ export function wrapBlockChildren(elements: readonly ReactNode[], edges: boolean): ReactNode[] {
181
+ const wrapped: ReactNode[] = []
182
+ for (const element of elements) {
183
+ if (edges || wrapped.length > 0) wrapped.push('\n')
184
+ wrapped.push(element)
185
+ }
186
+ if (edges && elements.length > 0) wrapped.push('\n')
187
+ return wrapped
188
+ }
189
+
190
+ /**
191
+ * A block child rendered for a parent that must tell paragraphs apart from
192
+ * other blocks (list items unwrap them when tight; footnote bodies receive
193
+ * their back-references inside the trailing paragraph).
194
+ */
195
+ type BlockEntry = { paragraph: ReactNode[] } | { element: ReactNode }
196
+
197
+ /** Render container children into {@link BlockEntry} values, dropping empty renders. */
198
+ function renderBlockEntries(
199
+ blocks: readonly Md.RootContent[],
200
+ context: MarkdownRenderContext,
201
+ ): BlockEntry[] {
202
+ const entries: BlockEntry[] = []
203
+ for (const [index, block] of blocks.entries()) {
204
+ if (block.type === 'paragraph') {
205
+ entries.push({ paragraph: renderChildren(block.children, context) })
206
+ } else {
207
+ const element = renderNode(block, index, context)
208
+ if (element !== null) entries.push({ element })
209
+ }
210
+ }
211
+ return entries
212
+ }
213
+
214
+ function renderChildren(
215
+ nodes: readonly Md.RootContent[],
216
+ context: MarkdownRenderContext,
217
+ ): ReactNode[] {
218
+ return nodes.map((node, index) => renderNode(node, index, context))
219
+ }
220
+
221
+ function renderNode(node: Md.RootContent, key: Key, context: MarkdownRenderContext): ReactNode {
222
+ switch (node.type) {
223
+ case 'text':
224
+ return context.renderText ? context.renderText(node.value, (context.sourceOffset ?? 0) + (node.position?.start.offset ?? 0)) : node.value
225
+ case 'paragraph':
226
+ return <p key={key}>{renderChildren(node.children, context)}</p>
227
+ case 'heading':
228
+ return createElement(`h${node.depth}`, { key }, ...renderChildren(node.children, context))
229
+ case 'blockquote':
230
+ return (
231
+ <blockquote key={key}>
232
+ {wrapBlockChildren(
233
+ renderChildren(node.children, { ...context, inBlockquote: true }).filter(child => child !== null),
234
+ true,
235
+ )}
236
+ </blockquote>
237
+ )
238
+ case 'thematicBreak':
239
+ return <hr key={key} />
240
+ case 'break':
241
+ // The replaced pipeline emitted a newline text node after each <br>.
242
+ return <Fragment key={key}><br />{'\n'}</Fragment>
243
+ case 'strong':
244
+ return <strong key={key}>{renderChildren(node.children, context)}</strong>
245
+ case 'emphasis':
246
+ return <em key={key}>{renderChildren(node.children, context)}</em>
247
+ case 'delete':
248
+ return <del key={key}>{renderChildren(node.children, context)}</del>
249
+ case 'inlineCode': {
250
+ // Parity with mdast-util-to-hast: inline code renders line endings as spaces.
251
+ const value = node.value.replace(/\r?\n|\r/g, ' ')
252
+ // Native code chips are inline-flex. Keep a single inline formatting
253
+ // context so motion spans do not become flex items or drop code spaces.
254
+ const content = context.renderText ? <span>{context.renderText(value, (context.sourceOffset ?? 0) + (node.position?.start.offset ?? 0), true)}</span> : value
255
+ // An inline-code token that is entirely an absolute HTTP(S) URL keeps
256
+ // its code chrome and gains the same safe external anchor as a link;
257
+ // commands, partial URLs, and other schemes stay inert. The value is
258
+ // authored text, not a parsed destination, so no normalizeUri: port,
259
+ // path, and query render unchanged.
260
+ const href = inlineCodeHttpUrl(value)
261
+ if (href !== undefined) return <code key={key}>{renderSafeLink(href, [content], 'link')}</code>
262
+ // A token the owner's file-mention vocabulary recognizes opens that
263
+ // file; the resolver, not this renderer, decides what names a file.
264
+ // Inside an anchor the token stays inert — a button cannot nest there.
265
+ const mention = context.inLink === true ? undefined : context.fileMentions?.resolve(value)
266
+ if (mention !== undefined) {
267
+ return (
268
+ <code key={key}>
269
+ <button
270
+ type="button"
271
+ className={css.fileMention}
272
+ title={mention.title}
273
+ aria-label={mention.label}
274
+ onClick={mention.open}
275
+ >
276
+ {content}
277
+ </button>
278
+ </code>
279
+ )
280
+ }
281
+ return <code key={key}>{content}</code>
282
+ }
283
+ case 'html':
284
+ // No HTML parser enters the pipeline: raw HTML stays literal text.
285
+ return context.renderText ? context.renderText(node.value, (context.sourceOffset ?? 0) + (node.position?.start.offset ?? 0)) : node.value
286
+ case 'code': {
287
+ const code = renderCode(node, key, context)
288
+ return context.renderAtom ? <Fragment key={key}>{context.renderAtom(code, (context.sourceOffset ?? 0) + (node.position?.start.offset ?? 0))}</Fragment> : code
289
+ }
290
+ case 'math':
291
+ return <Fragment key={key}>{renderTexToReact(node.value, true)}</Fragment>
292
+ case 'inlineMath':
293
+ return <Fragment key={key}>{renderTexToReact(node.value, false)}</Fragment>
294
+ case 'list':
295
+ return renderList(node, key, context)
296
+ case 'listItem':
297
+ // Reachable only in hand-built trees: the grammar emits items inside lists.
298
+ return renderListItem(node, listItemLoose(node), key, context)
299
+ case 'table':
300
+ return renderTable(node, key, context)
301
+ case 'link':
302
+ return renderAnchor(node.url, renderChildren(node.children, { ...context, inLink: true }), key)
303
+ case 'linkReference':
304
+ return renderLinkReference(node, key, context)
305
+ case 'image':
306
+ return renderImage(node.url, node.alt ?? '', key, context)
307
+ case 'imageReference':
308
+ return renderImageReference(node, key, context)
309
+ case 'footnoteReference':
310
+ return renderFootnoteReference(node, key, context)
311
+ case 'definition':
312
+ case 'footnoteDefinition':
313
+ // Targets render elsewhere: definitions resolve references in place;
314
+ // footnote bodies render in the trailing section.
315
+ return null
316
+ default:
317
+ // Documented default for the merge-extensible union: node types without
318
+ // a mapping (tableRow/tableCell outside a table, frontmatter, future
319
+ // grammar contributions) render nothing.
320
+ return null
321
+ }
322
+ }
323
+
324
+ function renderCode(node: Md.Code, key: Key, context: MarkdownRenderContext): ReactNode {
325
+ const language = node.lang ?? undefined
326
+ if (node.value === '') {
327
+ // Parity: the replaced pipeline kept the stock <pre> for an empty fence.
328
+ return (
329
+ <pre key={key}>
330
+ <code className={language === undefined ? undefined : `language-${language}`} />
331
+ </pre>
332
+ )
333
+ }
334
+ // The replaced pipeline recovered the grammar id from the hast class with
335
+ // /language-([\w-]+)/, which truncates at the first non-word character.
336
+ const lang = language === undefined ? undefined : /^[\w-]+/.exec(language)?.[0]
337
+
338
+ if (isMcpAppCodeBlock(node.lang, node.meta, node.value)) {
339
+ const title = extractMcpAppTitle(node.meta, node.value)
340
+ const initialHeight = extractMcpAppHeight(node.meta)
341
+ if (context.streaming) {
342
+ return <StreamingMcpAppPlaceholder key={key} title={title} />
343
+ }
344
+ return <McpAppCodeBlock key={key} html={node.value} title={title} initialHeight={initialHeight} />
345
+ }
346
+
347
+ if (!context.streaming && lang === 'math') {
348
+ // ```math fences render as display TeX once settled (rehype-katex parity);
349
+ // its text extraction saw the code block's trailing newline.
350
+ return <Fragment key={key}>{renderTexToReact(`${node.value}\n`, true)}</Fragment>
351
+ }
352
+ return (
353
+ <CodeBlock
354
+ key={key}
355
+ // The replaced hast pipeline appended one synthetic newline that
356
+ // CodeBlock's display trim removes; feeding the bare value would make
357
+ // that trim eat a REAL trailing blank line inside the fence instead.
358
+ code={`${node.value}\n`}
359
+ lang={context.streaming ? undefined : lang}
360
+ copyLabel={context.codeLabels?.copyLabel ?? '复制代码'}
361
+ copiedLabel={context.codeLabels?.copiedLabel ?? '已复制'}
362
+ />
363
+ )
364
+ }
365
+
366
+ /** A list is loose when it or any of its items is spread; every item then keeps its paragraphs. */
367
+ function listLoose(list: Md.List): boolean {
368
+ return (list.spread ?? false) || list.children.some(listItemLoose)
369
+ }
370
+
371
+ function listItemLoose(item: Md.ListItem): boolean {
372
+ return item.spread ?? item.children.length > 1
373
+ }
374
+
375
+ function renderList(node: Md.List, key: Key, context: MarkdownRenderContext): ReactNode {
376
+ const loose = listLoose(node)
377
+ const properties: { start?: number; className?: string } = {}
378
+ if (typeof node.start === 'number' && node.start !== 1) properties.start = node.start
379
+ if (node.children.some(item => typeof item.checked === 'boolean')) {
380
+ properties.className = 'contains-task-list'
381
+ }
382
+ return createElement(
383
+ node.ordered === true ? 'ol' : 'ul',
384
+ { key, ...properties },
385
+ ...node.children.map((item, index) => renderListItem(item, loose, index, context)),
386
+ )
387
+ }
388
+
389
+ function renderListItem(
390
+ item: Md.ListItem,
391
+ loose: boolean,
392
+ key: Key,
393
+ context: MarkdownRenderContext,
394
+ ): ReactNode {
395
+ const entries = renderBlockEntries(item.children, context)
396
+ const task = typeof item.checked === 'boolean'
397
+ if (task) {
398
+ const checkbox = <input key="task-checkbox" type="checkbox" checked={item.checked === true} disabled />
399
+ const head = entries[0]
400
+ if (head !== undefined && 'paragraph' in head) {
401
+ head.paragraph = head.paragraph.length > 0 ? [checkbox, ' ', ...head.paragraph] : [checkbox]
402
+ } else {
403
+ entries.unshift({ paragraph: [checkbox] })
404
+ }
405
+ }
406
+ // Newline placement and tight-paragraph unwrapping mirror
407
+ // mdast-util-to-hast's list-item handler: a newline before every child
408
+ // except a tight leading paragraph, and after a trailing non-paragraph
409
+ // (or any trailing child when loose).
410
+ const parts: ReactNode[] = []
411
+ for (const [index, entry] of entries.entries()) {
412
+ const isParagraph = 'paragraph' in entry
413
+ if (loose || index !== 0 || !isParagraph) parts.push('\n')
414
+ if (!isParagraph) parts.push(entry.element)
415
+ else if (loose) parts.push(<p key={`p-${index}`}>{entry.paragraph}</p>)
416
+ else parts.push(<Fragment key={`p-${index}`}>{entry.paragraph}</Fragment>)
417
+ }
418
+ const tail = entries[entries.length - 1]
419
+ if (tail !== undefined && (loose || !('paragraph' in tail))) parts.push('\n')
420
+ return (
421
+ <li key={key} className={task ? 'task-list-item' : undefined}>
422
+ {parts}
423
+ </li>
424
+ )
425
+ }
426
+
427
+ function renderTable(node: Md.Table, key: Key, context: MarkdownRenderContext): ReactNode {
428
+ const align = node.align ?? null
429
+ const [headRow, ...bodyRows] = node.children
430
+ const columns = align === null ? headRow?.children.length ?? 0 : align.length
431
+ // Four or more columns read as a comparison matrix: the block keeps the
432
+ // table at natural width and exposes the stable `md-table-wide` hook so a
433
+ // hosting layout (the chat transcript) can widen it past the message
434
+ // column. Narrower tables — and any table inside a blockquote — fill the
435
+ // column and wrap instead (deepsuite chat TableWrapper parity).
436
+ const wide = columns >= 4 && context.inBlockquote !== true
437
+ return (
438
+ // Wide tables rest with overflow-x hidden (the hover-revealed bar in
439
+ // MarkdownText.module.css), which drops Chromium's implicit scroller
440
+ // focusability — the explicit tabindex keeps them keyboard-reachable,
441
+ // and :focus-visible restores scrolling.
442
+ <div
443
+ key={key}
444
+ className={clsx(css.tableScroll, wide ? 'md-table-wide' : css.tableFill)}
445
+ tabIndex={wide ? 0 : undefined}
446
+ >
447
+ <table>
448
+ {headRow !== undefined && <thead>{renderTableRow(headRow, 'th', align, 0, context)}</thead>}
449
+ {bodyRows.length > 0 && (
450
+ <tbody>
451
+ {bodyRows.map((row, index) => renderTableRow(row, 'td', align, index + 1, context))}
452
+ </tbody>
453
+ )}
454
+ </table>
455
+ </div>
456
+ )
457
+ }
458
+
459
+ function renderTableRow(
460
+ row: Md.TableRow,
461
+ cellTag: 'th' | 'td',
462
+ align: readonly Md.AlignType[] | null,
463
+ key: Key,
464
+ context: MarkdownRenderContext,
465
+ ): ReactNode {
466
+ // With column alignment present, every row renders exactly one cell per
467
+ // column, padding or truncating the row (mdast-util-to-hast parity).
468
+ const length = align === null ? row.children.length : align.length
469
+ const cells: ReactNode[] = []
470
+ for (let index = 0; index < length; index++) {
471
+ const cell = row.children[index]
472
+ const alignValue = align?.[index]
473
+ cells.push(createElement(
474
+ cellTag,
475
+ // hast-util-to-jsx-runtime's default tableCellAlignToStyle turned the
476
+ // deprecated align attribute into an inline style; keep that DOM.
477
+ { key: index, style: alignValue == null ? undefined : { textAlign: alignValue } },
478
+ ...(cell === undefined ? [] : renderChildren(cell.children, context)),
479
+ ))
480
+ }
481
+ return <tr key={key}>{cells}</tr>
482
+ }
483
+
484
+ /** Anchor over an already-authored href: allowlisted or unwrapped, external links get the safe attributes. */
485
+ function renderSafeLink(href: string, children: ReactNode[], key: Key): ReactNode {
486
+ const safeHref = sanitizeUrl(href)
487
+ if (safeHref === '') return <Fragment key={key}>{children}</Fragment>
488
+ const external = ['http:', 'https:'].includes(new URL(safeHref).protocol)
489
+ return (
490
+ <a
491
+ key={key}
492
+ href={safeHref}
493
+ {...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
494
+ >
495
+ {children}
496
+ </a>
497
+ )
498
+ }
499
+
500
+ /** Anchor over a parsed markdown destination, which hast normalized before the allowlist saw it. */
501
+ function renderAnchor(url: string, children: ReactNode[], key: Key): ReactNode {
502
+ return renderSafeLink(normalizeUri(url), children, key)
503
+ }
504
+
505
+ /**
506
+ * The complete inline-code value when it is exactly an absolute HTTP(S) URL
507
+ * (no surrounding whitespace); anything else stays inert code.
508
+ */
509
+ function inlineCodeHttpUrl(value: string): string | undefined {
510
+ if (value.trim() !== value) return undefined
511
+ try {
512
+ const protocol = new URL(value).protocol
513
+ return protocol === 'http:' || protocol === 'https:' ? value : undefined
514
+ } catch {
515
+ // Not an absolute URL at all — the only way new URL() rejects a string.
516
+ return undefined
517
+ }
518
+ }
519
+
520
+ function renderImage(url: string, alt: string, key: Key, context: MarkdownRenderContext): ReactNode {
521
+ // RC2 semantics, including the revalidation of a trusted vocabulary result.
522
+ const remote = remoteImageUrl(sanitizeUrl(normalizeUri(url)))
523
+ const rewritten = remote === undefined ? context.pathImages?.resolve(url) : undefined
524
+ let imageSrc = remote
525
+ if (rewritten !== undefined) {
526
+ try {
527
+ const parsed = new URL(rewritten)
528
+ if (['http:', 'https:', 'blob:', 'data:'].includes(parsed.protocol) || rewritten.startsWith('dsh-app://app/api/file?')) imageSrc = rewritten
529
+ } catch { /* A vocabulary result must be an absolute URL. */ }
530
+ }
531
+ if (imageSrc === undefined) {
532
+ return <span key={key} className={css.imageAlt}>{alt}</span>
533
+ }
534
+ return <MarkdownImage key={`${key}:${imageSrc}`} src={imageSrc} alt={alt} destination={url} />
535
+ }
536
+
537
+ function MarkdownImage({ src, alt, destination }: { src: string; alt: string; destination: string }): ReactNode {
538
+ const [failed, setFailed] = useState(false)
539
+ if (failed) return <span className={css.imageAlt}>{alt || destination}</span>
540
+ return (
541
+ <img
542
+ className={css.image}
543
+ src={src}
544
+ alt={alt}
545
+ onError={() => { setFailed(true) }}
546
+ loading="lazy"
547
+ decoding="async"
548
+ referrerPolicy="no-referrer"
549
+ />
550
+ )
551
+ }
552
+
553
+ /** The bracketed source text a reference reverts to when its definition is missing. */
554
+ function referenceSuffix(node: Md.LinkReference | Md.ImageReference): string {
555
+ if (node.referenceType === 'collapsed') return '][]'
556
+ if (node.referenceType === 'full') return `][${node.label ?? node.identifier}]`
557
+ return ']'
558
+ }
559
+
560
+ function renderLinkReference(
561
+ node: Md.LinkReference,
562
+ key: Key,
563
+ context: MarkdownRenderContext,
564
+ ): ReactNode {
565
+ const definition = context.targets.definitions.get(node.identifier.toUpperCase())
566
+ if (definition === undefined) {
567
+ // The grammar only emits references whose definitions exist somewhere in
568
+ // the same parse, but incremental segments and hand-built trees may still
569
+ // present unresolved ones: revert to the bracketed source text — which is
570
+ // not an anchor, so mentions inside it stay live.
571
+ return <Fragment key={key}>{'['}{renderChildren(node.children, context)}{referenceSuffix(node)}</Fragment>
572
+ }
573
+ return renderAnchor(definition.url, renderChildren(node.children, { ...context, inLink: true }), key)
574
+ }
575
+
576
+ function renderImageReference(
577
+ node: Md.ImageReference,
578
+ key: Key,
579
+ context: MarkdownRenderContext,
580
+ ): ReactNode {
581
+ const definition = context.targets.definitions.get(node.identifier.toUpperCase())
582
+ if (definition === undefined) return `![${node.alt ?? ''}${referenceSuffix(node)}`
583
+ return renderImage(definition.url, node.alt ?? '', key, context)
584
+ }
585
+
586
+ function renderFootnoteReference(
587
+ node: Md.FootnoteReference,
588
+ key: Key,
589
+ context: MarkdownRenderContext,
590
+ ): ReactNode {
591
+ const id = node.identifier.toUpperCase()
592
+ const seen = context.footnoteCounts.get(id)
593
+ if (seen === undefined) context.footnoteOrder.push(id)
594
+ context.footnoteCounts.set(id, (seen ?? 0) + 1)
595
+ // The in-page anchor fails the protocol allowlist, so only the numbered
596
+ // superscript renders (matching the replaced pipeline's unwrapped link).
597
+ return <sup key={key}>{String(context.footnoteOrder.indexOf(id) + 1)}</sup>
598
+ }
599
+
600
+ /**
601
+ * Render the trailing footnote section for every footnote referenced during
602
+ * the pass, in first-reference order, with one plain-text back-reference
603
+ * marker per rendered reference.
604
+ * @param context - The pass state after all blocks rendered.
605
+ * @returns The section, or null when no referenced footnote has a definition.
606
+ */
607
+ export function renderFootnoteSection(context: MarkdownRenderContext): ReactNode | null {
608
+ const items: ReactNode[] = []
609
+ for (const id of context.footnoteOrder) {
610
+ const definition = context.targets.footnotes.get(id)
611
+ if (definition === undefined) continue
612
+ const count = context.footnoteCounts.get(id) ?? 0
613
+ const backrefs: ReactNode[] = []
614
+ for (let reference = 1; reference <= count; reference++) {
615
+ if (backrefs.length > 0) backrefs.push(' ')
616
+ backrefs.push('↩')
617
+ if (reference > 1) backrefs.push(<sup key={`re-${reference}`}>{String(reference)}</sup>)
618
+ }
619
+ const entries = renderBlockEntries(definition.children, context)
620
+ const tail = entries[entries.length - 1]
621
+ const body: ReactNode[] = entries.map((entry, index) => (
622
+ 'paragraph' in entry
623
+ ? (
624
+ <p key={`p-${index}`}>
625
+ {entry.paragraph}
626
+ {entry === tail && <>{' '}{backrefs}</>}
627
+ </p>
628
+ )
629
+ : entry.element
630
+ ))
631
+ // Without a trailing paragraph the back-references join the block list
632
+ // itself (and pick up the wrap newlines), as in the replaced pipeline.
633
+ if (tail === undefined || !('paragraph' in tail)) body.push(...backrefs)
634
+ items.push(
635
+ <li key={id} id={`user-content-fn-${normalizeUri(id.toLowerCase())}`}>
636
+ {wrapBlockChildren(body, true)}
637
+ </li>,
638
+ )
639
+ }
640
+ if (items.length === 0) return null
641
+ return (
642
+ <section key="footnotes" data-footnotes className="footnotes">
643
+ <h2 id="footnote-label" className="sr-only">Footnotes</h2>
644
+ <ol>{items}</ol>
645
+ </section>
646
+ )
647
+ }