Files
sori.studio/lib/markdown-editor-preview-blocks.js

432 lines
12 KiB
JavaScript

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<Object>} 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<Object>} 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<Object>} 이미지 목록
*/
const getGalleryImagesFromLines = (lines, startLine, endLine) => lines
.slice(startLine + 1, endLine)
.map((line) => parseEditorImageLine(line))
.filter(Boolean)
/**
* 남은 갤러리 이미지 수에 맞는 대체 줄 목록을 만든다.
* @param {Array<Object>} 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')
}