432 lines
12 KiB
JavaScript
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')
|
|
}
|