공개 본문 목록 편집 분리
This commit is contained in:
222
composables/useContentMarkdownLists.js
Normal file
222
composables/useContentMarkdownLists.js
Normal file
@@ -0,0 +1,222 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user