라이브 모드 코드/콜아웃/토글 편집, 슬래시 명령, 홈 Latest List·Compact·Cards 보기, 사이트 설정 메인 화면 커버(720px) 및 HomeHero 반영. Co-authored-by: Cursor <cursoragent@cursor.com>
196 lines
5.1 KiB
JavaScript
196 lines
5.1 KiB
JavaScript
/**
|
|
* 라이브/마크다운 에디터 슬래시 명령 정의
|
|
* @typedef {Object} MarkdownSlashCommand
|
|
* @property {string} id - 명령 ID
|
|
* @property {string} label - 표시 이름
|
|
* @property {string} description - 설명
|
|
* @property {string[]} keywords - 검색 키워드
|
|
* @property {'media-image'|'media-gallery'|'lines'} action - 실행 유형
|
|
* @property {string[]} [lines] - 삽입할 마크다운 줄(action이 lines일 때)
|
|
* @property {boolean} [showInDefaultMenu=true] - `/`만 입력했을 때 메뉴에 표시할지
|
|
*/
|
|
|
|
/** @type {MarkdownSlashCommand[]} */
|
|
export const MARKDOWN_SLASH_COMMANDS = [
|
|
{
|
|
id: 'image',
|
|
label: '이미지',
|
|
description: '단일 이미지 삽입',
|
|
keywords: ['image', 'img', 'photo', '사진', '이미지'],
|
|
action: 'media-image'
|
|
},
|
|
{
|
|
id: 'gallery',
|
|
label: '갤러리',
|
|
description: '여러 이미지 갤러리',
|
|
keywords: ['gallery', 'images', '갤러리'],
|
|
action: 'media-gallery'
|
|
},
|
|
{
|
|
id: 'h1',
|
|
label: '제목 1',
|
|
description: '큰 제목(게시물 제목 외에는 비권장)',
|
|
keywords: ['h1', 'heading1'],
|
|
action: 'lines',
|
|
lines: ['# '],
|
|
showInDefaultMenu: false
|
|
},
|
|
{
|
|
id: 'h2',
|
|
label: '제목 2',
|
|
description: '섹션 제목',
|
|
keywords: ['h2', 'heading', 'subtitle', '제목'],
|
|
action: 'lines',
|
|
lines: ['## ']
|
|
},
|
|
{
|
|
id: 'h3',
|
|
label: '제목 3',
|
|
description: '작은 섹션 제목',
|
|
keywords: ['h3', 'heading', '제목'],
|
|
action: 'lines',
|
|
lines: ['### ']
|
|
},
|
|
{
|
|
id: 'h4',
|
|
label: '제목 4',
|
|
description: '소제목',
|
|
keywords: ['h4', 'heading', '제목'],
|
|
action: 'lines',
|
|
lines: ['#### ']
|
|
},
|
|
{
|
|
id: 'quote',
|
|
label: '인용',
|
|
description: '인용문 블록',
|
|
keywords: ['quote', 'blockquote', '인용'],
|
|
action: 'lines',
|
|
lines: ['> ']
|
|
},
|
|
{
|
|
id: 'list',
|
|
label: '목록',
|
|
description: '불릿 목록',
|
|
keywords: ['list', 'bullet', 'ul', '목록'],
|
|
action: 'lines',
|
|
lines: ['- ']
|
|
},
|
|
{
|
|
id: 'code',
|
|
label: '코드',
|
|
description: '코드 블록',
|
|
keywords: ['code', 'pre', '코드'],
|
|
action: 'lines',
|
|
lines: ['```', '', '```']
|
|
},
|
|
{
|
|
id: 'divider',
|
|
label: '구분선',
|
|
description: '가로 구분선',
|
|
keywords: ['divider', 'hr', 'line', '구분선'],
|
|
action: 'lines',
|
|
lines: ['---']
|
|
},
|
|
{
|
|
id: 'callout',
|
|
label: '콜아웃',
|
|
description: '강조 안내(첫 줄: :::callout emoji=💡 bg=blue)',
|
|
keywords: ['callout', 'notice', 'info', '콜아웃'],
|
|
action: 'lines',
|
|
lines: [':::callout emoji=💡 bg=blue', '', ':::']
|
|
},
|
|
{
|
|
id: 'toggle',
|
|
label: '토글',
|
|
description: '접기/펼치기 블록',
|
|
keywords: ['toggle', 'details', '토글'],
|
|
action: 'lines',
|
|
lines: [':::toggle 제목', '', ':::']
|
|
},
|
|
{
|
|
id: 'embed',
|
|
label: '임베드',
|
|
description: 'YouTube·X 등 외부 링크',
|
|
keywords: ['embed', 'youtube', 'link', '임베드', '유튜브'],
|
|
action: 'lines',
|
|
lines: [':::embed', '', ':::']
|
|
}
|
|
]
|
|
|
|
/**
|
|
* 슬래시 입력 문자열을 파싱한다.
|
|
* @param {string} value - 편집 값
|
|
* @returns {{ query: string, raw: string }|null} 파싱 결과
|
|
*/
|
|
export const parseSlashInput = (value) => {
|
|
const raw = String(value ?? '')
|
|
|
|
if (!raw.startsWith('/') || raw.includes('\n')) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
query: raw.slice(1).trim().toLowerCase(),
|
|
raw
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 검색어에 맞는 슬래시 명령을 필터링한다.
|
|
* @param {string} query - 검색어(/ 제외)
|
|
* @returns {MarkdownSlashCommand[]} 명령 목록
|
|
*/
|
|
export const filterSlashCommands = (query) => {
|
|
const normalized = String(query ?? '').trim().toLowerCase()
|
|
|
|
if (!normalized) {
|
|
return MARKDOWN_SLASH_COMMANDS.filter((command) => command.showInDefaultMenu !== false)
|
|
}
|
|
|
|
return MARKDOWN_SLASH_COMMANDS.filter((command) => [
|
|
command.id,
|
|
command.label,
|
|
command.description,
|
|
...command.keywords
|
|
].some((keyword) => String(keyword).toLowerCase().includes(normalized)))
|
|
}
|
|
|
|
/**
|
|
* 검색어와 일치하는 최우선 명령을 반환한다.
|
|
* @param {string} query - 검색어
|
|
* @returns {MarkdownSlashCommand|null} 명령
|
|
*/
|
|
export const resolveSlashCommand = (query) => {
|
|
const normalized = String(query ?? '').trim().toLowerCase()
|
|
const matches = filterSlashCommands(normalized)
|
|
|
|
if (!matches.length) {
|
|
return null
|
|
}
|
|
|
|
const exact = matches.find((command) => command.id === normalized
|
|
|| command.keywords.some((keyword) => keyword.toLowerCase() === normalized))
|
|
|
|
return exact ?? matches[0]
|
|
}
|
|
|
|
/**
|
|
* 슬래시 명령 적용 후 포커스할 줄·커서 오프셋을 계산한다.
|
|
* @param {number} startLine - 교체 시작 줄(0-based)
|
|
* @param {string[]} lines - 삽입 줄
|
|
* @returns {{ line: number, offset: number }} 포커스 대상
|
|
*/
|
|
export const getSlashCommandFocusTarget = (startLine, lines) => {
|
|
const safeStart = Number.isFinite(startLine) ? startLine : 0
|
|
|
|
if (!Array.isArray(lines) || !lines.length) {
|
|
return { line: safeStart, offset: 0 }
|
|
}
|
|
|
|
if (lines.length === 1) {
|
|
return { line: safeStart, offset: String(lines[0] ?? '').length }
|
|
}
|
|
|
|
return { line: safeStart + 1, offset: 0 }
|
|
}
|