90 lines
2.2 KiB
JavaScript
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
|
|
}
|