/** * 닫힘 표식까지의 행 목록을 반환한다. * @param {Array} lines - 전체 마크다운 행 * @param {number} startIndex - 본문 시작 인덱스 * @returns {{contentLines: Array, endLine: number, nextIndex: number}} 블록 본문과 다음 인덱스 */ export const collectFencedLines = (lines, startIndex) => { const contentLines = [] let index = startIndex while (index < lines.length && lines[index].trim() !== ':::') { contentLines.push(lines[index]) index += 1 } return { contentLines, endLine: index < lines.length ? index : Math.max(lines.length - 1, startIndex - 1, 0), nextIndex: index < lines.length ? index + 1 : lines.length } } /** * 북마크 fenced 블록 본문에서 URL·제목·설명·썸네일을 파싱한다. * @param {string} raw - fenced 내부 텍스트 * @returns {{url: string, title: string, description: string, thumbnail: string}} 북마크 메타 */ export const parseBookmarkMeta = (raw) => { const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean) const meta = { url: '', title: '', description: '', thumbnail: '' } for (const line of lines) { const kv = line.match(/^(\w+)=(.*)$/) if (kv) { const key = kv[1].toLowerCase() const value = kv[2].trim() if (key === 'url') { meta.url = value } else if (key === 'title') { meta.title = value } else if (key === 'description' || key === 'desc') { meta.description = value } else if (key === 'thumbnail' || key === 'image') { meta.thumbnail = value } continue } if (!meta.url && /^https?:\/\//i.test(line)) { meta.url = line continue } if (meta.url && !meta.title) { meta.title = line continue } if (meta.url && meta.title && !meta.description) { meta.description = line } } return meta } /** * 회원가입(CTA) fenced 블록 본문에서 표시 문구를 파싱한다. * @param {string} raw - fenced 내부 텍스트 * @returns {{title: string, description: string, button: string, placeholder: string}} CTA 메타 */ export const parseSignupMeta = (raw) => { const meta = { title: '뉴스레터에 가입하세요', description: '새 글이 올라오면 받아보실 수 있어요.', button: '구독하기', placeholder: 'you@example.com' } const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean) for (const line of lines) { const kv = line.match(/^(\w+)=(.*)$/) if (!kv) { continue } const key = kv[1].toLowerCase() const value = kv[2].trim() if (key === 'title') { meta.title = value } else if (key === 'description' || key === 'desc') { meta.description = value } else if (key === 'button') { meta.button = value } else if (key === 'placeholder') { meta.placeholder = value } } return meta } /** * 미디어 fenced 블록 본문에서 URL과 표시 메타를 파싱한다. * @param {string} raw - fenced 내부 텍스트 * @returns {{url: string, title: string, description: string, poster: string, caption: string, fileName: string, size: string}} 미디어 메타 */ export const parseMediaMeta = (raw) => { const meta = { url: '', title: '', description: '', poster: '', caption: '', fileName: '', size: '' } const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean) for (const line of lines) { const kv = line.match(/^(\w+)=(.*)$/) if (kv) { const key = kv[1].toLowerCase() const value = kv[2].trim() if (key === 'url' || key === 'src') { meta.url = value } else if (key === 'title') { meta.title = value } else if (key === 'description' || key === 'desc') { meta.description = value } else if (key === 'poster' || key === 'thumbnail') { meta.poster = value } else if (key === 'caption') { meta.caption = value } else if (key === 'name' || key === 'filename' || key === 'file') { meta.fileName = value } else if (key === 'size') { meta.size = value } continue } if (!meta.url && (/^https?:\/\//i.test(line) || line.startsWith('/'))) { meta.url = line continue } if (meta.url && !meta.title) { meta.title = line continue } if (meta.url && meta.title && !meta.description) { meta.description = line } } return meta }