@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.
- package/dist/chat-protocol/decode.d.ts +58 -0
- package/dist/chat-protocol/decode.d.ts.map +1 -0
- package/dist/chat-protocol/encode.d.ts +44 -0
- package/dist/chat-protocol/encode.d.ts.map +1 -0
- package/dist/chat-protocol/env-flag.d.ts +35 -0
- package/dist/chat-protocol/env-flag.d.ts.map +1 -0
- package/dist/chat-protocol/events.d.ts +196 -0
- package/dist/chat-protocol/events.d.ts.map +1 -0
- package/dist/chat-protocol/frames.d.ts +213 -0
- package/dist/chat-protocol/frames.d.ts.map +1 -0
- package/dist/chat-protocol/index.cjs +494 -0
- package/dist/chat-protocol/index.cjs.map +1 -0
- package/dist/chat-protocol/index.d.ts +18 -0
- package/dist/chat-protocol/index.d.ts.map +1 -0
- package/dist/chat-protocol/index.js +478 -0
- package/dist/chat-protocol/index.js.map +1 -0
- package/dist/chat-protocol/ip-normalize.d.ts +44 -0
- package/dist/chat-protocol/ip-normalize.d.ts.map +1 -0
- package/dist/chat-protocol/nats-decoder.d.ts +26 -0
- package/dist/chat-protocol/nats-decoder.d.ts.map +1 -0
- package/dist/{chunk-AJNDDXKC.js → chunk-4NWARV5I.js} +3 -3
- package/dist/{chunk-GQX6LFFA.cjs → chunk-5CJR2GZ4.cjs} +42 -42
- package/dist/{chunk-GQX6LFFA.cjs.map → chunk-5CJR2GZ4.cjs.map} +1 -1
- package/dist/{chunk-EUYIFALO.cjs → chunk-5EYRXSY5.cjs} +14 -14
- package/dist/{chunk-EUYIFALO.cjs.map → chunk-5EYRXSY5.cjs.map} +1 -1
- package/dist/{chunk-5RXKOGH6.cjs → chunk-5LMGFI4J.cjs} +7011 -4777
- package/dist/chunk-5LMGFI4J.cjs.map +1 -0
- package/dist/{chunk-KFQXO7QB.js → chunk-6GT557KN.js} +1 -1
- package/dist/chunk-6GT557KN.js.map +1 -0
- package/dist/{chunk-77DFPD35.js → chunk-BE6DK7QY.js} +12267 -10033
- package/dist/chunk-BE6DK7QY.js.map +1 -0
- package/dist/{chunk-A7IPON2V.cjs → chunk-BIKOJ4FH.cjs} +5 -5
- package/dist/{chunk-A7IPON2V.cjs.map → chunk-BIKOJ4FH.cjs.map} +1 -1
- package/dist/{chunk-HQYJINFV.js → chunk-CORIBKHG.js} +4 -4
- package/dist/{chunk-YEEHB4T6.cjs → chunk-CSYSYJCF.cjs} +40 -40
- package/dist/{chunk-YEEHB4T6.cjs.map → chunk-CSYSYJCF.cjs.map} +1 -1
- package/dist/{chunk-SIKCXKGB.cjs → chunk-DSDZ5T4P.cjs} +7 -7
- package/dist/{chunk-SIKCXKGB.cjs.map → chunk-DSDZ5T4P.cjs.map} +1 -1
- package/dist/{chunk-PNXNWGBY.js → chunk-GTWO5NAY.js} +2 -2
- package/dist/{chunk-SCCTJ2UI.js → chunk-HSACATCD.js} +4 -4
- package/dist/{chunk-PECGZWQR.cjs → chunk-KP5KRRD6.cjs} +3 -3
- package/dist/{chunk-PECGZWQR.cjs.map → chunk-KP5KRRD6.cjs.map} +1 -1
- package/dist/{chunk-2B6773QM.cjs → chunk-LILQJ3UZ.cjs} +1 -1
- package/dist/chunk-LILQJ3UZ.cjs.map +1 -0
- package/dist/{chunk-RZSA6MZL.cjs → chunk-NWX7MMF5.cjs} +15 -15
- package/dist/{chunk-RZSA6MZL.cjs.map → chunk-NWX7MMF5.cjs.map} +1 -1
- package/dist/{chunk-V3CNNMY3.cjs → chunk-P4WTZU6L.cjs} +18 -18
- package/dist/{chunk-V3CNNMY3.cjs.map → chunk-P4WTZU6L.cjs.map} +1 -1
- package/dist/{chunk-KLYHZRAD.js → chunk-PB3BTZDU.js} +2 -2
- package/dist/{chunk-FZOBQG6M.cjs → chunk-Q6E7VJ4W.cjs} +37 -36
- package/dist/chunk-Q6E7VJ4W.cjs.map +1 -0
- package/dist/{chunk-EYEIVYYF.cjs → chunk-QSCHCZL3.cjs} +7 -7
- package/dist/{chunk-EYEIVYYF.cjs.map → chunk-QSCHCZL3.cjs.map} +1 -1
- package/dist/{chunk-YJSKQ2ZV.js → chunk-RQLBHKXN.js} +3 -3
- package/dist/{chunk-CWQACI4Z.js → chunk-RW2LPL72.js} +2 -2
- package/dist/{chunk-DX6AOHNU.cjs → chunk-S2NHJDKQ.cjs} +77 -77
- package/dist/{chunk-DX6AOHNU.cjs.map → chunk-S2NHJDKQ.cjs.map} +1 -1
- package/dist/{chunk-AHDJMEEN.js → chunk-T2EGSZFN.js} +9 -8
- package/dist/chunk-T2EGSZFN.js.map +1 -0
- package/dist/{chunk-S52LJNMU.js → chunk-TKN5667N.js} +13 -13
- package/dist/chunk-TKN5667N.js.map +1 -0
- package/dist/{chunk-3BFX6HJM.js → chunk-UUYRJPGJ.js} +6 -6
- package/dist/{chunk-F6BYUO5U.js → chunk-VHG6AX5V.js} +3 -3
- package/dist/{chunk-FOQV6RCO.cjs → chunk-VQ523ORK.cjs} +29 -29
- package/dist/{chunk-FOQV6RCO.cjs.map → chunk-VQ523ORK.cjs.map} +1 -1
- package/dist/{chunk-PXF5J24T.js → chunk-XQ2B74SI.js} +2 -2
- package/dist/{chunk-H7JX2BG7.cjs → chunk-YHOIAA2E.cjs} +76 -76
- package/dist/chunk-YHOIAA2E.cjs.map +1 -0
- package/dist/{chunk-LLWBPPZ4.js → chunk-YZ2OIYQ3.js} +6 -6
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/chat-message-list.d.ts.map +1 -1
- package/dist/components/chat/hooks/index.d.ts +0 -1
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chat-history-hydration.d.ts +47 -0
- package/dist/components/chat/hooks/use-chat-history-hydration.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-chat.d.ts +1 -0
- package/dist/components/chat/hooks/use-chat.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +15 -48
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts +10 -53
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +12 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +23 -15
- package/dist/components/chat/stream/chat-dialog-store.d.ts +184 -0
- package/dist/components/chat/stream/chat-dialog-store.d.ts.map +1 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts +367 -0
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/stream/delta-batcher.d.ts +51 -0
- package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -0
- package/dist/components/chat/stream/index.d.ts +17 -0
- package/dist/components/chat/stream/index.d.ts.map +1 -0
- package/dist/components/chat/stream/message-mutations.d.ts +77 -0
- package/dist/components/chat/stream/message-mutations.d.ts.map +1 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts +22 -0
- package/dist/components/chat/stream/use-chat-stream-reducer.d.ts.map +1 -0
- package/dist/components/chat/types/api.types.d.ts +1 -81
- package/dist/components/chat/types/api.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +2 -90
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +5 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chat-conversation-storage.d.ts +34 -0
- package/dist/components/chat/utils/chat-conversation-storage.d.ts.map +1 -0
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts +34 -6
- package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
- package/dist/components/chat/utils/index.d.ts +2 -3
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts +59 -11
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +182 -140
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +66 -24
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/index.cjs +46 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -2
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +57 -15
- package/dist/components/ui/markdown/base-components.d.ts +51 -0
- package/dist/components/ui/markdown/base-components.d.ts.map +1 -0
- package/dist/components/ui/markdown/engine.d.ts +99 -0
- package/dist/components/ui/markdown/engine.d.ts.map +1 -0
- package/dist/components/ui/markdown/heading-ids.d.ts +65 -0
- package/dist/components/ui/markdown/heading-ids.d.ts.map +1 -0
- package/dist/components/ui/markdown/index.d.ts +18 -0
- package/dist/components/ui/markdown/index.d.ts.map +1 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts +79 -0
- package/dist/components/ui/markdown/mermaid-diagram.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts +14 -0
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts +40 -0
- package/dist/components/ui/markdown/rich/rich-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts +8 -0
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -0
- package/dist/components/ui/markdown/sanitize.d.ts +153 -0
- package/dist/components/ui/markdown/sanitize.d.ts.map +1 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts +23 -0
- package/dist/components/ui/markdown/simple-markdown-renderer.d.ts.map +1 -0
- package/dist/components/ui/markdown/streaming.d.ts +78 -0
- package/dist/components/ui/markdown/streaming.d.ts.map +1 -0
- package/dist/components/ui/markdown/text-size.d.ts +34 -0
- package/dist/components/ui/markdown/text-size.d.ts.map +1 -0
- package/dist/contexts/chat-runtime-context.d.ts +8 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +62 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +73 -15
- package/dist/utils/index.cjs +169 -40
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +162 -41
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/markdown-fences.d.ts +42 -0
- package/dist/utils/markdown-fences.d.ts.map +1 -0
- package/dist/utils/markdown-heading-id.d.ts +85 -0
- package/dist/utils/markdown-heading-id.d.ts.map +1 -0
- package/dist/utils/markdown-section-extractor.d.ts.map +1 -1
- package/package.json +7 -1
- package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +307 -0
- package/src/chat-protocol/__tests__/chat-protocol.test.ts +516 -0
- package/src/chat-protocol/__tests__/env-flag.test.ts +52 -0
- package/src/chat-protocol/__tests__/ip-normalize.test.ts +136 -0
- package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +228 -0
- package/src/chat-protocol/decode.ts +278 -0
- package/src/chat-protocol/encode.ts +71 -0
- package/src/chat-protocol/env-flag.ts +39 -0
- package/src/chat-protocol/events.ts +232 -0
- package/src/chat-protocol/frames.ts +245 -0
- package/src/chat-protocol/index.ts +21 -0
- package/src/chat-protocol/ip-normalize.ts +146 -0
- package/src/chat-protocol/nats-decoder.ts +252 -0
- package/src/components/chat/__tests__/chat-message-list.test.tsx +293 -5
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +209 -0
- package/src/components/chat/chat-message-enhanced.tsx +111 -17
- package/src/components/chat/chat-message-list.tsx +358 -109
- package/src/components/chat/embeddable-chat.tsx +6 -0
- package/src/components/chat/hooks/.index.md +30 -33
- package/src/components/chat/hooks/.use-nats-chat-adapter.md +36 -55
- package/src/components/chat/hooks/__tests__/__snapshots__/conversation-id-persistence-golden.test.ts.snap +37 -0
- package/src/components/chat/hooks/__tests__/__snapshots__/sse-stream-golden.test.ts.snap +0 -0
- package/src/components/chat/hooks/__tests__/conversation-id-persistence-golden.test.ts +438 -0
- package/src/components/chat/hooks/__tests__/sse-stream-golden.test.ts +318 -0
- package/src/components/chat/hooks/index.ts +0 -1
- package/src/components/chat/hooks/use-chat-history-hydration.ts +142 -0
- package/src/components/chat/hooks/use-chat.ts +20 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +243 -823
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +417 -774
- package/src/components/chat/index.ts +5 -0
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +940 -0
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +756 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +460 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +894 -0
- package/src/components/chat/stream/__tests__/delta-batcher.test.ts +255 -0
- package/src/components/chat/stream/__tests__/use-chat-stream-reducer.test.ts +98 -0
- package/src/components/chat/stream/chat-dialog-store.ts +536 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +1796 -0
- package/src/components/chat/stream/delta-batcher.ts +159 -0
- package/src/components/chat/stream/index.ts +55 -0
- package/src/components/chat/stream/message-mutations.ts +338 -0
- package/src/components/chat/stream/use-chat-stream-reducer.ts +126 -0
- package/src/components/chat/thinking-display.tsx +1 -1
- package/src/components/chat/types/.api.types.md +39 -49
- package/src/components/chat/types/.processing.types.md +29 -58
- package/src/components/chat/types/api.types.ts +6 -73
- package/src/components/chat/types/processing.types.ts +11 -52
- package/src/components/chat/types/unified-chat-state.types.ts +6 -0
- package/src/components/chat/utils/.index.md +31 -40
- package/src/components/chat/utils/__tests__/__snapshots__/process-historical-messages-golden.test.ts.snap +420 -0
- package/src/components/chat/utils/__tests__/__snapshots__/segment-accumulator-golden.test.ts.snap +605 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +317 -0
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
- package/src/components/chat/utils/chat-conversation-storage.ts +83 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +63 -5
- package/src/components/chat/utils/index.ts +5 -8
- package/src/components/chat/utils/process-historical-messages.ts +352 -384
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +22 -4
- package/src/components/shared/legal-document/legal-document-page.tsx +1 -1
- package/src/components/shared/product-release/release-detail-page.tsx +19 -4
- package/src/components/ui/__tests__/__snapshots__/markdown-parity.test.tsx.snap +2420 -0
- package/src/components/ui/__tests__/markdown-parity.test.tsx +2528 -0
- package/src/components/ui/index.ts +1 -2
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +115 -0
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +95 -0
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +309 -0
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +388 -0
- package/src/components/ui/markdown/base-components.tsx +360 -0
- package/src/components/ui/markdown/engine.tsx +315 -0
- package/src/components/ui/markdown/heading-ids.ts +239 -0
- package/src/components/ui/markdown/index.ts +49 -0
- package/src/components/ui/markdown/mermaid-diagram.tsx +291 -0
- package/src/components/ui/markdown/rich/embed-overrides.tsx +199 -0
- package/src/components/ui/markdown/rich/rich-markdown-renderer.tsx +184 -0
- package/src/components/ui/markdown/rich/shortcodes.ts +170 -0
- package/src/components/ui/markdown/sanitize.ts +3499 -0
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
- package/src/components/ui/markdown/streaming.ts +362 -0
- package/src/components/ui/markdown/text-size.ts +106 -0
- package/src/components/ui/release-changelog-section.tsx +1 -1
- package/src/components/ui/ticket-info-section.tsx +1 -1
- package/src/contexts/chat-runtime-context.tsx +8 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown-fences.ts +127 -0
- package/src/utils/markdown-heading-id.ts +348 -0
- package/src/utils/markdown-section-extractor.ts +41 -56
- package/dist/chunk-2B6773QM.cjs.map +0 -1
- package/dist/chunk-5RXKOGH6.cjs.map +0 -1
- package/dist/chunk-77DFPD35.js.map +0 -1
- package/dist/chunk-AHDJMEEN.js.map +0 -1
- package/dist/chunk-FZOBQG6M.cjs.map +0 -1
- package/dist/chunk-H7JX2BG7.cjs.map +0 -1
- package/dist/chunk-KFQXO7QB.js.map +0 -1
- package/dist/chunk-S52LJNMU.js.map +0 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts +0 -6
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +0 -1
- package/dist/components/chat/utils/chunk-parser.d.ts +0 -25
- package/dist/components/chat/utils/chunk-parser.d.ts.map +0 -1
- package/dist/components/ui/rich-markdown-renderer.d.ts +0 -34
- package/dist/components/ui/rich-markdown-renderer.d.ts.map +0 -1
- package/dist/components/ui/simple-markdown-renderer.d.ts +0 -73
- package/dist/components/ui/simple-markdown-renderer.d.ts.map +0 -1
- package/src/components/chat/hooks/.use-realtime-chunk-processor.md +0 -77
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +0 -479
- package/src/components/chat/utils/.chunk-parser.md +0 -62
- package/src/components/chat/utils/chunk-parser.ts +0 -256
- package/src/components/ui/.simple-markdown-renderer.md +0 -52
- package/src/components/ui/rich-markdown-renderer.tsx +0 -1223
- package/src/components/ui/simple-markdown-renderer.tsx +0 -964
- /package/dist/{chunk-AJNDDXKC.js.map → chunk-4NWARV5I.js.map} +0 -0
- /package/dist/{chunk-HQYJINFV.js.map → chunk-CORIBKHG.js.map} +0 -0
- /package/dist/{chunk-PNXNWGBY.js.map → chunk-GTWO5NAY.js.map} +0 -0
- /package/dist/{chunk-SCCTJ2UI.js.map → chunk-HSACATCD.js.map} +0 -0
- /package/dist/{chunk-KLYHZRAD.js.map → chunk-PB3BTZDU.js.map} +0 -0
- /package/dist/{chunk-YJSKQ2ZV.js.map → chunk-RQLBHKXN.js.map} +0 -0
- /package/dist/{chunk-CWQACI4Z.js.map → chunk-RW2LPL72.js.map} +0 -0
- /package/dist/{chunk-3BFX6HJM.js.map → chunk-UUYRJPGJ.js.map} +0 -0
- /package/dist/{chunk-F6BYUO5U.js.map → chunk-VHG6AX5V.js.map} +0 -0
- /package/dist/{chunk-PXF5J24T.js.map → chunk-XQ2B74SI.js.map} +0 -0
- /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 './
|
|
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('<textarea>')
|
|
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  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('<textarea>')
|
|
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
|
+
})
|