/** @type {string[]} */ export const CALLOUT_BACKGROUND_OPTIONS = ['gray', 'blue', 'green', 'yellow', 'red', 'purple'] /** @type {string[]} */ export const QUOTE_BACKGROUND_OPTIONS = ['gray', 'blue', 'green', 'yellow', 'red', 'purple'] /** @type {Record} */ export const QUOTE_BACKGROUND_LABELS = { gray: '회색', blue: '파랑', green: '초록', yellow: '노랑', red: '빨강', purple: '보라' } /** @type {Record} */ export const QUOTE_BACKGROUND_SWATCHES = { gray: '#050505', blue: '#0055ff', green: '#16ae68', yellow: '#ffff00', red: '#ff0000', purple: '#8800ff' } /** @type {Record} */ export const CALLOUT_BACKGROUND_SWATCHES = { gray: 'color-mix(in srgb, #050505 10%, #ffffff)', blue: 'color-mix(in srgb, #0055ff 10%, #ffffff)', green: 'color-mix(in srgb, #16ae68 10%, #ffffff)', yellow: 'color-mix(in srgb, #ffff00 10%, #ffffff)', red: 'color-mix(in srgb, #ff0000 10%, #ffffff)', purple: 'color-mix(in srgb, #8800ff 10%, #ffffff)' } /** @type {string[]} */ export const CALLOUT_EMOJI_OPTIONS = ['💡', '⚠️', '❗', '✅', '📌', '🔥', '💬'] /** * 옵션 선언부를 key=value 토큰 목록으로 분리한다. * @param {string} input - 옵션 문자열 * @returns {string[]} 토큰 목록 */ const tokenizeOptionString = (input) => { const tokens = [] const pattern = /[^\s"']+(?:"[^"]*"|'[^']*')?/g const matches = String(input ?? '').match(pattern) return matches ? tokens.concat(matches) : tokens } /** * 선언부 옵션 값을 정리한다. * @param {string} value - 원본 값 * @returns {string} 정리된 값 */ const normalizeOptionValue = (value) => { const normalized = String(value ?? '').trim() if ((normalized.startsWith('"') && normalized.endsWith('"')) || (normalized.startsWith("'") && normalized.endsWith("'"))) { return normalized.slice(1, -1) } return normalized } /** * 선언부 옵션 값을 직렬화한다. * @param {string} value - 옵션 값 * @returns {string} 직렬화 값 */ const serializeOptionValue = (value) => { const normalized = String(value ?? '').trim() if (!normalized) { return '' } if (/[\s"']/.test(normalized)) { return `"${normalized.replace(/"/g, '\\"')}"` } return normalized } /** * 콜아웃 선언부 옵션을 파싱한다. * @param {string} line - 콜아웃 선언 라인 * @returns {{ calloutEmojiEnabled: boolean, calloutEmoji: string, calloutBackground: string, title: string }} */ export const parseCalloutOptions = (line) => { const options = { calloutEmojiEnabled: false, calloutEmoji: '💡', calloutBackground: 'blue', title: '' } const tokens = tokenizeOptionString(String(line ?? '').trim()).slice(1) tokens.forEach((token) => { const [rawKey, ...rawValueParts] = token.split('=') if (!rawKey || !rawValueParts.length) { return } const key = rawKey.toLowerCase() const value = normalizeOptionValue(rawValueParts.join('=')) if (key === 'emoji') { if (!value || value === 'none') { options.calloutEmojiEnabled = false options.calloutEmoji = '💡' } else { options.calloutEmojiEnabled = true options.calloutEmoji = value } return } if (key === 'bg' && CALLOUT_BACKGROUND_OPTIONS.includes(value)) { options.calloutBackground = value return } if (key === 'title') { options.title = value } }) return options } /** * 콜아웃 선언 줄을 만든다. * @param {{ calloutEmojiEnabled?: boolean, calloutEmoji?: string, calloutBackground?: string, title?: string }} options - 옵션 * @returns {string} 선언 줄 */ export const buildCalloutOpenerLine = (options = {}) => { const emojiEnabled = options.calloutEmojiEnabled === true const emoji = emojiEnabled ? (options.calloutEmoji || '💡') : 'none' const background = CALLOUT_BACKGROUND_OPTIONS.includes(options.calloutBackground) ? options.calloutBackground : 'blue' const title = serializeOptionValue(options.title) const titleOption = title ? ` title=${title}` : '' return `:::callout emoji=${emoji} bg=${background}${titleOption}` }