Files
sori.studio/lib/markdown-content-normalizer.js

182 lines
4.4 KiB
JavaScript

import { buildCalloutOpenerLine } from './markdown-callout.js'
const BLANK_PARAGRAPH_MARKER = '<!--sori:blank-paragraph-->'
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<Object>} 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 ''
}