/** * 제목 텍스트에서 인라인 마크다운 기호를 제거한다. * @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 }