236 lines
9.3 KiB
JavaScript
236 lines
9.3 KiB
JavaScript
/**
|
|
* 마크다운 본문의 줄 시작 오프셋을 반환한다.
|
|
* @param {string} value - 마크다운 본문
|
|
* @param {number} lineIndex - 줄 번호(0-based)
|
|
* @returns {number} 문자 오프셋
|
|
*/
|
|
export const getMarkdownLineStartOffset = (value, lineIndex) => {
|
|
const lines = String(value ?? '').split('\n')
|
|
const safeLineIndex = Math.min(Math.max(0, Number(lineIndex) || 0), Math.max(0, lines.length - 1))
|
|
|
|
return lines.slice(0, safeLineIndex).join('\n').length + (safeLineIndex > 0 ? 1 : 0)
|
|
}
|
|
|
|
/**
|
|
* 문자 오프셋이 속한 마크다운 줄 번호를 반환한다.
|
|
* @param {string} value - 마크다운 본문
|
|
* @param {number} offset - 문자 오프셋
|
|
* @returns {number} 줄 번호(0-based)
|
|
*/
|
|
export const getMarkdownLineIndexAtOffset = (value, offset) => {
|
|
const safeValue = String(value ?? '')
|
|
const safeOffset = Math.min(Math.max(0, Number(offset) || 0), safeValue.length)
|
|
|
|
return Math.max(0, safeValue.slice(0, safeOffset).split('\n').length - 1)
|
|
}
|
|
|
|
/**
|
|
* textarea 또는 fallback 선택 위치에서 현재 선택 정보를 만든다.
|
|
* @param {Object} options - 선택 정보 옵션
|
|
* @param {string} options.value - 마크다운 본문
|
|
* @param {{ selectionStart?: number, selectionEnd?: number }|null} [options.textarea] - textarea 유사 객체
|
|
* @returns {{ start: number, end: number, value: string }} 선택 정보
|
|
*/
|
|
export const getMarkdownSelectionState = ({ value, textarea = null }) => {
|
|
const safeValue = String(value ?? '')
|
|
|
|
if (!textarea) {
|
|
return {
|
|
start: safeValue.length,
|
|
end: safeValue.length,
|
|
value: safeValue
|
|
}
|
|
}
|
|
|
|
return {
|
|
start: Math.min(Math.max(0, Number(textarea.selectionStart) || 0), safeValue.length),
|
|
end: Math.min(Math.max(0, Number(textarea.selectionEnd) || 0), safeValue.length),
|
|
value: safeValue
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 선택 영역을 지정 문자열로 교체한 결과를 반환한다.
|
|
* @param {Object} options - 교체 옵션
|
|
* @param {string} options.value - 마크다운 본문
|
|
* @param {number} options.start - 선택 시작
|
|
* @param {number} options.end - 선택 끝
|
|
* @param {string} options.replacement - 대체 문자열
|
|
* @param {number} [options.cursorOffset] - 대체 문자열 안 커서 위치
|
|
* @param {number|null} [options.selectionLength] - 교체 후 선택 길이
|
|
* @returns {{ value: string, selectionStart: number, selectionEnd: number }} 교체 결과
|
|
*/
|
|
export const replaceMarkdownSelection = ({
|
|
value,
|
|
start,
|
|
end,
|
|
replacement,
|
|
cursorOffset = String(replacement ?? '').length,
|
|
selectionLength = null
|
|
}) => {
|
|
const safeValue = String(value ?? '')
|
|
const safeStart = Math.min(Math.max(0, Number(start) || 0), safeValue.length)
|
|
const safeEnd = Math.min(Math.max(0, Number(end) || 0), safeValue.length)
|
|
const from = Math.min(safeStart, safeEnd)
|
|
const to = Math.max(safeStart, safeEnd)
|
|
const safeReplacement = String(replacement ?? '')
|
|
const nextValue = `${safeValue.slice(0, from)}${safeReplacement}${safeValue.slice(to)}`
|
|
const nextStart = from + Math.max(0, Number(cursorOffset) || 0)
|
|
const nextEnd = nextStart + Math.max(0, Number(selectionLength) || 0)
|
|
|
|
return {
|
|
value: nextValue,
|
|
selectionStart: Math.min(nextStart, nextValue.length),
|
|
selectionEnd: Math.min(nextEnd, nextValue.length)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 블록형 마크다운 조각 삽입 결과를 반환한다.
|
|
* @param {Object} options - 삽입 옵션
|
|
* @param {string} options.value - 마크다운 본문
|
|
* @param {number} options.start - 선택 시작
|
|
* @param {number} options.end - 선택 끝
|
|
* @param {string} options.snippet - 삽입할 마크다운
|
|
* @returns {{ value: string, selectionStart: number, selectionEnd: number }} 삽입 결과
|
|
*/
|
|
export const insertMarkdownBlockSnippet = ({ value, start, end, snippet }) => {
|
|
const safeValue = String(value ?? '')
|
|
const safeStart = Math.min(Math.max(0, Number(start) || 0), safeValue.length)
|
|
const safeEnd = Math.min(Math.max(0, Number(end) || 0), safeValue.length)
|
|
const needsBeforeLine = safeStart > 0 && safeValue[safeStart - 1] !== '\n'
|
|
const needsAfterLine = safeEnd < safeValue.length && safeValue[safeEnd] !== '\n'
|
|
const replacement = `${needsBeforeLine ? '\n\n' : ''}${String(snippet ?? '')}${needsAfterLine ? '\n\n' : ''}`
|
|
|
|
return replaceMarkdownSelection({
|
|
value: safeValue,
|
|
start: safeStart,
|
|
end: safeEnd,
|
|
replacement
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 선택 텍스트를 인라인 마크다운으로 감싼 결과를 반환한다.
|
|
* @param {Object} options - 감싸기 옵션
|
|
* @param {string} options.value - 마크다운 본문
|
|
* @param {number} options.start - 선택 시작
|
|
* @param {number} options.end - 선택 끝
|
|
* @param {string} options.prefix - 앞 표식
|
|
* @param {string} options.suffix - 뒤 표식
|
|
* @param {string} options.placeholder - 선택이 없을 때 들어갈 텍스트
|
|
* @returns {{ value: string, selectionStart: number, selectionEnd: number }} 교체 결과
|
|
*/
|
|
export const wrapMarkdownInlineSelection = ({ value, start, end, prefix, suffix, placeholder }) => {
|
|
const safeValue = String(value ?? '')
|
|
const safeStart = Math.min(Math.max(0, Number(start) || 0), safeValue.length)
|
|
const safeEnd = Math.min(Math.max(0, Number(end) || 0), safeValue.length)
|
|
const from = Math.min(safeStart, safeEnd)
|
|
const to = Math.max(safeStart, safeEnd)
|
|
const selected = safeValue.slice(from, to)
|
|
const inner = selected || String(placeholder ?? '')
|
|
const replacement = `${String(prefix ?? '')}${inner}${String(suffix ?? '')}`
|
|
const result = replaceMarkdownSelection({
|
|
value: safeValue,
|
|
start: from,
|
|
end: to,
|
|
replacement
|
|
})
|
|
const selectionStart = from + String(prefix ?? '').length
|
|
|
|
return {
|
|
value: result.value,
|
|
selectionStart,
|
|
selectionEnd: selectionStart + inner.length
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 선택 텍스트를 링크 마크다운으로 바꾼 결과를 반환한다.
|
|
* @param {Object} options - 링크 삽입 옵션
|
|
* @param {string} options.value - 마크다운 본문
|
|
* @param {number} options.start - 선택 시작
|
|
* @param {number} options.end - 선택 끝
|
|
* @returns {{ value: string, selectionStart: number, selectionEnd: number }} 교체 결과
|
|
*/
|
|
export const insertMarkdownInlineLink = ({ value, start, end }) => {
|
|
const safeValue = String(value ?? '')
|
|
const safeStart = Math.min(Math.max(0, Number(start) || 0), safeValue.length)
|
|
const safeEnd = Math.min(Math.max(0, Number(end) || 0), safeValue.length)
|
|
const from = Math.min(safeStart, safeEnd)
|
|
const to = Math.max(safeStart, safeEnd)
|
|
const selected = safeValue.slice(from, to) || '링크 텍스트'
|
|
const replacement = `[${selected}](https://)`
|
|
const result = replaceMarkdownSelection({
|
|
value: safeValue,
|
|
start: from,
|
|
end: to,
|
|
replacement
|
|
})
|
|
const urlStart = from + selected.length + 3
|
|
|
|
return {
|
|
value: result.value,
|
|
selectionStart: urlStart,
|
|
selectionEnd: urlStart + 'https://'.length
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 선택 범위가 걸친 줄을 변환한 결과를 반환한다.
|
|
* @param {Object} options - 줄 변환 옵션
|
|
* @param {string} options.value - 마크다운 본문
|
|
* @param {number} options.start - 선택 시작
|
|
* @param {number} options.end - 선택 끝
|
|
* @param {(line: string) => string} options.transformLine - 줄 변환 함수
|
|
* @returns {{ value: string, selectionStart: number, selectionEnd: number }} 변환 결과
|
|
*/
|
|
export const transformMarkdownSelectedLines = ({ value, start, end, transformLine }) => {
|
|
const safeValue = String(value ?? '')
|
|
const safeStart = Math.min(Math.max(0, Number(start) || 0), safeValue.length)
|
|
const safeEnd = Math.min(Math.max(0, Number(end) || 0), safeValue.length)
|
|
const from = Math.min(safeStart, safeEnd)
|
|
const to = Math.max(safeStart, safeEnd)
|
|
const lineStart = safeValue.lastIndexOf('\n', Math.max(0, from - 1)) + 1
|
|
const nextLineBreak = safeValue.indexOf('\n', to)
|
|
const lineEnd = nextLineBreak === -1 ? safeValue.length : nextLineBreak
|
|
const selectedBlock = safeValue.slice(lineStart, lineEnd)
|
|
const replacement = selectedBlock.split('\n').map(transformLine).join('\n')
|
|
|
|
return {
|
|
value: `${safeValue.slice(0, lineStart)}${replacement}${safeValue.slice(lineEnd)}`,
|
|
selectionStart: lineStart,
|
|
selectionEnd: lineStart + replacement.length
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 선택 범위가 걸친 줄을 삭제한 결과를 반환한다.
|
|
* @param {Object} options - 삭제 옵션
|
|
* @param {string} options.value - 마크다운 본문
|
|
* @param {number} options.start - 선택 시작
|
|
* @param {number} options.end - 선택 끝
|
|
* @returns {{ value: string, selectionStart: number, selectionEnd: number }} 삭제 결과
|
|
*/
|
|
export const deleteMarkdownSelectedLines = ({ value, start, end }) => {
|
|
const safeValue = String(value ?? '')
|
|
const safeStart = Math.min(Math.max(0, Number(start) || 0), safeValue.length)
|
|
const safeEnd = Math.min(Math.max(0, Number(end) || 0), safeValue.length)
|
|
const from = Math.min(safeStart, safeEnd)
|
|
const to = Math.max(safeStart, safeEnd)
|
|
const lineStart = safeValue.lastIndexOf('\n', Math.max(0, from - 1)) + 1
|
|
const nextLineBreak = safeValue.indexOf('\n', to)
|
|
const lineEnd = nextLineBreak === -1 ? safeValue.length : nextLineBreak + 1
|
|
const previousLineBreak = safeValue.lastIndexOf('\n', Math.max(0, lineStart - 2))
|
|
const nextValue = `${safeValue.slice(0, lineStart)}${safeValue.slice(lineEnd)}`
|
|
const nextSelection = lineStart > 0
|
|
? previousLineBreak + 1
|
|
: 0
|
|
|
|
return {
|
|
value: nextValue,
|
|
selectionStart: Math.min(nextSelection, nextValue.length),
|
|
selectionEnd: Math.min(nextSelection, nextValue.length)
|
|
}
|
|
}
|