/** * 인용 접두사 제거 * @param {string} value - 원본 * @returns {string} 본문 */ export const stripQuoteMarker = (value) => String(value ?? '').replace(/^(?:>\s*)+/, '').trim() /** * 목록 접두사 제거 * @param {string} value - 원본 * @param {boolean} ordered - 순서 목록 여부 * @returns {string} 본문 */ export const stripListMarker = (value, ordered = false) => { const raw = String(value ?? '').trim() if (ordered) { return raw.replace(/^\d+\.\s*/, '').trim() } return raw.replace(/^[-*+]\s+/, '').trim() } /** * 인용 마커 포함 여부 * @param {string} value - 원본 * @returns {boolean} */ export const hasQuoteMarker = (value) => /^\s*>/.test(String(value ?? '')) /** * 목록 마커 포함 여부 * @param {string} value - 원본 * @param {boolean} [ordered=false] - 순서 목록 여부 * @returns {boolean} */ export const hasListMarker = (value, ordered = false) => { const raw = String(value ?? '').trim() if (ordered) { return /^\d+\.\s*/.test(raw) } return /^[-*+]\s/.test(raw) } /** * 순서 목록 마커를 파싱한다. * @param {string} value - 원본 줄 * @returns {{ number: number, body: string }|null} */ export const parseOrderedListMarker = (value) => { const raw = String(value ?? '').trim() const match = raw.match(/^(\d+)\.\s*(.*)$/) if (!match) { return null } return { number: Number(match[1]), body: match[2].trim() } } /** * 순서 목록 다음 줄 마커를 만든다. * @param {string} line - 현재 줄 * @returns {string} 다음 줄 마커 */ export const getNextOrderedListLine = (line) => { const parsed = parseOrderedListMarker(line) if (!parsed) { return '' } return `${parsed.number + 1}. ` } /** * 마커만 있고 본문이 없는 목록 줄인지 확인한다. * @param {string} line - 마크다운 줄 * @param {boolean} ordered - 순서 목록 여부 * @returns {boolean} */ export const isEmptyListMarkerLine = (line, ordered = false) => { if (!hasListMarker(line, ordered)) { return false } if (ordered) { const parsed = parseOrderedListMarker(line) return parsed ? !parsed.body : false } return !stripListMarker(line, false).trim() } /** * 마커만 있고 본문이 없는 인용 줄인지 확인한다. * @param {string} line - 마크다운 줄 * @returns {boolean} */ export const isEmptyQuoteMarkerLine = (line) => { if (!hasQuoteMarker(line)) { return false } return !stripQuoteMarker(line).trim() } /** * 이전 마크다운 줄 끝에 텍스트를 이어 붙인다. * @param {string} line - 이전 줄 * @param {string} appendText - 붙일 본문 * @returns {string} 병합된 줄 */ export const appendTextToMarkdownLine = (line, appendText) => { const prev = String(line ?? '') const add = String(appendText ?? '') if (!add) { return prev } const ordered = parseOrderedListMarker(prev) if (ordered) { return `${ordered.number}. ${ordered.body}${add}` } if (hasQuoteMarker(prev)) { const body = stripQuoteMarker(prev) return `> ${body}${add}` } if (hasListMarker(prev, false)) { const body = stripListMarker(prev, false) return `- ${body}${add}` } return `${prev}${add}` } /** * 편집 값에서 이전 줄에 붙일 본문만 추출한다. * @param {string} value - 편집 값 * @param {string} previousLine - 이전 마크다운 줄 * @param {boolean} raw - 원문 모드 여부 * @returns {string} 본문 */ export const getAppendTextForMerge = (value, previousLine, raw = false) => { const text = String(value ?? '').trim() if (!text) { return '' } if (raw) { if (hasQuoteMarker(text)) { return stripQuoteMarker(text) } if (hasListMarker(text, true)) { return stripListMarker(text, true) } if (hasListMarker(text, false)) { return stripListMarker(text, false) } return text } if (hasListMarker(previousLine, true)) { return stripListMarker(text, true) } if (hasListMarker(previousLine, false)) { return stripListMarker(text, false) } if (hasQuoteMarker(previousLine)) { return stripQuoteMarker(text) } return text } /** * 줄 병합 후 편집 영역에 둘 커서 오프셋(이전 줄 본문 끝)을 반환한다. * @param {string} previousLine - 이전 마크다운 줄 * @param {boolean} [raw=false] - 원문 모드 여부 * @returns {number} 표시 텍스트 기준 오프셋 */ export const getMergeJunctionDisplayOffset = (previousLine, raw = false) => { const prev = String(previousLine ?? '') if (raw) { return prev.length } const ordered = parseOrderedListMarker(prev) if (ordered) { return ordered.body.length } if (hasListMarker(prev, true)) { return stripListMarker(prev, true).length } if (hasListMarker(prev, false)) { return stripListMarker(prev, false).length } if (hasQuoteMarker(prev)) { return stripQuoteMarker(prev).length } return prev.length } /** * 제목 접두사 제거 * @param {string} value - 원본 * @returns {{ level: number, text: string }} 레벨·본문 */ export const stripHeadingMarker = (value) => { const raw = String(value ?? '').trim() const match = raw.match(/^(#{1,6})\s+(.*)$/) if (!match) { return { level: 0, text: raw } } return { level: match[1].length, text: match[2].trim() } }