Files
sori.studio/components/content/ContentMarkdownRenderer.vue

124 lines
3.4 KiB
Vue

<script setup>
const props = defineProps({
content: {
type: String,
default: ''
}
})
/**
* 마크다운 블록을 생성
* @param {string} type - 블록 타입
* @param {string|Array<string>} text - 블록 텍스트
* @param {number|null} level - 제목 레벨
* @param {string} id - 블록 ID
* @returns {{ id: string, type: string, text: string|Array<string>, level: number|null }} 블록
*/
const createBlock = (type = 'paragraph', text = '', level = null, id = '') => ({
id,
type,
text,
level
})
/**
* 마크다운 문자열을 렌더링용 블록 목록으로 변환
* @param {string} markdown - 마크다운 문자열
* @returns {Array<Object>} 블록 목록
*/
const parseMarkdownBlocks = (markdown) => {
const lines = markdown.split('\n')
const blocks = []
let index = 0
while (index < lines.length) {
const line = lines[index]
const trimmedLine = line.trim()
if (!trimmedLine) {
index += 1
continue
}
if (trimmedLine.startsWith('```')) {
const codeLines = []
index += 1
while (index < lines.length && !lines[index].trim().startsWith('```')) {
codeLines.push(lines[index])
index += 1
}
blocks.push(createBlock('code', codeLines.join('\n'), null, `block-${blocks.length}`))
index += 1
continue
}
if (trimmedLine === '---') {
blocks.push(createBlock('divider', '', null, `block-${blocks.length}`))
index += 1
continue
}
const headingMatch = trimmedLine.match(/^(#{1,3})\s+(.+)$/)
if (headingMatch) {
blocks.push(createBlock('heading', headingMatch[2], headingMatch[1].length, `block-${blocks.length}`))
index += 1
continue
}
if (trimmedLine.startsWith('> ')) {
blocks.push(createBlock('quote', trimmedLine.replace(/^>\s?/, ''), null, `block-${blocks.length}`))
index += 1
continue
}
if (/^- /.test(trimmedLine)) {
const items = []
while (index < lines.length && /^- /.test(lines[index].trim())) {
items.push(lines[index].trim().replace(/^- /, ''))
index += 1
}
blocks.push(createBlock('list', items, null, `block-${blocks.length}`))
continue
}
blocks.push(createBlock('paragraph', trimmedLine, null, `block-${blocks.length}`))
index += 1
}
return blocks
}
const blocks = computed(() => parseMarkdownBlocks(props.content))
</script>
<template>
<div class="content-markdown-renderer">
<template v-for="block in blocks" :key="block.id">
<ProseHeading v-if="block.type === 'heading'" :level="block.level">
{{ block.text }}
</ProseHeading>
<ProseBlockquote v-else-if="block.type === 'quote'">
{{ block.text }}
</ProseBlockquote>
<ProseList v-else-if="block.type === 'list'">
<li v-for="(item, itemIndex) in block.text" :key="`${block.id}-${itemIndex}`">
{{ item }}
</li>
</ProseList>
<pre
v-else-if="block.type === 'code'"
class="content-markdown-renderer__code my-6 overflow-x-auto rounded bg-[#15171a] px-4 py-3 text-sm leading-6 text-white"
><code>{{ block.text }}</code></pre>
<hr v-else-if="block.type === 'divider'" class="content-markdown-renderer__divider my-10 border-line">
<p v-else class="content-markdown-renderer__paragraph my-5 leading-8">
{{ block.text }}
</p>
</template>
</div>
</template>