import { nextTick } from 'vue' import { createPreviewAppendParagraphValue, createPreviewDeleteLineValue, createPreviewExtractGalleryImageValue, createPreviewGalleryReorderValue, createPreviewImagesMergeValue, createPreviewInsertAfterLineResult, createPreviewInsertImageToGalleryValue, createPreviewMergeWithPreviousLineValue, createPreviewRemoveGalleryImageResult } from '../lib/markdown-editor-preview-blocks.js' import { shouldIgnorePendingMediaSlashCommit } from '../lib/markdown-slash-commands.js' /** * 관리자 Markdown 라이브 미리보기 이벤트 적용 함수를 만든다. * @param {Object} options - 미리보기 액션 옵션 * @param {import('vue').Ref} options.value - Markdown 본문 ref * @param {import('vue').Ref} options.activeLogicalLineIndex - 활성 논리 줄 ref * @param {import('vue').Ref} options.liveSlashInsertLine - 슬래시 미디어 삽입 대기 줄 ref * @param {(startLine: number, endLine: number, replacementLines: string[], focusEditor?: boolean) => void} options.replaceLineRange - 줄 범위 교체 함수 * @param {(startLine: number, fallbackEndLine: number) => number} options.resolveCurrentFencedBlockEndLine - 현재 fenced 블록 끝 줄 계산 함수 * @param {() => void} options.resetBlockPanelContext - 블록 패널 컨텍스트 초기화 함수 * @param {(line: number, column?: number, align?: string, offset?: number, block?: string) => void} options.focusPreviewEditableAtLine - 미리보기 줄 포커스 함수 * @returns {Object} 미리보기 이벤트 적용 함수 */ export const useAdminMarkdownPreviewActions = (options) => { /** * 계산된 Markdown 값을 현재 본문에 반영한다. * @param {string|null} nextValue - 다음 Markdown 값 * @returns {boolean} 적용 여부 */ const applyNextValue = (nextValue) => { if (nextValue === null) { return false } options.value.value = nextValue return true } /** * 미리보기에서 갤러리 순서 변경을 반영한다. * @param {{ startLine: number, endLine: number, images: Array }} payload - 갤러리 줄 범위·이미지 목록 * @returns {void} */ const onPreviewGalleryReorder = ({ startLine, endLine, images }) => { applyNextValue(createPreviewGalleryReorderValue({ value: options.value.value, startLine, endLine, images })) } /** * 라이브 모드에서 두 단일 이미지 블록을 갤러리로 병합한다. * @param {{ sourceLine: number, targetLine: number }} payload - 원본·대상 줄 * @returns {void} */ const onPreviewMergeImagesToGallery = ({ sourceLine, targetLine }) => { applyNextValue(createPreviewImagesMergeValue({ value: options.value.value, sourceLine, targetLine })) } /** * 라이브 모드에서 단일 이미지 블록을 기존 갤러리에 추가한다. * @param {{ sourceLine: number, startLine: number, endLine: number, targetIndex: number, image?: Object }} payload - 이미지 줄·갤러리 범위·삽입 위치 * @returns {void} */ const onPreviewInsertImageToGallery = ({ sourceLine, startLine, endLine, targetIndex, image }) => { applyNextValue(createPreviewInsertImageToGalleryValue({ value: options.value.value, sourceLine, startLine, endLine, targetIndex, image })) } /** * 라이브 모드 갤러리에서 특정 이미지를 삭제한다. * @param {{ startLine: number, endLine: number, imageIndex: number }} payload - 갤러리 범위·이미지 인덱스 * @returns {void} */ const onPreviewRemoveGalleryImage = ({ startLine, endLine, imageIndex }) => { const result = createPreviewRemoveGalleryImageResult({ value: options.value.value, startLine, endLine, imageIndex }) if (!result) { return } options.value.value = result.value options.activeLogicalLineIndex.value = result.focusLine } /** * 라이브 모드에서 갤러리 이미지를 블록 사이에 단일 이미지 줄로 분리한다. * @param {{ startLine: number, endLine: number, imageIndex: number, insertBeforeLine: number, image?: Object }} payload - 갤러리 범위·삽입 위치 * @returns {void} */ const onPreviewExtractGalleryImage = ({ startLine, endLine, imageIndex, insertBeforeLine, image }) => { applyNextValue(createPreviewExtractGalleryImageValue({ value: options.value.value, startLine, endLine, imageIndex, insertBeforeLine, image })) } /** * 미리보기 인라인 편집 결과를 마크다운 본문에 반영한다. * @param {{ startLine: number, endLine: number, replacementLines: string[] }} payload - 줄 범위·대체 줄 * @returns {void} */ const onPreviewBlockContentChange = ({ startLine, endLine, replacementLines }) => { if (typeof startLine !== 'number' || typeof endLine !== 'number' || !Array.isArray(replacementLines)) { return } if (shouldIgnorePendingMediaSlashCommit({ pendingInsertLine: options.liveSlashInsertLine.value, startLine, replacementLines })) { return } options.replaceLineRange( startLine, options.resolveCurrentFencedBlockEndLine(startLine, endLine), replacementLines, false ) } /** * 라이브 모드 교차 선택 삭제 결과를 본문에 반영한다. * @param {{ value: string }} payload - 갱신된 마크다운 * @returns {void} */ const onPreviewContentReplace = ({ value }) => { if (typeof value === 'string') { options.value.value = value } } /** * 라이브 모드 하단에서 새 문단 줄을 추가한다. * @returns {void} */ const onPreviewAppendParagraph = () => { options.value.value = createPreviewAppendParagraphValue(options.value.value) } /** * 라이브 모드에서 지정 줄 아래에 마크다운 줄을 삽입한다. * @param {{ afterLine: number, lines: string[], focusLine?: number }} payload - 삽입 위치·줄 * @returns {void} */ const onPreviewInsertAfterLine = ({ afterLine, lines, focusLine }) => { const result = createPreviewInsertAfterLineResult({ value: options.value.value, afterLine, lines, focusLine }) if (!result) { return } options.value.value = result.value options.activeLogicalLineIndex.value = result.focusLine options.resetBlockPanelContext() nextTick(() => { options.focusPreviewEditableAtLine(result.focusLine, 0, 'start') }) } /** * 라이브 모드에서 현재 줄을 삭제한다. * @param {number} lineIndex - 줄 번호 * @returns {void} */ const onPreviewDeleteLine = (lineIndex) => { applyNextValue(createPreviewDeleteLineValue({ value: options.value.value, lineIndex })) } /** * 라이브 모드에서 현재 줄을 이전 줄 끝에 병합한다. * @param {{ lineIndex: number, mergedLine: string }} payload - 병합 위치·결과 줄 * @returns {void} */ const onPreviewMergeWithPreviousLine = ({ lineIndex, mergedLine }) => { applyNextValue(createPreviewMergeWithPreviousLineValue({ value: options.value.value, lineIndex, mergedLine })) } return { onPreviewAppendParagraph, onPreviewBlockContentChange, onPreviewContentReplace, onPreviewDeleteLine, onPreviewExtractGalleryImage, onPreviewGalleryReorder, onPreviewInsertAfterLine, onPreviewInsertImageToGallery, onPreviewMergeImagesToGallery, onPreviewMergeWithPreviousLine, onPreviewRemoveGalleryImage } }