import { buildCalloutOpenerLine } from './markdown-callout.js' const BLANK_PARAGRAPH_MARKER = '' const blockSpacingTypes = new Set(['list']) /** * 이미지 블록을 마크다운 문자열로 변환한다. * @param {Object} image - 이미지 데이터 * @returns {string} 이미지 마크다운 */ const serializeImageBlock = (image = {}) => { const url = String(image.url || '').trim() if (!url) { return '' } const width = image.width && image.width !== 'regular' ? `{width=${image.width}}` : '' return `![${image.alt || ''}](${url})${width}` } /** * 레거시 블록 하나를 마크다운 조각으로 변환한다. * @param {Object} block - 레거시 에디터 블록 * @param {number} index - 블록 인덱스 * @param {number} total - 전체 블록 수 * @returns {{ type: string, value: string }|null} 마크다운 조각 */ const serializeLegacyBlock = (block = {}, index = 0, total = 1) => { if (typeof block.value === 'string') { return block.value.trim() ? { type: block.type || 'paragraph', value: block.value } : null } const type = block.type || 'paragraph' const rawText = String(block.text || '') const text = rawText.trim() if (type === 'divider') { return { type, value: '---' } } if (type === 'image') { const image = serializeImageBlock(block) return image ? { type, value: image } : null } if (type === 'gallery') { const images = Array.isArray(block.images) ? block.images.map(serializeImageBlock).filter(Boolean) : [] return images.length ? { type, value: [':::gallery', ...images, ':::'].join('\n') } : null } if (type === 'callout') { return text ? { type, value: [ buildCalloutOpenerLine({ calloutEmojiEnabled: block.calloutEmojiEnabled, calloutEmoji: block.calloutEmoji, calloutBackground: block.calloutBackground, title: block.title }), text, ':::' ].join('\n') } : null } if (type === 'toggle') { const title = String(block.title || '').trim() return title || text ? { type, value: `:::toggle ${title || '더 보기'}\n${text}\n:::` } : null } if (type === 'embed') { const url = String(block.url || '').trim() return url ? { type, value: url } : null } if (type === 'paragraph' && !text) { return index === total - 1 ? null : { type, value: BLANK_PARAGRAPH_MARKER } } if (!text) { return null } if (type === 'heading') { return { type, value: `${'#'.repeat(block.level || 2)} ${text}` } } if (type === 'quote') { return { type, value: `> ${text}` } } if (type === 'list') { return { type, value: `- ${text}` } } if (type === 'code') { return { type, value: `\`\`\`\n${rawText}\n\`\`\`` } } return { type, value: text } } /** * 레거시 블록 배열을 저장용 마크다운 문자열로 변환한다. * @param {Array} blocks - 레거시 블록 목록 * @returns {string} 마크다운 문자열 */ const serializeLegacyBlocks = (blocks) => blocks .map((block, index) => serializeLegacyBlock(block, index, blocks.length)) .filter(Boolean) .reduce((markdown, block, index, blocksList) => { if (index === 0) { return block.value } const previousBlock = blocksList[index - 1] const joiner = blockSpacingTypes.has(previousBlock.type) && blockSpacingTypes.has(block.type) ? '\n' : '\n\n' return `${markdown}${joiner}${block.value}` }, '') /** * 게시물/페이지 본문 값을 저장 가능한 마크다운 문자열로 정규화한다. * @param {unknown} value - 본문 값 * @returns {string} 마크다운 문자열 */ export const normalizeMarkdownContent = (value) => { if (typeof value === 'string') { return value } if (Array.isArray(value)) { return serializeLegacyBlocks(value) } if (value && typeof value === 'object') { if (typeof value.content === 'string') { return value.content } if (Array.isArray(value.blocks)) { return serializeLegacyBlocks(value.blocks) } if (typeof value.markdown === 'string') { return value.markdown } if (typeof value.type === 'string') { const block = serializeLegacyBlock(value) return block?.value || '' } } return '' }