import { unref } from 'vue' import { hasListMarker, isEmptyListMarkerLine, parseOrderedListMarker, stripListMarker } from '../lib/markdown-live-edit.js' /** * 관리자 라이브 Markdown 목록 편집 이벤트 상태를 만든다. * @param {Object} options - 목록 편집 옵션 * @param {import('vue').Ref|string} options.content - Markdown 본문 * @param {import('vue').Ref} options.pendingFocusLine - 다음 포커스 줄 * @param {import('vue').Ref<'start'|'end'|'auto'>} options.pendingFocusPosition - 다음 포커스 커서 위치 * @param {(block: Object, replacementLines: string[]) => void} options.commitInlineBlockLines - 블록 줄 반영 콜백 * @param {(block: Object, options?: { lines?: string[] }) => void} options.insertBelowBlock - 블록 아래 삽입 콜백 * @returns {Object} 목록 이벤트 핸들러 */ export const useContentMarkdownLists = (options) => { /** * commit 이벤트 페이로드를 정규화한다. * @param {string|{ value: string, raw?: boolean }} payload - 페이로드 * @returns {{ value: string, raw: boolean }} */ const normalizeCommitPayload = (payload) => { if (typeof payload === 'string') { return { value: payload, raw: false } } return { value: String(payload?.value ?? ''), raw: payload?.raw === true } } /** * insert-below 이벤트 페이로드를 정규화한다. * @param {string|Object} payload - 페이로드 * @returns {{ value: string, raw: boolean, before: string, after: string, caretAtStart: boolean, caretAtEnd: boolean }} */ const normalizeInsertBelowPayload = (payload) => { if (typeof payload === 'string') { return { value: payload, raw: false, before: '', after: '', caretAtStart: false, caretAtEnd: true } } return { value: String(payload?.value ?? ''), raw: payload?.raw === true, before: String(payload?.before ?? ''), after: String(payload?.after ?? ''), caretAtStart: payload?.caretAtStart === true, caretAtEnd: payload?.caretAtEnd === true } } /** * 원본 마크다운 줄을 반환한다. * @param {number} lineIndex - 줄 번호 * @returns {string} 줄 텍스트 */ const getMarkdownLine = (lineIndex) => String(unref(options.content) || '').split('\n')[lineIndex] ?? '' /** * 블록에 해당하는 원본 마크다운 줄 목록을 반환한다. * @param {Object} block - 블록 * @returns {string[]} 줄 목록 */ const getBlockSourceLines = (block) => String(unref(options.content) || '').split('\n').slice( block.meta.startLine, (block.meta.endLine ?? block.meta.startLine) + 1 ) /** * 목록 줄 반영 후 포커스를 준비한다. * @param {number} line - 줄 번호 * @returns {void} */ const prepareListFocus = (line) => { options.pendingFocusLine.value = line options.pendingFocusPosition.value = 'start' } /** * 순서 목록 항목에 표시할 번호를 반환한다. * @param {Object} block - 목록 블록 * @param {number} itemIndex - 항목 인덱스 * @returns {number} 목록 번호 */ const getListMarkerNumber = (block, itemIndex) => { if (block.listNumbers?.[itemIndex] != null) { return block.listNumbers[itemIndex] } const line = getMarkdownLine(block.meta.startLine + itemIndex) const parsed = parseOrderedListMarker(line) return parsed?.number ?? itemIndex + 1 } /** * 목록 항목 마크다운 줄을 만든다. * @param {string} value - 편집 값 * @param {boolean} raw - 원문 모드 여부 * @param {Object} block - 목록 블록 * @param {number} itemIndex - 항목 인덱스 * @returns {string} 마크다운 줄 */ const formatListLine = (value, raw, block, itemIndex) => { if (raw) { if (!hasListMarker(value, block.ordered)) { return String(value ?? '').trim() } return value } const clean = stripListMarker(value, block.ordered) if (block.ordered) { const number = getListMarkerNumber(block, itemIndex) return clean ? `${number}. ${clean}` : `${number}. ` } return clean ? `- ${clean}` : '- ' } /** * 빈 목록 마커 줄을 만든다. * @param {Object} block - 목록 블록 * @param {number} itemIndex - 항목 인덱스 * @returns {string} 마크다운 줄 */ const createEmptyListMarkerLine = (block, itemIndex) => { if (block.ordered) { return `${getListMarkerNumber(block, itemIndex)}. ` } return '- ' } /** * 목록 항목 인라인 편집 반영 * @param {Object} block - 블록 * @param {number} itemIndex - 항목 인덱스 * @param {string|{ value: string, raw?: boolean }} payload - 편집 내용 * @returns {void} */ const onListItemInlineCommit = (block, itemIndex, payload) => { const { value, raw } = normalizeCommitPayload(payload) const sourceLines = getBlockSourceLines(block) const nextLines = sourceLines.map((line, index) => { if (index !== itemIndex) { return line } return formatListLine(value, raw, block, index) }) options.commitInlineBlockLines(block, nextLines) } /** * 목록 항목 Enter — 빈 마커 줄이면 문단으로 탈출, 내용이 있으면 아래에 빈 줄만 삽입한다. * @param {Object} block - 목록 블록 * @param {number} itemIndex - 항목 인덱스 * @param {string|{ value: string, raw?: boolean }} payload - 편집 내용 * @returns {void} */ const onListItemInsertBelow = (block, itemIndex, payload) => { const { value, raw, before, after, caretAtStart, caretAtEnd } = normalizeInsertBelowPayload(payload) const nextLines = getBlockSourceLines(block) if (caretAtStart && after.length) { nextLines[itemIndex] = formatListLine(after, raw, block, itemIndex) nextLines.splice(itemIndex, 0, createEmptyListMarkerLine(block, itemIndex)) prepareListFocus(block.meta.startLine + itemIndex) options.commitInlineBlockLines(block, nextLines) return } if (before.length && after.length) { nextLines[itemIndex] = formatListLine(before, raw, block, itemIndex) nextLines.splice(itemIndex + 1, 0, formatListLine(after, raw, block, itemIndex + 1)) prepareListFocus(block.meta.startLine + itemIndex + 1) options.commitInlineBlockLines(block, nextLines) return } if (itemIndex < nextLines.length) { nextLines[itemIndex] = formatListLine(value, raw, block, itemIndex) } const committedLine = nextLines[itemIndex] ?? '' if (isEmptyListMarkerLine(committedLine, block.ordered)) { nextLines[itemIndex] = '' prepareListFocus(block.meta.startLine + itemIndex) options.commitInlineBlockLines(block, nextLines) return } options.commitInlineBlockLines(block, nextLines) if (caretAtEnd || !before.length) { prepareListFocus((block.meta.endLine ?? block.meta.startLine) + 1) options.insertBelowBlock(block, { lines: [''] }) } } return { getListMarkerNumber, onListItemInlineCommit, onListItemInsertBelow } }