@@ -467,6 +467,90 @@ const parseMediaMeta = (raw) => {
|
||||
return meta
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 행 후보인지 확인한다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {boolean} 표 행 후보 여부
|
||||
*/
|
||||
const isMarkdownTableRow = (line) => {
|
||||
const value = String(line || '').trim()
|
||||
return value.startsWith('|') && value.endsWith('|') && value.split('|').length >= 4
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 구분선 행인지 확인한다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {boolean} 구분선 여부
|
||||
*/
|
||||
const isMarkdownTableSeparator = (line) => {
|
||||
if (!isMarkdownTableRow(line)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return splitMarkdownTableRow(line).every((cell) => /^:?-{3,}:?$/.test(cell.trim()))
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 행을 셀 배열로 나눈다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {string[]} 셀 목록
|
||||
*/
|
||||
const splitMarkdownTableRow = (line) => String(line || '')
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim().replace(/\\\|/g, '|'))
|
||||
|
||||
/**
|
||||
* 표 정렬 행에서 셀 정렬 값을 파싱한다.
|
||||
* @param {string} cell - 구분선 셀
|
||||
* @returns {'left'|'center'|'right'} 정렬 값
|
||||
*/
|
||||
const parseMarkdownTableAlignment = (cell) => {
|
||||
const value = String(cell || '').trim()
|
||||
|
||||
if (value.startsWith(':') && value.endsWith(':')) {
|
||||
return 'center'
|
||||
}
|
||||
|
||||
if (value.endsWith(':')) {
|
||||
return 'right'
|
||||
}
|
||||
|
||||
return 'left'
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 블록을 파싱한다.
|
||||
* @param {string[]} lines - 전체 마크다운 줄
|
||||
* @param {number} startIndex - 표 시작 후보 줄
|
||||
* @returns {{headers: string[], alignments: string[], rows: string[][], endLine: number, nextIndex: number}|null} 표 메타
|
||||
*/
|
||||
const parseMarkdownTableBlock = (lines, startIndex) => {
|
||||
if (!isMarkdownTableRow(lines[startIndex]) || !isMarkdownTableSeparator(lines[startIndex + 1])) {
|
||||
return null
|
||||
}
|
||||
|
||||
const headers = splitMarkdownTableRow(lines[startIndex])
|
||||
const alignments = splitMarkdownTableRow(lines[startIndex + 1]).map(parseMarkdownTableAlignment)
|
||||
const rows = []
|
||||
let index = startIndex + 2
|
||||
|
||||
while (index < lines.length && isMarkdownTableRow(lines[index]) && !isMarkdownTableSeparator(lines[index])) {
|
||||
rows.push(splitMarkdownTableRow(lines[index]))
|
||||
index += 1
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
alignments,
|
||||
rows,
|
||||
endLine: index - 1,
|
||||
nextIndex: index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 문자열을 렌더링용 블록 목록으로 변환
|
||||
* @param {string} markdown - 마크다운 문자열
|
||||
@@ -697,6 +781,24 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
continue
|
||||
}
|
||||
|
||||
const tableBlock = parseMarkdownTableBlock(lines, index)
|
||||
if (tableBlock) {
|
||||
const startLine = index
|
||||
blocks.push(attachSourceRange(
|
||||
createBlock('table', '', null, `block-${blocks.length}`, {
|
||||
meta: {
|
||||
headers: tableBlock.headers,
|
||||
alignments: tableBlock.alignments,
|
||||
rows: tableBlock.rows
|
||||
}
|
||||
}),
|
||||
startLine,
|
||||
tableBlock.endLine
|
||||
))
|
||||
index = tableBlock.nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (isQuoteMarkerLine(line)) {
|
||||
const startLine = index
|
||||
const rawQuoteLines = []
|
||||
@@ -1283,6 +1385,23 @@ const getHeadingEditableClass = (level) => {
|
||||
return `${base} text-[clamp(0.9rem,0.875rem+0.1vw,1rem)]`
|
||||
}
|
||||
|
||||
/**
|
||||
* 표 셀 정렬 클래스를 반환한다.
|
||||
* @param {string} alignment - 정렬 값
|
||||
* @returns {string} Tailwind 정렬 클래스
|
||||
*/
|
||||
const getTableCellAlignClass = (alignment) => {
|
||||
if (alignment === 'center') {
|
||||
return 'text-center'
|
||||
}
|
||||
|
||||
if (alignment === 'right') {
|
||||
return 'text-right'
|
||||
}
|
||||
|
||||
return 'text-left'
|
||||
}
|
||||
|
||||
/**
|
||||
* 제목 블록 마크다운 줄을 만든다.
|
||||
* @param {number} level - 제목 레벨
|
||||
@@ -3332,6 +3451,55 @@ onBeforeUnmount(() => {
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="block.type === 'table'"
|
||||
class="content-markdown-renderer__table-wrap my-6 overflow-x-auto"
|
||||
:data-source-line="block.meta.startLine"
|
||||
:data-source-line-end="block.meta.endLine"
|
||||
>
|
||||
<table class="content-markdown-renderer__table w-full min-w-[520px] border-collapse text-sm text-[var(--site-text)]">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="(header, headerIndex) in block.meta.headers"
|
||||
:key="`${block.id}-table-head-${headerIndex}`"
|
||||
class="border border-line bg-[color-mix(in_srgb,var(--site-text)_6%,transparent)] px-3 py-2 font-semibold"
|
||||
:class="getTableCellAlignClass(block.meta.alignments?.[headerIndex])"
|
||||
>
|
||||
<template v-for="(segment, segmentIndex) in parseInlineSegments(header)" :key="`${block.id}-table-head-${headerIndex}-${segmentIndex}`">
|
||||
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
|
||||
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
|
||||
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
|
||||
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
|
||||
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
|
||||
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
|
||||
<template v-else>{{ segment.text }}</template>
|
||||
</template>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIndex) in block.meta.rows" :key="`${block.id}-table-row-${rowIndex}`">
|
||||
<td
|
||||
v-for="(_, cellIndex) in block.meta.headers"
|
||||
:key="`${block.id}-table-cell-${rowIndex}-${cellIndex}`"
|
||||
class="border border-line px-3 py-2 align-top"
|
||||
:class="getTableCellAlignClass(block.meta.alignments?.[cellIndex])"
|
||||
>
|
||||
<template v-for="(segment, segmentIndex) in parseInlineSegments(row[cellIndex] || '')" :key="`${block.id}-table-cell-${rowIndex}-${cellIndex}-${segmentIndex}`">
|
||||
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
|
||||
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
|
||||
<sub v-else-if="segment.type === 'subscript'">{{ segment.text }}</sub>
|
||||
<sup v-else-if="segment.type === 'superscript'">{{ segment.text }}</sup>
|
||||
<code v-else-if="segment.type === 'code'" class="content-markdown-renderer__inline-code rounded bg-[#252525] px-1.5 py-0.5 text-[0.9em] text-white">{{ segment.text }}</code>
|
||||
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
|
||||
<template v-else>{{ segment.text }}</template>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<ContentMarkdownCodeBlockEditor
|
||||
v-else-if="block.type === 'code' && interactive"
|
||||
:language="block.codeLanguage"
|
||||
|
||||
Reference in New Issue
Block a user