import { isImageUrl, parseImageMarkdownLine, serializeImageMarkdown } from './markdown-image.js' /** * 마크다운 이미지 줄과 단독 이미지 URL 줄을 같은 이미지 데이터로 해석한다. * @param {string} line - 원본 마크다운 줄 * @returns {{ url: string, width: string, pixelWidth: number|null, caption: string, useAlt: boolean }|null} 이미지 데이터 */ export const parseEditorImageLine = (line) => { const image = parseImageMarkdownLine(line) if (image) { return image } const trimmed = String(line || '').trim() if (!isImageUrl(trimmed)) { return null } return { url: trimmed, width: 'regular', pixelWidth: null, caption: '', useAlt: false } } /** * 줄 범위를 새 줄 목록으로 교체한 Markdown 값을 만든다. * @param {Object} options - 교체 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.startLine - 시작 줄 * @param {number} options.endLine - 끝 줄 * @param {string[]} options.replacementLines - 대체 줄 목록 * @returns {string|null} 변경된 Markdown */ export const replacePreviewLineRangeValue = ({ value, startLine, endLine, replacementLines }) => { if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || !Array.isArray(replacementLines)) { return null } const lines = String(value || '').split('\n') if (startLine < 0 || endLine < startLine || startLine >= lines.length) { return null } return [ ...lines.slice(0, startLine), ...replacementLines, ...lines.slice(endLine + 1) ].join('\n') } /** * 이미지 목록을 갤러리 fenced block 줄로 직렬화한다. * @param {Array} images - 이미지 목록 * @returns {string[]} 갤러리 줄 목록 */ const createGalleryLines = (images) => [ ':::gallery', ...images.map(serializeImageMarkdown), ':::' ] /** * 갤러리 순서 변경 결과 Markdown 값을 만든다. * @param {Object} options - 변경 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.startLine - 갤러리 시작 줄 * @param {number} options.endLine - 갤러리 끝 줄 * @param {Array} options.images - 변경된 이미지 목록 * @returns {string|null} 변경된 Markdown */ export const createPreviewGalleryReorderValue = ({ value, startLine, endLine, images }) => { if (!Array.isArray(images)) { return null } return replacePreviewLineRangeValue({ value, startLine, endLine, replacementLines: createGalleryLines(images) }) } /** * 두 단일 이미지 줄을 갤러리로 병합한 Markdown 값을 만든다. * @param {Object} options - 병합 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.sourceLine - 드래그 원본 줄 * @param {number} options.targetLine - 드롭 대상 줄 * @returns {string|null} 변경된 Markdown */ export const createPreviewImagesMergeValue = ({ value, sourceLine, targetLine }) => { if (!Number.isInteger(sourceLine) || !Number.isInteger(targetLine) || sourceLine === targetLine) { return null } const lines = String(value || '').split('\n') const firstLine = Math.min(sourceLine, targetLine) const secondLine = Math.max(sourceLine, targetLine) const firstImage = parseEditorImageLine(lines[firstLine] || '') const secondImage = parseEditorImageLine(lines[secondLine] || '') if (!firstImage || !secondImage) { return null } const orderedImages = sourceLine < targetLine ? [firstImage, secondImage] : [secondImage, firstImage] return [ ...lines.slice(0, firstLine), ...createGalleryLines(orderedImages), ...lines.slice(secondLine + 1) ].join('\n') } /** * 갤러리 안 이미지 목록을 읽는다. * @param {string[]} lines - 전체 Markdown 줄 * @param {number} startLine - 갤러리 시작 줄 * @param {number} endLine - 갤러리 끝 줄 * @returns {Array} 이미지 목록 */ const getGalleryImagesFromLines = (lines, startLine, endLine) => lines .slice(startLine + 1, endLine) .map((line) => parseEditorImageLine(line)) .filter(Boolean) /** * 남은 갤러리 이미지 수에 맞는 대체 줄 목록을 만든다. * @param {Array} images - 남은 이미지 목록 * @returns {string[]} 대체 줄 목록 */ export const createGalleryReplacementLines = (images) => { if (!Array.isArray(images) || images.length === 0) { return [] } if (images.length === 1) { return [serializeImageMarkdown(images[0])] } return createGalleryLines(images) } /** * 단일 이미지 줄을 기존 갤러리에 삽입한 Markdown 값을 만든다. * @param {Object} options - 삽입 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.sourceLine - 단일 이미지 줄 * @param {number} options.startLine - 갤러리 시작 줄 * @param {number} options.endLine - 갤러리 끝 줄 * @param {number} options.targetIndex - 삽입 인덱스 * @param {Object} [options.image] - 전달받은 이미지 데이터 * @returns {string|null} 변경된 Markdown */ export const createPreviewInsertImageToGalleryValue = ({ value, sourceLine, startLine, endLine, targetIndex, image }) => { if (!Number.isInteger(sourceLine) || !Number.isInteger(startLine) || !Number.isInteger(endLine) || !Number.isInteger(targetIndex) || (sourceLine >= startLine && sourceLine <= endLine)) { return null } const lines = String(value || '').split('\n') const sourceImage = image && image.url ? image : parseEditorImageLine(lines[sourceLine] || '') const galleryImages = getGalleryImagesFromLines(lines, startLine, endLine) if (!sourceImage || !galleryImages.length) { return null } const nextGalleryImages = [...galleryImages] const insertIndex = Math.max(0, Math.min(targetIndex, nextGalleryImages.length)) nextGalleryImages.splice(insertIndex, 0, sourceImage) const withoutSource = [ ...lines.slice(0, sourceLine), ...lines.slice(sourceLine + 1) ] const adjustedStartLine = startLine - (sourceLine < startLine ? 1 : 0) const adjustedEndLine = endLine - (sourceLine < endLine ? 1 : 0) return [ ...withoutSource.slice(0, adjustedStartLine), ...createGalleryLines(nextGalleryImages), ...withoutSource.slice(adjustedEndLine + 1) ].join('\n') } /** * 갤러리 이미지 삭제 결과를 만든다. * @param {Object} options - 삭제 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.startLine - 갤러리 시작 줄 * @param {number} options.endLine - 갤러리 끝 줄 * @param {number} options.imageIndex - 삭제할 이미지 인덱스 * @returns {{ value: string, focusLine: number }|null} 변경 결과 */ export const createPreviewRemoveGalleryImageResult = ({ value, startLine, endLine, imageIndex }) => { if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || !Number.isInteger(imageIndex)) { return null } const lines = String(value || '').split('\n') const galleryImages = getGalleryImagesFromLines(lines, startLine, endLine) if (imageIndex < 0 || imageIndex >= galleryImages.length) { return null } const replacementLines = createGalleryReplacementLines( galleryImages.filter((_, index) => index !== imageIndex) ) const nextValue = replacePreviewLineRangeValue({ value, startLine, endLine, replacementLines }) if (nextValue === null) { return null } return { value: nextValue, focusLine: Math.max(0, Math.min(startLine + imageIndex, startLine + replacementLines.length - 1)) } } /** * 갤러리 이미지를 단일 이미지 줄로 분리한 Markdown 값을 만든다. * @param {Object} options - 분리 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.startLine - 갤러리 시작 줄 * @param {number} options.endLine - 갤러리 끝 줄 * @param {number} options.imageIndex - 분리할 이미지 인덱스 * @param {number} options.insertBeforeLine - 삽입 위치 * @param {Object} [options.image] - 전달받은 이미지 데이터 * @returns {string|null} 변경된 Markdown */ export const createPreviewExtractGalleryImageValue = ({ value, startLine, endLine, imageIndex, insertBeforeLine, image }) => { if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || !Number.isInteger(imageIndex) || !Number.isInteger(insertBeforeLine)) { return null } const lines = String(value || '').split('\n') const galleryImages = getGalleryImagesFromLines(lines, startLine, endLine) if (imageIndex < 0 || imageIndex >= galleryImages.length) { return null } const extracted = image && image.url ? image : galleryImages[imageIndex] const galleryReplacement = createGalleryReplacementLines( galleryImages.filter((_, index) => index !== imageIndex) ) const withoutGallery = [ ...lines.slice(0, startLine), ...lines.slice(endLine + 1) ] const removedSpan = endLine - startLine + 1 const addedSpan = galleryReplacement.length let insertAt = insertBeforeLine if (insertBeforeLine > endLine) { insertAt -= removedSpan - addedSpan } else if (insertBeforeLine > startLine) { insertAt = startLine + addedSpan } insertAt = Math.max(0, Math.min(insertAt, withoutGallery.length)) withoutGallery.splice(insertAt, 0, serializeImageMarkdown(extracted)) let galleryAt = startLine if (startLine > insertAt) { galleryAt += 1 } withoutGallery.splice(galleryAt, 0, ...galleryReplacement) return withoutGallery.join('\n') } /** * 라이브 하단 문단 추가 결과 Markdown 값을 만든다. * @param {string} value - 원본 Markdown * @returns {string} 변경된 Markdown */ export const createPreviewAppendParagraphValue = (value) => { const currentValue = String(value ?? '') const trimmed = currentValue.replace(/\n+$/, '') return trimmed ? `${trimmed}\n\n` : '' } /** * 지정 줄 아래에 새 줄을 삽입한 결과를 만든다. * @param {Object} options - 삽입 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.afterLine - 삽입 기준 줄 * @param {string[]} options.lines - 삽입할 줄 목록 * @param {number} [options.focusLine] - 포커스할 줄 * @returns {{ value: string, focusLine: number }|null} 변경 결과 */ export const createPreviewInsertAfterLineResult = ({ value, afterLine, lines, focusLine }) => { if (!Number.isInteger(afterLine) || !Array.isArray(lines)) { return null } const sourceLines = String(value ?? '').split('\n') const nextFocusLine = Number.isInteger(focusLine) ? focusLine : afterLine + 1 return { value: [ ...sourceLines.slice(0, afterLine + 1), ...lines, ...sourceLines.slice(afterLine + 1) ].join('\n'), focusLine: nextFocusLine } } /** * 지정 줄을 삭제한 Markdown 값을 만든다. * @param {Object} options - 삭제 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.lineIndex - 삭제할 줄 * @returns {string|null} 변경된 Markdown */ export const createPreviewDeleteLineValue = ({ value, lineIndex }) => { if (!Number.isInteger(lineIndex) || lineIndex < 0) { return null } const sourceLines = String(value ?? '').split('\n') if (lineIndex >= sourceLines.length) { return null } const nextLines = [ ...sourceLines.slice(0, lineIndex), ...sourceLines.slice(lineIndex + 1) ] return nextLines.length ? nextLines.join('\n') : '' } /** * 현재 줄을 이전 줄 끝에 병합한 Markdown 값을 만든다. * @param {Object} options - 병합 옵션 * @param {string} options.value - 원본 Markdown * @param {number} options.lineIndex - 병합할 줄 * @param {string} options.mergedLine - 병합된 줄 내용 * @returns {string|null} 변경된 Markdown */ export const createPreviewMergeWithPreviousLineValue = ({ value, lineIndex, mergedLine }) => { if (!Number.isInteger(lineIndex) || lineIndex <= 0) { return null } const sourceLines = String(value ?? '').split('\n') if (lineIndex >= sourceLines.length) { return null } return [ ...sourceLines.slice(0, lineIndex - 1), String(mergedLine ?? ''), ...sourceLines.slice(lineIndex + 1) ].join('\n') }