124 lines
3.4 KiB
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>
|