공개 Markdown 본문 fragment 주석을 줄이도록 렌더링 구조 정리 (v1.10.4)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
56
components/content/ContentMarkdownBlockGroup.vue
Normal file
56
components/content/ContentMarkdownBlockGroup.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user