Files
sori.studio/lib/markdown-block-context.js
zenn 6919669330 v1.4.2: 라이브 이미지·갤러리 편집 UX와 공개 화면 색상 정리
라이브 모드 이미지·갤러리 드래그 병합·분리, 갤러리 개별 편집, 블록 패널 유지, 다크모드 인용·사이드바·리스트 마커 색상을 보정한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 17:07:52 +09:00

163 lines
4.3 KiB
JavaScript

import { isImageUrl, parseImageMarkdownLine } from './markdown-image.js'
/**
* fenced 블록 시작 줄 인덱스를 찾는다.
* @param {string[]} lines - 본문 줄 목록
* @param {number} currentLine - 현재 줄
* @param {string} opener - 시작 토큰
* @returns {number} 시작 줄 또는 -1
*/
const findFencedBlockStart = (lines, currentLine, opener) => {
for (let index = currentLine; index >= 0; index -= 1) {
if ((lines[index] || '').trim() === opener) {
return index
}
if ((lines[index] || '').trim() === ':::') {
break
}
}
return -1
}
/**
* fenced 블록 종료 줄 인덱스를 찾는다.
* @param {string[]} lines - 본문 줄 목록
* @param {number} startLine - 시작 줄
* @returns {number} 종료 줄 또는 -1
*/
const findFencedBlockEnd = (lines, startLine) => {
for (let index = startLine + 1; index < lines.length; index += 1) {
if ((lines[index] || '').trim() === ':::') {
return index
}
}
return -1
}
/**
* 단독 URL 줄인지 확인한다.
* @param {string} line - 마크다운 줄
* @returns {boolean} 단독 URL 여부
*/
const isStandaloneUrlLine = (line) => /^https?:\/\/\S+$/i.test(String(line || '').trim())
/**
* 단독 이미지 URL 줄인지 확인한다.
* @param {string} line - 마크다운 줄
* @returns {boolean} 이미지 URL 여부
*/
const isStandaloneImageUrlLine = (line) => isStandaloneUrlLine(line) && isImageUrl(line)
/**
* 갤러리 fenced 블록을 파싱한다.
* @param {string[]} lines - 본문 줄 목록
* @param {number} currentLine - 현재 줄
* @returns {{ kind: 'gallery', startLine: number, endLine: number, images: Array<Object> }|null}
*/
const resolveGalleryBlock = (lines, currentLine) => {
const galleryStart = findFencedBlockStart(lines, currentLine, ':::gallery')
if (galleryStart === -1) {
return null
}
const galleryEnd = findFencedBlockEnd(lines, galleryStart)
if (galleryEnd === -1 || currentLine > galleryEnd) {
return null
}
return {
kind: 'gallery',
startLine: galleryStart,
endLine: galleryEnd,
selectedImageIndex: currentLine > galleryStart && currentLine < galleryEnd
? currentLine - galleryStart - 1
: null,
images: lines
.slice(galleryStart + 1, galleryEnd)
.map(parseImageMarkdownLine)
.filter(Boolean)
}
}
/**
* 임베드 fenced 블록을 파싱한다.
* @param {string[]} lines - 본문 줄 목록
* @param {number} currentLine - 현재 줄
* @returns {{ kind: 'embed', startLine: number, endLine: number, url: string }|null}
*/
const resolveEmbedBlock = (lines, currentLine) => {
const standaloneUrl = String(lines[currentLine] || '').trim()
if (isStandaloneUrlLine(standaloneUrl) && !isStandaloneImageUrlLine(standaloneUrl)) {
return {
kind: 'embed',
startLine: currentLine,
endLine: currentLine,
url: standaloneUrl
}
}
const embedStart = findFencedBlockStart(lines, currentLine, ':::embed')
if (embedStart === -1) {
return null
}
const embedEnd = findFencedBlockEnd(lines, embedStart)
if (embedEnd === -1 || currentLine > embedEnd) {
return null
}
return {
kind: 'embed',
startLine: embedStart,
endLine: embedEnd,
url: lines.slice(embedStart + 1, embedEnd).join('\n').trim()
}
}
/**
* 커서 줄 기준 활성 블록 컨텍스트를 반환한다.
* @param {string} markdown - 본문 마크다운
* @param {number} lineIndex - 현재 줄(0-based)
* @returns {Object|null} 블록 컨텍스트
*/
export const resolveActiveBlockContext = (markdown, lineIndex) => {
const lines = String(markdown || '').split('\n')
const currentLine = Math.min(Math.max(0, lineIndex), Math.max(0, lines.length - 1))
const activeImage = parseImageMarkdownLine(lines[currentLine] || '')
const activeImageUrl = String(lines[currentLine] || '').trim()
if (activeImage) {
return {
kind: 'image',
startLine: currentLine,
endLine: currentLine,
images: [activeImage]
}
}
if (isStandaloneImageUrlLine(activeImageUrl)) {
return {
kind: 'image',
startLine: currentLine,
endLine: currentLine,
images: [{ url: activeImageUrl, width: 'regular', caption: '', useAlt: false }]
}
}
const gallery = resolveGalleryBlock(lines, currentLine)
if (gallery) {
return gallery
}
return resolveEmbedBlock(lines, currentLine)
}