Files
sori.studio/lib/markdown-image.js
zenn 0c051cbe3b v1.2.5: 갤러리 드롭 위치 표시 및 파일명 캡션 토글 정리
미리보기 갤러리 드래그 시 드롭 대상 셀을 시각적으로 표시하고, 파일명 토글을 캡션(figcaption) 표시로 맞춤. 미리보기 클릭→작성 모드 전환은 제거.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 19:00:26 +09:00

149 lines
4.3 KiB
JavaScript

/** @type {RegExp} 이미지 마크다운 한 줄 */
const IMAGE_MARKDOWN_LINE_RE = /^!\[(.*?)\]\((\S+?)(?:\s+"((?:[^"\\]|\\.)*)")?\)(?:\{width=(regular|wide|full)\})?$/
/**
* 캡션 문자열 이스케이프 해제
* @param {string} value - 이스케이프된 캡션
* @returns {string} 캡션
*/
const unescapeImageCaption = (value) => String(value || '').replace(/\\"/g, '"')
/**
* 캡션 문자열 이스케이프
* @param {string} value - 캡션
* @returns {string} 이스케이프된 캡션
*/
const escapeImageCaption = (value) => String(value || '').replace(/"/g, '\\"')
/**
* URL에서 기본 대체 텍스트(파일명) 추출
* @param {string} url - 이미지 URL
* @returns {string} 파일명 기반 라벨
*/
/**
* 파일명·대괄호 라벨 비교용 정규화
* @param {string} value - 원본 문자열
* @returns {string} 정규화된 문자열
*/
const normalizeFilenameLabel = (value) => {
const raw = String(value || '').trim()
if (!raw) {
return ''
}
try {
return decodeURIComponent(raw)
} catch {
return raw
}
}
export const getImageDefaultAltLabel = (url) => {
const raw = String(url || '').trim()
if (!raw) {
return ''
}
try {
const pathname = new URL(raw, 'https://sori.studio').pathname
return decodeURIComponent(pathname.split('/').filter(Boolean).pop() || '')
} catch {
const withoutQuery = raw.split('?')[0].split('#')[0]
return decodeURIComponent(withoutQuery.split('/').filter(Boolean).pop() || '')
}
}
/**
* 이미지 마크다운 한 줄 파싱
* @param {string} line - 마크다운 줄
* @returns {{ url: string, width: string, caption: string, useAlt: boolean }|null}
*/
export const parseImageMarkdownLine = (line) => {
const match = String(line || '').trim().match(IMAGE_MARKDOWN_LINE_RE)
if (!match) {
return null
}
const url = match[2] || ''
const altBracket = (match[1] || '').trim()
const quotedCaption = unescapeImageCaption(match[3])
const filenameAlt = getImageDefaultAltLabel(url)
/** 레거시 `![파일명](url)` 또는 따옴표 캡션이 URL 파일명과 같을 때 파일명 캡션 모드 */
const useAlt = (altBracket !== ''
&& normalizeFilenameLabel(altBracket) === normalizeFilenameLabel(filenameAlt))
|| (quotedCaption !== ''
&& altBracket === ''
&& normalizeFilenameLabel(quotedCaption) === normalizeFilenameLabel(filenameAlt))
return {
url,
/** `![](url "캡션")` — 파일명 캡션 모드면 편집 필드에 파일명 반영 */
caption: quotedCaption || (useAlt ? filenameAlt : ''),
/** 레거시 `![표시문구](url)` — 캡션 따옴표 없을 때 미리보기용 */
legacyBracketLabel: !quotedCaption && altBracket && !useAlt ? altBracket : '',
width: match[4] || 'regular',
useAlt
}
}
/**
* 이미지 마크다운 한 줄 생성
* @param {{ url: string, caption?: string, width?: string, useAlt?: boolean }} image - 이미지 정보
* @returns {string} 마크다운 줄
*/
export const serializeImageMarkdown = (image) => {
const url = String(image.url || '').trim()
if (!url) {
return ''
}
const filename = getImageDefaultAltLabel(url)
let caption = String(image.caption ?? '').trim()
if (image.useAlt === true && !caption) {
caption = filename
}
const titlePart = caption ? ` "${escapeImageCaption(caption)}"` : ''
const width = image.width && image.width !== 'regular' ? `{width=${image.width}}` : ''
return `![](${url}${titlePart})${width}`
}
/**
* 공개 렌더용 img alt 텍스트
* @param {{ url?: string, useAlt?: boolean }} image - 이미지 정보
* @returns {string} alt 속성값
*/
export const getImageAltAttribute = () => ''
/**
* 공개 렌더용 캡션(표시용 figcaption)
* @param {{ caption?: string }} image - 이미지 정보
* @returns {string} 캡션
*/
export const getImageCaption = (image) => {
const caption = String(image?.caption || '').trim()
if (caption) {
return caption
}
if (image?.useAlt) {
return getImageDefaultAltLabel(image.url)
}
return String(image?.legacyBracketLabel || '').trim()
}
/**
* 표시용 캡션(단일·갤러리 공통)
* @param {Object} image - 이미지 블록 데이터
* @returns {string} figcaption 텍스트
*/
export const getImageDisplayCaption = (image) => getImageCaption(image)