게시글 상세 목차 사이드바 추가 v1.5.12
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
import { buildCodeBlockLines, parseCodeFenceLine } from '../../lib/markdown-code-block.js'
|
||||
import { buildToggleBlockLines } from '../../lib/markdown-toggle.js'
|
||||
import { CALLOUT_BACKGROUND_OPTIONS, parseCalloutOptions } from '../../lib/markdown-callout.js'
|
||||
import { createHeadingIdFactory } from '../../lib/markdown-toc.js'
|
||||
import ContentMarkdownCodeBlockEditor from './ContentMarkdownCodeBlockEditor.vue'
|
||||
import ProseCodeBlock from './ProseCodeBlock.vue'
|
||||
import ContentMarkdownCalloutEditor from './ContentMarkdownCalloutEditor.vue'
|
||||
@@ -731,6 +732,18 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
}
|
||||
|
||||
const blocks = computed(() => parseMarkdownBlocks(props.content))
|
||||
const headingIdsByBlock = computed(() => {
|
||||
const createHeadingId = createHeadingIdFactory()
|
||||
const ids = {}
|
||||
|
||||
for (const block of blocks.value) {
|
||||
if (block.type === 'heading' && block.level >= 1 && block.level <= 3) {
|
||||
ids[block.id] = createHeadingId(block.text)
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
})
|
||||
|
||||
/** @type {import('vue').ComputedRef<number>} 문서 맨 아래 단일 이미지 삽입 줄 */
|
||||
const tailInsertBeforeLine = computed(() => {
|
||||
@@ -2323,7 +2336,7 @@ onBeforeUnmount(() => {
|
||||
@delete-line="onDeleteLine"
|
||||
@merge-with-previous="onMergeWithPreviousLine(block.meta.startLine, $event)"
|
||||
/>
|
||||
<ProseHeading v-else-if="block.type === 'heading'" :level="block.level">
|
||||
<ProseHeading v-else-if="block.type === 'heading'" :id="headingIdsByBlock[block.id]" :level="block.level">
|
||||
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-heading-${segmentIndex}`">
|
||||
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
|
||||
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
|
||||
|
||||
@@ -3,6 +3,10 @@ const props = defineProps({
|
||||
level: {
|
||||
type: Number,
|
||||
default: 2
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,7 +16,8 @@ const tagName = computed(() => `h${Math.min(Math.max(props.level, 1), 6)}`)
|
||||
<template>
|
||||
<component
|
||||
:is="tagName"
|
||||
class="prose-heading mb-2.5 font-semibold leading-[1.25] tracking-normal first:mt-0"
|
||||
:id="id || undefined"
|
||||
class="prose-heading mb-2.5 scroll-mt-20 font-semibold leading-[1.25] tracking-normal first:mt-0"
|
||||
:class="{
|
||||
'text-[clamp(1.35rem,1.25rem+0.35vw,1.6rem)] leading-[1.15]': level === 1,
|
||||
'text-[clamp(1.2rem,1.15rem+0.3vw,1.4rem)]': level === 2,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup>
|
||||
import { getExternalFaviconUrl } from '~/lib/external-favicon-url.js'
|
||||
|
||||
const route = useRoute()
|
||||
const postToc = useState('post-detail-toc', () => [])
|
||||
|
||||
const followLinks = [
|
||||
{ id: 'facebook', label: 'Facebook', href: 'https://facebook.com', icon: 'facebook' },
|
||||
{ id: 'x', label: 'X', href: 'https://x.com', icon: 'x' },
|
||||
@@ -39,6 +42,8 @@ const recommendedSites = computed(() => {
|
||||
}
|
||||
return list.filter((x) => x?.isVisible !== false)
|
||||
})
|
||||
const isPostDetailRoute = computed(() => route.path.startsWith('/post/'))
|
||||
const postTocItems = computed(() => Array.isArray(postToc.value) ? postToc.value : [])
|
||||
|
||||
/**
|
||||
* 새 탭으로 열 외부 URL인지
|
||||
@@ -202,7 +207,38 @@ const showAboutSection = false
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="recommendedSites.length"
|
||||
v-if="isPostDetailRoute"
|
||||
class="right-sidebar__block right-sidebar__toc site-sidebar-section py-5 pl-5 pr-0"
|
||||
>
|
||||
<div class="right-sidebar__row flex items-center justify-between">
|
||||
<p class="right-sidebar__eyebrow text-xs font-semibold uppercase site-muted">
|
||||
TOC
|
||||
</p>
|
||||
</div>
|
||||
<nav class="right-sidebar__toc-nav mt-4" aria-label="게시글 목차">
|
||||
<ul v-if="postTocItems.length" class="right-sidebar__toc-list list-none space-y-2 p-0">
|
||||
<li v-for="item in postTocItems" :key="item.id">
|
||||
<a
|
||||
class="right-sidebar__toc-link site-interactive block rounded-md py-1.5 pr-3 text-sm leading-snug text-[var(--site-text)] hover:text-[var(--site-accent)]"
|
||||
:class="{
|
||||
'pl-0 font-semibold': item.level === 1,
|
||||
'pl-3': item.level === 2,
|
||||
'pl-6 text-xs site-muted': item.level === 3
|
||||
}"
|
||||
:href="`#${item.id}`"
|
||||
>
|
||||
{{ item.text }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="right-sidebar__toc-empty text-sm site-muted">
|
||||
목차로 표시할 제목이 없습니다.
|
||||
</p>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="recommendedSites.length"
|
||||
class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-0"
|
||||
>
|
||||
<div class="right-sidebar__row flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user