v1.2.9: 라이브 에디터·홈 피드·메인 커버 개선
라이브 모드 코드/콜아웃/토글 편집, 슬래시 명령, 홈 Latest List·Compact·Cards 보기, 사이트 설정 메인 화면 커버(720px) 및 HomeHero 반영. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
195
lib/markdown-slash-commands.js
Normal file
195
lib/markdown-slash-commands.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 라이브/마크다운 에디터 슬래시 명령 정의
|
||||
* @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 }
|
||||
}
|
||||
Reference in New Issue
Block a user