Compare commits

...

2 Commits

Author SHA1 Message Date
hyungi 96bd849bcb fix(documents): 절뷰(hasSections) 본문을 MarkdownDoc로 렌더 — 수식·이미지 살림
★진짜 원인: 절 있는 문서(useSectionView)는 절 본문을 plain marked(renderMd)로 렌더해
수식(katex 없음 → raw $$)·이미지(docimg → DOMPurify 미지원프로토콜 제거 → 사라짐)가
전부 깨졌다. 앞선 renderDocMarkdown 수정들은 !hasSections 경로뿐이라 절뷰 문서엔 미적용.
절 bodyText 에 docimg·$$ 실재 확인(3791: docimg 21·blockmath). 데스크탑/모바일 절 본문
{@html renderMd} → <MarkdownDoc documentId mdContent={bodyText}> 로 교체 → pre-render
(수식·이미지 placeholder) + swap(실제 이미지) 적용.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 06:42:24 +09:00
hyungi db7ede04b7 fix(markdown): 이미지 ref pre-render — 렌더러 미발화 시에도 placeholder 표시
docMarked image 렌더러가 런타임 미발화 시 ![](docimg:img_NNN) 가 기본 <img src=docimg:>
로 떨어지고 DOMPurify(미지원 프로토콜)가 제거 → placeholder·이미지 둘 다 사라지던 문제
(수식 토크나이저 미발화와 동형). marked 이전에 image ref 를 placeholder figure 로 직접
pre-render(슬롯 보호, 수식과 동일 우회). 이후 MarkdownDoc swap effect 가 실제 <img> 로 교체.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:34:34 +09:00
2 changed files with 42 additions and 4 deletions
+39 -1
View File
@@ -126,11 +126,49 @@ function _protectMath(text: string, slots: string[]): string {
}); });
} }
// ── 이미지 pre-render ─────────────────────────────────────────────────────────
// docMarked 의 image 렌더러(.use renderer)가 런타임에 미발화하면 `![](docimg:img_NNN)` 가
// 기본 `<img src="docimg:..">` 로 떨어지고, 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 (
`<figure class="md-image-placeholder" data-md-img="1" data-md-image-src="${safeHref}" data-md-image-internal="${internalFlag}" data-md-image-alt="${safeAlt}">` +
`<div class="md-image-placeholder-card">` +
`<span class="md-image-placeholder-icon" aria-hidden="true">🖼️</span>` +
`<span class="md-image-placeholder-label">${safeLabel}</span>` +
`</div>` +
`</figure>`
);
}
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 { export function renderDocMarkdown(text: string | null | undefined): string {
if (!text) return ''; if (!text) return '';
try { try {
const slots: string[] = []; 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; let html = docMarked.parse(protectedText) as string;
if (slots.length) { if (slots.length) {
// 블록 수식이 단독 문단이면 marked 가 <p> 로 감싸므로 그 <p> 를 벗겨 블록 수식이 문단에 // 블록 수식이 단독 문단이면 marked 가 <p> 로 감싸므로 그 <p> 를 벗겨 블록 수식이 문단에
@@ -239,8 +239,8 @@
{/if} {/if}
</div> </div>
{/if} {/if}
{#if selectedBodyHtml} {#if selectedItem?.bodyText}
<div class="prose prose-base max-w-none text-text">{@html selectedBodyHtml}</div> <MarkdownDoc documentId={doc.id} mdContent={selectedItem.bodyText} mdStatus={null} class="prose prose-base max-w-none text-text" />
{:else} {:else}
<p style="color:#9aa090;font-size:14px;font-style:italic;">이 절의 본문은 추출되지 않았습니다. 헤더의 '원본'에서 확인하세요.</p> <p style="color:#9aa090;font-size:14px;font-style:italic;">이 절의 본문은 추출되지 않았습니다. 헤더의 '원본'에서 확인하세요.</p>
{/if} {/if}
@@ -339,7 +339,7 @@
{#if it.bodyText} {#if it.bodyText}
<details class="m-secbody" ontoggle={(e) => { if (e.currentTarget.open) mBodyOpen[s.chunk_id] = true; }}> <details class="m-secbody" ontoggle={(e) => { if (e.currentTarget.open) mBodyOpen[s.chunk_id] = true; }}>
<summary style="cursor:pointer;list-style:none;font-size:12px;color:#697061;padding:5px 0;user-select:none;display:flex;align-items:center;gap:5px;">본문 보기 <span class="m-chev" style="transition:transform .16s;color:#9aa090;"></span></summary> <summary style="cursor:pointer;list-style:none;font-size:12px;color:#697061;padding:5px 0;user-select:none;display:flex;align-items:center;gap:5px;">본문 보기 <span class="m-chev" style="transition:transform .16s;color:#9aa090;"></span></summary>
{#if mBodyOpen[s.chunk_id]}<div class="prose prose-sm max-w-none text-text" style="margin-top:6px;">{@html bodyHtml(it)}</div>{/if} {#if mBodyOpen[s.chunk_id]}<div style="margin-top:6px;"><MarkdownDoc documentId={doc.id} mdContent={it.bodyText} mdStatus={null} class="prose prose-sm max-w-none text-text" /></div>{/if}
</details> </details>
{/if} {/if}
</div> </div>