공개 Markdown 본문 fragment 주석을 줄이도록 렌더링 구조 정리 (v1.10.4)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-01 17:01:14 +09:00
parent c686b1c848
commit 943794ae34
9 changed files with 144 additions and 22 deletions

View File

@@ -0,0 +1,56 @@
<script>
import { h, mergeProps } from 'vue'
export default {
name: 'ContentMarkdownBlockGroup',
inheritAttrs: false,
props: {
/** 라이브 편집 모드 여부 */
interactive: {
type: Boolean,
default: false
},
/** 블록 앞 삽입 영역 표시 여부 */
showInsert: {
type: Boolean,
default: false
},
/** 블록 앞 삽입 영역 활성 상태 */
insertActive: {
type: Boolean,
default: false
}
},
setup(props, { slots, attrs }) {
/**
* 삽입 영역과 블록 본문을 조건부로 묶어 fragment 주석을 줄인다.
* @returns {import('vue').VNode|import('vue').VNode[]} 렌더 노드
*/
return () => {
const children = []
if (props.interactive && props.showInsert) {
children.push(h('div', mergeProps({
class: [
'content-markdown-renderer__block-insert',
props.insertActive ? 'content-markdown-renderer__block-insert--active' : ''
]
}, attrs)))
}
const defaultContent = slots.default?.()
if (defaultContent) {
const normalized = Array.isArray(defaultContent) ? defaultContent : [defaultContent]
children.push(...normalized)
}
if (children.length === 1) {
return children[0]
}
return children
}
}
}
</script>

View File

@@ -39,14 +39,37 @@ const getInlineColorClass = (color) => `content-markdown-inline-segments__color-
<template>
<span class="content-markdown-inline-segments">
<span
v-for="(lineSegments, lineIndex) in segmentLines"
:key="`inline-line-${lineIndex}`"
v-for="(segment, segmentIndex) in segmentLines[0] || []"
:key="`inline-segment-0-${segmentIndex}`"
class="content-markdown-inline-segments__segment"
>
<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-inline-segments__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="linkClass" :href="segment.href" target="_blank" rel="noreferrer">
<ContentMarkdownInlineSegments :text="segment.text" :link-class="linkClass" :multiline="multiline" />
</a>
<span
v-else-if="segment.type === 'color'"
class="content-markdown-inline-segments__color"
:class="getInlineColorClass(segment.color)"
:data-inline-color="segment.color"
>
<ContentMarkdownInlineSegments :text="segment.text" :link-class="linkClass" :multiline="multiline" />
</span>
<span v-else>{{ segment.text }}</span>
</span>
<span
v-for="(lineSegments, offsetIndex) in segmentLines.slice(1)"
:key="`inline-line-${offsetIndex + 1}`"
class="content-markdown-inline-segments__line"
>
<br v-if="lineIndex > 0">
<br>
<span
v-for="(segment, segmentIndex) in lineSegments"
:key="`inline-segment-${lineIndex}-${segmentIndex}`"
:key="`inline-segment-${offsetIndex + 1}-${segmentIndex}`"
class="content-markdown-inline-segments__segment"
>
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>

View File

@@ -963,8 +963,6 @@ const inArticleAdAfterBlockIds = computed(() => {
.map((item) => item.block.id)
})
const inArticleAdAfterBlockIdSet = computed(() => new Set(inArticleAdAfterBlockIds.value))
const headingIdsByBlock = computed(() => {
const createHeadingId = createHeadingIdFactory()
const ids = {}
@@ -978,6 +976,39 @@ const headingIdsByBlock = computed(() => {
return ids
})
/**
* 공개 본문 렌더링용 블록 목록을 반환한다.
* 인아티클 광고는 별도 v-if 대신 블록으로 삽입해 fragment 주석을 줄인다.
* @returns {Array<Object>} 렌더링 블록 목록
*/
const renderBlocks = computed(() => {
const sourceBlocks = blocks.value
const adAfterBlockIds = inArticleAdAfterBlockIds.value
if (props.interactive || !adAfterBlockIds.length) {
return sourceBlocks
}
const adAfterIdSet = new Set(adAfterBlockIds)
const result = []
for (const block of sourceBlocks) {
result.push(block)
if (adAfterIdSet.has(block.id)) {
result.push({
id: `in-article-ad-after-${block.id}`,
type: 'in-article-ad',
meta: {
afterBlockId: block.id
}
})
}
}
return result
})
/** @type {import('vue').ComputedRef<number>} 문서 맨 아래 단일 이미지 삽입 줄 */
const tailInsertBeforeLine = computed(() => {
const lastBlock = blocks.value[blocks.value.length - 1]
@@ -3040,15 +3071,16 @@ onBeforeUnmount(() => {
@focusout.capture="emitLiveLineBlur"
@keydown.capture="onRendererSelectionKeydown"
>
<template v-for="block in blocks" :key="block.id">
<div
v-if="interactive && getBlockInsertBeforeLine(block) !== null"
class="content-markdown-renderer__block-insert"
:class="{ 'content-markdown-renderer__block-insert--active': blockInsertDropTarget?.insertBeforeLine === getBlockInsertBeforeLine(block) }"
@dragover="onBlockInsertDragOver($event, block)"
@dragleave="onBlockInsertDragLeave"
@drop="onBlockInsertDrop($event, block)"
/>
<ContentMarkdownBlockGroup
v-for="block in renderBlocks"
:key="block.id"
:interactive="interactive"
:show-insert="getBlockInsertBeforeLine(block) !== null"
:insert-active="blockInsertDropTarget?.insertBeforeLine === getBlockInsertBeforeLine(block)"
@dragover="onBlockInsertDragOver($event, block)"
@dragleave="onBlockInsertDragLeave"
@drop="onBlockInsertDrop($event, block)"
>
<ContentMarkdownEditableInline
v-if="block.type === 'spacer' && interactive"
tag="p"
@@ -3509,14 +3541,13 @@ onBeforeUnmount(() => {
<p v-else-if="block.type === 'paragraph'" class="content-markdown-renderer__paragraph mb-2.5 text-base text-[var(--site-text)] last:mb-0">
<ContentMarkdownInlineSegments :text="block.text" multiline />
</p>
<SiteAdSlot
v-if="inArticleAdAfterBlockIdSet.has(block.id)"
v-else-if="block.type === 'in-article-ad'"
class="content-markdown-renderer__in-article-ad my-8"
:code="normalizedInArticleAdCode"
location="post-in-article"
/>
</template>
</ContentMarkdownBlockGroup>
<div
v-if="interactive"