콜아웃 인라인 링크 렌더링 개선
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
/** @type {string[]} 인라인 색상 프리셋 */
|
||||
export const INLINE_COLOR_NAMES = ['gray', 'blue', 'green', 'yellow', 'red', 'purple']
|
||||
|
||||
/** @type {RegExp} 인라인 색상 시작 패턴 */
|
||||
const INLINE_COLOR_START_RE = /::(gray|blue|green|yellow|red|purple)\[/g
|
||||
|
||||
/** @type {RegExp} 인라인 마크다운 패턴 */
|
||||
const INLINE_MARKDOWN_RE = /(::(gray|blue|green|yellow|red|purple)\[((?:\\\]|[^\]\n])+)\]|\$([^$\n]+?)\$|\[([^\]]+)\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)|\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*)/g
|
||||
const INLINE_MARKDOWN_RE = /(\$([^$\n]+?)\$|\[([^\]]+)\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)|\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*)/g
|
||||
|
||||
/**
|
||||
* 지원하는 인라인 색상인지 확인한다.
|
||||
@@ -149,6 +152,77 @@ export const formatInlineColorMarkdown = (color, value) => {
|
||||
return `::${safeColor}[${text}]`
|
||||
}
|
||||
|
||||
/**
|
||||
* 색상 토큰 본문을 대괄호 중첩 기준으로 읽는다.
|
||||
* @param {string} source - 원본 문자열
|
||||
* @param {number} startIndex - 색상 토큰 시작 위치
|
||||
* @param {string} color - 색상 이름
|
||||
* @returns {{ color: string, text: string, nextIndex: number }|null} 색상 토큰
|
||||
*/
|
||||
const readInlineColorToken = (source, startIndex, color) => {
|
||||
const prefix = `::${color}[`
|
||||
const contentStartIndex = startIndex + prefix.length
|
||||
let depth = 1
|
||||
let escaped = false
|
||||
|
||||
for (let index = contentStartIndex; index < source.length; index += 1) {
|
||||
const character = source[index]
|
||||
|
||||
if (escaped) {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (character === '\\') {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (character === '[') {
|
||||
depth += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (character !== ']') {
|
||||
continue
|
||||
}
|
||||
|
||||
depth -= 1
|
||||
|
||||
if (depth === 0) {
|
||||
return {
|
||||
color,
|
||||
text: source.slice(contentStartIndex, index).replaceAll('\\]', ']'),
|
||||
nextIndex: index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 인라인 색상 토큰 후보를 찾는다.
|
||||
* @param {string} source - 원본 문자열
|
||||
* @param {number} startIndex - 검색 시작 위치
|
||||
* @returns {RegExpExecArray|null} 정규식 매치
|
||||
*/
|
||||
const findNextInlineColorStart = (source, startIndex) => {
|
||||
INLINE_COLOR_START_RE.lastIndex = startIndex
|
||||
return INLINE_COLOR_START_RE.exec(source)
|
||||
}
|
||||
|
||||
/**
|
||||
* 다음 일반 인라인 토큰 후보를 찾는다.
|
||||
* @param {string} source - 원본 문자열
|
||||
* @param {number} startIndex - 검색 시작 위치
|
||||
* @returns {RegExpExecArray|null} 정규식 매치
|
||||
*/
|
||||
const findNextInlineMarkdownToken = (source, startIndex) => {
|
||||
INLINE_MARKDOWN_RE.lastIndex = startIndex
|
||||
return INLINE_MARKDOWN_RE.exec(source)
|
||||
}
|
||||
|
||||
/**
|
||||
* 인라인 마크다운을 표시 세그먼트로 변환한다.
|
||||
* @param {string} value - 원본 문자열
|
||||
@@ -158,50 +232,77 @@ export const parseInlineSegments = (value) => {
|
||||
const source = String(value || '')
|
||||
const segments = []
|
||||
let lastIndex = 0
|
||||
INLINE_MARKDOWN_RE.lastIndex = 0
|
||||
let match = INLINE_MARKDOWN_RE.exec(source)
|
||||
let searchIndex = 0
|
||||
let colorMatch = findNextInlineColorStart(source, searchIndex)
|
||||
let match = findNextInlineMarkdownToken(source, searchIndex)
|
||||
|
||||
while (match) {
|
||||
if (match.index > lastIndex) {
|
||||
while (colorMatch || match) {
|
||||
const useColorMatch = colorMatch && (!match || colorMatch.index <= match.index)
|
||||
const activeMatch = useColorMatch ? colorMatch : match
|
||||
|
||||
if (activeMatch.index > lastIndex) {
|
||||
segments.push({
|
||||
type: 'text',
|
||||
text: source.slice(lastIndex, match.index)
|
||||
text: source.slice(lastIndex, activeMatch.index)
|
||||
})
|
||||
}
|
||||
|
||||
if (match[2] && match[3]) {
|
||||
segments.push({
|
||||
type: 'color',
|
||||
color: match[2],
|
||||
text: match[3].replaceAll('\\]', ']')
|
||||
})
|
||||
} else if (match[4]) {
|
||||
segments.push(...parseObsidianMathSegments(match[4]))
|
||||
} else if (match[5] && match[6]) {
|
||||
if (useColorMatch) {
|
||||
const colorToken = readInlineColorToken(source, colorMatch.index, colorMatch[1])
|
||||
|
||||
if (!colorToken) {
|
||||
segments.push({
|
||||
type: 'text',
|
||||
text: source.slice(colorMatch.index, colorMatch.index + colorMatch[0].length)
|
||||
})
|
||||
lastIndex = colorMatch.index + colorMatch[0].length
|
||||
searchIndex = lastIndex
|
||||
} else {
|
||||
segments.push({
|
||||
type: 'color',
|
||||
color: colorToken.color,
|
||||
text: colorToken.text
|
||||
})
|
||||
lastIndex = colorToken.nextIndex
|
||||
searchIndex = colorToken.nextIndex
|
||||
}
|
||||
} else if (match[2]) {
|
||||
segments.push(...parseObsidianMathSegments(match[2]))
|
||||
lastIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
searchIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
} else if (match[3] && match[4]) {
|
||||
segments.push({
|
||||
type: 'link',
|
||||
text: match[5],
|
||||
href: match[6]
|
||||
text: match[3],
|
||||
href: match[4]
|
||||
})
|
||||
} else if (match[7]) {
|
||||
lastIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
searchIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
} else if (match[5]) {
|
||||
segments.push({
|
||||
type: 'strong',
|
||||
text: match[7]
|
||||
text: match[5]
|
||||
})
|
||||
} else if (match[8]) {
|
||||
lastIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
searchIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
} else if (match[6]) {
|
||||
segments.push({
|
||||
type: 'code',
|
||||
text: match[8]
|
||||
text: match[6]
|
||||
})
|
||||
} else if (match[9]) {
|
||||
lastIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
searchIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
} else if (match[7]) {
|
||||
segments.push({
|
||||
type: 'em',
|
||||
text: match[9]
|
||||
text: match[7]
|
||||
})
|
||||
lastIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
searchIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
}
|
||||
|
||||
lastIndex = INLINE_MARKDOWN_RE.lastIndex
|
||||
match = INLINE_MARKDOWN_RE.exec(source)
|
||||
colorMatch = findNextInlineColorStart(source, searchIndex)
|
||||
match = findNextInlineMarkdownToken(source, searchIndex)
|
||||
}
|
||||
|
||||
if (lastIndex < source.length) {
|
||||
@@ -221,19 +322,19 @@ export const parseInlineSegments = (value) => {
|
||||
*/
|
||||
export const segmentsToInlineHtml = (segments) => segments.map((segment) => {
|
||||
if (segment.type === 'strong') {
|
||||
return `<strong>${escapeHtml(segment.text)}</strong>`
|
||||
return `<strong>${segmentsToInlineHtml(parseInlineSegments(segment.text))}</strong>`
|
||||
}
|
||||
|
||||
if (segment.type === 'em') {
|
||||
return `<em>${escapeHtml(segment.text)}</em>`
|
||||
return `<em>${segmentsToInlineHtml(parseInlineSegments(segment.text))}</em>`
|
||||
}
|
||||
|
||||
if (segment.type === 'subscript') {
|
||||
return `<sub>${escapeHtml(segment.text)}</sub>`
|
||||
return `<sub>${segmentsToInlineHtml(parseInlineSegments(segment.text))}</sub>`
|
||||
}
|
||||
|
||||
if (segment.type === 'superscript') {
|
||||
return `<sup>${escapeHtml(segment.text)}</sup>`
|
||||
return `<sup>${segmentsToInlineHtml(parseInlineSegments(segment.text))}</sup>`
|
||||
}
|
||||
|
||||
if (segment.type === 'code') {
|
||||
|
||||
Reference in New Issue
Block a user