import { buildCodeFenceOpener } from '../lib/markdown-code-block.js' import { buildCalloutOpenerLine } from '../lib/markdown-callout.js' /** * 관리자 라이브 Markdown 문단 편집 이벤트 상태를 만든다. * @param {Object} options - 문단 편집 옵션 * @param {import('vue').Ref} options.pendingFocusLine - 다음 포커스 줄 * @param {import('vue').Ref<'start'|'end'|'auto'>} options.pendingFocusPosition - 다음 포커스 커서 위치 * @param {(block: Object, replacementLines: string[]) => void} options.commitInlineBlockLines - 블록 줄 반영 콜백 * @returns {Object} 문단 이벤트 핸들러 */ export const useContentMarkdownParagraphs = (options) => { /** @type {number} 문단 분리 연속 호출 방지 */ let lastParagraphSplitAt = 0 /** * 문단 줄 반영 후 포커스를 준비한다. * @param {number} line - 줄 번호 * @returns {void} */ const prepareParagraphFocus = (line) => { options.pendingFocusLine.value = line options.pendingFocusPosition.value = 'start' } /** * 문단 인라인 편집 반영 * @param {Object} block - 문단 블록 * @param {string} text - 편집된 텍스트 * @returns {void} */ const onParagraphInlineCommit = (block, text) => { const value = String(text ?? '').replace(/\r/g, '') const lines = value.split('\n').map((line) => line.trimEnd()) if (lines.length <= 1) { options.commitInlineBlockLines(block, [lines[0] ?? '']) return } options.commitInlineBlockLines(block, lines.filter((line) => line.length > 0)) } /** * 빈 줄(스페이서) 편집 반영 * @param {Object} block - 블록 * @param {string} text - 편집된 텍스트 * @returns {void} */ const onSpacerInlineCommit = (block, text) => { if (!String(text ?? '').trim()) { return } options.commitInlineBlockLines(block, [text]) } /** * 문단 입력 중 인용 단축 변환을 처리한다. * @param {Object} block - 문단 블록 * @param {string} text - 입력 텍스트 * @returns {void} */ const onParagraphLiveInput = (block, text) => { if (String(text ?? '').trim() !== '>') { return } prepareParagraphFocus(block.meta.startLine) options.commitInlineBlockLines(block, ['> ']) } /** * 문단 Enter 분리 결과 줄 배열을 만든다. * @param {string} head - 커서 앞 텍스트 * @param {string} tail - 커서 뒤 텍스트 * @returns {string[]} 분리된 줄 */ const buildParagraphSplitLines = (head, tail) => { const h = String(head ?? '').replace(/\n/g, ' ') const t = String(tail ?? '').replace(/\n/g, ' ') if (!t.length) { return [h, ''] } if (!h.length) { return ['', t] } return [h, t] } /** * 문단 Enter 입력을 블록 단축 입력으로 변환할지 확인한다. * @param {Object} block - 문단 블록 * @param {string} before - 커서 앞 텍스트 * @param {string} after - 커서 뒤 텍스트 * @returns {boolean} 변환 처리 여부 */ const applyParagraphShortcutSplit = (block, before, after) => { const head = String(before ?? '').trim() const tail = String(after ?? '') if (tail.trim()) { return false } if (/^```[A-Za-z0-9_-]*$/.test(head)) { const opener = head === '```' ? buildCodeFenceOpener({ language: '', showLineNumbers: false }) : head prepareParagraphFocus(block.meta.startLine + 1) options.commitInlineBlockLines(block, [opener, '', '```']) return true } if (head === '!!!' || head === ':::callout') { prepareParagraphFocus(block.meta.startLine + 1) options.commitInlineBlockLines(block, [ buildCalloutOpenerLine({ calloutEmojiEnabled: false, calloutEmoji: '💡', calloutBackground: 'blue', title: '' }), '', '', ':::' ]) return true } return false } /** * 문단 Enter 분리 — 마크다운에 빈 줄을 넣어 다음 문단을 만든다. * @param {Object} block - 블록 * @param {{ before: string, after: string }} payload - 커서 앞·뒤 텍스트 * @returns {void} */ const onParagraphSplit = (block, { before, after }) => { const now = Date.now() if (now - lastParagraphSplitAt < 120) { return } lastParagraphSplitAt = now if (applyParagraphShortcutSplit(block, before, after)) { return } const replacementLines = buildParagraphSplitLines(before, after) const focusLine = block.meta.startLine + Math.max(replacementLines.length - 1, 1) prepareParagraphFocus(focusLine) options.commitInlineBlockLines(block, replacementLines) } return { buildParagraphSplitLines, onParagraphInlineCommit, onSpacerInlineCommit, onParagraphLiveInput, onParagraphSplit } }