관리자 목록과 이미지 설정 개선

This commit is contained in:
2026-07-20 12:37:53 +09:00
parent cf7d6ca0de
commit ad38644171
19 changed files with 788 additions and 92 deletions

View File

@@ -1,5 +1,7 @@
/** @type {RegExp} 이미지 마크다운 한 줄 */
const IMAGE_MARKDOWN_LINE_RE = /^!\[(.*?)\]\((\S+?)(?:\s+"((?:[^"\\]|\\.)*)")?\)(?:\{width=(regular|wide|full)\})?$/
/** @type {RegExp} 이미지 마크다운 기본 줄 */
const IMAGE_MARKDOWN_BASE_RE = /^!\[(.*?)\]\((\S+?)(?:\s+"((?:[^"\\]|\\.)*)")?\)((?:\{[^{}]+\})*)$/
/** @type {RegExp} 이미지 옵션 토큰 */
const IMAGE_MARKDOWN_OPTION_RE = /\{([^{}=]+)=([^{}]+)\}/g
/** @type {RegExp} 이미지 파일 확장자 */
const IMAGE_URL_EXTENSION_RE = /\.(?:jpe?g|png|webp|gif|avif|svg)(?:$|[?#])/i
@@ -17,6 +19,48 @@ const unescapeImageCaption = (value) => String(value || '').replace(/\\"/g, '"')
*/
const escapeImageCaption = (value) => String(value || '').replace(/"/g, '\\"')
/**
* 이미지 픽셀 너비를 정규화한다.
* @param {unknown} value - 입력 너비
* @returns {number|null} 정규화된 너비
*/
export const normalizeImagePixelWidth = (value) => {
const width = Math.round(Number(value) || 0)
if (!width) {
return null
}
return Math.min(Math.max(width, 120), 1600)
}
/**
* 이미지 마크다운 옵션을 파싱한다.
* @param {string} rawOptions - 옵션 문자열
* @returns {{ width: string, pixelWidth: number|null }} 이미지 옵션
*/
const parseImageMarkdownOptions = (rawOptions) => {
const options = {
width: 'regular',
pixelWidth: null
}
for (const match of String(rawOptions || '').matchAll(IMAGE_MARKDOWN_OPTION_RE)) {
const key = String(match[1] || '').trim()
const value = String(match[2] || '').trim()
if (key === 'width' && ['regular', 'wide', 'full'].includes(value)) {
options.width = value
}
if (key === 'size') {
options.pixelWidth = normalizeImagePixelWidth(value)
}
}
return options
}
/**
* URL에서 기본 대체 텍스트(파일명) 추출
* @param {string} url - 이미지 URL
@@ -79,16 +123,17 @@ export const isImageUrl = (url) => {
/**
* 이미지 마크다운 한 줄 파싱
* @param {string} line - 마크다운 줄
* @returns {{ url: string, width: string, caption: string, useAlt: boolean }|null}
* @returns {{ url: string, width: string, pixelWidth: number|null, caption: string, useAlt: boolean }|null}
*/
export const parseImageMarkdownLine = (line) => {
const match = String(line || '').trim().match(IMAGE_MARKDOWN_LINE_RE)
const match = String(line || '').trim().match(IMAGE_MARKDOWN_BASE_RE)
if (!match) {
return null
}
const url = match[2] || ''
const options = parseImageMarkdownOptions(match[4])
const altBracket = (match[1] || '').trim()
const quotedCaption = unescapeImageCaption(match[3])
const filenameAlt = getImageDefaultAltLabel(url)
@@ -105,14 +150,15 @@ export const parseImageMarkdownLine = (line) => {
caption: quotedCaption || (useAlt ? filenameAlt : ''),
/** 레거시 `![표시문구](url)` — 캡션 따옴표 없을 때 미리보기용 */
legacyBracketLabel: !quotedCaption && altBracket && !useAlt ? altBracket : '',
width: match[4] || 'regular',
width: options.width,
pixelWidth: options.pixelWidth,
useAlt
}
}
/**
* 이미지 마크다운 한 줄 생성
* @param {{ url: string, caption?: string, width?: string, useAlt?: boolean }} image - 이미지 정보
* @param {{ url: string, caption?: string, width?: string, pixelWidth?: number|null, useAlt?: boolean }} image - 이미지 정보
* @returns {string} 마크다운 줄
*/
export const serializeImageMarkdown = (image) => {
@@ -131,8 +177,10 @@ export const serializeImageMarkdown = (image) => {
const titlePart = caption ? ` "${escapeImageCaption(caption)}"` : ''
const width = image.width && image.width !== 'regular' ? `{width=${image.width}}` : ''
const pixelWidth = normalizeImagePixelWidth(image.pixelWidth)
const size = pixelWidth ? `{size=${pixelWidth}}` : ''
return `![](${url}${titlePart})${width}`
return `![](${url}${titlePart})${width}${size}`
}
/**