공개 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"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 요약
|
||||
|
||||
## v1.10.4
|
||||
|
||||
- 공개 Markdown 본문 블록 사이에 보이던 Vue fragment 주석을 줄이도록 렌더링 구조를 정리했다.
|
||||
- 인라인 문단 첫 줄에서 불필요한 조건 분기 주석을 제거했다.
|
||||
|
||||
## v1.10.3
|
||||
|
||||
- 게시물 목록과 태그 목록 하단 페이지네이션을 번호형 빠른 이동 UI로 바꿨다.
|
||||
|
||||
@@ -151,6 +151,7 @@
|
||||
| components/content/ContentMarkdownRenderer.vue | 마크다운 문자열 기반 본문 렌더링, 문단 text-base(16px), 빈 줄 spacer 보존·hard break `<br>` 처리, 표준 마크다운 표 파싱·렌더링(셀 내부 인라인 링크와 `<br>` 줄바꿈 포함), 공개 게시물 인아티클 광고를 본문 길이에 따라 0~2회 삽입, Obsidian식 위첨자·아래첨자 인라인 렌더링, 확장 블록 파싱, `:::` fenced 블록 원본 범위 보정, 닫히지 않은 코드 펜스 하위 콘텐츠 보호, 인용 막대 색상 옵션(`> [!bg=...]`), 라이브 문단 `>` 즉시 인용 변환과 ` ``` `·`!!!` Enter 코드 블록·콜아웃 단축 생성, 라이브 콜아웃·인용 포커스 기반 오른쪽 설정 패널 연결, 라이브 인용·콜아웃 멀티라인 편집 줄 범위 포커스와 위/아래 방향키 외부 문단 이탈, 인용 Backspace 문단 복귀, 라이브 방향키 이동 시 편집 가능한 줄·카드형 블록 탐색, 라이브 코드·콜아웃·토글 내부 줄 삭제와 마지막 줄 블록 삭제, 라이브 이미지·갤러리 드래그 병합·추가·분리 UI, 본문 표시용 WebP 이미지 우선 표시와 원본 fallback, 단일 이미지·갤러리 통합 라이트박스, 갤러리 비율 기반 행 레이아웃, 라이브 이미지·갤러리 hover 삭제 액션, 리스트 마커 파란 계열 통일과 여러 줄 항목 첫 줄 기준 정렬 |
|
||||
| components/content/ContentMarkdownEditableInline.vue | 라이브 모드 공통 인라인 편집 영역, 일반 인라인 마크다운·Obsidian식 첨자 렌더링과 plain text 멀티라인 본문·끝 줄바꿈 보존, 한글 IME 조합 확정 Enter의 블록별 동작 연결, 위/아래 인접 블록 이동 시 편집 스크롤 추적, Shift 위/아래 인접 블록 선택 확장·단계적 `Cmd/Ctrl+A` 처리, 멀티라인 Enter 텍스트 값 치환, 첫 줄 빈 줄 포함 줄바꿈 유지 |
|
||||
| components/content/ContentMarkdownInlineSegments.vue | 공개·라이브 본문 공통 인라인 세그먼트 렌더링(링크·굵게·기울임·인라인 코드·첨자·인라인 색상), 링크·색상 내부 중첩 인라인 렌더링, 사용자 화면 fragment 주석 반복을 줄이는 단일 루트 구조 |
|
||||
| components/content/ContentMarkdownBlockGroup.vue | Markdown 본문 블록 단위 렌더 그룹, 라이브 삽입 영역을 조건부로만 DOM에 추가해 공개 화면 fragment 주석 감소 |
|
||||
| lib/markdown-live-selection.js | 라이브 모드 Selection Bridge, 인접 contenteditable 범위 확장·블록/문서 전체 선택·교차 선택 삭제 마크다운 반영, 콜아웃·인용 전체 선택 삭제 시 빈 본문 줄 보존, Selection focus 기준 연속 확장 |
|
||||
| components/content/ProseHeading.vue | h1~h6 제목, 기본 mt-12 제거 |
|
||||
| components/content/ProseImage.vue | 본문 내 이미지, 클릭 확대 요청, 본문 표시용 이미지 실패 시 원본 fallback, lazy 로딩, 로드 실패·빈 URL placeholder |
|
||||
|
||||
@@ -88,7 +88,8 @@
|
||||
- 댓글 아바타 이미지 로드 실패 시 이니셜 아바타로 즉시 대체한다.
|
||||
- 공개 게시물 본문은 콘텐츠 타입별 컴포넌트로 분리해 추후 스타일 변경이 쉽도록 구성
|
||||
- 공개 게시물 오른쪽 목차는 H1~H3 제목 표시 텍스트에서 링크·굵게·기울임·인라인 코드 같은 마크다운 장식만 제거하고 `~` 같은 일반 제목 문자는 보존한다.
|
||||
- 공개 Markdown 인라인 렌더러는 단일 루트와 `display: contents`를 사용해 사용자 화면 DOM의 Vue fragment 주석 반복을 줄이고, 기존 인라인 흐름은 유지한다. 링크 텍스트와 색상 토큰 내부의 굵게·기울임·인라인 코드·색상·첨자 문법은 재귀적으로 렌더링한다.
|
||||
- 공개 Markdown 인라인 렌더러는 단일 루트와 `display: contents`를 사용해 사용자 화면 DOM의 Vue fragment 주석 반복을 줄이고, 기존 인라인 흐름은 유지한다. 링크 텍스트와 색상 토큰 내부의 굵게·기울임·인라인 코드·색상·첨자 문법은 재귀적으로 렌더링한다. 여러 줄 문단은 첫 줄과 이후 줄을 분리해 첫 줄에 불필요한 `br` 조건 분기를 두지 않는다.
|
||||
- 공개 Markdown 본문 블록 렌더링은 `ContentMarkdownBlockGroup`으로 블록 단위 루트를 정리하고, 인아티클 광고는 별도 `v-if` 대신 렌더 블록 목록에 삽입한다.
|
||||
- Markdown 리스트 마커는 항목이 여러 줄로 줄바꿈되어도 항목 전체 높이 중앙이 아니라 첫 줄 기준으로 정렬한다.
|
||||
- 표준 마크다운 표(`| 헤더 | ... |`, `| --- | ... |`)는 본문에서 가로 스크롤 가능한 HTML table로 렌더링한다. 정렬 구분선(`:---`, `:---:`, `---:`)은 각각 좌/중앙/우 정렬로 반영한다. 표 헤더와 셀 내부의 인라인 링크·굵게·기울임·인라인 코드를 렌더링하고, 셀 내부 `<br>` 또는 `<br/>`은 줄바꿈으로 표시한다. 링크 텍스트 안의 `**굵게**` 같은 중첩 인라인 문법도 렌더링한다.
|
||||
- 인용문(`>`)은 왼쪽 세로 막대형 기본 스타일로 표시한다. 기본 인용 텍스트는 라이트·다크 모드 모두 사이트 본문 텍스트 색상(`--site-text`)을 따른다. 첫 줄 옵션 `> [!bg=blue]` 또는 `> {bg=blue}`는 인용 막대 색상으로 반영하며, 지원 값은 콜아웃과 같은 `gray`, `blue`, `green`, `yellow`, `red`, `purple`이다.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.10.4
|
||||
|
||||
- 공개 Markdown 본문 블록 사이 Vue fragment 주석(`<!---->`)을 줄이도록 블록 그룹 렌더러와 인아티클 광고 블록 삽입 방식 정리.
|
||||
- 인라인 렌더러 첫 줄 `br` 조건 분기를 제거해 단일 줄 문단 내부 주석을 줄임.
|
||||
|
||||
## v1.10.3
|
||||
|
||||
- 공개 게시물 목록과 태그 게시물 목록 페이지네이션을 번호형 빠른 이동 UI로 교체.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.3",
|
||||
"version": "1.10.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.3",
|
||||
"version": "1.10.4",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.10.3",
|
||||
"version": "1.10.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
Reference in New Issue
Block a user