@flamingo-stack/openframe-frontend-core 0.0.508 → 0.0.509
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 +216 -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 +506 -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 +490 -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-3SQ5KXHQ.cjs → chunk-2T4GTV25.cjs} +9 -9
- package/dist/{chunk-3SQ5KXHQ.cjs.map → chunk-2T4GTV25.cjs.map} +1 -1
- package/dist/{chunk-CWAOV2FK.cjs → chunk-3CGZPAGR.cjs} +3 -3
- package/dist/{chunk-CWAOV2FK.cjs.map → chunk-3CGZPAGR.cjs.map} +1 -1
- package/dist/{chunk-TLMJHMXJ.js → chunk-3QSR22IC.js} +12 -12
- package/dist/chunk-3QSR22IC.js.map +1 -0
- package/dist/{chunk-TWYNR4TZ.cjs → chunk-4G5TLHJD.cjs} +7141 -4799
- package/dist/chunk-4G5TLHJD.cjs.map +1 -0
- package/dist/{chunk-ADPMHWOE.js → chunk-72XAID7Y.js} +4 -4
- package/dist/{chunk-PAGKRNWK.js → chunk-7WZHBQ4J.js} +2 -2
- package/dist/{chunk-OCEKO5CW.js → chunk-AEELJC4N.js} +12106 -9764
- package/dist/chunk-AEELJC4N.js.map +1 -0
- package/dist/{chunk-Z42BGM6Q.cjs → chunk-BT2WWDFP.cjs} +5 -5
- package/dist/{chunk-Z42BGM6Q.cjs.map → chunk-BT2WWDFP.cjs.map} +1 -1
- package/dist/{chunk-7LEL3RIX.cjs → chunk-C7OY3PQP.cjs} +11 -11
- package/dist/{chunk-7LEL3RIX.cjs.map → chunk-C7OY3PQP.cjs.map} +1 -1
- package/dist/{chunk-RHGGEKPQ.cjs → chunk-CIOLOQKQ.cjs} +4 -4
- package/dist/{chunk-RHGGEKPQ.cjs.map → chunk-CIOLOQKQ.cjs.map} +1 -1
- package/dist/{chunk-TSZHM74B.js → chunk-DC2TKS7C.js} +2 -2
- package/dist/{chunk-M2GOR3XQ.cjs → chunk-DYPYR7FR.cjs} +87 -87
- package/dist/{chunk-M2GOR3XQ.cjs.map → chunk-DYPYR7FR.cjs.map} +1 -1
- package/dist/{chunk-I64ABCDX.js → chunk-I2B6X77L.js} +2 -2
- package/dist/{chunk-NIZDKTGL.cjs → chunk-K5MSCMDK.cjs} +37 -37
- package/dist/{chunk-NIZDKTGL.cjs.map → chunk-K5MSCMDK.cjs.map} +1 -1
- package/dist/{chunk-DTYRYB2N.js → chunk-K6QTZ7CZ.js} +2 -2
- package/dist/{chunk-4LTMXDUS.js → chunk-KBN7PMFT.js} +2 -2
- package/dist/{chunk-TBMV7I5N.js → chunk-KXZO2WZX.js} +2 -2
- package/dist/{chunk-QTYZMP6D.cjs → chunk-LBAKLT6T.cjs} +62 -62
- package/dist/chunk-LBAKLT6T.cjs.map +1 -0
- package/dist/{chunk-KEBYLU3U.js → chunk-LFUPI7VX.js} +2 -2
- package/dist/{chunk-LDTR4IVZ.cjs → chunk-M37PBG4N.cjs} +31 -31
- package/dist/{chunk-LDTR4IVZ.cjs.map → chunk-M37PBG4N.cjs.map} +1 -1
- package/dist/{chunk-WA7RR64F.js → chunk-PO5O654H.js} +6 -5
- package/dist/chunk-PO5O654H.js.map +1 -0
- package/dist/{chunk-WK4N5VBX.cjs → chunk-QJJISLPA.cjs} +28 -27
- package/dist/chunk-QJJISLPA.cjs.map +1 -0
- package/dist/{chunk-2F44PLTV.cjs → chunk-T7MCSA55.cjs} +26 -26
- package/dist/{chunk-2F44PLTV.cjs.map → chunk-T7MCSA55.cjs.map} +1 -1
- package/dist/{chunk-WJQHJD7J.js → chunk-TWSKTJHW.js} +2 -2
- package/dist/{chunk-ZSQHZYCO.cjs → chunk-UQDMJ6DH.cjs} +13 -13
- package/dist/{chunk-ZSQHZYCO.cjs.map → chunk-UQDMJ6DH.cjs.map} +1 -1
- package/dist/{chunk-TZRUCD56.js → chunk-V3ZAAFE3.js} +5 -5
- package/dist/{chunk-LRGHJPET.js → chunk-XGIFOHKE.js} +2 -2
- package/dist/{chunk-POOMO3PA.cjs → chunk-YQEPRYT5.cjs} +7 -7
- package/dist/{chunk-POOMO3PA.cjs.map → chunk-YQEPRYT5.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- 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.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-chunk-catchup.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 -57
- package/dist/components/chat/hooks/use-sse-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +10 -2
- 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 +21 -13
- 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 +380 -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 -93
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/types/unified-chat-state.types.d.ts +15 -0
- package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
- 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/history-merge.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 +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +174 -132
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +65 -23
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +34 -34
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/index.cjs +44 -2
- 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 +55 -13
- 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/index.cjs +60 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +71 -13
- 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 +319 -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 +237 -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 +254 -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 +264 -0
- package/src/components/chat/.chat-message-list.md +10 -6
- package/src/components/chat/__tests__/chat-message-list.test.tsx +450 -9
- package/src/components/chat/__tests__/chat-message-streaming-memo.test.tsx +207 -0
- package/src/components/chat/__tests__/chat-pending-turn.test.tsx +250 -0
- package/src/components/chat/chat-message-enhanced.tsx +122 -20
- package/src/components/chat/chat-message-list.tsx +515 -133
- 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__/chunk-catchup-dialog-staleness.test.ts +92 -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.ts +8 -0
- package/src/components/chat/hooks/use-chunk-catchup.ts +26 -1
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +254 -825
- package/src/components/chat/hooks/use-sse-chat-adapter.ts +380 -684
- package/src/components/chat/index.ts +5 -0
- package/src/components/chat/stream/__tests__/__snapshots__/chat-stream-reducer-golden.test.ts.snap +944 -0
- package/src/components/chat/stream/__tests__/chat-dialog-store.test.ts +806 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer-golden.test.ts +467 -0
- package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +1330 -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 +105 -0
- package/src/components/chat/stream/chat-dialog-store.ts +536 -0
- package/src/components/chat/stream/chat-stream-reducer.ts +1928 -0
- package/src/components/chat/stream/delta-batcher.ts +168 -0
- package/src/components/chat/stream/index.ts +55 -0
- package/src/components/chat/stream/message-mutations.ts +340 -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/.unified-chat-state.types.md +2 -2
- package/src/components/chat/types/api.types.ts +6 -73
- package/src/components/chat/types/processing.types.ts +11 -53
- package/src/components/chat/types/unified-chat-state.types.ts +16 -0
- package/src/components/chat/utils/.extract-incomplete-message-state.md +21 -3
- package/src/components/chat/utils/.history-merge.md +44 -3
- 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__/extract-incomplete-message-state.test.ts +106 -0
- package/src/components/chat/utils/__tests__/history-merge.test.ts +160 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +61 -0
- package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +394 -0
- package/src/components/chat/utils/__tests__/segment-accumulator-golden.test.ts +270 -0
- package/src/components/chat/utils/extract-incomplete-message-state.ts +93 -6
- package/src/components/chat/utils/history-merge.ts +85 -5
- package/src/components/chat/utils/index.ts +5 -8
- package/src/components/chat/utils/process-historical-messages.ts +361 -385
- 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 +2540 -0
- package/src/components/ui/index.ts +1 -2
- package/src/components/ui/markdown/__tests__/mermaid-security.test.ts +176 -0
- package/src/components/ui/markdown/__tests__/mermaid-stale-render.test.tsx +151 -0
- package/src/components/ui/markdown/__tests__/sanitize-invariant.test.ts +354 -0
- package/src/components/ui/markdown/__tests__/sanitize-render.test.tsx +118 -0
- package/src/components/ui/markdown/__tests__/streaming.test.tsx +436 -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 +323 -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 +3584 -0
- package/src/components/ui/markdown/simple-markdown-renderer.tsx +28 -0
- package/src/components/ui/markdown/streaming.ts +390 -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/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-OCEKO5CW.js.map +0 -1
- package/dist/chunk-QTYZMP6D.cjs.map +0 -1
- package/dist/chunk-TLMJHMXJ.js.map +0 -1
- package/dist/chunk-TWYNR4TZ.cjs.map +0 -1
- package/dist/chunk-WA7RR64F.js.map +0 -1
- package/dist/chunk-WK4N5VBX.cjs.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 -489
- package/src/components/chat/utils/.chunk-parser.md +0 -62
- package/src/components/chat/utils/chunk-parser.ts +0 -262
- 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-ADPMHWOE.js.map → chunk-72XAID7Y.js.map} +0 -0
- /package/dist/{chunk-PAGKRNWK.js.map → chunk-7WZHBQ4J.js.map} +0 -0
- /package/dist/{chunk-TSZHM74B.js.map → chunk-DC2TKS7C.js.map} +0 -0
- /package/dist/{chunk-I64ABCDX.js.map → chunk-I2B6X77L.js.map} +0 -0
- /package/dist/{chunk-DTYRYB2N.js.map → chunk-K6QTZ7CZ.js.map} +0 -0
- /package/dist/{chunk-4LTMXDUS.js.map → chunk-KBN7PMFT.js.map} +0 -0
- /package/dist/{chunk-TBMV7I5N.js.map → chunk-KXZO2WZX.js.map} +0 -0
- /package/dist/{chunk-KEBYLU3U.js.map → chunk-LFUPI7VX.js.map} +0 -0
- /package/dist/{chunk-WJQHJD7J.js.map → chunk-TWSKTJHW.js.map} +0 -0
- /package/dist/{chunk-TZRUCD56.js.map → chunk-V3ZAAFE3.js.map} +0 -0
- /package/dist/{chunk-LRGHJPET.js.map → chunk-XGIFOHKE.js.map} +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rich-composition component overrides: the embed layer (Video SSOT,
|
|
5
|
+
* Reddit/Twitter/LinkedIn embeds, OG link previews, Figma, MarkdownImage).
|
|
6
|
+
*
|
|
7
|
+
* These imports are deliberately isolated in this file — keeping them out
|
|
8
|
+
* of the engine and the simple composition keeps the CHAT bundle free of
|
|
9
|
+
* embed code (a runtime `variant` prop could not be tree-shaken; a
|
|
10
|
+
* separate composition module can).
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import type { Components } from 'react-markdown';
|
|
14
|
+
import { RedditEmbedClient } from '../../../embeds/reddit-embed-client';
|
|
15
|
+
import { TwitterEmbedClient } from '../../../embeds/twitter-embed-client';
|
|
16
|
+
import { LinkedInEmbedClient } from '../../../embeds/linkedin-embed-client';
|
|
17
|
+
import { Video } from '../../../features/video';
|
|
18
|
+
import { OGLinkPreview, OGLinkErrorBoundary } from '../../../embeds/og-link-preview';
|
|
19
|
+
import { FigmaEmbed } from '../../../embeds/figma-embed';
|
|
20
|
+
import { MarkdownImage } from '../../../embeds/markdown-image';
|
|
21
|
+
import { buildStandardLeafRenderers, hasRenderableSrc } from '../base-components';
|
|
22
|
+
import type { TextSizeElement } from '../text-size';
|
|
23
|
+
|
|
24
|
+
/** Depth-first search of a hast node for the first `<a href>` matching `hostRe`. */
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
function findFirstHref(node: any, hostRe: RegExp): string | null {
|
|
27
|
+
if (!node) return null;
|
|
28
|
+
if (node.tagName === 'a') {
|
|
29
|
+
const href = node.properties?.href;
|
|
30
|
+
if (typeof href === 'string' && hostRe.test(href)) return href;
|
|
31
|
+
}
|
|
32
|
+
for (const child of node.children ?? []) {
|
|
33
|
+
const found = findFirstHref(child, hostRe);
|
|
34
|
+
if (found) return found;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface BuildRichEmbedOverridesOptions {
|
|
40
|
+
ogApiBaseUrl: string;
|
|
41
|
+
ogEndpointPath: string;
|
|
42
|
+
textSizes: Record<TextSizeElement, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Overrides spread LAST onto the engine's base map: `code` (fence-language
|
|
47
|
+
* embeds), `div` (shortcode-expanded embeds), `img` (MarkdownImage with
|
|
48
|
+
* runtime transformImageSrc), `video` (raw tags → Video SSOT).
|
|
49
|
+
*/
|
|
50
|
+
export function buildRichEmbedOverrides({
|
|
51
|
+
ogApiBaseUrl,
|
|
52
|
+
ogEndpointPath,
|
|
53
|
+
textSizes,
|
|
54
|
+
}: BuildRichEmbedOverridesOptions): Partial<Components> {
|
|
55
|
+
// Standard `code` / `blockquote` / `div` come from the base SSOT — this
|
|
56
|
+
// module owns ONLY the embed special cases and delegates every
|
|
57
|
+
// fall-through, so the shared markup can never drift between the two
|
|
58
|
+
// compositions.
|
|
59
|
+
//
|
|
60
|
+
// The delegations below CALL those renderers as plain functions rather
|
|
61
|
+
// than rendering them as components (that is what the `as any` casts are
|
|
62
|
+
// for). That is only sound because `buildStandardLeafRenderers` is
|
|
63
|
+
// documented HOOK-FREE — if a hook ever appears in one of them, these
|
|
64
|
+
// call sites must become real elements (`<Standard.code {...props} />`)
|
|
65
|
+
// first.
|
|
66
|
+
const standard = buildStandardLeafRenderers({ textSizes });
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
// Fence-language embeds (```youtube-embed etc.); everything else
|
|
70
|
+
// (mermaid, highlighted blocks, inline code) falls through to `standard`.
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
72
|
+
code: (codeProps: any) => {
|
|
73
|
+
const { inline, className: codeClassName, children } = codeProps;
|
|
74
|
+
const match = /language-(\w+)/.exec(codeClassName || '');
|
|
75
|
+
const language = match ? match[1] : '';
|
|
76
|
+
const fenceText = () => String(children).replace(/\n$/, '').trim();
|
|
77
|
+
|
|
78
|
+
if (!inline && language === 'youtube-embed') {
|
|
79
|
+
return <Video kind="youtube" url={fenceText()} />;
|
|
80
|
+
}
|
|
81
|
+
if (!inline && language === 'reddit-embed') {
|
|
82
|
+
return <RedditEmbedClient url={fenceText()} />;
|
|
83
|
+
}
|
|
84
|
+
if (!inline && language === 'tweet-embed') {
|
|
85
|
+
return <TwitterEmbedClient url={fenceText()} />;
|
|
86
|
+
}
|
|
87
|
+
if (!inline && language === 'link-preview') {
|
|
88
|
+
return (
|
|
89
|
+
<OGLinkPreview
|
|
90
|
+
url={fenceText()}
|
|
91
|
+
variant="compact"
|
|
92
|
+
enablePlaceholder={false}
|
|
93
|
+
apiBaseUrl={ogApiBaseUrl}
|
|
94
|
+
ogEndpointPath={ogEndpointPath}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (!inline && language === 'figma-embed') {
|
|
99
|
+
return <FigmaEmbed url={fenceText()} height="70vh" />;
|
|
100
|
+
}
|
|
101
|
+
if (!inline && language === 'linkedin-embed') {
|
|
102
|
+
return <LinkedInEmbedClient url={fenceText()} />;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
return (standard.code as any)(codeProps);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// Shortcode-expanded embeds: <div class="youtube-embed" data-video-id>…
|
|
110
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
+
div: (divProps: any) => {
|
|
112
|
+
const { className, ...props } = divProps;
|
|
113
|
+
if (className === 'youtube-embed') {
|
|
114
|
+
return <Video kind="youtube" url={props['data-video-id']} />;
|
|
115
|
+
}
|
|
116
|
+
if (className === 'reddit-embed') {
|
|
117
|
+
return <RedditEmbedClient url={props['data-post-url']} />;
|
|
118
|
+
}
|
|
119
|
+
if (className === 'tweet-embed') {
|
|
120
|
+
return <TwitterEmbedClient url={props['data-tweet-url']} />;
|
|
121
|
+
}
|
|
122
|
+
if (className === 'link-preview') {
|
|
123
|
+
const url = props['data-url'];
|
|
124
|
+
if (!url || typeof url !== 'string') {
|
|
125
|
+
console.warn('Invalid URL for link preview:', url);
|
|
126
|
+
return <div className="text-ods-text-secondary text-sm">Invalid link</div>;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
new URL(url);
|
|
130
|
+
return (
|
|
131
|
+
<OGLinkErrorBoundary fallback={<div className="text-ods-text-secondary text-sm">Link preview unavailable</div>}>
|
|
132
|
+
<OGLinkPreview
|
|
133
|
+
url={url}
|
|
134
|
+
variant="compact"
|
|
135
|
+
enablePlaceholder={false}
|
|
136
|
+
apiBaseUrl={ogApiBaseUrl}
|
|
137
|
+
ogEndpointPath={ogEndpointPath}
|
|
138
|
+
/>
|
|
139
|
+
</OGLinkErrorBoundary>
|
|
140
|
+
);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
console.warn('Malformed URL for link preview:', url, e);
|
|
143
|
+
return <div className="text-ods-text-secondary text-sm">Malformed URL: {url}</div>;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (className === 'figma-embed') {
|
|
147
|
+
return <FigmaEmbed url={props['data-figma-url']} height="70vh" />;
|
|
148
|
+
}
|
|
149
|
+
if (className === 'linkedin-embed') {
|
|
150
|
+
return <LinkedInEmbedClient url={props['data-post-url']} />;
|
|
151
|
+
}
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
return (standard.div as any)(divProps);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// Reddit's OWN embed markup lives in 58 published blog posts as
|
|
157
|
+
// `<blockquote class="reddit-embed-bq">…<a href="post-url">` paired with
|
|
158
|
+
// a `<script src="embed.reddit.com/widgets.js">` loader. The sanitize
|
|
159
|
+
// stack strips the script (correctly), so the composition rehydrates the
|
|
160
|
+
// blockquote itself: extract the post URL from the first reddit link and
|
|
161
|
+
// render the RedditEmbedClient SSOT. Non-reddit blockquotes fall through
|
|
162
|
+
// to the engine's base blockquote.
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
|
+
blockquote: (bqProps: any) => {
|
|
165
|
+
const { node, className } = bqProps;
|
|
166
|
+
const classNames: string = Array.isArray(className) ? className.join(' ') : (className ?? '');
|
|
167
|
+
if (classNames.includes('reddit-embed-bq')) {
|
|
168
|
+
const postUrl = findFirstHref(node, /reddit\.com/);
|
|
169
|
+
if (postUrl) return <RedditEmbedClient url={postUrl} />;
|
|
170
|
+
}
|
|
171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
+
return (standard.blockquote as any)(bqProps);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// In-article images: MarkdownImage reads `transformImageSrc` from the
|
|
176
|
+
// rich-markdown runtime (Supabase optimization on the hub, identity
|
|
177
|
+
// elsewhere). Guard against empty `![]()`.
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
+
img: ({ src, alt }: any) => {
|
|
180
|
+
// Empty-`![]()` guard shared with the base `img` renderer.
|
|
181
|
+
if (!hasRenderableSrc(src)) return null;
|
|
182
|
+
return <MarkdownImage src={src.trim()} alt={alt} />;
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// Raw <video> tags in stored content (blog publisher injection) route
|
|
186
|
+
// to the <Video> SSOT (MuxPlayer) so HLS manifests play everywhere.
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
+
video: ({ src, poster, className }: any) => {
|
|
189
|
+
if (!hasRenderableSrc(src)) return null;
|
|
190
|
+
return (
|
|
191
|
+
<div className={`overflow-hidden ${className || 'w-full my-8 rounded-lg'}`}>
|
|
192
|
+
<div className="w-full aspect-video">
|
|
193
|
+
<Video kind="file" url={src.trim()} poster={typeof poster === 'string' ? poster : undefined} />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RichMarkdownRenderer — the content composition over the unified
|
|
5
|
+
* `MarkdownEngine`: RuntimeProvider + shortcodes + embed overrides +
|
|
6
|
+
* fetch-based internal-link resolver + `article` typography.
|
|
7
|
+
*
|
|
8
|
+
* This IS the real component (the old 1223-line standalone implementation
|
|
9
|
+
* was deleted in the unification). Public prop surface is unchanged, so
|
|
10
|
+
* every hub/app call site compiles untouched. Deltas vs the old
|
|
11
|
+
* implementation are the ones reviewed in the unification plan: the
|
|
12
|
+
* sanitize stack + tag-escape pre-pass now apply to content surfaces
|
|
13
|
+
* (`extraAllowedHtmlTags={['video','source']}` keeps authored video
|
|
14
|
+
* working), the engine is memoized, and the dead light-mode branches are
|
|
15
|
+
* gone.
|
|
16
|
+
*/
|
|
17
|
+
import React, { memo, useCallback, useMemo } from 'react';
|
|
18
|
+
import { MarkdownEngine, NO_BROKEN_LINKS, type MarkdownEngineProps } from '../engine';
|
|
19
|
+
import type { ResolveLinkResult } from '../../../../types/doc-source';
|
|
20
|
+
import {
|
|
21
|
+
RichMarkdownRuntimeProvider,
|
|
22
|
+
useRichMarkdownRuntime,
|
|
23
|
+
type RichMarkdownRuntime,
|
|
24
|
+
} from '../../../embeds/rich-markdown-runtime';
|
|
25
|
+
import { resolveTextSizeConfig } from '../text-size';
|
|
26
|
+
import { buildRichEmbedOverrides } from './embed-overrides';
|
|
27
|
+
import { processShortcodes } from './shortcodes';
|
|
28
|
+
|
|
29
|
+
/** Raw-HTML tags authored content needs beyond the chat-safe baseline. */
|
|
30
|
+
const RICH_EXTRA_TAGS = ['video', 'source'];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The engine props this composition OWNS and therefore does not forward:
|
|
34
|
+
* shortcode preprocessing, the embed component map, the `article` text
|
|
35
|
+
* preset, the fetch link resolver, the rich tag allowlist, and streaming
|
|
36
|
+
* (authored content is never streamed).
|
|
37
|
+
*/
|
|
38
|
+
type RichOwnedEngineProps =
|
|
39
|
+
| 'extraAllowedHtmlTags'
|
|
40
|
+
| 'preprocessContent'
|
|
41
|
+
| 'componentOverrides'
|
|
42
|
+
| 'textSize'
|
|
43
|
+
| 'onResolveLink'
|
|
44
|
+
| 'additionalRemarkPlugins'
|
|
45
|
+
| 'streaming';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Derived from `MarkdownEngineProps` rather than hand-copied. The previous
|
|
49
|
+
* shape restated 7 engine props here and a THIRD time in `InnerProps`, so
|
|
50
|
+
* every engine prop doc/type change had to be mirrored in three places (and
|
|
51
|
+
* a missed mirror type-checks fine while silently dropping the prop).
|
|
52
|
+
*/
|
|
53
|
+
export interface RichMarkdownRendererProps
|
|
54
|
+
extends Partial<RichMarkdownRuntime>,
|
|
55
|
+
Omit<MarkdownEngineProps, RichOwnedEngineProps> {
|
|
56
|
+
/** Source for resolving internal links (default: 'openframe-docs'). Registry id from DOC_SOURCES. */
|
|
57
|
+
resolveSource?: string;
|
|
58
|
+
/** Path of the internal link-resolver endpoint. Default '/api/docs/resolve-link'. */
|
|
59
|
+
resolveLinkEndpointUrl?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const RichMarkdownRendererImpl: React.FC<RichMarkdownRendererProps> = ({
|
|
63
|
+
content,
|
|
64
|
+
className = '',
|
|
65
|
+
sectionIds,
|
|
66
|
+
onInternalLinkClick,
|
|
67
|
+
brokenLinks = NO_BROKEN_LINKS,
|
|
68
|
+
currentPath,
|
|
69
|
+
resolveSource = 'openframe-docs',
|
|
70
|
+
resolveLinkEndpointUrl = '/api/docs/resolve-link',
|
|
71
|
+
demoteMarkdownH1ToH2 = false,
|
|
72
|
+
// Runtime overrides; provider fills the defaults
|
|
73
|
+
redditProxyUrl,
|
|
74
|
+
twitterProxyUrl,
|
|
75
|
+
ogScraperUrl,
|
|
76
|
+
transformImageSrc,
|
|
77
|
+
}) => {
|
|
78
|
+
return (
|
|
79
|
+
<RichMarkdownRuntimeProvider
|
|
80
|
+
redditProxyUrl={redditProxyUrl}
|
|
81
|
+
twitterProxyUrl={twitterProxyUrl}
|
|
82
|
+
ogScraperUrl={ogScraperUrl}
|
|
83
|
+
transformImageSrc={transformImageSrc}
|
|
84
|
+
>
|
|
85
|
+
<RichMarkdownInner
|
|
86
|
+
content={content}
|
|
87
|
+
className={className}
|
|
88
|
+
sectionIds={sectionIds}
|
|
89
|
+
onInternalLinkClick={onInternalLinkClick}
|
|
90
|
+
brokenLinks={brokenLinks}
|
|
91
|
+
currentPath={currentPath}
|
|
92
|
+
resolveSource={resolveSource}
|
|
93
|
+
resolveLinkEndpointUrl={resolveLinkEndpointUrl}
|
|
94
|
+
demoteMarkdownH1ToH2={demoteMarkdownH1ToH2}
|
|
95
|
+
/>
|
|
96
|
+
</RichMarkdownRuntimeProvider>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* "The outer component already applied these defaults", ENCODED IN THE TYPE:
|
|
102
|
+
* the runtime props are consumed by the provider (so they are gone here),
|
|
103
|
+
* and the three defaulted props are `Required` — a missed default upstream
|
|
104
|
+
* is a type error rather than a silent `undefined`.
|
|
105
|
+
*/
|
|
106
|
+
type InnerProps = Omit<RichMarkdownRendererProps, keyof RichMarkdownRuntime> &
|
|
107
|
+
Required<
|
|
108
|
+
Pick<
|
|
109
|
+
RichMarkdownRendererProps,
|
|
110
|
+
'resolveSource' | 'resolveLinkEndpointUrl' | 'demoteMarkdownH1ToH2'
|
|
111
|
+
>
|
|
112
|
+
>;
|
|
113
|
+
|
|
114
|
+
const RichMarkdownInner: React.FC<InnerProps> = ({
|
|
115
|
+
content,
|
|
116
|
+
className = '',
|
|
117
|
+
sectionIds,
|
|
118
|
+
onInternalLinkClick,
|
|
119
|
+
brokenLinks,
|
|
120
|
+
currentPath,
|
|
121
|
+
resolveSource,
|
|
122
|
+
resolveLinkEndpointUrl,
|
|
123
|
+
demoteMarkdownH1ToH2,
|
|
124
|
+
}) => {
|
|
125
|
+
const { ogScraperUrl } = useRichMarkdownRuntime();
|
|
126
|
+
|
|
127
|
+
// The OG link-preview endpoint is `${apiBaseUrl}${ogEndpointPath}` —
|
|
128
|
+
// split the runtime URL once. Full URLs route through the cross-origin
|
|
129
|
+
// proxy; path-only values are used as the path with an empty base.
|
|
130
|
+
const { ogApiBaseUrl, ogEndpointPath } = useMemo(() => {
|
|
131
|
+
try {
|
|
132
|
+
const u = new URL(ogScraperUrl);
|
|
133
|
+
return {
|
|
134
|
+
ogApiBaseUrl: `${u.protocol}//${u.host}`,
|
|
135
|
+
ogEndpointPath: u.pathname,
|
|
136
|
+
};
|
|
137
|
+
} catch {
|
|
138
|
+
return { ogApiBaseUrl: '', ogEndpointPath: ogScraperUrl };
|
|
139
|
+
}
|
|
140
|
+
}, [ogScraperUrl]);
|
|
141
|
+
|
|
142
|
+
// ONE link-resolution code path: the engine's callback seam. This is the
|
|
143
|
+
// fetch implementation the old Rich renderer had inline in its <a>
|
|
144
|
+
// handler — the engine's base `a` component now drives it.
|
|
145
|
+
const onResolveLink = useCallback(
|
|
146
|
+
async (href: string, path: string): Promise<ResolveLinkResult> => {
|
|
147
|
+
const response = await fetch(resolveLinkEndpointUrl, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: { 'Content-Type': 'application/json' },
|
|
150
|
+
body: JSON.stringify({ link: href, currentPath: path, source: resolveSource }),
|
|
151
|
+
});
|
|
152
|
+
return response.json();
|
|
153
|
+
},
|
|
154
|
+
[resolveLinkEndpointUrl, resolveSource],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const textSizes = useMemo(() => resolveTextSizeConfig('article'), []);
|
|
158
|
+
|
|
159
|
+
const componentOverrides = useMemo(
|
|
160
|
+
() => buildRichEmbedOverrides({ ogApiBaseUrl, ogEndpointPath, textSizes }),
|
|
161
|
+
[ogApiBaseUrl, ogEndpointPath, textSizes],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<MarkdownEngine
|
|
166
|
+
content={content}
|
|
167
|
+
className={className}
|
|
168
|
+
sectionIds={sectionIds}
|
|
169
|
+
demoteMarkdownH1ToH2={demoteMarkdownH1ToH2}
|
|
170
|
+
brokenLinks={brokenLinks}
|
|
171
|
+
currentPath={currentPath}
|
|
172
|
+
onInternalLinkClick={onInternalLinkClick}
|
|
173
|
+
onResolveLink={onResolveLink}
|
|
174
|
+
preprocessContent={processShortcodes}
|
|
175
|
+
componentOverrides={componentOverrides}
|
|
176
|
+
textSize="article"
|
|
177
|
+
extraAllowedHtmlTags={RICH_EXTRA_TAGS}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** Memoized — see the engine's memo rationale. */
|
|
183
|
+
export const RichMarkdownRenderer = memo(RichMarkdownRendererImpl);
|
|
184
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shortcode + auto-URL-embed preprocessing for the rich (content)
|
|
3
|
+
* composition. Moved verbatim from the old RichMarkdownRenderer — the
|
|
4
|
+
* `{{youtube:...}}` / `{% youtube %}` / thumbnail-link / auto-URL grammar
|
|
5
|
+
* is authored-content SSOT; chat surfaces never run this.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const processShortcodes = (content: string): string => {
|
|
9
|
+
let processedContent = content;
|
|
10
|
+
|
|
11
|
+
// Escape values interpolated into the raw HTML `data-*` attributes generated below.
|
|
12
|
+
// With rehypeRaw enabled, an unescaped `"`/`<`/`>` in a URL or id could break out of
|
|
13
|
+
// the attribute and inject markup, so every interpolated embed value goes through this.
|
|
14
|
+
const escapeAttr = (value: string) =>
|
|
15
|
+
value
|
|
16
|
+
.replace(/&/g, '&')
|
|
17
|
+
.replace(/"/g, '"')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>');
|
|
20
|
+
|
|
21
|
+
// First, process explicit shortcodes
|
|
22
|
+
processedContent = processedContent
|
|
23
|
+
// YouTube embeds: {{youtube:VIDEO_ID}}
|
|
24
|
+
.replace(/\{\{youtube:([^}]+)\}\}/g, (match, videoId) => {
|
|
25
|
+
return `\n\n<div class="youtube-embed" data-video-id="${escapeAttr(videoId.trim())}"></div>\n\n`;
|
|
26
|
+
})
|
|
27
|
+
// Markdoc-style YouTube: {% youtube id="VIDEO_ID" /%} or {% youtube id="VIDEO_ID" title="..." /%}
|
|
28
|
+
.replace(/\{%\s*youtube\s+id="([^"]+)"(?:\s+title="[^"]*")?\s*\/?%\}/g, (match, videoId) => {
|
|
29
|
+
return `\n\n<div class="youtube-embed" data-video-id="${escapeAttr(videoId.trim())}"></div>\n\n`;
|
|
30
|
+
})
|
|
31
|
+
/**
|
|
32
|
+
* SHORTCODE: YouTube Thumbnail Link (RECOMMENDED - GitHub + Flamingo Compatible)
|
|
33
|
+
*
|
|
34
|
+
* SYNTAX: [](https://www.youtube.com/watch?v=VIDEO_ID)
|
|
35
|
+
* - On GitHub: renders as a clickable thumbnail image linking to YouTube
|
|
36
|
+
* - On Flamingo: converts to a full embedded YouTube player
|
|
37
|
+
* Use this format for docs that must work on BOTH GitHub and Flamingo;
|
|
38
|
+
* {{youtube:ID}} / {% youtube id="ID" /%} are Flamingo-only.
|
|
39
|
+
*/
|
|
40
|
+
.replace(/\[!\[([^\]]*)\]\(https?:\/\/img\.youtube\.com\/vi\/([a-zA-Z0-9_-]+)\/[^)]+\)\]\(https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+)[^)]*\)/g,
|
|
41
|
+
(match, altText, thumbId, videoId) => {
|
|
42
|
+
return `\n\n<div class="youtube-embed" data-video-id="${videoId}"></div>\n\n`;
|
|
43
|
+
})
|
|
44
|
+
// Reddit embeds: {{reddit:POST_URL}}
|
|
45
|
+
.replace(/\{\{reddit:([^}]+)\}\}/g, (match, urlOrId) => {
|
|
46
|
+
const postUrl = urlOrId.trim();
|
|
47
|
+
const fullUrl = postUrl.startsWith('http') ? postUrl : `https://reddit.com/r/${postUrl}`;
|
|
48
|
+
return `\n\n<div class="reddit-embed" data-post-url="${escapeAttr(fullUrl)}"></div>\n\n`;
|
|
49
|
+
})
|
|
50
|
+
// Twitter/X embeds: {{tweet:TWEET_URL}} or {{twitter:TWEET_URL}}
|
|
51
|
+
.replace(/\{\{(?:tweet|twitter):([^}]+)\}\}/g, (match, urlOrId) => {
|
|
52
|
+
const tweetInput = urlOrId.trim();
|
|
53
|
+
const tweetUrl = tweetInput.startsWith('http')
|
|
54
|
+
? tweetInput
|
|
55
|
+
: `https://twitter.com/twitter/status/${tweetInput}`;
|
|
56
|
+
return `\n\n<div class="tweet-embed" data-tweet-url="${escapeAttr(tweetUrl)}"></div>\n\n`;
|
|
57
|
+
})
|
|
58
|
+
// Figma embeds: {{figma:URL}}
|
|
59
|
+
.replace(/\{\{figma:([^}]+)\}\}/g, (match, url) => {
|
|
60
|
+
return `\n\n<div class="figma-embed" data-figma-url="${escapeAttr(url.trim())}"></div>\n\n`;
|
|
61
|
+
})
|
|
62
|
+
// LinkedIn embeds: {{linkedin:POST_URL}}
|
|
63
|
+
.replace(/\{\{linkedin:([^}]+)\}\}/g, (match, url) => {
|
|
64
|
+
return `\n\n<div class="linkedin-embed" data-post-url="${escapeAttr(url.trim())}"></div>\n\n`;
|
|
65
|
+
})
|
|
66
|
+
// Link previews: {{link:URL}}
|
|
67
|
+
.replace(/\{\{link:([^}]+)\}\}/g, (match, url) => {
|
|
68
|
+
return `\n\n<div class="link-preview" data-url="${escapeAttr(url.trim())}"></div>\n\n`;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Next, auto-detect standalone URLs (but NOT those already in markdown links or code blocks)
|
|
72
|
+
|
|
73
|
+
// Step 1: Temporarily replace code blocks to protect them
|
|
74
|
+
const codeBlocks: string[] = [];
|
|
75
|
+
processedContent = processedContent.replace(/```[\s\S]*?```|`[^`]+`/g, (match) => {
|
|
76
|
+
const placeholder = `__CODE_BLOCK_${codeBlocks.length}__`;
|
|
77
|
+
codeBlocks.push(match);
|
|
78
|
+
return placeholder;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Step 2: Temporarily replace markdown links to protect them
|
|
82
|
+
const markdownLinks: string[] = [];
|
|
83
|
+
processedContent = processedContent.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match) => {
|
|
84
|
+
const placeholder = `__MARKDOWN_LINK_${markdownLinks.length}__`;
|
|
85
|
+
markdownLinks.push(match);
|
|
86
|
+
return placeholder;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Step 2.5: Temporarily replace table rows to protect URLs inside tables
|
|
90
|
+
const tableRows: string[] = [];
|
|
91
|
+
processedContent = processedContent.replace(/^\|.+\|$/gm, (match) => {
|
|
92
|
+
const placeholder = `__TABLE_ROW_${tableRows.length}__`;
|
|
93
|
+
tableRows.push(match);
|
|
94
|
+
return placeholder;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Step 3: Auto-detect standalone URLs and convert to appropriate embeds
|
|
98
|
+
processedContent = processedContent
|
|
99
|
+
// YouTube URLs (standalone only)
|
|
100
|
+
.replace(/(?:^|\s)(https?:\/\/(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]+))(?:\s|$)/g,
|
|
101
|
+
(match, fullUrl, videoId) => {
|
|
102
|
+
return match.replace(fullUrl, `\n\n<div class="youtube-embed" data-video-id="${videoId}"></div>\n\n`);
|
|
103
|
+
})
|
|
104
|
+
// Reddit URLs (standalone only)
|
|
105
|
+
.replace(/(?:^|\s)(https?:\/\/(?:www\.)?reddit\.com\/[^\s]+)(?:\s|$)/g,
|
|
106
|
+
(match, redditUrl) => {
|
|
107
|
+
return match.replace(redditUrl, `\n\n<div class="reddit-embed" data-post-url="${escapeAttr(redditUrl)}"></div>\n\n`);
|
|
108
|
+
})
|
|
109
|
+
// Twitter/X URLs (standalone only)
|
|
110
|
+
.replace(/(?:^|\s)(https?:\/\/(?:www\.)?(?:twitter\.com|x\.com)\/[^\/\s]+\/status\/\d+)(?:\s|$)/g,
|
|
111
|
+
(match, tweetUrl) => {
|
|
112
|
+
return match.replace(tweetUrl, `\n\n<div class="tweet-embed" data-tweet-url="${escapeAttr(tweetUrl)}"></div>\n\n`);
|
|
113
|
+
})
|
|
114
|
+
// Figma URLs (standalone only) - design/file/proto/board/deck/slides → interactive embed
|
|
115
|
+
.replace(/(?:^|\s)(https?:\/\/(?:www\.|embed\.)?figma\.com\/(?:design|file|proto|board|deck|slides)\/[^\s]+)(?:\s|$)/g,
|
|
116
|
+
(match, figmaUrl) => {
|
|
117
|
+
return match.replace(figmaUrl, `\n\n<div class="figma-embed" data-figma-url="${escapeAttr(figmaUrl)}"></div>\n\n`);
|
|
118
|
+
})
|
|
119
|
+
// LinkedIn post URLs (standalone only) → native post embed
|
|
120
|
+
.replace(/(?:^|\s)(https?:\/\/(?:www\.)?linkedin\.com\/(?:posts|feed\/update|embed\/feed\/update)\/[^\s]+)(?:\s|$)/g,
|
|
121
|
+
(match, liUrl) => {
|
|
122
|
+
return match.replace(liUrl, `\n\n<div class="linkedin-embed" data-post-url="${escapeAttr(liUrl)}"></div>\n\n`);
|
|
123
|
+
})
|
|
124
|
+
// Other external URLs (standalone only) - convert to link previews
|
|
125
|
+
.replace(/(?:^|\s)(https?:\/\/[^\s]+)(?:\s|$)/g,
|
|
126
|
+
(match, url) => {
|
|
127
|
+
try {
|
|
128
|
+
const urlObj = new URL(url);
|
|
129
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
130
|
+
|
|
131
|
+
// Exact host match (or a subdomain of it) — substring checks like
|
|
132
|
+
// `hostname.includes('x.com')` false-positive on "zabbix.com".
|
|
133
|
+
const hostIs = (domain: string) =>
|
|
134
|
+
hostname === domain || hostname.endsWith(`.${domain}`);
|
|
135
|
+
// Non-video YouTube URLs (channels, playlists, `@handle`) fall
|
|
136
|
+
// through to the og-scraper; only video URLs become inline players.
|
|
137
|
+
const isYouTubeVideo =
|
|
138
|
+
(hostIs('youtube.com') && urlObj.searchParams.has('v')) || hostIs('youtu.be');
|
|
139
|
+
// LinkedIn non-post URLs (profiles, companies) fall through to the
|
|
140
|
+
// og-scraper too; only interactive post embeds have handlers above.
|
|
141
|
+
if (isYouTubeVideo ||
|
|
142
|
+
hostIs('reddit.com') || hostIs('twitter.com') || hostIs('x.com') ||
|
|
143
|
+
hostIs('figma.com')) {
|
|
144
|
+
return match;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return match.replace(url, `\n\n<div class="link-preview" data-url="${escapeAttr(url)}"></div>\n\n`);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.warn('Failed to parse URL for link preview:', url, e);
|
|
150
|
+
return match;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Step 3.5: Restore table rows
|
|
155
|
+
tableRows.forEach((row, index) => {
|
|
156
|
+
processedContent = processedContent.replace(`__TABLE_ROW_${index}__`, row);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Step 4: Restore markdown links
|
|
160
|
+
markdownLinks.forEach((link, index) => {
|
|
161
|
+
processedContent = processedContent.replace(`__MARKDOWN_LINK_${index}__`, link);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Step 5: Restore code blocks (MUST be last to prevent link preview in code)
|
|
165
|
+
codeBlocks.forEach((block, index) => {
|
|
166
|
+
processedContent = processedContent.replace(`__CODE_BLOCK_${index}__`, block);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return processedContent;
|
|
170
|
+
};
|