From db7ede04b772654d28b6e4995d38767d3c444fc5 Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 15 Jun 2026 16:34:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(markdown):=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?ref=20pre-render=20=E2=80=94=20=EB=A0=8C=EB=8D=94=EB=9F=AC=20?= =?UTF-8?q?=EB=AF=B8=EB=B0=9C=ED=99=94=20=EC=8B=9C=EC=97=90=EB=8F=84=20pla?= =?UTF-8?q?ceholder=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docMarked image 렌더러가 런타임 미발화 시 ![](docimg:img_NNN) 가 기본 로 떨어지고 DOMPurify(미지원 프로토콜)가 제거 → placeholder·이미지 둘 다 사라지던 문제 (수식 토크나이저 미발화와 동형). marked 이전에 image ref 를 placeholder figure 로 직접 pre-render(슬롯 보호, 수식과 동일 우회). 이후 MarkdownDoc swap effect 가 실제 로 교체. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/lib/utils/docMarkdown.ts | 40 ++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/utils/docMarkdown.ts b/frontend/src/lib/utils/docMarkdown.ts index 9af6d30..5405208 100644 --- a/frontend/src/lib/utils/docMarkdown.ts +++ b/frontend/src/lib/utils/docMarkdown.ts @@ -126,11 +126,49 @@ function _protectMath(text: string, slots: string[]): string { }); } +// ── 이미지 pre-render ───────────────────────────────────────────────────────── +// docMarked 의 image 렌더러(.use renderer)가 런타임에 미발화하면 `![](docimg:img_NNN)` 가 +// 기본 `` 로 떨어지고, DOMPurify(ALLOW_UNKNOWN_PROTOCOLS:false)가 +// `docimg:` 를 미지원 프로토콜로 제거 → placeholder 도 이미지도 둘 다 사라진다(수식 토크나이저 +// 미발화와 동형 증상). → marked 가 손대기 전에 image ref 를 placeholder figure 로 직접 변환해 +// 슬롯 보호(렌더러 발화 여부와 무관). 슬롯/복원 메커니즘은 수식과 공유. +const _IMG_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g; + +function _imagePlaceholder(alt: string, href: string): string { + const isInternal = href.startsWith('docimg:'); + const basename = href.split('/').pop() ?? href; + const labelSrc = alt || basename || '이미지'; + const safeHref = escAttr(href); + const safeAlt = escAttr(alt); + const safeLabel = escText(`[이미지: ${labelSrc} — 아직 표시되지 않음]`); + const internalFlag = isInternal ? '1' : '0'; + return ( + `
` + + `
` + + `` + + `${safeLabel}` + + `
` + + `
` + ); +} + +function _protectImages(text: string, slots: string[]): string { + return text.replace(_IMG_RE, (m, alt, href) => { + try { + slots.push(_imagePlaceholder(String(alt ?? ''), String(href ?? ''))); + return _MATH_SLOT(slots.length - 1); + } catch { + return m; + } + }); +} + export function renderDocMarkdown(text: string | null | undefined): string { if (!text) return ''; try { const slots: string[] = []; - const protectedText = _protectMath(text, slots); + // 이미지 먼저 placeholder 로 pre-render(렌더러 우회) → 그 다음 수식. 슬롯 공유. + const protectedText = _protectMath(_protectImages(text, slots), slots); let html = docMarked.parse(protectedText) as string; if (slots.length) { // 블록 수식이 단독 문단이면 marked 가

로 감싸므로 그

를 벗겨 블록 수식이 문단에