@flamingo-stack/openframe-frontend-core 0.0.488 → 0.0.490-1506.3944.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 (311) hide show
  1. package/dist/chat-protocol/decode.d.ts +58 -0
  2. package/dist/chat-protocol/decode.d.ts.map +1 -0
  3. package/dist/chat-protocol/encode.d.ts +44 -0
  4. package/dist/chat-protocol/encode.d.ts.map +1 -0
  5. package/dist/chat-protocol/env-flag.d.ts +35 -0
  6. package/dist/chat-protocol/env-flag.d.ts.map +1 -0
  7. package/dist/chat-protocol/events.d.ts +196 -0
  8. package/dist/chat-protocol/events.d.ts.map +1 -0
  9. package/dist/chat-protocol/frames.d.ts +213 -0
  10. package/dist/chat-protocol/frames.d.ts.map +1 -0
  11. package/dist/chat-protocol/index.cjs +494 -0
  12. package/dist/chat-protocol/index.cjs.map +1 -0
  13. package/dist/chat-protocol/index.d.ts +18 -0
  14. package/dist/chat-protocol/index.d.ts.map +1 -0
  15. package/dist/chat-protocol/index.js +478 -0
  16. package/dist/chat-protocol/index.js.map +1 -0
  17. package/dist/chat-protocol/ip-normalize.d.ts +44 -0
  18. package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
  19. package/dist/chat-protocol/nats-decoder.d.ts +26 -0
  20. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
  21. package/dist/{chunk-AJNDDXKC.js → chunk-4NWARV5I.js} +3 -3
  22. package/dist/{chunk-GQX6LFFA.cjs → chunk-5CJR2GZ4.cjs} +42 -42
  23. package/dist/{chunk-GQX6LFFA.cjs.map → chunk-5CJR2GZ4.cjs.map} +1 -1
  24. package/dist/{chunk-EUYIFALO.cjs → chunk-5EYRXSY5.cjs} +14 -14
  25. package/dist/{chunk-EUYIFALO.cjs.map → chunk-5EYRXSY5.cjs.map} +1 -1
  26. package/dist/{chunk-5RXKOGH6.cjs → chunk-5LMGFI4J.cjs} +7011 -4777
  27. package/dist/chunk-5LMGFI4J.cjs.map +1 -0
  28. package/dist/{chunk-KFQXO7QB.js → chunk-6GT557KN.js} +1 -1
  29. package/dist/chunk-6GT557KN.js.map +1 -0
  30. package/dist/{chunk-77DFPD35.js → chunk-BE6DK7QY.js} +12267 -10033
  31. package/dist/chunk-BE6DK7QY.js.map +1 -0
  32. package/dist/{chunk-A7IPON2V.cjs → chunk-BIKOJ4FH.cjs} +5 -5
  33. package/dist/{chunk-A7IPON2V.cjs.map → chunk-BIKOJ4FH.cjs.map} +1 -1
  34. package/dist/{chunk-HQYJINFV.js → chunk-CORIBKHG.js} +4 -4
  35. package/dist/{chunk-YEEHB4T6.cjs → chunk-CSYSYJCF.cjs} +40 -40
  36. package/dist/{chunk-YEEHB4T6.cjs.map → chunk-CSYSYJCF.cjs.map} +1 -1
  37. package/dist/{chunk-SIKCXKGB.cjs → chunk-DSDZ5T4P.cjs} +7 -7
  38. package/dist/{chunk-SIKCXKGB.cjs.map → chunk-DSDZ5T4P.cjs.map} +1 -1
  39. package/dist/{chunk-PNXNWGBY.js → chunk-GTWO5NAY.js} +2 -2
  40. package/dist/{chunk-SCCTJ2UI.js → chunk-HSACATCD.js} +4 -4
  41. package/dist/{chunk-PECGZWQR.cjs → chunk-KP5KRRD6.cjs} +3 -3
  42. package/dist/{chunk-PECGZWQR.cjs.map → chunk-KP5KRRD6.cjs.map} +1 -1
  43. package/dist/{chunk-2B6773QM.cjs → chunk-LILQJ3UZ.cjs} +1 -1
  44. package/dist/chunk-LILQJ3UZ.cjs.map +1 -0
  45. package/dist/{chunk-RZSA6MZL.cjs → chunk-NWX7MMF5.cjs} +15 -15
  46. package/dist/{chunk-RZSA6MZL.cjs.map → chunk-NWX7MMF5.cjs.map} +1 -1
  47. package/dist/{chunk-V3CNNMY3.cjs → chunk-P4WTZU6L.cjs} +18 -18
  48. package/dist/{chunk-V3CNNMY3.cjs.map → chunk-P4WTZU6L.cjs.map} +1 -1
  49. package/dist/{chunk-KLYHZRAD.js → chunk-PB3BTZDU.js} +2 -2
  50. package/dist/{chunk-FZOBQG6M.cjs → chunk-Q6E7VJ4W.cjs} +37 -36
  51. package/dist/chunk-Q6E7VJ4W.cjs.map +1 -0
  52. package/dist/{chunk-EYEIVYYF.cjs → chunk-QSCHCZL3.cjs} +7 -7
  53. package/dist/{chunk-EYEIVYYF.cjs.map → chunk-QSCHCZL3.cjs.map} +1 -1
  54. package/dist/{chunk-YJSKQ2ZV.js → chunk-RQLBHKXN.js} +3 -3
  55. package/dist/{chunk-CWQACI4Z.js → chunk-RW2LPL72.js} +2 -2
  56. package/dist/{chunk-DX6AOHNU.cjs → chunk-S2NHJDKQ.cjs} +77 -77
  57. package/dist/{chunk-DX6AOHNU.cjs.map → chunk-S2NHJDKQ.cjs.map} +1 -1
  58. package/dist/{chunk-AHDJMEEN.js → chunk-T2EGSZFN.js} +9 -8
  59. package/dist/chunk-T2EGSZFN.js.map +1 -0
  60. package/dist/{chunk-S52LJNMU.js → chunk-TKN5667N.js} +13 -13
  61. package/dist/chunk-TKN5667N.js.map +1 -0
  62. package/dist/{chunk-3BFX6HJM.js → chunk-UUYRJPGJ.js} +6 -6
  63. package/dist/{chunk-F6BYUO5U.js → chunk-VHG6AX5V.js} +3 -3
  64. package/dist/{chunk-FOQV6RCO.cjs → chunk-VQ523ORK.cjs} +29 -29
  65. package/dist/{chunk-FOQV6RCO.cjs.map → chunk-VQ523ORK.cjs.map} +1 -1
  66. package/dist/{chunk-PXF5J24T.js → chunk-XQ2B74SI.js} +2 -2
  67. package/dist/{chunk-H7JX2BG7.cjs → chunk-YHOIAA2E.cjs} +76 -76
  68. package/dist/chunk-YHOIAA2E.cjs.map +1 -0
  69. package/dist/{chunk-LLWBPPZ4.js → chunk-YZ2OIYQ3.js} +6 -6
  70. package/dist/components/case-studies/index.cjs +10 -10
  71. package/dist/components/case-studies/index.js +4 -4
  72. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  73. package/dist/components/chat/chat-message-list.d.ts.map +1 -1
  74. package/dist/components/chat/hooks/index.d.ts +0 -1
  75. package/dist/components/chat/hooks/index.d.ts.map +1 -1
  76. package/dist/components/chat/hooks/use-chat-history-hydration.d.ts +47 -0
  77. package/dist/components/chat/hooks/use-chat-history-hydration.d.ts.map +1 -0
  78. package/dist/components/chat/hooks/use-chat.d.ts +1 -0
  79. package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
  80. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
  81. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  82. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -53
  83. package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
  84. package/dist/components/chat/index.cjs +12 -4
  85. package/dist/components/chat/index.cjs.map +1 -1
  86. package/dist/components/chat/index.d.ts +1 -0
  87. package/dist/components/chat/index.d.ts.map +1 -1
  88. package/dist/components/chat/index.js +23 -15
  89. package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
  90. package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
  91. package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
  92. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
  93. package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
  94. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
  95. package/dist/components/chat/stream/index.d.ts +17 -0
  96. package/dist/components/chat/stream/index.d.ts.map +1 -0
  97. package/dist/components/chat/stream/message-mutations.d.ts +77 -0
  98. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
  99. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
  100. package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
  101. package/dist/components/chat/types/api.types.d.ts +1 -81
  102. package/dist/components/chat/types/api.types.d.ts.map +1 -1
  103. package/dist/components/chat/types/processing.types.d.ts +2 -90
  104. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  105. package/dist/components/chat/types/unified-chat-state.types.d.ts +5 -0
  106. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  107. package/dist/components/chat/utils/chat-conversation-storage.d.ts +34 -0
  108. package/dist/components/chat/utils/chat-conversation-storage.d.ts.map +1 -0
  109. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
  110. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  111. package/dist/components/chat/utils/index.d.ts +2 -3
  112. package/dist/components/chat/utils/index.d.ts.map +1 -1
  113. package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
  114. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  115. package/dist/components/contact/index.cjs +5 -5
  116. package/dist/components/contact/index.js +4 -4
  117. package/dist/components/docs/index.cjs +7 -7
  118. package/dist/components/docs/index.js +6 -6
  119. package/dist/components/embeds/index.cjs +5 -5
  120. package/dist/components/embeds/index.js +4 -4
  121. package/dist/components/faq/index.cjs +6 -6
  122. package/dist/components/faq/index.js +5 -5
  123. package/dist/components/features/index.cjs +4 -4
  124. package/dist/components/features/index.js +3 -3
  125. package/dist/components/help-center-pages/index.cjs +23 -23
  126. package/dist/components/help-center-pages/index.js +14 -14
  127. package/dist/components/index.cjs +182 -140
  128. package/dist/components/index.cjs.map +1 -1
  129. package/dist/components/index.js +66 -24
  130. package/dist/components/index.js.map +1 -1
  131. package/dist/components/navigation/index.cjs +4 -4
  132. package/dist/components/navigation/index.js +3 -3
  133. package/dist/components/onboarding-guides/index.cjs +8 -8
  134. package/dist/components/onboarding-guides/index.js +7 -7
  135. package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
  136. package/dist/components/related-content/index.cjs +6 -6
  137. package/dist/components/related-content/index.js +5 -5
  138. package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
  139. package/dist/components/tickets/index.cjs +7 -7
  140. package/dist/components/tickets/index.js +6 -6
  141. package/dist/components/ui/index.cjs +46 -4
  142. package/dist/components/ui/index.cjs.map +1 -1
  143. package/dist/components/ui/index.d.ts +1 -2
  144. package/dist/components/ui/index.d.ts.map +1 -1
  145. package/dist/components/ui/index.js +57 -15
  146. package/dist/components/ui/markdown/base-components.d.ts +51 -0
  147. package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
  148. package/dist/components/ui/markdown/engine.d.ts +99 -0
  149. package/dist/components/ui/markdown/engine.d.ts.map +1 -0
  150. package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
  151. package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
  152. package/dist/components/ui/markdown/index.d.ts +18 -0
  153. package/dist/components/ui/markdown/index.d.ts.map +1 -0
  154. package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
  155. package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
  156. package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
  157. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
  158. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
  159. package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
  160. package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
  161. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
  162. package/dist/components/ui/markdown/sanitize.d.ts +153 -0
  163. package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
  164. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
  165. package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
  166. package/dist/components/ui/markdown/streaming.d.ts +78 -0
  167. package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
  168. package/dist/components/ui/markdown/text-size.d.ts +34 -0
  169. package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
  170. package/dist/contexts/chat-runtime-context.d.ts +8 -0
  171. package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
  172. package/dist/contexts/index.cjs +2 -2
  173. package/dist/contexts/index.js +1 -1
  174. package/dist/hooks/index.cjs +3 -3
  175. package/dist/hooks/index.js +2 -2
  176. package/dist/index.cjs +62 -4
  177. package/dist/index.cjs.map +1 -1
  178. package/dist/index.js +73 -15
  179. package/dist/utils/index.cjs +169 -40
  180. package/dist/utils/index.cjs.map +1 -1
  181. package/dist/utils/index.d.ts +1 -0
  182. package/dist/utils/index.d.ts.map +1 -1
  183. package/dist/utils/index.js +162 -41
  184. package/dist/utils/index.js.map +1 -1
  185. package/dist/utils/markdown-fences.d.ts +42 -0
  186. package/dist/utils/markdown-fences.d.ts.map +1 -0
  187. package/dist/utils/markdown-heading-id.d.ts +85 -0
  188. package/dist/utils/markdown-heading-id.d.ts.map +1 -0
  189. package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
  190. package/package.json +7 -1
  191. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +307 -0
  192. package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
  193. package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
  194. package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
  195. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +228 -0
  196. package/src/chat-protocol/decode.ts +278 -0
  197. package/src/chat-protocol/encode.ts +71 -0
  198. package/src/chat-protocol/env-flag.ts +39 -0
  199. package/src/chat-protocol/events.ts +232 -0
  200. package/src/chat-protocol/frames.ts +245 -0
  201. package/src/chat-protocol/index.ts +21 -0
  202. package/src/chat-protocol/ip-normalize.ts +146 -0
  203. package/src/chat-protocol/nats-decoder.ts +252 -0
  204. package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
  205. package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
  206. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +209 -0
  207. package/src/components/chat/chat-message-enhanced.tsx +111 -17
  208. package/src/components/chat/chat-message-list.tsx +358 -109
  209. package/src/components/chat/embeddable-chat.tsx +6 -0
  210. package/src/components/chat/hooks/.index.md +30 -33
  211. package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
  212. package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
  213. package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
  214. package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
  215. package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
  216. package/src/components/chat/hooks/index.ts +0 -1
  217. package/src/components/chat/hooks/use-chat-history-hydration.ts +142 -0
  218. package/src/components/chat/hooks/use-chat.ts +20 -0
  219. package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -823
  220. package/src/components/chat/hooks/use-sse-chat-adapter.ts +417 -774
  221. package/src/components/chat/index.ts +5 -0
  222. package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +940 -0
  223. package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
  224. package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
  225. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +894 -0
  226. package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
  227. package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
  228. package/src/components/chat/stream/chat-dialog-store.ts +536 -0
  229. package/src/components/chat/stream/chat-stream-reducer.ts +1796 -0
  230. package/src/components/chat/stream/delta-batcher.ts +159 -0
  231. package/src/components/chat/stream/index.ts +55 -0
  232. package/src/components/chat/stream/message-mutations.ts +338 -0
  233. package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
  234. package/src/components/chat/thinking-display.tsx +1 -1
  235. package/src/components/chat/types/.api.types.md +39 -49
  236. package/src/components/chat/types/.processing.types.md +29 -58
  237. package/src/components/chat/types/api.types.ts +6 -73
  238. package/src/components/chat/types/processing.types.ts +11 -52
  239. package/src/components/chat/types/unified-chat-state.types.ts +6 -0
  240. package/src/components/chat/utils/.index.md +31 -40
  241. package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
  242. package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
  243. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +317 -0
  244. package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
  245. package/src/components/chat/utils/chat-conversation-storage.ts +83 -0
  246. package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
  247. package/src/components/chat/utils/index.ts +5 -8
  248. package/src/components/chat/utils/process-historical-messages.ts +352 -384
  249. package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
  250. package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
  251. package/src/components/shared/product-release/release-detail-page.tsx +19 -4
  252. package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
  253. package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
  254. package/src/components/ui/index.ts +1 -2
  255. package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
  256. package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
  257. package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
  258. package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
  259. package/src/components/ui/markdown/base-components.tsx +360 -0
  260. package/src/components/ui/markdown/engine.tsx +315 -0
  261. package/src/components/ui/markdown/heading-ids.ts +239 -0
  262. package/src/components/ui/markdown/index.ts +49 -0
  263. package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
  264. package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
  265. package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
  266. package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
  267. package/src/components/ui/markdown/sanitize.ts +3499 -0
  268. package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
  269. package/src/components/ui/markdown/streaming.ts +362 -0
  270. package/src/components/ui/markdown/text-size.ts +106 -0
  271. package/src/components/ui/release-changelog-section.tsx +1 -1
  272. package/src/components/ui/ticket-info-section.tsx +1 -1
  273. package/src/contexts/chat-runtime-context.tsx +8 -0
  274. package/src/utils/index.ts +1 -0
  275. package/src/utils/markdown-fences.ts +127 -0
  276. package/src/utils/markdown-heading-id.ts +348 -0
  277. package/src/utils/markdown-section-extractor.ts +41 -56
  278. package/dist/chunk-2B6773QM.cjs.map +0 -1
  279. package/dist/chunk-5RXKOGH6.cjs.map +0 -1
  280. package/dist/chunk-77DFPD35.js.map +0 -1
  281. package/dist/chunk-AHDJMEEN.js.map +0 -1
  282. package/dist/chunk-FZOBQG6M.cjs.map +0 -1
  283. package/dist/chunk-H7JX2BG7.cjs.map +0 -1
  284. package/dist/chunk-KFQXO7QB.js.map +0 -1
  285. package/dist/chunk-S52LJNMU.js.map +0 -1
  286. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
  287. package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
  288. package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
  289. package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
  290. package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
  291. package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
  292. package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
  293. package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
  294. package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
  295. package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -479
  296. package/src/components/chat/utils/.chunk-parser.md +0 -62
  297. package/src/components/chat/utils/chunk-parser.ts +0 -256
  298. package/src/components/ui/.simple-markdown-renderer.md +0 -52
  299. package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
  300. package/src/components/ui/simple-markdown-renderer.tsx +0 -964
  301. /package/dist/{chunk-AJNDDXKC.js.map → chunk-4NWARV5I.js.map} +0 -0
  302. /package/dist/{chunk-HQYJINFV.js.map → chunk-CORIBKHG.js.map} +0 -0
  303. /package/dist/{chunk-PNXNWGBY.js.map → chunk-GTWO5NAY.js.map} +0 -0
  304. /package/dist/{chunk-SCCTJ2UI.js.map → chunk-HSACATCD.js.map} +0 -0
  305. /package/dist/{chunk-KLYHZRAD.js.map → chunk-PB3BTZDU.js.map} +0 -0
  306. /package/dist/{chunk-YJSKQ2ZV.js.map → chunk-RQLBHKXN.js.map} +0 -0
  307. /package/dist/{chunk-CWQACI4Z.js.map → chunk-RW2LPL72.js.map} +0 -0
  308. /package/dist/{chunk-3BFX6HJM.js.map → chunk-UUYRJPGJ.js.map} +0 -0
  309. /package/dist/{chunk-F6BYUO5U.js.map → chunk-VHG6AX5V.js.map} +0 -0
  310. /package/dist/{chunk-PXF5J24T.js.map → chunk-XQ2B74SI.js.map} +0 -0
  311. /package/dist/{chunk-LLWBPPZ4.js.map → chunk-YZ2OIYQ3.js.map} +0 -0
@@ -176,7 +176,6 @@ export * from './ticket-attachments-list'
176
176
  export * from './ticket-note-card'
177
177
  export * from './ticket-notes-section'
178
178
  export * from './marquee-wall'
179
- export * from './simple-markdown-renderer'
180
- export { RichMarkdownRenderer, type RichMarkdownRendererProps } from './rich-markdown-renderer'
179
+ export * from './markdown'
181
180
  export * from './filter-pill-row'
182
181
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Mermaid renders MODEL-authored diagram source on the chat path, and the
3
+ * result is injected with `dangerouslySetInnerHTML`. This fixture pins the
4
+ * hardening in ../mermaid-diagram.tsx: a hostile node label must NOT survive
5
+ * as live HTML.
6
+ *
7
+ * The test drives `mermaid` with the SAME security knobs the component sets,
8
+ * by IMPORTING them: `MERMAID_SECURITY_OPTIONS` is the single source of truth
9
+ * and is spread into both `mermaid.initialize` calls. There are no literals
10
+ * here to drift from the component — flipping `securityLevel` to `'loose'` in
11
+ * the constant fails this suite (verified).
12
+ *
13
+ * `secure` is part of that constant: it is the allowlist of config keys a
14
+ * `%%{init}%%` directive inside the diagram SOURCE may not override. mermaid's
15
+ * default list omits `htmlLabels`, so untrusted source could otherwise
16
+ * re-enable HTML labels while `securityLevel` stayed locked.
17
+ *
18
+ * jsdom NOTE (recorded, not proven): before `htmlLabels` was added to `secure`,
19
+ * a `%%{init: {"htmlLabels": true}}%%` directive was observed to make
20
+ * `mermaid.render` never settle under jsdom (>60s, against a passing
21
+ * two-render control). That was NOT reproduced in a real browser and may be an
22
+ * artifact of jsdom having no layout — it is NOT a confirmed browser DoS. The
23
+ * component wraps its render in a timeout regardless.
24
+ */
25
+ import { beforeAll, describe, expect, it } from 'vitest'
26
+ import { MERMAID_SECURITY_OPTIONS } from '../mermaid-diagram'
27
+
28
+ // jsdom implements no SVG layout, and mermaid's dagre pass measures text.
29
+ // These stubs are only about making the renderer RUN; they do not touch the
30
+ // sanitization path under test.
31
+ function installSvgLayoutStubs(): void {
32
+ const proto = (globalThis as unknown as { SVGElement?: { prototype: Record<string, unknown> } }).SVGElement?.prototype
33
+ if (!proto) return
34
+ proto.getBBox = function getBBox() {
35
+ return { x: 0, y: 0, width: 100, height: 20 }
36
+ }
37
+ proto.getComputedTextLength = function getComputedTextLength() {
38
+ return 100
39
+ }
40
+ proto.getScreenCTM = function getScreenCTM() {
41
+ return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0, inverse: () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }) }
42
+ }
43
+ }
44
+
45
+ // Second tuple slot: text that MUST be present in the rendered output — a
46
+ // positive control proving the diagram was actually drawn, so the negative
47
+ // assertions below can't pass on empty output. Note the two payloads are
48
+ // neutralized differently: the `<img>` survives as ESCAPED label text, while
49
+ // the `<script>` is dropped outright, so its control is the sibling node.
50
+ const HOSTILE_LABELS: ReadonlyArray<readonly [chart: string, expectedText: string]> = [
51
+ ['graph TD\n A["<img src=x onerror=alert(1)>"] --> B["ok"]', '<img'],
52
+ ['graph TD\n A["<script>alert(1)</script>"] --> B["ok"]', 'ok'],
53
+ // A `%%{init}%%` directive trying to unlock the two knobs. Both keys are in
54
+ // `secure`, so the override is rejected and the payload stays escaped.
55
+ [
56
+ '%%{init:{"securityLevel":"loose"}}%%\ngraph TD\n A["<img src=x onerror=alert(1)>"] --> B["ok"]',
57
+ '<img',
58
+ ],
59
+ [
60
+ '%%{init:{"htmlLabels":true}}%%\ngraph TD\n A["<img src=x onerror=alert(1)>"] --> B["ok"]',
61
+ '<img',
62
+ ],
63
+ // YAML front-matter config — the other in-source config channel.
64
+ [
65
+ '---\nconfig:\n securityLevel: loose\n---\ngraph TD\n A["<img src=x onerror=alert(1)>"] --> B["ok"]',
66
+ '<img',
67
+ ],
68
+ // `click` interaction directive: disabled outright by 'strict'.
69
+ ['graph TD\n A["ok"] --> B["b"]\n click A "javascript:alert(1)"', 'ok'],
70
+ ]
71
+
72
+ /** Initialize with the COMPONENT's security options — imported, never
73
+ * re-typed — so the fixture cannot drift from the renderer. */
74
+ function initializeLikeComponent(mermaid: { initialize: (c: Record<string, unknown>) => void }): void {
75
+ mermaid.initialize({
76
+ startOnLoad: false,
77
+ theme: 'dark',
78
+ flowchart: { useMaxWidth: true },
79
+ ...MERMAID_SECURITY_OPTIONS,
80
+ })
81
+ }
82
+
83
+ describe('mermaid hostile-label hardening', () => {
84
+ beforeAll(() => {
85
+ installSvgLayoutStubs()
86
+ })
87
+
88
+ it.each(HOSTILE_LABELS)('does not emit live HTML for %j', async (chart, expectedText) => {
89
+ const { default: mermaid } = await import('mermaid')
90
+
91
+ initializeLikeComponent(mermaid)
92
+
93
+ const { svg } = await mermaid.render(`mermaid-security-${Math.random().toString(36).slice(2)}`, chart)
94
+
95
+ // No live element, and no event-handler attribute, anywhere in the output.
96
+ expect(svg).not.toMatch(/<img[\s>]/i)
97
+ expect(svg).not.toMatch(/<script[\s>]/i)
98
+ expect(svg).not.toMatch(/\son[a-z]+\s*=/i)
99
+ // `htmlLabels: false` means no HTML subtree is minted for labels at all.
100
+ expect(svg).not.toMatch(/<foreignObject/i)
101
+ // 'strict' drops `click` bindings, so no javascript: URL is ever emitted.
102
+ expect(svg).not.toMatch(/javascript:/i)
103
+
104
+ // And it stays inert once parsed as HTML: the hostile markup must appear
105
+ // as TEXT (escaped), never as an element node.
106
+ const host = document.createElement('div')
107
+ host.innerHTML = svg
108
+ expect(host.querySelector('img')).toBeNull()
109
+ expect(host.querySelector('script')).toBeNull()
110
+ const text = host.textContent ?? ''
111
+ expect(text).toContain(expectedText)
112
+ // The script payload never survives in any form.
113
+ expect(text).not.toContain('alert(1)')
114
+ })
115
+ })
@@ -0,0 +1,95 @@
1
+ /**
2
+ * MermaidDiagram's render effect re-runs on every `chart` change, and during
3
+ * STREAMING the chart text grows chunk by chunk — so several renders are in
4
+ * flight at once. Without a per-effect cancellation guard a slower EARLIER
5
+ * render can resolve last and overwrite the newer output (and, on the error
6
+ * path, paint an error for a chart that is no longer displayed).
7
+ *
8
+ * The 15s render timeout does not close this: it rejects the race, but does
9
+ * not abort the underlying `mermaid.render`, so the stale write is merely
10
+ * bounded, not prevented. These fixtures pin the guard.
11
+ *
12
+ * `mermaid` is mocked with a hand-settled deferred so resolution ORDER is
13
+ * under the test's control. (The real-mermaid security fixture lives in
14
+ * ./mermaid-security.test.ts and must stay unmocked, hence a separate file.)
15
+ */
16
+ import { render, act } from '@testing-library/react'
17
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
18
+
19
+ interface Deferred {
20
+ chart: string
21
+ resolve: (value: { svg: string }) => void
22
+ reject: (err: Error) => void
23
+ }
24
+
25
+ const renders: Deferred[] = []
26
+
27
+ vi.mock('mermaid', () => ({
28
+ default: {
29
+ initialize: vi.fn(),
30
+ render: vi.fn(
31
+ (_id: string, chart: string) =>
32
+ new Promise<{ svg: string }>((resolve, reject) => {
33
+ renders.push({ chart, resolve, reject })
34
+ }),
35
+ ),
36
+ },
37
+ }))
38
+
39
+ import { MermaidDiagram } from '../mermaid-diagram'
40
+
41
+ /** Let the mocked dynamic import + the resolved promise chain flush. */
42
+ const flush = async () => {
43
+ await act(async () => {
44
+ await Promise.resolve()
45
+ await Promise.resolve()
46
+ await Promise.resolve()
47
+ })
48
+ }
49
+
50
+ const svgOf = (container: HTMLElement) =>
51
+ container.querySelector('.mermaid-svg-container')?.innerHTML ?? ''
52
+
53
+ describe('MermaidDiagram stale-render guard', () => {
54
+ beforeEach(() => {
55
+ renders.length = 0
56
+ })
57
+
58
+ it('ignores an earlier render that resolves after a newer chart', async () => {
59
+ const view = render(<MermaidDiagram chart="graph TD\n A-->B" />)
60
+ await flush()
61
+ view.rerender(<MermaidDiagram chart="graph TD\n A-->B\n B-->C" />)
62
+ await flush()
63
+ expect(renders).toHaveLength(2)
64
+
65
+ // Newer render lands first, then the abandoned earlier one.
66
+ await act(async () => {
67
+ renders[1]!.resolve({ svg: '<svg id="new"></svg>' })
68
+ })
69
+ await act(async () => {
70
+ renders[0]!.resolve({ svg: '<svg id="stale"></svg>' })
71
+ })
72
+ await flush()
73
+
74
+ expect(svgOf(view.container)).toContain('id="new"')
75
+ expect(svgOf(view.container)).not.toContain('id="stale"')
76
+ })
77
+
78
+ it('ignores a late failure from an abandoned render', async () => {
79
+ const view = render(<MermaidDiagram chart="graph TD\n A-->B" />)
80
+ await flush()
81
+ view.rerender(<MermaidDiagram chart="graph TD\n A-->B\n B-->C" />)
82
+ await flush()
83
+
84
+ await act(async () => {
85
+ renders[1]!.resolve({ svg: '<svg id="new"></svg>' })
86
+ })
87
+ await act(async () => {
88
+ renders[0]!.reject(new Error('Diagram rendering timed out after 15000ms'))
89
+ })
90
+ await flush()
91
+
92
+ expect(view.container.textContent).not.toContain('Diagram Error')
93
+ expect(svgOf(view.container)).toContain('id="new"')
94
+ })
95
+ })
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Coupled-allowlist invariant (unification plan §D1), BOTH DIRECTIONS:
3
+ * the effective pre-pass tag set and the effective sanitizer tag set are
4
+ * EQUAL (case-insensitively), computed AFTER merging `extraAllowedHtmlTags`
5
+ * into both.
6
+ * - pre-pass ⊆ sanitizer: the pre-pass must never admit a raw tag
7
+ * rehype-sanitize then silently drops.
8
+ * - sanitizer ⊆ pre-pass: the pre-pass must never ESCAPE a tag the
9
+ * sanitizer would keep (the direction that regressed `<strike>` and
10
+ * every other `defaultSchema`-only tag into visible source text).
11
+ */
12
+ import { describe, it, expect } from 'vitest'
13
+ import { defaultSchema } from 'rehype-sanitize'
14
+ import {
15
+ __buildCloserHaystackForTest,
16
+ buildEffectiveTagSet,
17
+ buildSanitizeSchema,
18
+ escapeUnknownHtmlTags,
19
+ SAFE_HTML_TAGS,
20
+ SVG_TAGS,
21
+ } from '../sanitize'
22
+
23
+ function assertEqualSets(pre: Set<string>, schemaTags: string[]) {
24
+ const sanitizerTags = new Set(schemaTags.map((t) => t.toLowerCase()))
25
+ const escapedButKept = [...sanitizerTags].filter((t) => !pre.has(t))
26
+ const admittedButDropped = [...pre].filter((t) => !sanitizerTags.has(t))
27
+ expect(
28
+ admittedButDropped,
29
+ `pre-pass admits tags the sanitizer would drop: ${admittedButDropped.join(', ')}`,
30
+ ).toEqual([])
31
+ expect(
32
+ escapedButKept,
33
+ `pre-pass escapes tags the sanitizer would keep: ${escapedButKept.join(', ')}`,
34
+ ).toEqual([])
35
+ }
36
+
37
+ describe('coupled-allowlist invariant', () => {
38
+ it('baseline: pre-pass set === sanitizer tagNames (both directions)', () => {
39
+ assertEqualSets(buildEffectiveTagSet(), buildSanitizeSchema().tagNames)
40
+ })
41
+
42
+ it('with extraAllowedHtmlTags merged into BOTH lists (rich: video/source)', () => {
43
+ const extra = ['video', 'source']
44
+ assertEqualSets(
45
+ buildEffectiveTagSet(extra),
46
+ buildSanitizeSchema({ extraAllowedHtmlTags: extra }).tagNames,
47
+ )
48
+ })
49
+
50
+ it('every defaultSchema tag survives the pre-pass (strike regression)', () => {
51
+ const pre = buildEffectiveTagSet()
52
+ for (const tag of defaultSchema.tagNames ?? []) {
53
+ expect(pre.has(tag.toLowerCase()), `pre-pass must not escape <${tag}>`).toBe(true)
54
+ }
55
+ expect(pre.has('strike')).toBe(true)
56
+ })
57
+
58
+ it('SVG elements are admitted by BOTH the pre-pass and the schema', () => {
59
+ const pre = buildEffectiveTagSet()
60
+ const schema = buildSanitizeSchema()
61
+ for (const tag of SVG_TAGS) {
62
+ expect(pre.has(tag.toLowerCase()), `pre-pass must admit <${tag}>`).toBe(true)
63
+ expect(schema.tagNames, `schema must keep <${tag}>`).toContain(tag)
64
+ }
65
+ })
66
+
67
+ it('SVG geometry/presentation attributes survive the schema', () => {
68
+ const schema = buildSanitizeSchema()
69
+ const svgAttrs = (schema.attributes?.svg ?? []).map((a) => (Array.isArray(a) ? a[0] : a))
70
+ for (const required of ['viewBox', 'xmlns', 'fill']) {
71
+ expect(svgAttrs, `svg must keep '${required}'`).toContain(required)
72
+ }
73
+ const circleAttrs = (schema.attributes?.circle ?? []).map((a) => (Array.isArray(a) ? a[0] : a))
74
+ // Only the camelCase names are load-bearing: property-information
75
+ // normalizes `stroke-width` → `strokeWidth` BEFORE the sanitizer runs, so
76
+ // the dashed spellings never matched anything and are gone.
77
+ for (const required of ['cx', 'cy', 'r', 'strokeWidth']) {
78
+ expect(circleAttrs, `circle must keep '${required}'`).toContain(required)
79
+ }
80
+ expect(circleAttrs, 'dashed spellings are dead weight').not.toContain('stroke-width')
81
+ // The exact hast spellings — a near-miss (`strokeDasharray`) fails
82
+ // silently, so pin the ones real authored SVG uses.
83
+ const textAttrs = (schema.attributes?.text ?? []).map((a) => (Array.isArray(a) ? a[0] : a))
84
+ for (const required of ['fontSize', 'textAnchor', 'dominantBaseline', 'style']) {
85
+ expect(textAttrs, `text must keep '${required}'`).toContain(required)
86
+ }
87
+ const rectAttrs = (schema.attributes?.rect ?? []).map((a) => (Array.isArray(a) ? a[0] : a))
88
+ // `strokeDashArray` — capital A — is CORRECT and verified against the
89
+ // installed `property-information`, which capitalizes the segment AFTER
90
+ // `stroke-`:
91
+ // find(svg, 'stroke-dasharray') -> 'strokeDashArray'
92
+ // find(svg, 'stroke-dashoffset') -> 'strokeDashOffset'
93
+ // find(svg, 'stroke-miterlimit') -> 'strokeMiterLimit'
94
+ // It does NOT follow the DOM/React `strokeDasharray` spelling. Reviewers
95
+ // (human and bot) keep "fixing" this to `strokeDasharray`, which would
96
+ // INTRODUCE the silent near-miss this assertion exists to catch.
97
+ for (const required of ['strokeDashArray', 'fillOpacity', 'style']) {
98
+ expect(rectAttrs, `rect must keep '${required}'`).toContain(required)
99
+ }
100
+ })
101
+
102
+ it('SVG-only tags are pinned to an svg ancestor (bare <title> cannot hijack)', () => {
103
+ const schema = buildSanitizeSchema()
104
+ // `title`/`desc`/`text`/`g`/… are ALSO HTML element names. Unconstrained,
105
+ // a bare `<title>` in a post or a chat message is hoisted into <head> by
106
+ // React 19 (tab + SEO title hijack) and swallows the rest of the document
107
+ // via its RAWTEXT content model.
108
+ for (const tag of ['title', 'desc', 'text', 'g', 'line', 'use', 'symbol', 'marker', 'mask', 'pattern']) {
109
+ expect(schema.ancestors?.[tag], `<${tag}> must require an svg ancestor`).toEqual(['svg'])
110
+ }
111
+ expect(schema.ancestors?.tspan).toEqual(['svg', 'text'])
112
+ // defaultSchema's own table constraints must survive the merge.
113
+ expect(schema.ancestors?.tbody).toEqual(defaultSchema.ancestors?.tbody)
114
+ })
115
+
116
+ it('input carries the GFM tasklist contract WITHOUT coercing authored inputs', () => {
117
+ const schema = buildSanitizeSchema()
118
+ const inputAttrs = schema.attributes?.input ?? []
119
+ const names = inputAttrs.map((a) => (Array.isArray(a) ? a[0] : a))
120
+ // `required.input` is cleared: defaultSchema force-ADDS
121
+ // `type=checkbox disabled` to EVERY input regardless of its attributes,
122
+ // so an authored `<input type="text">` came out a disabled checkbox.
123
+ expect(schema.required?.input).toBeUndefined()
124
+ // The contract now lives in the attribute allowlist: `type` is pinned to
125
+ // the literal `checkbox`, so a text input degrades to a bare `<input>`.
126
+ expect(inputAttrs).toContainEqual(['type', 'checkbox'])
127
+ expect(names).toContain('checked')
128
+ expect(names).toContain('disabled')
129
+ for (const forbidden of ['name', 'value', 'placeholder', 'readOnly']) {
130
+ expect(names, `input must NOT widen '${forbidden}'`).not.toContain(forbidden)
131
+ }
132
+ })
133
+
134
+ it('legacy presentational tags survive (center/font/big regression)', () => {
135
+ const pre = buildEffectiveTagSet()
136
+ const schema = buildSanitizeSchema()
137
+ for (const tag of ['center', 'font', 'big', 'strike', 'tt']) {
138
+ expect(pre.has(tag), `pre-pass must not escape <${tag}>`).toBe(true)
139
+ expect(schema.tagNames, `schema must keep <${tag}>`).toContain(tag)
140
+ }
141
+ const fontAttrs = (schema.attributes?.font ?? []).map((a) => (Array.isArray(a) ? a[0] : a))
142
+ for (const required of ['color', 'size', 'face']) {
143
+ expect(fontAttrs, `font must keep '${required}'`).toContain(required)
144
+ }
145
+ })
146
+
147
+ it('video keeps its source attributes through the schema (attribute survival)', () => {
148
+ const schema = buildSanitizeSchema({ extraAllowedHtmlTags: ['video', 'source'] })
149
+ const videoAttrs = (schema.attributes?.video ?? []).map((a) =>
150
+ Array.isArray(a) ? a[0] : a,
151
+ )
152
+ for (const required of ['src', 'poster', 'controls']) {
153
+ expect(videoAttrs, `video must keep '${required}'`).toContain(required)
154
+ }
155
+ const sourceAttrs = (schema.attributes?.source ?? []).map((a) =>
156
+ Array.isArray(a) ? a[0] : a,
157
+ )
158
+ expect(sourceAttrs).toContain('src')
159
+ })
160
+
161
+ it('id clobbering is disabled (raw-HTML #anchor deep-links survive)', () => {
162
+ const schema = buildSanitizeSchema()
163
+ expect(schema.clobber).toEqual([])
164
+ expect(schema.clobberPrefix).toBe('')
165
+ })
166
+
167
+ it('card/mention protocols are registered for href', () => {
168
+ const schema = buildSanitizeSchema()
169
+ expect(schema.protocols?.href).toContain('card')
170
+ expect(schema.protocols?.href).toContain('mention')
171
+ })
172
+
173
+ it('SAFE_HTML_TAGS still excludes video (chat baseline)', () => {
174
+ expect(SAFE_HTML_TAGS.has('video')).toBe(false)
175
+ })
176
+ })
177
+
178
+ /**
179
+ * ROUND-21 — the LINK REFERENCE DEFINITION spellings, pinned HERE rather than
180
+ * only in the parity corpus, exactly as the round-19/20 payload spellings are.
181
+ *
182
+ * A definition is consumed ENTIRELY by remark, which emits no node for it, so a
183
+ * `</textarea>` in its LABEL, DESTINATION or TITLE is never a real closer. The
184
+ * pass that blanks them used to be a set of line REGEXES, and a regex that fails
185
+ * to match leaves the line VISIBLE — the fail-OPEN direction. Two shape
186
+ * dimensions were never examined:
187
+ *
188
+ * ESCAPED DELIMITERS — `[^"\n]*` / `[^'\n]*` / `[^)\n]*` / `[^\]\n]*` stop at
189
+ * the first delimiter, escaped or not, while CommonMark permits `\"`, `\'`,
190
+ * `\)` and `\]` inside them.
191
+ *
192
+ * MULTI-LINE LABEL / TITLE — the continuation state modelled only what comes
193
+ * AFTER the `]:`, and `blankBracketLabels` deliberately excludes a bare `[…]`,
194
+ * so a label spanning lines had no pass at all.
195
+ *
196
+ * Every row below was a byte-identical no-op with one live RAWTEXT element.
197
+ * These assert on the MASK directly (no renderer), so a regression is pinned at
198
+ * the pass rather than at a rendered snapshot.
199
+ */
200
+ describe('link-definition shelters blank in every shape (round 21)', () => {
201
+ const PRE = 'The <textarea> element explained.\n\n'
202
+ const TAIL = '\n\n## After heading\n\nsecret tail\n'
203
+
204
+ it.each([
205
+ // The unescaped CONTROL — already correct before round 21, which is what
206
+ // makes the ESCAPE (not the shape) the cause of the rows below it.
207
+ ['control: unescaped title', '[a]: /x "</textarea>"'],
208
+ ['escaped double quote in title', '[a]: /x "a\\"</textarea>"'],
209
+ ["escaped apostrophe in title", "[a]: /x 'it\\'s </textarea>'"],
210
+ ['escaped paren in title', '[a]: /x (a\\)</textarea>)'],
211
+ ['escaped bracket in label, title shelter', '[a\\]b]: /x "</textarea>"'],
212
+ ['escaped bracket in label, destination shelter', '[a\\]b]: </textarea>'],
213
+ ['multi-line label', '[foo\n</textarea>]: /x'],
214
+ ['multi-line label, closer on the FIRST line', '[</textarea>\nfoo]: /x'],
215
+ ['three-line label', '[foo\n</textarea>\nbar]: /x'],
216
+ ['blockquoted multi-line label', '> [foo\n> </textarea>]: /x'],
217
+ ['multi-line title', '[a]: /x "line1\n</textarea>"'],
218
+ ])('blanks the sheltered closer: %s', (_label, body) => {
219
+ const md = PRE + body + TAIL
220
+ const masked = __buildCloserHaystackForTest(md)
221
+ // The mask is LENGTH-PRESERVING, and the fake closer is gone from it…
222
+ expect(masked.length).toBe(md.length)
223
+ expect(masked).not.toContain('</textarea>')
224
+ // …so the prose opener above it is escaped rather than left live.
225
+ expect(escapeUnknownHtmlTags(md)).toContain('&lt;textarea&gt;')
226
+ })
227
+
228
+ /**
229
+ * THE RECOGNITION BOUNDARY, the complement that keeps the widened parser from
230
+ * eating prose. In each of these remark EMITS the text, so the closer is REAL
231
+ * and must stay VISIBLE in the haystack.
232
+ */
233
+ it.each([
234
+ ['no `]:` arrives in the paragraph', 'The <textarea> element.\n\n[foo\n</textarea> bar\n'],
235
+ ['`]` not followed by `:`', 'The <textarea> element.\n\n[foo\n</textarea>] bar\n'],
236
+ ['unescaped `[` inside the label', 'The <textarea> element.\n\n[foo[bar\n</textarea>]: /x\n'],
237
+ ['trailing content after a title', 'The <textarea> element.\n\n[a]: /x "t" </textarea>\n'],
238
+ ['trailing content after a destination', 'The <textarea> element.\n\n[a]: /x </textarea>\n'],
239
+ ['continuation line that is not a title', 'The <textarea> element.\n\n[a]: /x\n</textarea> is real\n'],
240
+ ['GFM footnote definition (block-parsed body)', 'Body[^a].\n\n[^a]: <textarea>hi</textarea>\n'],
241
+ ])('leaves a NON-definition visible: %s', (_label, md) => {
242
+ const masked = __buildCloserHaystackForTest(md)
243
+ expect(masked.length).toBe(md.length)
244
+ expect(masked).toContain('</textarea>')
245
+ })
246
+
247
+ /**
248
+ * THE CONSUMPTION SIDE: a title that opens and never closes is BLANKED to the
249
+ * paragraph bound, never declined — and the blanking STOPS at that bound, so
250
+ * the pass cannot run away down the document.
251
+ */
252
+ /**
253
+ * ROUND-23 — the FOOTNOTE-REFERENCE half, pinned at the MASK for the same
254
+ * reason: `blankUnreferencedFootnotes` decides whether a definition's body
255
+ * stays in the haystack, and it decides it from a REFERENCE COUNT. Counting
256
+ * that on the current mask made every `[^…]` remark consumes into an
257
+ * ATTRIBUTE or drops outright look like a live reference (a PHANTOM), so the
258
+ * definition — and the `</textarea>` in it — stayed visible and the opener
259
+ * above stayed LIVE. References now come from a separate, more-blanked copy.
260
+ */
261
+ const FOOT_TAIL = '\n\n[^f]: b </textarea>\n\n## After heading\n'
262
+ it.each([
263
+ ['image alt', 'v ![[^f]](/i.png) t'],
264
+ ['full-reference label', 'v [txt][[^f]] t'],
265
+ ['raw HTML block', 'v\n\n<div>\n[^f]\n</div>'],
266
+ ['inline link title', 'v [a](/x "[^f]") t'],
267
+ ['image title', "v ![a](/x '[^f]') t"],
268
+ ['angle destination', 'v [a](<[^f]>) t'],
269
+ ['HTML comment', 'v\n\n<!-- [^f] -->'],
270
+ ['inline tag attribute', 'v <span title="[^f]">x</span> t'],
271
+ ['angle autolink', 'v <https://e.example/[^f]> t'],
272
+ ['literal autolink', 'v https://e.example/x[^f]y t'],
273
+ ])('blanks a definition whose only "reference" is a PHANTOM: %s', (_label, body) => {
274
+ const md = PRE + body + FOOT_TAIL
275
+ const masked = __buildCloserHaystackForTest(md)
276
+ expect(masked.length).toBe(md.length)
277
+ expect(masked).not.toContain('</textarea>')
278
+ expect(escapeUnknownHtmlTags(md)).toContain('&lt;textarea&gt;')
279
+ })
280
+
281
+ /**
282
+ * The complement: where remark really DOES resolve the reference, the
283
+ * definition body is document text, so its closer must stay VISIBLE. Over-
284
+ * blanking here would show a genuine element as escaped source.
285
+ */
286
+ it.each([
287
+ ['plain reference', 'v [^f] t'],
288
+ ['inline link text', 'v [[^f]](/x) t'],
289
+ ['shortcut reference text', 'v [[^f]] t'],
290
+ ['table cell', '| h |\n| - |\n| [^f] |'],
291
+ ['after an autolink', 'v https://e.example/x [^f] t'],
292
+ ['email autolink (the `[` voids it)', 'v <a[^f]b@e.example> t'],
293
+ ])('keeps a GENUINELY referenced definition visible: %s', (_label, body) => {
294
+ const md = PRE + body + FOOT_TAIL
295
+ const masked = __buildCloserHaystackForTest(md)
296
+ expect(masked.length).toBe(md.length)
297
+ expect(masked).toContain('</textarea>')
298
+ })
299
+
300
+ it('blanks an unterminated title to the paragraph bound, and no further', () => {
301
+ const md =
302
+ 'The <textarea> element explained.\n\n[a]: /x "never closes\n</textarea>\nstill inside\n\n## After heading\n'
303
+ const masked = __buildCloserHaystackForTest(md)
304
+ expect(masked.length).toBe(md.length)
305
+ expect(masked).not.toContain('</textarea>')
306
+ // The haystack is case-folded; the heading past the bound is untouched.
307
+ expect(masked).toContain('## after heading')
308
+ })
309
+ })