인라인 텍스트 색상 패널 추가

This commit is contained in:
2026-06-16 16:54:01 +09:00
parent 0bb07b5e03
commit 6c4064d8b5
16 changed files with 581 additions and 126 deletions

View File

@@ -1,5 +1,15 @@
/** @type {string[]} 인라인 색상 프리셋 */
export const INLINE_COLOR_NAMES = ['gray', 'blue', 'green', 'yellow', 'red', 'purple']
/** @type {RegExp} 인라인 마크다운 패턴 */
const INLINE_MARKDOWN_RE = /(\$([^$\n]+?)\$|\[([^\]]+)\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)|\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*)/g
const INLINE_MARKDOWN_RE = /(::(gray|blue|green|yellow|red|purple)\[((?:\\\]|[^\]\n])+)\]|\$([^$\n]+?)\$|\[([^\]]+)\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)|\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*)/g
/**
* 지원하는 인라인 색상인지 확인한다.
* @param {string|null|undefined} color - 색상 이름
* @returns {boolean} 지원 여부
*/
export const isInlineColorName = (color) => INLINE_COLOR_NAMES.includes(String(color || '').trim().toLowerCase())
/**
* HTML 특수문자 이스케이프
@@ -126,10 +136,23 @@ const formatScriptMarkdown = (marker, value) => {
return `$${marker}${body}$`
}
/**
* 색상 인라인 마크다운을 만든다.
* @param {string} color - 색상 이름
* @param {string} value - 표시 텍스트
* @returns {string} 색상 인라인 마크다운
*/
export const formatInlineColorMarkdown = (color, value) => {
const normalizedColor = String(color || '').trim().toLowerCase()
const safeColor = isInlineColorName(normalizedColor) ? normalizedColor : 'red'
const text = String(value || '').replaceAll(']', '\\]')
return `::${safeColor}[${text}]`
}
/**
* 인라인 마크다운을 표시 세그먼트로 변환한다.
* @param {string} value - 원본 문자열
* @returns {Array<{ type: string, text: string, href?: string }>} 인라인 세그먼트
* @returns {Array<{ type: string, text: string, href?: string, color?: string }>} 인라인 세그먼트
*/
export const parseInlineSegments = (value) => {
const source = String(value || '')
@@ -146,29 +169,35 @@ export const parseInlineSegments = (value) => {
})
}
if (match[2]) {
segments.push(...parseObsidianMathSegments(match[2]))
} else if (match[3] && match[4]) {
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]) {
segments.push({
type: 'link',
text: match[3],
href: match[4]
})
} else if (match[5]) {
segments.push({
type: 'strong',
text: match[5]
})
} else if (match[6]) {
segments.push({
type: 'code',
text: match[6]
text: match[5],
href: match[6]
})
} else if (match[7]) {
segments.push({
type: 'em',
type: 'strong',
text: match[7]
})
} else if (match[8]) {
segments.push({
type: 'code',
text: match[8]
})
} else if (match[9]) {
segments.push({
type: 'em',
text: match[9]
})
}
lastIndex = INLINE_MARKDOWN_RE.lastIndex
@@ -187,7 +216,7 @@ export const parseInlineSegments = (value) => {
/**
* 인라인 세그먼트를 HTML 문자열로 변환한다.
* @param {Array<{ type: string, text: string, href?: string }>} segments - 세그먼트
* @param {Array<{ type: string, text: string, href?: string, color?: string }>} segments - 세그먼트
* @returns {string} HTML
*/
export const segmentsToInlineHtml = (segments) => segments.map((segment) => {
@@ -215,6 +244,12 @@ export const segmentsToInlineHtml = (segments) => segments.map((segment) => {
return `<a href="${escapeHtml(segment.href || '')}">${escapeHtml(segment.text)}</a>`
}
if (segment.type === 'color' && isInlineColorName(segment.color)) {
const color = String(segment.color || '').trim().toLowerCase()
const nestedHtml = segmentsToInlineHtml(parseInlineSegments(segment.text))
return `<span data-inline-color="${escapeHtml(color)}">${nestedHtml}</span>`
}
return escapeHtml(segment.text)
}).join('')
@@ -278,6 +313,11 @@ export const convertHtmlInlineNodeToMarkdown = (node) => {
return href ? `[${childText || href}](${href})` : childText
}
if (tagName === 'span') {
const inlineColor = element.getAttribute('data-inline-color')
return isInlineColorName(inlineColor) ? formatInlineColorMarkdown(inlineColor, childText) : childText
}
if (tagName === 'img') {
const src = element.getAttribute('src')
const alt = element.getAttribute('alt') || ''
@@ -288,7 +328,7 @@ export const convertHtmlInlineNodeToMarkdown = (node) => {
return '\n'
}
if (tagName === 'div' || tagName === 'p' || tagName === 'span') {
if (tagName === 'div' || tagName === 'p') {
return childText
}