223 lines
7.1 KiB
JavaScript
223 lines
7.1 KiB
JavaScript
import { unref } from 'vue'
|
|
import {
|
|
hasListMarker,
|
|
isEmptyListMarkerLine,
|
|
parseOrderedListMarker,
|
|
stripListMarker
|
|
} from '../lib/markdown-live-edit.js'
|
|
|
|
/**
|
|
* 관리자 라이브 Markdown 목록 편집 이벤트 상태를 만든다.
|
|
* @param {Object} options - 목록 편집 옵션
|
|
* @param {import('vue').Ref<string>|string} options.content - Markdown 본문
|
|
* @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 - 블록 줄 반영 콜백
|
|
* @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
|
|
}
|
|
}
|