Files
sori.studio/composables/useContentMarkdownParagraphs.js

170 lines
4.8 KiB
JavaScript

import { buildCodeFenceOpener } from '../lib/markdown-code-block.js'
import { buildCalloutOpenerLine } from '../lib/markdown-callout.js'
/**
* 관리자 라이브 Markdown 문단 편집 이벤트 상태를 만든다.
* @param {Object} options - 문단 편집 옵션
* @param {import('vue').Ref<number|null>} 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
}
}