From c31a2cf2e78a18e6fc4e9183e8d72236ef9bfd19 Mon Sep 17 00:00:00 2001 From: windpacer Date: Sun, 24 May 2026 13:08:07 +0900 Subject: [PATCH] feat: box-drawing table to GFM pipe table conversion --- src/Web/wwwroot/js/docs.js | 54 ++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/Web/wwwroot/js/docs.js b/src/Web/wwwroot/js/docs.js index 6ae862a..8a8fe1c 100644 --- a/src/Web/wwwroot/js/docs.js +++ b/src/Web/wwwroot/js/docs.js @@ -333,22 +333,66 @@ function docsExportPdf() { /* ── 마크다운 렌더 파이프라인 ──────────────────────────────── */ -/// box-drawing 문자(┌─┬┐│├─┼┤└─┴┘) 라인을 fenced code block으로 감싸 고정폭 렌더링 보장 +/// box-drawing 문자(┌─┬┐│├─┼┤└─┴┘) 라인을 감지하여 표는 GFM pipe table(|---|)로, +/// 일반 box line은 ``` fence로 감싸 고정폭 렌더링 보장 function docsWrapBoxDrawing(text) { const BOX_RE = /^[ \t]*[┌┐└┘├┤┬┴┼│─━]/; const lines = text.split('\n'); const out = []; let inBlock = false; + let buf = []; for (const line of lines) { const isBox = BOX_RE.test(line); - if (isBox && !inBlock) { out.push('```'); inBlock = true; } - else if (!isBox && inBlock) { out.push('```'); inBlock = false; } - out.push(line); + if (isBox && !inBlock) { inBlock = true; buf = [line]; } + else if (isBox && inBlock) { buf.push(line); } + else { + if (inBlock) { out.push(docsBoxBlockToGfm(buf)); inBlock = false; buf = []; } + out.push(line); + } } - if (inBlock) out.push('```'); + if (inBlock) out.push(docsBoxBlockToGfm(buf)); return out.join('\n'); } +/// box-drawing block이 표(┬┴┼ + │)면 GFM pipe table로, 아니면 ``` fence로 +function docsBoxBlockToGfm(buf) { + var joined = buf.join(''); + if (!/[┬┴┼]/.test(joined) || !/│/.test(joined)) + return '```\n' + buf.join('\n') + '\n```'; + + // ┬/┴/┼ 포함 라인에서 컬럼 경계 위치 추출 + var pat = null; + for (var i = 0; i < buf.length; i++) { if (/[┬┴┼]/.test(buf[i])) { pat = buf[i]; break; } } + if (!pat) return '```\n' + buf.join('\n') + '\n```'; + + var seps = []; + for (var i = 0; i < pat.length; i++) { + if ('┬┴┼┌└├┐┘┤'.indexOf(pat[i]) >= 0) seps.push(i); + } + if (seps.length < 2) return '```\n' + buf.join('\n') + '\n```'; + + var numCols = seps.length - 1; + var dataRows = []; + for (var i = 0; i < buf.length; i++) { + if (/^[ \t]*│/.test(buf[i])) dataRows.push(buf[i]); + } + if (dataRows.length === 0) return '```\n' + buf.join('\n') + '\n```'; + + var result = []; + // header + var hdr = []; + for (var c = 0; c < numCols; c++) hdr.push(dataRows[0].substring(seps[c] + 1, seps[c + 1]).trim()); + result.push('| ' + hdr.join(' | ') + ' |'); + result.push('|' + ' --- |'.repeat(numCols)); + // data + for (var r = 1; r < dataRows.length; r++) { + var row = []; + for (var c = 0; c < numCols; c++) row.push(dataRows[r].substring(seps[c] + 1, seps[c + 1]).trim()); + result.push('| ' + row.join(' | ') + ' |'); + } + return result.join('\n'); +} + function docsRenderMarkdownInto(el, text) { text = docsWrapBoxDrawing(text); const rawHtml = marked.parse(text, { gfm: true, breaks: false });