158 lines
4.8 KiB
JavaScript
158 lines
4.8 KiB
JavaScript
/**
|
|
* 닫는 fenced 줄에서 대응하는 여는 줄을 찾는다.
|
|
* @param {string[]} lines - 마크다운 줄
|
|
* @param {number} closingLine - 닫는 줄 번호
|
|
* @param {(line: string) => boolean} isOpeningLine - 여는 줄 판별
|
|
* @returns {number|null} 여는 줄 번호
|
|
*/
|
|
export const findPreviousFencedOpeningLine = (lines, closingLine, isOpeningLine) => {
|
|
const safeLines = Array.isArray(lines) ? lines : []
|
|
|
|
for (let index = Number(closingLine) - 1; index >= 0; index -= 1) {
|
|
const trimmed = String(safeLines[index] ?? '').trim()
|
|
|
|
if (isOpeningLine(trimmed)) {
|
|
return index
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* 여는 fenced 줄에서 닫는 줄을 찾는다.
|
|
* @param {string[]} lines - 마크다운 줄
|
|
* @param {number} openingLine - 여는 줄 번호
|
|
* @param {string} closingMarker - 닫는 표식
|
|
* @returns {number|null} 닫는 줄 번호
|
|
*/
|
|
export const findNextFencedClosingLine = (lines, openingLine, closingMarker) => {
|
|
const safeLines = Array.isArray(lines) ? lines : []
|
|
const startLine = Number(openingLine)
|
|
|
|
for (let index = startLine + 1; index < safeLines.length; index += 1) {
|
|
if (String(safeLines[index] ?? '').trim() === closingMarker) {
|
|
return index
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* 지정 줄이 속한 코드 펜스 범위를 찾는다.
|
|
* @param {string[]} lines - 마크다운 줄
|
|
* @param {number} targetLine - 확인할 줄 번호
|
|
* @returns {{ openingLine: number, closingLine: number|null }|null} 코드 펜스 범위
|
|
*/
|
|
export const findCodeFenceRangeAtLine = (lines, targetLine) => {
|
|
const safeLines = Array.isArray(lines) ? lines : []
|
|
const safeTargetLine = Number(targetLine)
|
|
|
|
for (let index = 0; index < safeLines.length; index += 1) {
|
|
if (!String(safeLines[index] ?? '').trim().startsWith('```')) {
|
|
continue
|
|
}
|
|
|
|
const closingLine = findNextFencedClosingLine(safeLines, index, '```')
|
|
|
|
if (safeTargetLine === index
|
|
|| (closingLine !== null && safeTargetLine >= index && safeTargetLine <= closingLine)) {
|
|
return { openingLine: index, closingLine }
|
|
}
|
|
|
|
if (closingLine !== null) {
|
|
index = closingLine
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* 지정 줄이 코드 펜스 본문 줄인지 확인한다.
|
|
* @param {string[]} lines - 마크다운 줄 목록
|
|
* @param {number} lineIndex - 줄 번호
|
|
* @returns {boolean}
|
|
*/
|
|
export const isCodeFenceBodyLine = (lines, lineIndex) => {
|
|
const safeLines = Array.isArray(lines) ? lines : []
|
|
const safeLineIndex = Number(lineIndex)
|
|
|
|
for (let index = 0; index <= safeLineIndex; index += 1) {
|
|
if (!String(safeLines[index] ?? '').trim().startsWith('```')) {
|
|
continue
|
|
}
|
|
|
|
const closingLine = findNextFencedClosingLine(safeLines, index, '```')
|
|
|
|
if (safeLineIndex > index && (closingLine === null || safeLineIndex < closingLine)) {
|
|
return true
|
|
}
|
|
|
|
if (closingLine !== null) {
|
|
index = closingLine
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* 라이브 모드에 실제로 존재하는 편집 줄로 포커스 대상을 보정한다.
|
|
* @param {Object} options - 포커스 옵션
|
|
* @param {string} options.value - 마크다운 본문
|
|
* @param {number} options.line - 원본 줄 번호
|
|
* @param {number} options.offset - 원본 줄 내부 오프셋
|
|
* @returns {{ line: number, offset: number }} 라이브 포커스 대상
|
|
*/
|
|
export const resolvePreviewFocusPosition = ({ value, line, offset }) => {
|
|
const lines = String(value ?? '').split('\n')
|
|
const safeLine = Math.min(Math.max(0, Number(line) || 0), Math.max(0, lines.length - 1))
|
|
const safeOffset = Math.max(0, Number(offset) || 0)
|
|
const trimmed = String(lines[safeLine] ?? '').trim()
|
|
|
|
if (trimmed.startsWith('```')) {
|
|
const codeFenceRange = findCodeFenceRangeAtLine(lines, safeLine)
|
|
|
|
if (!codeFenceRange || codeFenceRange.closingLine === null) {
|
|
return { line: safeLine, offset: safeOffset }
|
|
}
|
|
|
|
const focusLine = Math.min(
|
|
Math.max(codeFenceRange.openingLine + 1, safeLine),
|
|
Math.max(codeFenceRange.openingLine + 1, codeFenceRange.closingLine - 1)
|
|
)
|
|
|
|
return {
|
|
line: focusLine,
|
|
offset: safeLine === codeFenceRange.openingLine ? 0 : safeOffset
|
|
}
|
|
}
|
|
|
|
if (trimmed.startsWith(':::callout') || trimmed.startsWith(':::toggle')) {
|
|
return { line: safeLine + 1, offset: 0 }
|
|
}
|
|
|
|
if (trimmed === ':::') {
|
|
const previousFencedOpening = findPreviousFencedOpeningLine(
|
|
lines,
|
|
safeLine,
|
|
(candidate) => candidate.startsWith(':::callout') || candidate.startsWith(':::toggle')
|
|
)
|
|
|
|
if (previousFencedOpening !== null) {
|
|
return {
|
|
line: Math.max(previousFencedOpening + 1, safeLine - 1),
|
|
offset: safeOffset
|
|
}
|
|
}
|
|
}
|
|
|
|
if (/^>\s*(?:\[!bg=|\{bg=)/.test(trimmed) && String(lines[safeLine + 1] ?? '').trim().startsWith('>')) {
|
|
return { line: safeLine + 1, offset: 0 }
|
|
}
|
|
|
|
return { line: safeLine, offset: safeOffset }
|
|
}
|