@things-factory/board-ai 10.0.0-beta.108
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/PLAN-styling-tools.md +1253 -0
- package/client/components/board-ai-chat.test.ts +149 -0
- package/client/components/board-ai-chat.ts +2574 -0
- package/client/components/chat-echo-dedup.test.ts +116 -0
- package/client/components/chat-echo-dedup.ts +89 -0
- package/client/components/chat-input-builder.ts +47 -0
- package/client/components/default-catalog-entries.test.ts +59 -0
- package/client/components/default-catalog-entries.ts +100 -0
- package/client/components/default-slash-templates.test.ts +55 -0
- package/client/components/default-slash-templates.ts +129 -0
- package/client/components/markdown.test.ts +518 -0
- package/client/components/markdown.ts +508 -0
- package/client/components/mention-popup-helpers.ts +433 -0
- package/client/components/mention-popup.test.ts +588 -0
- package/client/components/mention-popup.ts +238 -0
- package/client/index.ts +11 -0
- package/client/tsconfig.json +13 -0
- package/client/utils/board-edit-patch.ts +374 -0
- package/config/config.development.js +43 -0
- package/config/config.production.js +15 -0
- package/dist-client/client/components/board-ai-chat.d.ts +277 -0
- package/dist-client/client/components/board-ai-chat.js +2444 -0
- package/dist-client/client/components/board-ai-chat.js.map +1 -0
- package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
- package/dist-client/client/components/board-ai-chat.test.js +138 -0
- package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
- package/dist-client/client/components/chat-echo-dedup.js +58 -0
- package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
- package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
- package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
- package/dist-client/client/components/chat-input-builder.d.ts +39 -0
- package/dist-client/client/components/chat-input-builder.js +26 -0
- package/dist-client/client/components/chat-input-builder.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
- package/dist-client/client/components/default-catalog-entries.js +86 -0
- package/dist-client/client/components/default-catalog-entries.js.map +1 -0
- package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
- package/dist-client/client/components/default-catalog-entries.test.js +52 -0
- package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.d.ts +17 -0
- package/dist-client/client/components/default-slash-templates.js +113 -0
- package/dist-client/client/components/default-slash-templates.js.map +1 -0
- package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
- package/dist-client/client/components/default-slash-templates.test.js +51 -0
- package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
- package/dist-client/client/components/markdown.d.ts +60 -0
- package/dist-client/client/components/markdown.js +422 -0
- package/dist-client/client/components/markdown.js.map +1 -0
- package/dist-client/client/components/markdown.test.d.ts +1 -0
- package/dist-client/client/components/markdown.test.js +418 -0
- package/dist-client/client/components/markdown.test.js.map +1 -0
- package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
- package/dist-client/client/components/mention-popup-helpers.js +330 -0
- package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
- package/dist-client/client/components/mention-popup.d.ts +35 -0
- package/dist-client/client/components/mention-popup.js +223 -0
- package/dist-client/client/components/mention-popup.js.map +1 -0
- package/dist-client/client/components/mention-popup.test.d.ts +1 -0
- package/dist-client/client/components/mention-popup.test.js +474 -0
- package/dist-client/client/components/mention-popup.test.js.map +1 -0
- package/dist-client/client/index.d.ts +11 -0
- package/dist-client/client/index.js +12 -0
- package/dist-client/client/index.js.map +1 -0
- package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
- package/dist-client/client/utils/board-edit-patch.js +284 -0
- package/dist-client/client/utils/board-edit-patch.js.map +1 -0
- package/dist-client/server/service/agentic-loop.d.ts +87 -0
- package/dist-client/server/service/agentic-loop.js +276 -0
- package/dist-client/server/service/agentic-loop.js.map +1 -0
- package/dist-client/server/service/assistant.d.ts +84 -0
- package/dist-client/server/service/assistant.js +2220 -0
- package/dist-client/server/service/assistant.js.map +1 -0
- package/dist-client/server/service/catalog-resolver.d.ts +18 -0
- package/dist-client/server/service/catalog-resolver.js +93 -0
- package/dist-client/server/service/catalog-resolver.js.map +1 -0
- package/dist-client/server/service/component-tree.d.ts +68 -0
- package/dist-client/server/service/component-tree.js +68 -0
- package/dist-client/server/service/component-tree.js.map +1 -0
- package/dist-client/server/service/mention-resolver.d.ts +96 -0
- package/dist-client/server/service/mention-resolver.js +172 -0
- package/dist-client/server/service/mention-resolver.js.map +1 -0
- package/dist-client/server/service/prefab-registry.d.ts +73 -0
- package/dist-client/server/service/prefab-registry.js +252 -0
- package/dist-client/server/service/prefab-registry.js.map +1 -0
- package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
- package/dist-client/server/service/styling/clear-tools.js +50 -0
- package/dist-client/server/service/styling/clear-tools.js.map +1 -0
- package/dist-client/server/service/styling/color-utils.d.ts +43 -0
- package/dist-client/server/service/styling/color-utils.js +205 -0
- package/dist-client/server/service/styling/color-utils.js.map +1 -0
- package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
- package/dist-client/server/service/styling/copy-tools.js +78 -0
- package/dist-client/server/service/styling/copy-tools.js.map +1 -0
- package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
- package/dist-client/server/service/styling/effect-tools.js +85 -0
- package/dist-client/server/service/styling/effect-tools.js.map +1 -0
- package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
- package/dist-client/server/service/styling/fill-tools.js +226 -0
- package/dist-client/server/service/styling/fill-tools.js.map +1 -0
- package/dist-client/server/service/styling/material-tools.d.ts +32 -0
- package/dist-client/server/service/styling/material-tools.js +88 -0
- package/dist-client/server/service/styling/material-tools.js.map +1 -0
- package/dist-client/server/service/styling/read-tools.d.ts +34 -0
- package/dist-client/server/service/styling/read-tools.js +42 -0
- package/dist-client/server/service/styling/read-tools.js.map +1 -0
- package/dist-client/server/service/styling/registry.d.ts +91 -0
- package/dist-client/server/service/styling/registry.js +345 -0
- package/dist-client/server/service/styling/registry.js.map +1 -0
- package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-client/server/service/styling/stroke-tools.js +94 -0
- package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
- package/dist-client/server/service/styling/text-tools.d.ts +32 -0
- package/dist-client/server/service/styling/text-tools.js +101 -0
- package/dist-client/server/service/styling/text-tools.js.map +1 -0
- package/dist-client/server/service/types.d.ts +265 -0
- package/dist-client/server/service/types.js +2 -0
- package/dist-client/server/service/types.js.map +1 -0
- package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-client/server/service/validation/board-model-schema.js +402 -0
- package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
- package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
- package/dist-client/server/service/validation/tool-validation.js +193 -0
- package/dist-client/server/service/validation/tool-validation.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +23 -0
- package/dist-server/index.js +27 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/agentic-loop.d.ts +87 -0
- package/dist-server/service/agentic-loop.js +280 -0
- package/dist-server/service/agentic-loop.js.map +1 -0
- package/dist-server/service/apply-patch.d.ts +47 -0
- package/dist-server/service/apply-patch.js +369 -0
- package/dist-server/service/apply-patch.js.map +1 -0
- package/dist-server/service/assistant.d.ts +84 -0
- package/dist-server/service/assistant.js +2237 -0
- package/dist-server/service/assistant.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +54 -0
- package/dist-server/service/board-ai-resolver.js +409 -0
- package/dist-server/service/board-ai-resolver.js.map +1 -0
- package/dist-server/service/catalog-resolver.d.ts +18 -0
- package/dist-server/service/catalog-resolver.js +98 -0
- package/dist-server/service/catalog-resolver.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
- package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
- package/dist-server/service/chat-message/chat-message-publish.js +27 -0
- package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
- package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
- package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
- package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-message.d.ts +56 -0
- package/dist-server/service/chat-message/chat-message.js +149 -0
- package/dist-server/service/chat-message/chat-message.js.map +1 -0
- package/dist-server/service/chat-message/index.d.ts +5 -0
- package/dist-server/service/chat-message/index.js +12 -0
- package/dist-server/service/chat-message/index.js.map +1 -0
- package/dist-server/service/chat-session/chat-session.d.ts +31 -0
- package/dist-server/service/chat-session/chat-session.js +131 -0
- package/dist-server/service/chat-session/chat-session.js.map +1 -0
- package/dist-server/service/chat-session/index.d.ts +3 -0
- package/dist-server/service/chat-session/index.js +7 -0
- package/dist-server/service/chat-session/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
- package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
- package/dist-server/service/chat-session-participant/index.d.ts +4 -0
- package/dist-server/service/chat-session-participant/index.js +10 -0
- package/dist-server/service/chat-session-participant/index.js.map +1 -0
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
- package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
- package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
- package/dist-server/service/chat-session-participant/presence.js +22 -0
- package/dist-server/service/chat-session-participant/presence.js.map +1 -0
- package/dist-server/service/chat-session-resolver.d.ts +20 -0
- package/dist-server/service/chat-session-resolver.js +356 -0
- package/dist-server/service/chat-session-resolver.js.map +1 -0
- package/dist-server/service/component-tree.d.ts +68 -0
- package/dist-server/service/component-tree.js +73 -0
- package/dist-server/service/component-tree.js.map +1 -0
- package/dist-server/service/index.d.ts +20 -0
- package/dist-server/service/index.js +34 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/mention-marker.d.ts +78 -0
- package/dist-server/service/mention-marker.js +162 -0
- package/dist-server/service/mention-marker.js.map +1 -0
- package/dist-server/service/mention-resolver.d.ts +96 -0
- package/dist-server/service/mention-resolver.js +177 -0
- package/dist-server/service/mention-resolver.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
- package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
- package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
- package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
- package/dist-server/service/patch-entry/index.d.ts +4 -0
- package/dist-server/service/patch-entry/index.js +12 -0
- package/dist-server/service/patch-entry/index.js.map +1 -0
- package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
- package/dist-server/service/patch-entry/patch-entry.js +96 -0
- package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
- package/dist-server/service/prefab-registry.d.ts +73 -0
- package/dist-server/service/prefab-registry.js +258 -0
- package/dist-server/service/prefab-registry.js.map +1 -0
- package/dist-server/service/styling/clear-tools.d.ts +15 -0
- package/dist-server/service/styling/clear-tools.js +53 -0
- package/dist-server/service/styling/clear-tools.js.map +1 -0
- package/dist-server/service/styling/color-utils.d.ts +43 -0
- package/dist-server/service/styling/color-utils.js +210 -0
- package/dist-server/service/styling/color-utils.js.map +1 -0
- package/dist-server/service/styling/copy-tools.d.ts +27 -0
- package/dist-server/service/styling/copy-tools.js +81 -0
- package/dist-server/service/styling/copy-tools.js.map +1 -0
- package/dist-server/service/styling/effect-tools.d.ts +45 -0
- package/dist-server/service/styling/effect-tools.js +90 -0
- package/dist-server/service/styling/effect-tools.js.map +1 -0
- package/dist-server/service/styling/fill-tools.d.ts +60 -0
- package/dist-server/service/styling/fill-tools.js +229 -0
- package/dist-server/service/styling/fill-tools.js.map +1 -0
- package/dist-server/service/styling/material-tools.d.ts +32 -0
- package/dist-server/service/styling/material-tools.js +92 -0
- package/dist-server/service/styling/material-tools.js.map +1 -0
- package/dist-server/service/styling/read-tools.d.ts +34 -0
- package/dist-server/service/styling/read-tools.js +45 -0
- package/dist-server/service/styling/read-tools.js.map +1 -0
- package/dist-server/service/styling/registry.d.ts +91 -0
- package/dist-server/service/styling/registry.js +352 -0
- package/dist-server/service/styling/registry.js.map +1 -0
- package/dist-server/service/styling/stroke-tools.d.ts +35 -0
- package/dist-server/service/styling/stroke-tools.js +97 -0
- package/dist-server/service/styling/stroke-tools.js.map +1 -0
- package/dist-server/service/styling/text-tools.d.ts +32 -0
- package/dist-server/service/styling/text-tools.js +105 -0
- package/dist-server/service/styling/text-tools.js.map +1 -0
- package/dist-server/service/subscribe-auth.d.ts +14 -0
- package/dist-server/service/subscribe-auth.js +33 -0
- package/dist-server/service/subscribe-auth.js.map +1 -0
- package/dist-server/service/types.d.ts +265 -0
- package/dist-server/service/types.js +3 -0
- package/dist-server/service/types.js.map +1 -0
- package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
- package/dist-server/service/validation/board-model-schema.js +409 -0
- package/dist-server/service/validation/board-model-schema.js.map +1 -0
- package/dist-server/service/validation/tool-validation.d.ts +31 -0
- package/dist-server/service/validation/tool-validation.js +196 -0
- package/dist-server/service/validation/tool-validation.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +48 -0
- package/server/index.ts +23 -0
- package/server/service/agentic-loop.test.ts +705 -0
- package/server/service/agentic-loop.ts +431 -0
- package/server/service/apply-patch-drift.test.ts +829 -0
- package/server/service/apply-patch.test.ts +757 -0
- package/server/service/apply-patch.ts +411 -0
- package/server/service/assistant-integration.test.ts +922 -0
- package/server/service/assistant-llm-smoke.test.ts +296 -0
- package/server/service/assistant.test.ts +2034 -0
- package/server/service/assistant.ts +2378 -0
- package/server/service/board-ai-resolver.ts +407 -0
- package/server/service/catalog-resolver.test.ts +137 -0
- package/server/service/catalog-resolver.ts +102 -0
- package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
- package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
- package/server/service/chat-message/chat-message-broadcast.ts +27 -0
- package/server/service/chat-message/chat-message-publish.ts +22 -0
- package/server/service/chat-message/chat-message-subscription.ts +41 -0
- package/server/service/chat-message/chat-message.ts +169 -0
- package/server/service/chat-message/index.ts +12 -0
- package/server/service/chat-session/chat-session.ts +130 -0
- package/server/service/chat-session/index.ts +5 -0
- package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
- package/server/service/chat-session-participant/index.ts +6 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
- package/server/service/chat-session-participant/invite-notification.ts +48 -0
- package/server/service/chat-session-participant/presence.test.ts +39 -0
- package/server/service/chat-session-participant/presence.ts +21 -0
- package/server/service/chat-session-resolver.ts +312 -0
- package/server/service/component-tree.test.ts +248 -0
- package/server/service/component-tree.ts +135 -0
- package/server/service/index.ts +32 -0
- package/server/service/mention-marker.extract.test.ts +61 -0
- package/server/service/mention-marker.test.ts +228 -0
- package/server/service/mention-marker.ts +184 -0
- package/server/service/mention-resolver.test.ts +273 -0
- package/server/service/mention-resolver.ts +264 -0
- package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
- package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
- package/server/service/patch-entry/board-patch-subscription.ts +50 -0
- package/server/service/patch-entry/index.ts +12 -0
- package/server/service/patch-entry/patch-entry.ts +89 -0
- package/server/service/prefab-registry.test.ts +172 -0
- package/server/service/prefab-registry.ts +332 -0
- package/server/service/styling/clear-tools.test.ts +164 -0
- package/server/service/styling/clear-tools.ts +69 -0
- package/server/service/styling/color-utils.test.ts +130 -0
- package/server/service/styling/color-utils.ts +226 -0
- package/server/service/styling/copy-tools.test.ts +152 -0
- package/server/service/styling/copy-tools.ts +115 -0
- package/server/service/styling/effect-tools.test.ts +277 -0
- package/server/service/styling/effect-tools.ts +124 -0
- package/server/service/styling/fill-tools.test.ts +331 -0
- package/server/service/styling/fill-tools.ts +293 -0
- package/server/service/styling/material-tools.test.ts +294 -0
- package/server/service/styling/material-tools.ts +114 -0
- package/server/service/styling/read-tools.test.ts +78 -0
- package/server/service/styling/read-tools.ts +67 -0
- package/server/service/styling/registry.test.ts +203 -0
- package/server/service/styling/registry.ts +423 -0
- package/server/service/styling/stroke-tools.test.ts +245 -0
- package/server/service/styling/stroke-tools.ts +145 -0
- package/server/service/styling/text-tools.test.ts +253 -0
- package/server/service/styling/text-tools.ts +131 -0
- package/server/service/styling/tier1-tools.test.ts +300 -0
- package/server/service/subscribe-auth.test.ts +57 -0
- package/server/service/subscribe-auth.ts +34 -0
- package/server/service/types.ts +235 -0
- package/server/service/validation/board-model-schema.test.ts +778 -0
- package/server/service/validation/board-model-schema.ts +446 -0
- package/server/service/validation/tool-validation.ts +228 -0
- package/things-factory.config.js +1 -0
- package/translations/en.json +49 -0
- package/translations/ja.json +48 -0
- package/translations/ko.json +49 -0
- package/translations/ms.json +48 -0
- package/translations/zh.json +48 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board AI 채팅 메시지의 Markdown 렌더링.
|
|
3
|
+
*
|
|
4
|
+
* 보안 (defense-in-depth):
|
|
5
|
+
* 1. raw HTML 차단 — marked 의 renderer.html → '' 로 block-level HTML 무력화.
|
|
6
|
+
* 2. 위험 link 스킴 차단 — javascript: / data: / vbscript: 등 거부.
|
|
7
|
+
* 3. DOMPurify 후처리 — marked 출력 전체를 sanitize. 우회되는 inline HTML
|
|
8
|
+
* (이미지 onerror, span style="javascript:..." 등) 까지 차단.
|
|
9
|
+
*
|
|
10
|
+
* 1·2 만으로도 표면이 좁지만, 다음의 *알려진 우회 경로* 가 존재하므로 3 이 필수:
|
|
11
|
+
* - marked 가 토큰화 실패한 일부 입력 (특히 코드블록 안 백틱 escape 미스) 에서
|
|
12
|
+
* 생 HTML 이 escape 안 된 채 흘러나갈 수 있음
|
|
13
|
+
* - mention span (`<span class="mention" ...>`) 이 우리 손으로 만드는 element 라
|
|
14
|
+
* allowlist 기반 정화 필요
|
|
15
|
+
* - 멀티유저 환경 (다른 사용자 메시지 echo) 시 표면 ↑
|
|
16
|
+
*
|
|
17
|
+
* CJK 보강:
|
|
18
|
+
* - CommonMark의 left/right-flanking 규칙 때문에 한국어/일본어/중국어 텍스트와
|
|
19
|
+
* ** 강조 마커 사이에 구두점이 끼면 마커가 닫히지 않는 케이스가 발생한다.
|
|
20
|
+
* 예) "**'rect'**의" → 닫는 ** 가 [구두점] + [CJK] 사이에 위치하므로 right-flanking 실패
|
|
21
|
+
* - 이런 케이스는 marked 에 넘기기 전에 자체적으로 추출 → 자리표시자로 치환 → marked
|
|
22
|
+
* 수행 → 후처리에서 <strong>...</strong> 로 복원하는 방식으로 처리한다.
|
|
23
|
+
* - 이 우회는 사용자가 의도적으로 넣은 ** 만 대상으로 하므로 보안적으로 안전하다 (내용은
|
|
24
|
+
* escapeHtml 로 항상 escape).
|
|
25
|
+
*/
|
|
26
|
+
import DOMPurify from 'dompurify'
|
|
27
|
+
import { marked } from 'marked'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* DOMPurify 정화 정책.
|
|
31
|
+
*
|
|
32
|
+
* 우리가 markdown 출력에서 의도적으로 사용하는 element / attribute 만 명시 허용:
|
|
33
|
+
* - 표준 markdown elements (p, h1~6, ul/ol/li, strong/em, a, img, code, pre, blockquote, hr, br, table 계열)
|
|
34
|
+
* - mention span (class + data-refid + data-userid)
|
|
35
|
+
*
|
|
36
|
+
* 그 외는 모두 strip — 이벤트 핸들러 (onerror/onclick), inline style, javascript:
|
|
37
|
+
* URL 등 자동 차단. 우리 구조에 없는 element (script/iframe/object/embed 등) 도 모두 제거.
|
|
38
|
+
*/
|
|
39
|
+
const PURIFY_CONFIG: any = {
|
|
40
|
+
ALLOWED_TAGS: [
|
|
41
|
+
'p', 'br', 'hr',
|
|
42
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
43
|
+
'strong', 'em', 'del', 's', 'u',
|
|
44
|
+
'ul', 'ol', 'li',
|
|
45
|
+
'a', 'img',
|
|
46
|
+
'code', 'pre',
|
|
47
|
+
'blockquote',
|
|
48
|
+
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
49
|
+
'span'
|
|
50
|
+
],
|
|
51
|
+
ALLOWED_ATTR: [
|
|
52
|
+
'href', 'title', 'alt', 'src', 'class',
|
|
53
|
+
'data-refid', 'data-userid'
|
|
54
|
+
],
|
|
55
|
+
// javascript: / data: / vbscript: 등 위험 스킴 자동 차단
|
|
56
|
+
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel|ftp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
|
|
57
|
+
// target="_blank" 같이 우리가 명시하는 link 외엔 추가 attr 안 붙이도록
|
|
58
|
+
ADD_ATTR: ['target', 'rel'],
|
|
59
|
+
// raw HTML 입력은 marked.renderer.html → '' 로 이미 막혀있지만,
|
|
60
|
+
// dompurify 가 전체 input 을 한 번 더 정화 — 우회 차단
|
|
61
|
+
KEEP_CONTENT: true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const markdownRenderer = new marked.Renderer()
|
|
65
|
+
markdownRenderer.html = () => ''
|
|
66
|
+
|
|
67
|
+
const DANGEROUS_SCHEME_RE = /^(javascript|data|vbscript|file):/i
|
|
68
|
+
|
|
69
|
+
const origLink = markdownRenderer.link.bind(markdownRenderer)
|
|
70
|
+
markdownRenderer.link = (token: any) => {
|
|
71
|
+
const href = typeof token?.href === 'string' ? token.href.trim() : ''
|
|
72
|
+
if (DANGEROUS_SCHEME_RE.test(href)) {
|
|
73
|
+
return token?.text ?? ''
|
|
74
|
+
}
|
|
75
|
+
return origLink(token)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 이미지 renderer — src scheme 검사 + alt 의 inline HTML escape
|
|
79
|
+
const origImage = markdownRenderer.image.bind(markdownRenderer)
|
|
80
|
+
markdownRenderer.image = (token: any) => {
|
|
81
|
+
const src = typeof token?.href === 'string' ? token.href.trim() : ''
|
|
82
|
+
if (DANGEROUS_SCHEME_RE.test(src)) {
|
|
83
|
+
// 위험 src — 이미지 자체 제거. alt 텍스트는 escape 해서 평문 노출
|
|
84
|
+
return escapeHtml(token?.text ?? '')
|
|
85
|
+
}
|
|
86
|
+
return origImage(token)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
marked.setOptions({
|
|
90
|
+
gfm: true,
|
|
91
|
+
breaks: true,
|
|
92
|
+
pedantic: false,
|
|
93
|
+
renderer: markdownRenderer,
|
|
94
|
+
async: false
|
|
95
|
+
} as any)
|
|
96
|
+
|
|
97
|
+
export function escapeHtml(text: string): string {
|
|
98
|
+
return text
|
|
99
|
+
.replace(/&/g, '&')
|
|
100
|
+
.replace(/</g, '<')
|
|
101
|
+
.replace(/>/g, '>')
|
|
102
|
+
.replace(/"/g, '"')
|
|
103
|
+
.replace(/'/g, ''')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// CJK 문자 범위: Hiragana, Katakana, CJK Unified Ideographs (Ext A 포함), Hangul, 전각/반각
|
|
107
|
+
const CJK_RE = /[-ヿ㐀-䶿一-鿿가--]/
|
|
108
|
+
const PUNCT_RE = /\p{P}/u
|
|
109
|
+
|
|
110
|
+
// marked 가 절대 변형하지 않는 토큰 (영숫자만 사용)
|
|
111
|
+
const PLACEHOLDER_PREFIX = 'BAICJKSTRONG'
|
|
112
|
+
const PLACEHOLDER_SUFFIX = 'ENDMARK'
|
|
113
|
+
const MENTION_PLACEHOLDER_PREFIX = 'BAIMENTION'
|
|
114
|
+
const MENTION_PLACEHOLDER_SUFFIX = 'ENDMENTION'
|
|
115
|
+
|
|
116
|
+
// mention 토큰에 허용되는 문자 — alphanumeric + 한글/일본/중국어. 공백/구두점에서 끊김.
|
|
117
|
+
const MENTION_WORD_RE = /[\w\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u
|
|
118
|
+
|
|
119
|
+
interface ExtractResult {
|
|
120
|
+
processed: string
|
|
121
|
+
stored: string[]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface MentionExtractResult {
|
|
125
|
+
processed: string
|
|
126
|
+
stored: StoredMention[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `**X**` 패턴 중 CJK + 구두점 인접 때문에 marked 의 flanking 규칙에서 누락되는
|
|
131
|
+
* 케이스를 추출. 코드 펜스(```)와 인라인 코드(`...`) 안쪽은 건드리지 않는다.
|
|
132
|
+
*
|
|
133
|
+
* 검출 기준 (CommonMark right-flanking 실패 ↔ left-flanking 실패):
|
|
134
|
+
* - 닫는 ** 직전 문자가 구두점 AND 직후 문자가 CJK letter
|
|
135
|
+
* - 여는 ** 직전 문자가 CJK letter AND 직후 문자가 구두점
|
|
136
|
+
*/
|
|
137
|
+
export function extractCjkStrongs(text: string): ExtractResult {
|
|
138
|
+
const stored: string[] = []
|
|
139
|
+
const lines = text.split('\n')
|
|
140
|
+
const outLines: string[] = []
|
|
141
|
+
let inFence = false
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (/^\s{0,3}```/.test(line)) {
|
|
145
|
+
inFence = !inFence
|
|
146
|
+
outLines.push(line)
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
if (inFence) {
|
|
150
|
+
outLines.push(line)
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
outLines.push(processLine(line, stored, () => outLines[outLines.length - 1] ?? ''))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { processed: outLines.join('\n'), stored }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function processLine(line: string, stored: string[], prevLine: () => string): string {
|
|
160
|
+
let i = 0
|
|
161
|
+
let result = ''
|
|
162
|
+
while (i < line.length) {
|
|
163
|
+
const ch = line[i]
|
|
164
|
+
|
|
165
|
+
if (ch === '`') {
|
|
166
|
+
const end = line.indexOf('`', i + 1)
|
|
167
|
+
if (end === -1) {
|
|
168
|
+
result += line.slice(i)
|
|
169
|
+
break
|
|
170
|
+
}
|
|
171
|
+
result += line.slice(i, end + 1)
|
|
172
|
+
i = end + 1
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (ch === '*' && line[i + 1] === '*') {
|
|
177
|
+
const close = findCloseStrong(line, i + 2)
|
|
178
|
+
if (close === -1) {
|
|
179
|
+
result += '**'
|
|
180
|
+
i += 2
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
const inner = line.slice(i + 2, close)
|
|
184
|
+
if (inner.length === 0) {
|
|
185
|
+
result += '**'
|
|
186
|
+
i += 2
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
const before = result.length > 0 ? result.slice(-1) : prevLine().slice(-1)
|
|
190
|
+
const after = close + 2 < line.length ? line[close + 2] : ''
|
|
191
|
+
if (needsCjkFix(before, after, inner)) {
|
|
192
|
+
stored.push(inner)
|
|
193
|
+
result += `${PLACEHOLDER_PREFIX}${stored.length - 1}${PLACEHOLDER_SUFFIX}`
|
|
194
|
+
} else {
|
|
195
|
+
result += line.slice(i, close + 2)
|
|
196
|
+
}
|
|
197
|
+
i = close + 2
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
result += ch
|
|
202
|
+
i++
|
|
203
|
+
}
|
|
204
|
+
return result
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function findCloseStrong(line: string, start: number): number {
|
|
208
|
+
let i = start
|
|
209
|
+
while (i < line.length - 1) {
|
|
210
|
+
if (line[i] === '`') {
|
|
211
|
+
const end = line.indexOf('`', i + 1)
|
|
212
|
+
if (end === -1) return -1
|
|
213
|
+
i = end + 1
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
if (line[i] === '*' && line[i + 1] === '*') return i
|
|
217
|
+
i++
|
|
218
|
+
}
|
|
219
|
+
return -1
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function needsCjkFix(before: string, after: string, inner: string): boolean {
|
|
223
|
+
const innerStart = inner[0]
|
|
224
|
+
const innerEnd = inner[inner.length - 1]
|
|
225
|
+
return (
|
|
226
|
+
(before !== '' && CJK_RE.test(before) && PUNCT_RE.test(innerStart)) ||
|
|
227
|
+
(after !== '' && CJK_RE.test(after) && PUNCT_RE.test(innerEnd))
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function restoreCjkStrongs(html: string, stored: string[]): string {
|
|
232
|
+
let out = html
|
|
233
|
+
for (let n = 0; n < stored.length; n++) {
|
|
234
|
+
const token = `${PLACEHOLDER_PREFIX}${n}${PLACEHOLDER_SUFFIX}`
|
|
235
|
+
const replacement = `<strong>${escapeHtml(stored[n])}</strong>`
|
|
236
|
+
out = out.split(token).join(replacement)
|
|
237
|
+
}
|
|
238
|
+
return out
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `#token` mention 패턴을 추출 → placeholder 로 치환.
|
|
243
|
+
*
|
|
244
|
+
* 활성 조건 (chat textarea 의 extractMentionContext 와 일관):
|
|
245
|
+
* - `#` 직전이 문자열 시작 또는 word char 가 아님 (공백/구두점/줄 시작)
|
|
246
|
+
* - `#` 직후가 word char (영숫자/한글/일본/중국어)
|
|
247
|
+
* - 코드 펜스(```)와 인라인 코드(`...`) 안쪽은 건드리지 않음
|
|
248
|
+
*
|
|
249
|
+
* Markdown ATX 헤딩 (`# Title`) 과 충돌하지 않음 — 헤딩은 `#` 다음에 공백이 오므로
|
|
250
|
+
* 본 정규식 (직후가 word char) 매칭 안 됨.
|
|
251
|
+
*/
|
|
252
|
+
export function extractMentions(text: string): MentionExtractResult {
|
|
253
|
+
const stored: StoredMention[] = []
|
|
254
|
+
const lines = text.split('\n')
|
|
255
|
+
const outLines: string[] = []
|
|
256
|
+
let inFence = false
|
|
257
|
+
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
if (/^\s{0,3}```/.test(line)) {
|
|
260
|
+
inFence = !inFence
|
|
261
|
+
outLines.push(line)
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
if (inFence) {
|
|
265
|
+
outLines.push(line)
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
outLines.push(processMentionLine(line, stored))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { processed: outLines.join('\n'), stored }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** 추출된 mention — trigger 와 함께 보존해 restore 시 trigger 별 다른 span 생성. */
|
|
275
|
+
interface StoredMention {
|
|
276
|
+
trigger: '#' | '@'
|
|
277
|
+
token: string
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function processMentionLine(line: string, stored: StoredMention[]): string {
|
|
281
|
+
let result = ''
|
|
282
|
+
let i = 0
|
|
283
|
+
while (i < line.length) {
|
|
284
|
+
const ch = line[i]
|
|
285
|
+
if (ch === '`') {
|
|
286
|
+
// 인라인 코드 — 닫는 backtick 까지 그대로 통과
|
|
287
|
+
const end = line.indexOf('`', i + 1)
|
|
288
|
+
if (end === -1) {
|
|
289
|
+
result += line.slice(i)
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
result += line.slice(i, end + 1)
|
|
293
|
+
i = end + 1
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
if (ch === '#' || ch === '@') {
|
|
297
|
+
const prev = i > 0 ? line[i - 1] : ''
|
|
298
|
+
const next = line[i + 1] ?? ''
|
|
299
|
+
if (
|
|
300
|
+
(prev === '' || !MENTION_WORD_RE.test(prev)) &&
|
|
301
|
+
next !== '' &&
|
|
302
|
+
MENTION_WORD_RE.test(next)
|
|
303
|
+
) {
|
|
304
|
+
// 토큰 끝 위치 찾기
|
|
305
|
+
let j = i + 1
|
|
306
|
+
while (j < line.length && MENTION_WORD_RE.test(line[j])) j++
|
|
307
|
+
const token = line.slice(i + 1, j)
|
|
308
|
+
stored.push({ trigger: ch as '#' | '@', token })
|
|
309
|
+
result += `${MENTION_PLACEHOLDER_PREFIX}${stored.length - 1}${MENTION_PLACEHOLDER_SUFFIX}`
|
|
310
|
+
i = j
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
result += ch
|
|
315
|
+
i++
|
|
316
|
+
}
|
|
317
|
+
return result
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* placeholder 를 mention span 으로 복원. trigger 별로 다른 span:
|
|
322
|
+
* - `#component` → `<span class="mention" data-refid="N">#token</span>`
|
|
323
|
+
* - `@user` → `<span class="user-mention" data-userid="UUID">@token</span>`
|
|
324
|
+
* 매핑 없으면 data-* 속성 없이 단순 span.
|
|
325
|
+
*/
|
|
326
|
+
function restoreMentions(
|
|
327
|
+
html: string,
|
|
328
|
+
stored: StoredMention[],
|
|
329
|
+
maps: InlineMentionMaps
|
|
330
|
+
): string {
|
|
331
|
+
let out = html
|
|
332
|
+
for (let n = 0; n < stored.length; n++) {
|
|
333
|
+
const placeholder = `${MENTION_PLACEHOLDER_PREFIX}${n}${MENTION_PLACEHOLDER_SUFFIX}`
|
|
334
|
+
const { trigger, token } = stored[n]
|
|
335
|
+
let replacement: string
|
|
336
|
+
if (trigger === '#') {
|
|
337
|
+
const refid = maps.refidByToken.get(token)
|
|
338
|
+
const attr =
|
|
339
|
+
typeof refid === 'number' && Number.isFinite(refid)
|
|
340
|
+
? ` data-refid="${refid}"`
|
|
341
|
+
: ''
|
|
342
|
+
replacement = `<span class="mention"${attr}>#${escapeHtml(token)}</span>`
|
|
343
|
+
} else {
|
|
344
|
+
const userId = maps.userIdByToken.get(token)
|
|
345
|
+
const attr = userId ? ` data-userid="${escapeHtml(userId)}"` : ''
|
|
346
|
+
replacement = `<span class="user-mention"${attr}>@${escapeHtml(token)}</span>`
|
|
347
|
+
}
|
|
348
|
+
out = out.split(placeholder).join(replacement)
|
|
349
|
+
}
|
|
350
|
+
return out
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 인라인 마커 정규식 — `{key:value}` 형태. 현재 사용:
|
|
355
|
+
* - `{refid:N}` — `#component` 멘션
|
|
356
|
+
* - `{userId:UUID}` — `@user` 멘션
|
|
357
|
+
* 향후 추가 종류 (agentId / threadId / 외부 시스템 id 등) 도 동일 형식.
|
|
358
|
+
*/
|
|
359
|
+
const MENTION_MARKER_RE = /\{[a-zA-Z][a-zA-Z0-9]*:[^}\s]+\}/g
|
|
360
|
+
|
|
361
|
+
const WORD_CLASS =
|
|
362
|
+
'\\w\\p{Script=Hangul}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Han}'
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 본문에서 모든 `{key:value}` 마커 제거 — textarea 편집 / LLM 입력 / 평문 표출 등.
|
|
366
|
+
* 토큰 (#component / @user) 자체는 보존.
|
|
367
|
+
*/
|
|
368
|
+
export function stripMentionRefids(text: string | null | undefined): string {
|
|
369
|
+
if (!text) return ''
|
|
370
|
+
return text.replace(MENTION_MARKER_RE, '')
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface InlineMentionMaps {
|
|
374
|
+
/** trigger='#' 의 token → refid (component) */
|
|
375
|
+
refidByToken: Map<string, number>
|
|
376
|
+
/** trigger='@' 의 token → userId (user) */
|
|
377
|
+
userIdByToken: Map<string, string>
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* `#token{refid:N}` / `@user{userId:UUID}` 마커를 추출해 매핑 + 마커 제거된 본문 반환.
|
|
382
|
+
* 렌더링 직전에 호출 — 마킹 정보 분리 후 markdown 처리에 깨끗한 텍스트만.
|
|
383
|
+
*/
|
|
384
|
+
function extractInlineMentions(text: string): {
|
|
385
|
+
cleaned: string
|
|
386
|
+
maps: InlineMentionMaps
|
|
387
|
+
} {
|
|
388
|
+
const refidByToken = new Map<string, number>()
|
|
389
|
+
const userIdByToken = new Map<string, string>()
|
|
390
|
+
|
|
391
|
+
const componentRe = new RegExp(
|
|
392
|
+
`(^|[^${WORD_CLASS}])#([${WORD_CLASS}-]+)\\{refid:(\\d+)\\}`,
|
|
393
|
+
'gu'
|
|
394
|
+
)
|
|
395
|
+
const userRe = new RegExp(
|
|
396
|
+
`(^|[^${WORD_CLASS}])@([${WORD_CLASS}-]+)\\{userId:([^}\\s]+)\\}`,
|
|
397
|
+
'gu'
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
let cleaned = text.replace(componentRe, (_m, leading, token, refidStr) => {
|
|
401
|
+
const refid = Number(refidStr)
|
|
402
|
+
if (Number.isFinite(refid) && !refidByToken.has(token)) refidByToken.set(token, refid)
|
|
403
|
+
return `${leading}#${token}`
|
|
404
|
+
})
|
|
405
|
+
cleaned = cleaned.replace(userRe, (_m, leading, token, userId) => {
|
|
406
|
+
if (!userIdByToken.has(token)) userIdByToken.set(token, userId)
|
|
407
|
+
return `${leading}@${token}`
|
|
408
|
+
})
|
|
409
|
+
return { cleaned, maps: { refidByToken, userIdByToken } }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 일반화된 inline marker 주입.
|
|
414
|
+
* injectInlineMarkers(text, '#', 'refid', [{token, value: 35}])
|
|
415
|
+
* injectInlineMarkers(text, '@', 'userId', [{token, value: 'uuid'}])
|
|
416
|
+
*/
|
|
417
|
+
function injectInlineMarkers(
|
|
418
|
+
text: string,
|
|
419
|
+
trigger: string,
|
|
420
|
+
markerKey: string,
|
|
421
|
+
picks: ReadonlyArray<{ token: string; value: string | number }> | null | undefined
|
|
422
|
+
): string {
|
|
423
|
+
if (!text || !picks || picks.length === 0) return text ?? ''
|
|
424
|
+
const map = new Map<string, string | number>()
|
|
425
|
+
for (const p of picks) map.set(p.token, p.value)
|
|
426
|
+
const escapedTrig = trigger.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
427
|
+
const re = new RegExp(
|
|
428
|
+
`(^|[^${WORD_CLASS}])(${escapedTrig})([${WORD_CLASS}-]+)(\\{${markerKey}:[^}\\s]+\\})?`,
|
|
429
|
+
'gu'
|
|
430
|
+
)
|
|
431
|
+
return text.replace(re, (match, leading, trig, token, existingMarker) => {
|
|
432
|
+
if (existingMarker) return match
|
|
433
|
+
const value = map.get(token)
|
|
434
|
+
if (value === undefined) return match
|
|
435
|
+
return `${leading}${trig}${token}{${markerKey}:${value}}`
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** `#component` 멘션 picks → `{refid:N}` 마커 주입 (호환 유지). */
|
|
440
|
+
export function injectMentionRefids(
|
|
441
|
+
text: string,
|
|
442
|
+
picks: ReadonlyArray<{ token: string; refid: number }> | null | undefined
|
|
443
|
+
): string {
|
|
444
|
+
return injectInlineMarkers(
|
|
445
|
+
text,
|
|
446
|
+
'#',
|
|
447
|
+
'refid',
|
|
448
|
+
picks?.map(p => ({ token: p.token, value: p.refid }))
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** `@user` 멘션 picks → `{userId:UUID}` 마커 주입. */
|
|
453
|
+
export function injectMentionUserIds(
|
|
454
|
+
text: string,
|
|
455
|
+
picks: ReadonlyArray<{ token: string; userId: string }> | null | undefined
|
|
456
|
+
): string {
|
|
457
|
+
return injectInlineMarkers(
|
|
458
|
+
text,
|
|
459
|
+
'@',
|
|
460
|
+
'userId',
|
|
461
|
+
picks?.map(p => ({ token: p.token, value: p.userId }))
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function renderMarkdown(text: string): string {
|
|
466
|
+
if (!text) return ''
|
|
467
|
+
try {
|
|
468
|
+
// 0) inline 마커 추출 (`{refid:N}` / `{userId:UUID}`) → 매핑 분리 + 본문 정화
|
|
469
|
+
const { cleaned, maps } = extractInlineMentions(text)
|
|
470
|
+
// 1) mention placeholder — `#abc` / `@abc` 가 marked 의 ATX 헤딩 / 이메일 등에
|
|
471
|
+
// 휩쓸리지 않도록 영숫자 placeholder 로 치환.
|
|
472
|
+
const m = extractMentions(cleaned)
|
|
473
|
+
// 2) CJK strong placeholder
|
|
474
|
+
const c = extractCjkStrongs(m.processed)
|
|
475
|
+
const out = marked.parse(c.processed)
|
|
476
|
+
if (typeof out !== 'string') {
|
|
477
|
+
// marked 가 Promise 를 반환했다면 (async lexer 등) 안전한 fallback
|
|
478
|
+
return escapeHtml(stripMentionRefids(text)).replace(/\n/g, '<br>')
|
|
479
|
+
}
|
|
480
|
+
// 3) 역순 복원 — CJK strong → mention span. maps 로 data-refid / data-userid embed.
|
|
481
|
+
const restored = restoreMentions(restoreCjkStrongs(out, c.stored), m.stored, maps)
|
|
482
|
+
// 4) DOMPurify 후처리 — defense-in-depth. marked / 우리 mention span 둘 다 한 번 더 정화.
|
|
483
|
+
return sanitizeHtml(restored)
|
|
484
|
+
} catch {
|
|
485
|
+
return escapeHtml(stripMentionRefids(text)).replace(/\n/g, '<br>')
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* DOMPurify wrapper. Node 환경 (테스트) 에서도 안전하게 동작하도록 fallback.
|
|
491
|
+
*
|
|
492
|
+
* 브라우저: window.DOMParser 가 있으므로 DOMPurify 가 직접 정화.
|
|
493
|
+
* Node (테스트): jsdom 없이 DOMPurify 호출은 실패 — 실패 시 escapeHtml fallback
|
|
494
|
+
* (테스트에서 sanitize 동작은 별도 검증, 본 fallback 은 회귀 안전).
|
|
495
|
+
*/
|
|
496
|
+
export function sanitizeHtml(html: string): string {
|
|
497
|
+
try {
|
|
498
|
+
if (typeof window === 'undefined' || !DOMPurify.sanitize) {
|
|
499
|
+
return html
|
|
500
|
+
}
|
|
501
|
+
const result = DOMPurify.sanitize(html, PURIFY_CONFIG)
|
|
502
|
+
return typeof result === 'string' ? result : String(result)
|
|
503
|
+
} catch {
|
|
504
|
+
// DOM 환경 부재 또는 dompurify 초기화 실패 — 입력 그대로 통과
|
|
505
|
+
// (브라우저에선 정상 동작 — Node 테스트에선 sanitize 의 효과를 직접 검증)
|
|
506
|
+
return html
|
|
507
|
+
}
|
|
508
|
+
}
|