라이브 미리보기 블록 계산 분리

This commit is contained in:
2026-07-29 10:10:51 +09:00
parent d709b5d94c
commit 57dac3f81a
10 changed files with 662 additions and 233 deletions

View File

@@ -2,8 +2,6 @@
import { normalizeMarkdownContent } from '../../lib/markdown-content-normalizer.js'
import { resolveActiveBlockContext } from '../../lib/markdown-block-context.js'
import {
isImageUrl,
parseImageMarkdownLine,
serializeImageMarkdown
} from '../../lib/markdown-image.js'
import {
@@ -54,6 +52,18 @@ import {
createSourceInlineColorValue,
createToggleOptionsReplacementPlan
} from '../../lib/markdown-editor-block-options.js'
import {
createPreviewAppendParagraphValue,
createPreviewDeleteLineValue,
createPreviewExtractGalleryImageValue,
createPreviewGalleryReorderValue,
createPreviewImagesMergeValue,
createPreviewInsertAfterLineResult,
createPreviewInsertImageToGalleryValue,
createPreviewMergeWithPreviousLineValue,
createPreviewRemoveGalleryImageResult,
parseEditorImageLine
} from '../../lib/markdown-editor-preview-blocks.js'
import {
createSlashCommandApplyPlan,
createSlashCommandEditorEffectPlan,
@@ -1093,33 +1103,6 @@ const insertCodeBlock = () => {
*/
const createImageMarkdown = (image) => serializeImageMarkdown(image)
/**
* 마크다운 이미지 줄과 단독 이미지 URL 줄을 같은 이미지 데이터로 해석한다.
* @param {string} line - 원본 마크다운 줄
* @returns {{ url: string, width: string, pixelWidth: number|null, caption: string, useAlt: boolean }|null} 이미지 데이터
*/
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
}
}
/**
* 미디어 선택 모달 파생 이미지 로드 실패 시 원본 URL로 되돌린다.
* @param {Event} event - 이미지 오류 이벤트
@@ -1250,15 +1233,16 @@ const setActiveMediaUseAlt = (imageIndex, enabled) => {
* @returns {void}
*/
const onPreviewGalleryReorder = ({ startLine, endLine, images }) => {
if (typeof startLine !== 'number' || typeof endLine !== 'number' || !Array.isArray(images)) {
return
}
const nextValue = createPreviewGalleryReorderValue({
value: markdownValue.value,
startLine,
endLine,
images
})
replaceLineRange(startLine, endLine, [
':::gallery',
...images.map(createImageMarkdown),
':::'
], false)
if (nextValue !== null) {
markdownValue.value = nextValue
}
}
/**
@@ -1267,33 +1251,15 @@ const onPreviewGalleryReorder = ({ startLine, endLine, images }) => {
* @returns {void}
*/
const onPreviewMergeImagesToGallery = ({ sourceLine, targetLine }) => {
if (typeof sourceLine !== 'number' || typeof targetLine !== 'number' || sourceLine === targetLine) {
return
const nextValue = createPreviewImagesMergeValue({
value: markdownValue.value,
sourceLine,
targetLine
})
if (nextValue !== null) {
markdownValue.value = nextValue
}
const lines = (markdownValue.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
}
const orderedImages = sourceLine < targetLine
? [firstImage, secondImage]
: [secondImage, firstImage]
const nextLines = [
...lines.slice(0, firstLine),
':::gallery',
...orderedImages.map(createImageMarkdown),
':::',
...lines.slice(secondLine + 1)
]
markdownValue.value = nextLines.join('\n')
}
/**
@@ -1308,43 +1274,18 @@ const onPreviewInsertImageToGallery = ({
targetIndex,
image
}) => {
if (typeof sourceLine !== 'number'
|| typeof startLine !== 'number'
|| typeof endLine !== 'number'
|| typeof targetIndex !== 'number'
|| sourceLine >= startLine && sourceLine <= endLine) {
return
const nextValue = createPreviewInsertImageToGalleryValue({
value: markdownValue.value,
sourceLine,
startLine,
endLine,
targetIndex,
image
})
if (nextValue !== null) {
markdownValue.value = nextValue
}
const lines = (markdownValue.value || '').split('\n')
const sourceImage = image && image.url ? image : parseEditorImageLine(lines[sourceLine] || '')
const galleryImages = lines
.slice(startLine + 1, endLine)
.map((line) => parseEditorImageLine(line))
.filter(Boolean)
if (!sourceImage || !galleryImages.length) {
return
}
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)
markdownValue.value = [
...withoutSource.slice(0, adjustedStartLine),
':::gallery',
...nextGalleryImages.map(createImageMarkdown),
':::',
...withoutSource.slice(adjustedEndLine + 1)
].join('\n')
}
/**
@@ -1353,33 +1294,19 @@ const onPreviewInsertImageToGallery = ({
* @returns {void}
*/
const onPreviewRemoveGalleryImage = ({ startLine, endLine, imageIndex }) => {
if (typeof startLine !== 'number' || typeof endLine !== 'number' || typeof imageIndex !== 'number') {
const result = createPreviewRemoveGalleryImageResult({
value: markdownValue.value,
startLine,
endLine,
imageIndex
})
if (!result) {
return
}
const lines = (markdownValue.value || '').split('\n')
const galleryImages = lines
.slice(startLine + 1, endLine)
.map((line) => parseEditorImageLine(line))
.filter(Boolean)
if (imageIndex < 0 || imageIndex >= galleryImages.length) {
return
}
const remaining = galleryImages.filter((_, index) => index !== imageIndex)
const replacementLines = remaining.length === 0
? []
: remaining.length === 1
? [createImageMarkdown(remaining[0])]
: [
':::gallery',
...remaining.map(createImageMarkdown),
':::'
]
replaceLineRange(startLine, endLine, replacementLines, false)
activeLogicalLineIndex.value = Math.max(0, Math.min(startLine + imageIndex, startLine + replacementLines.length - 1))
markdownValue.value = result.value
activeLogicalLineIndex.value = result.focusLine
}
/**
@@ -1394,61 +1321,18 @@ const onPreviewExtractGalleryImage = ({
insertBeforeLine,
image
}) => {
if (typeof startLine !== 'number'
|| typeof endLine !== 'number'
|| typeof imageIndex !== 'number'
|| typeof insertBeforeLine !== 'number') {
return
const nextValue = createPreviewExtractGalleryImageValue({
value: markdownValue.value,
startLine,
endLine,
imageIndex,
insertBeforeLine,
image
})
if (nextValue !== null) {
markdownValue.value = nextValue
}
const lines = (markdownValue.value || '').split('\n')
const galleryImages = lines
.slice(startLine + 1, endLine)
.map((line) => parseEditorImageLine(line))
.filter(Boolean)
if (imageIndex < 0 || imageIndex >= galleryImages.length) {
return
}
const extracted = image && image.url ? image : galleryImages[imageIndex]
const remaining = galleryImages.filter((_, index) => index !== imageIndex)
const galleryReplacement = remaining.length === 0
? []
: remaining.length === 1
? [createImageMarkdown(remaining[0])]
: [
':::gallery',
...remaining.map(createImageMarkdown),
':::'
]
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, createImageMarkdown(extracted))
let galleryAt = startLine
if (startLine > insertAt) {
galleryAt += 1
}
withoutGallery.splice(galleryAt, 0, ...galleryReplacement)
markdownValue.value = withoutGallery.join('\n')
}
/**
@@ -1490,9 +1374,7 @@ const onPreviewContentReplace = ({ value }) => {
* @returns {void}
*/
const onPreviewAppendParagraph = () => {
const value = markdownValue.value ?? ''
const trimmed = value.replace(/\n+$/, '')
markdownValue.value = trimmed ? `${trimmed}\n\n` : ''
markdownValue.value = createPreviewAppendParagraphValue(markdownValue.value)
}
/**
@@ -1501,26 +1383,23 @@ const onPreviewAppendParagraph = () => {
* @returns {void}
*/
const onPreviewInsertAfterLine = ({ afterLine, lines, focusLine }) => {
if (typeof afterLine !== 'number' || !Array.isArray(lines)) {
const result = createPreviewInsertAfterLineResult({
value: markdownValue.value,
afterLine,
lines,
focusLine
})
if (!result) {
return
}
const sourceLines = (markdownValue.value ?? '').split('\n')
const nextFocusLine = typeof focusLine === 'number'
? focusLine
: afterLine + 1
markdownValue.value = [
...sourceLines.slice(0, afterLine + 1),
...lines,
...sourceLines.slice(afterLine + 1)
].join('\n')
activeLogicalLineIndex.value = nextFocusLine
markdownValue.value = result.value
activeLogicalLineIndex.value = result.focusLine
resetBlockPanelContext()
nextTick(() => {
focusPreviewEditableAtLine(nextFocusLine, 0, 'start')
focusPreviewEditableAtLine(result.focusLine, 0, 'start')
})
}
@@ -1530,22 +1409,14 @@ const onPreviewInsertAfterLine = ({ afterLine, lines, focusLine }) => {
* @returns {void}
*/
const onPreviewDeleteLine = (lineIndex) => {
if (typeof lineIndex !== 'number' || lineIndex < 0) {
return
const nextValue = createPreviewDeleteLineValue({
value: markdownValue.value,
lineIndex
})
if (nextValue !== null) {
markdownValue.value = nextValue
}
const sourceLines = (markdownValue.value ?? '').split('\n')
if (lineIndex >= sourceLines.length) {
return
}
const nextLines = [
...sourceLines.slice(0, lineIndex),
...sourceLines.slice(lineIndex + 1)
]
markdownValue.value = nextLines.length ? nextLines.join('\n') : ''
}
/**
@@ -1594,11 +1465,6 @@ const handlePreviewKeydownCapture = (event) => {
onPreviewDeleteLine(lineIndex)
}
/**
* 라이브 모드에서 현재 줄을 이전 줄 끝에 병합한다.
* @param {{ lineIndex: number, mergedLine: string }} payload - 병합 위치·결과 줄
* @returns {void}
*/
/**
* 슬래시 메뉴 컴포넌트가 노출한 실제 DOM 엘리먼트를 반환한다.
* @returns {HTMLElement|null} 메뉴 엘리먼트
@@ -1980,22 +1846,21 @@ const insertMarkdownAtLine = (lineIndex, insertLines) => {
markdownValue.value = insertMarkdownLinesAtSlashTarget(markdownValue.value, lineIndex, insertLines)
}
/**
* 라이브 모드에서 현재 줄을 이전 줄 끝에 병합한다.
* @param {{ lineIndex: number, mergedLine: string }} payload - 병합 위치·결과 줄
* @returns {void}
*/
const onPreviewMergeWithPreviousLine = ({ lineIndex, mergedLine }) => {
if (typeof lineIndex !== 'number' || lineIndex <= 0) {
return
const nextValue = createPreviewMergeWithPreviousLineValue({
value: markdownValue.value,
lineIndex,
mergedLine
})
if (nextValue !== null) {
markdownValue.value = nextValue
}
const sourceLines = (markdownValue.value ?? '').split('\n')
if (lineIndex >= sourceLines.length) {
return
}
markdownValue.value = [
...sourceLines.slice(0, lineIndex - 1),
String(mergedLine ?? ''),
...sourceLines.slice(lineIndex + 1)
].join('\n')
}
/**