라이브 미리보기 블록 계산 분리
This commit is contained in:
431
lib/markdown-editor-preview-blocks.js
Normal file
431
lib/markdown-editor-preview-blocks.js
Normal file
@@ -0,0 +1,431 @@
|
||||
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')
|
||||
}
|
||||
Reference in New Issue
Block a user