슬래시 명령 로직 분리
This commit is contained in:
@@ -19,8 +19,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
filterSlashCommands,
|
filterSlashCommands,
|
||||||
getSlashCommandFocusTarget,
|
getSlashCommandFocusTarget,
|
||||||
|
insertMarkdownLinesAtSlashTarget,
|
||||||
|
isMediaSlashAction,
|
||||||
parseSlashInput,
|
parseSlashInput,
|
||||||
resolveSlashCommand
|
resolveSlashCommand,
|
||||||
|
shouldIgnorePendingMediaSlashCommit
|
||||||
} from '../../lib/markdown-slash-commands.js'
|
} from '../../lib/markdown-slash-commands.js'
|
||||||
import { buildCalloutOpenerLine, CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js'
|
import { buildCalloutOpenerLine, CALLOUT_BACKGROUND_OPTIONS, QUOTE_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js'
|
||||||
import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js'
|
import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js'
|
||||||
@@ -2340,9 +2343,11 @@ const onPreviewBlockContentChange = ({ startLine, endLine, replacementLines }) =
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (liveSlashInsertLine.value === startLine
|
if (shouldIgnorePendingMediaSlashCommit({
|
||||||
&& replacementLines.length === 1
|
pendingInsertLine: liveSlashInsertLine.value,
|
||||||
&& parseSlashInput(replacementLines[0])) {
|
startLine,
|
||||||
|
replacementLines
|
||||||
|
})) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2794,7 +2799,7 @@ const applyLiveSlashCommand = (command) => {
|
|||||||
|
|
||||||
const line = liveSlashSourceLine.value
|
const line = liveSlashSourceLine.value
|
||||||
|
|
||||||
if (['media-image', 'media-gallery', 'media-video', 'media-audio', 'media-file'].includes(command.action)) {
|
if (isMediaSlashAction(command.action)) {
|
||||||
liveSlashInsertLine.value = line
|
liveSlashInsertLine.value = line
|
||||||
replaceLineRange(line, line, [], false)
|
replaceLineRange(line, line, [], false)
|
||||||
onLiveSlashEnd()
|
onLiveSlashEnd()
|
||||||
@@ -2843,14 +2848,7 @@ const onLiveSlashApply = (payload) => {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const insertMarkdownAtLine = (lineIndex, insertLines) => {
|
const insertMarkdownAtLine = (lineIndex, insertLines) => {
|
||||||
const sourceLines = (markdownValue.value ?? '').split('\n')
|
markdownValue.value = insertMarkdownLinesAtSlashTarget(markdownValue.value, lineIndex, insertLines)
|
||||||
const replaceCount = parseSlashInput(sourceLines[lineIndex] ?? '') ? 1 : 0
|
|
||||||
|
|
||||||
markdownValue.value = [
|
|
||||||
...sourceLines.slice(0, lineIndex),
|
|
||||||
...insertLines,
|
|
||||||
...sourceLines.slice(lineIndex + replaceCount)
|
|
||||||
].join('\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPreviewMergeWithPreviousLine = ({ lineIndex, mergedLine }) => {
|
const onPreviewMergeWithPreviousLine = ({ lineIndex, mergedLine }) => {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
| lib/markdown-image.js | 이미지 마크다운 직렬화·파싱, 단독 이미지 URL 판별 |
|
| lib/markdown-image.js | 이미지 마크다운 직렬화·파싱, 단독 이미지 URL 판별 |
|
||||||
| lib/markdown-table.js | 공개 Markdown 표 행·구분선·정렬·블록 파싱 |
|
| lib/markdown-table.js | 공개 Markdown 표 행·구분선·정렬·블록 파싱 |
|
||||||
| lib/markdown-toc.js | 공개 게시글 TOC용 H1~H3 제목 추출과 앵커 ID 생성 |
|
| lib/markdown-toc.js | 공개 게시글 TOC용 H1~H3 제목 추출과 앵커 ID 생성 |
|
||||||
| lib/markdown-slash-commands.js | 관리자 Markdown-first 에디터 슬래시 명령 목록과 삽입 줄 정의 |
|
| lib/markdown-slash-commands.js | 관리자 Markdown-first 에디터 슬래시 명령 목록과 삽입 줄 정의, 미디어 명령 action 판별, 미디어 명령 지연 commit 무시·슬래시 원문 줄 교체 삽입 helper |
|
||||||
| lib/markdown-inline.js | 공개·관리자 공통 인라인 마크다운 파서, 링크·굵게·기울임·인라인 코드·첨자·인라인 색상 토큰(`::color[텍스트]`) 변환, 링크·굵게·기울임·색상 내부 중첩 인라인 문법과 `<br>` 줄바꿈 처리, 라이브 멀티라인 본문의 기본 불릿/번호 목록 HTML 변환과 contenteditable 목록 마커 기준 커서 offset 계산·직렬화 |
|
| lib/markdown-inline.js | 공개·관리자 공통 인라인 마크다운 파서, 링크·굵게·기울임·인라인 코드·첨자·인라인 색상 토큰(`::color[텍스트]`) 변환, 링크·굵게·기울임·색상 내부 중첩 인라인 문법과 `<br>` 줄바꿈 처리, 라이브 멀티라인 본문의 기본 불릿/번호 목록 HTML 변환과 contenteditable 목록 마커 기준 커서 offset 계산·직렬화 |
|
||||||
| lib/post-tag-limit.js | 게시물별 태그 최대 개수 기본값·허용 범위·정규화 |
|
| lib/post-tag-limit.js | 게시물별 태그 최대 개수 기본값·허용 범위·정규화 |
|
||||||
| lib/analytics-shared.js | 통계 추적 경로 필터·체류/스크롤 상수(클라이언트·서버 공용) |
|
| lib/analytics-shared.js | 통계 추적 경로 필터·체류/스크롤 상수(클라이언트·서버 공용) |
|
||||||
@@ -117,6 +117,7 @@
|
|||||||
| tests/content-gallery-images.test.js | 공개 본문 라이트박스 이미지 목록과 갤러리 행 분할 단위 회귀 테스트 |
|
| tests/content-gallery-images.test.js | 공개 본문 라이트박스 이미지 목록과 갤러리 행 분할 단위 회귀 테스트 |
|
||||||
| tests/content-image-urls.test.js | 공개 본문 이미지 파생 URL·srcset·갤러리 sizes 단위 회귀 테스트 |
|
| tests/content-image-urls.test.js | 공개 본문 이미지 파생 URL·srcset·갤러리 sizes 단위 회귀 테스트 |
|
||||||
| tests/markdown-block-context.test.js | 관리자 블록 설정 패널의 활성 블록 컨텍스트 단위 회귀 테스트 |
|
| tests/markdown-block-context.test.js | 관리자 블록 설정 패널의 활성 블록 컨텍스트 단위 회귀 테스트 |
|
||||||
|
| tests/markdown-slash-commands.test.js | 관리자 슬래시 명령 파싱·미디어 action·포커스 대상·원문 줄 교체 삽입 단위 회귀 테스트 |
|
||||||
| tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 |
|
| tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 |
|
||||||
| tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 |
|
| tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 |
|
||||||
| tests/markdown-quote.test.js | Markdown 인용 옵션 파싱과 편집 줄 변환 단위 회귀 테스트 |
|
| tests/markdown-quote.test.js | Markdown 인용 옵션 파싱과 편집 줄 변환 단위 회귀 테스트 |
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v1.11.67
|
||||||
|
|
||||||
|
- 관리자 Markdown 에디터의 슬래시 명령 미디어 액션 판별·지연 commit 무시·명령 줄 교체 삽입 로직을 공통 helper로 분리.
|
||||||
|
- 슬래시 명령 파싱·미디어 액션·포커스 대상·미디어 삽입 줄 교체 단위 테스트 추가.
|
||||||
|
|
||||||
## v1.11.66
|
## v1.11.66
|
||||||
|
|
||||||
- 라이브 작성 모드에서 오른쪽 코드 블록 설정의 줄번호 토글을 누르면 패널이 닫히는 문제 수정.
|
- 라이브 작성 모드에서 오른쪽 코드 블록 설정의 줄번호 토글을 누르면 패널이 닫히는 문제 수정.
|
||||||
|
|||||||
@@ -219,6 +219,47 @@ export const resolveSlashCommand = (query) => {
|
|||||||
return exact ?? matches[0]
|
return exact ?? matches[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 미디어 선택 모달을 여는 슬래시 명령인지 확인한다.
|
||||||
|
* @param {string} action - 명령 action
|
||||||
|
* @returns {boolean} 미디어 명령 여부
|
||||||
|
*/
|
||||||
|
export const isMediaSlashAction = (action) => ['media-image', 'media-gallery', 'media-video', 'media-audio', 'media-file']
|
||||||
|
.includes(String(action || ''))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 라이브 모드 미디어 슬래시 명령 처리 중 지연 commit을 무시할지 확인한다.
|
||||||
|
* @param {Object} options - 판별 옵션
|
||||||
|
* @param {number|null} options.pendingInsertLine - 미디어 삽입 대기 줄
|
||||||
|
* @param {number} options.startLine - commit 시작 줄
|
||||||
|
* @param {string[]} options.replacementLines - commit 대체 줄
|
||||||
|
* @returns {boolean} 무시 여부
|
||||||
|
*/
|
||||||
|
export const shouldIgnorePendingMediaSlashCommit = ({ pendingInsertLine, startLine, replacementLines }) => pendingInsertLine === startLine
|
||||||
|
&& Array.isArray(replacementLines)
|
||||||
|
&& replacementLines.length === 1
|
||||||
|
&& Boolean(parseSlashInput(replacementLines[0]))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지정 줄에 마크다운 줄을 삽입하되, 해당 줄이 슬래시 명령이면 교체한다.
|
||||||
|
* @param {string} markdown - 원본 마크다운
|
||||||
|
* @param {number} lineIndex - 삽입 위치
|
||||||
|
* @param {string[]} insertLines - 삽입 줄
|
||||||
|
* @returns {string} 갱신된 마크다운
|
||||||
|
*/
|
||||||
|
export const insertMarkdownLinesAtSlashTarget = (markdown, lineIndex, insertLines) => {
|
||||||
|
const sourceLines = String(markdown ?? '').split('\n')
|
||||||
|
const safeLine = Math.max(0, Math.min(Number.isFinite(lineIndex) ? lineIndex : 0, sourceLines.length))
|
||||||
|
const lines = Array.isArray(insertLines) ? insertLines : []
|
||||||
|
const replaceCount = parseSlashInput(sourceLines[safeLine] ?? '') ? 1 : 0
|
||||||
|
|
||||||
|
return [
|
||||||
|
...sourceLines.slice(0, safeLine),
|
||||||
|
...lines,
|
||||||
|
...sourceLines.slice(safeLine + replaceCount)
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 슬래시 명령 적용 후 포커스할 줄·커서 오프셋을 계산한다.
|
* 슬래시 명령 적용 후 포커스할 줄·커서 오프셋을 계산한다.
|
||||||
* @param {number} startLine - 교체 시작 줄(0-based)
|
* @param {number} startLine - 교체 시작 줄(0-based)
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.66",
|
"version": "1.11.67",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.66",
|
"version": "1.11.67",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.66",
|
"version": "1.11.67",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
73
tests/markdown-slash-commands.test.js
Normal file
73
tests/markdown-slash-commands.test.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
import {
|
||||||
|
getSlashCommandFocusTarget,
|
||||||
|
insertMarkdownLinesAtSlashTarget,
|
||||||
|
isMediaSlashAction,
|
||||||
|
parseSlashInput,
|
||||||
|
resolveSlashCommand,
|
||||||
|
shouldIgnorePendingMediaSlashCommit
|
||||||
|
} from '../lib/markdown-slash-commands.js'
|
||||||
|
|
||||||
|
test('슬래시 입력은 단일 줄 명령만 파싱한다', () => {
|
||||||
|
assert.deepEqual(parseSlashInput('/image'), {
|
||||||
|
query: 'image',
|
||||||
|
raw: '/image'
|
||||||
|
})
|
||||||
|
assert.equal(parseSlashInput(' /image'), null)
|
||||||
|
assert.equal(parseSlashInput('/image\n다음 줄'), null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('정확한 슬래시 명령과 미디어 액션을 판별한다', () => {
|
||||||
|
assert.equal(resolveSlashCommand('image')?.action, 'media-image')
|
||||||
|
assert.equal(resolveSlashCommand('img')?.id, 'image')
|
||||||
|
assert.equal(isMediaSlashAction('media-image'), true)
|
||||||
|
assert.equal(isMediaSlashAction('lines'), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('미디어 슬래시 명령 지연 commit은 같은 줄 slash 원문일 때만 무시한다', () => {
|
||||||
|
assert.equal(shouldIgnorePendingMediaSlashCommit({
|
||||||
|
pendingInsertLine: 3,
|
||||||
|
startLine: 3,
|
||||||
|
replacementLines: ['/image']
|
||||||
|
}), true)
|
||||||
|
assert.equal(shouldIgnorePendingMediaSlashCommit({
|
||||||
|
pendingInsertLine: 3,
|
||||||
|
startLine: 4,
|
||||||
|
replacementLines: ['/image']
|
||||||
|
}), false)
|
||||||
|
assert.equal(shouldIgnorePendingMediaSlashCommit({
|
||||||
|
pendingInsertLine: 3,
|
||||||
|
startLine: 3,
|
||||||
|
replacementLines: ['본문']
|
||||||
|
}), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('슬래시 명령 줄에 미디어를 삽입할 때 원문 줄을 교체한다', () => {
|
||||||
|
const markdown = [
|
||||||
|
'첫 줄',
|
||||||
|
'/image',
|
||||||
|
'마지막 줄'
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
assert.equal(insertMarkdownLinesAtSlashTarget(markdown, 1, ['']), [
|
||||||
|
'첫 줄',
|
||||||
|
'',
|
||||||
|
'마지막 줄'
|
||||||
|
].join('\n'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('일반 줄에는 지정 위치 앞에 마크다운 줄을 삽입한다', () => {
|
||||||
|
assert.equal(insertMarkdownLinesAtSlashTarget('첫 줄\n마지막 줄', 1, ['중간 줄']), '첫 줄\n중간 줄\n마지막 줄')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('슬래시 명령 포커스 대상은 단일 줄과 블록 삽입을 구분한다', () => {
|
||||||
|
assert.deepEqual(getSlashCommandFocusTarget(2, ['## ']), {
|
||||||
|
line: 2,
|
||||||
|
offset: 3
|
||||||
|
})
|
||||||
|
assert.deepEqual(getSlashCommandFocusTarget(2, ['```', '', '```']), {
|
||||||
|
line: 3,
|
||||||
|
offset: 0
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user