Files
sori.studio/lib/markdown-toc.js

90 lines
2.2 KiB
JavaScript

/**
* 제목 텍스트에서 인라인 마크다운 기호를 제거한다.
* @param {string} value - 원본 제목 텍스트
* @returns {string} 정리된 제목 텍스트
*/
export const stripMarkdownHeadingText = (value) => String(value || '')
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/[`*_~]/g, '')
.replace(/<[^>]+>/g, '')
.replace(/\s+/g, ' ')
.trim()
/**
* 제목 텍스트를 앵커 ID 기본값으로 변환한다.
* @param {string} value - 제목 텍스트
* @returns {string} 앵커 기본값
*/
export const createHeadingSlugBase = (value) => {
const cleaned = stripMarkdownHeadingText(value)
.toLowerCase()
.replace(/[^\p{Letter}\p{Number}\s-]/gu, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
return cleaned || 'section'
}
/**
* 중복 제목을 보정하는 제목 ID 생성기를 만든다.
* @returns {(value: string) => string} 제목 ID 생성 함수
*/
export const createHeadingIdFactory = () => {
const counts = new Map()
return (value) => {
const base = createHeadingSlugBase(value)
const nextCount = (counts.get(base) || 0) + 1
counts.set(base, nextCount)
return nextCount === 1 ? base : `${base}-${nextCount}`
}
}
/**
* 마크다운 본문에서 H1~H3 목차 항목을 추출한다.
* @param {string} markdown - 마크다운 본문
* @returns {Array<{ id: string, level: number, text: string }>} 목차 항목
*/
export const extractMarkdownToc = (markdown) => {
const createHeadingId = createHeadingIdFactory()
const toc = []
const lines = String(markdown || '').split('\n')
let inCodeFence = false
for (const line of lines) {
const trimmedLine = line.trim()
if (trimmedLine.startsWith('```')) {
inCodeFence = !inCodeFence
continue
}
if (inCodeFence) {
continue
}
const headingMatch = trimmedLine.match(/^(#{1,3})\s+(.+)$/)
if (!headingMatch) {
continue
}
const text = stripMarkdownHeadingText(headingMatch[2])
if (!text) {
continue
}
toc.push({
id: createHeadingId(headingMatch[2]),
level: headingMatch[1].length,
text
})
}
return toc
}