공개 본문 문단 편집 분리
This commit is contained in:
@@ -18,11 +18,7 @@ import {
|
||||
parseOrderedListMarker,
|
||||
stripListMarker
|
||||
} from '../../lib/markdown-live-edit.js'
|
||||
import { buildCodeFenceOpener } from '../../lib/markdown-code-block.js'
|
||||
import {
|
||||
buildCalloutOpenerLine,
|
||||
CALLOUT_BACKGROUND_OPTIONS
|
||||
} from '../../lib/markdown-callout.js'
|
||||
import { CALLOUT_BACKGROUND_OPTIONS } from '../../lib/markdown-callout.js'
|
||||
import {
|
||||
formatQuoteLine,
|
||||
getQuoteContentStartLine,
|
||||
@@ -35,6 +31,7 @@ import { useContentMarkdownBlocks } from '../../composables/useContentMarkdownBl
|
||||
import { useContentMarkdownImageDrag } from '../../composables/useContentMarkdownImageDrag.js'
|
||||
import { useContentMarkdownLiveSelection } from '../../composables/useContentMarkdownLiveSelection.js'
|
||||
import { useContentMarkdownFencedBlocks } from '../../composables/useContentMarkdownFencedBlocks.js'
|
||||
import { useContentMarkdownParagraphs } from '../../composables/useContentMarkdownParagraphs.js'
|
||||
import ContentMarkdownCodeBlockEditor from './ContentMarkdownCodeBlockEditor.vue'
|
||||
import ProseCodeBlock from './ProseCodeBlock.vue'
|
||||
import ProseHtml from './ProseHtml.vue'
|
||||
@@ -91,9 +88,6 @@ const emit = defineEmits([
|
||||
const CONTENT_LIST_MARKER_COLOR = '#2eb6ea'
|
||||
/** @type {import('vue').Ref<Record<string, number>>} 갤러리 이미지 자연 비율 */
|
||||
const galleryImageAspectRatios = ref({})
|
||||
/** @type {number} 문단 분리 연속 호출 방지 */
|
||||
let lastParagraphSplitAt = 0
|
||||
|
||||
const {
|
||||
activeLightboxImages,
|
||||
activeLightboxImage,
|
||||
@@ -185,6 +179,17 @@ const {
|
||||
insertBelowBlock: (block, options) => onInsertBelowBlock(block, options)
|
||||
})
|
||||
|
||||
const {
|
||||
onParagraphInlineCommit,
|
||||
onSpacerInlineCommit,
|
||||
onParagraphLiveInput,
|
||||
onParagraphSplit
|
||||
} = useContentMarkdownParagraphs({
|
||||
pendingFocusLine,
|
||||
pendingFocusPosition,
|
||||
commitInlineBlockLines: (block, replacementLines) => commitInlineBlockLines(block, replacementLines)
|
||||
})
|
||||
|
||||
/**
|
||||
* 빈 줄 공백 블록 높이를 반환한다.
|
||||
* @param {Object} block - 렌더링 블록
|
||||
@@ -489,140 +494,6 @@ const onInsertAboveBlock = (block) => {
|
||||
})
|
||||
}
|
||||
|
||||
const onParagraphInlineCommit = (block, text) => {
|
||||
const value = String(text ?? '').replace(/\r/g, '')
|
||||
const lines = value.split('\n').map((line) => line.trimEnd())
|
||||
|
||||
if (lines.length <= 1) {
|
||||
commitInlineBlockLines(block, [lines[0] ?? ''])
|
||||
return
|
||||
}
|
||||
|
||||
commitInlineBlockLines(block, lines.filter((line) => line.length > 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 줄(스페이서) 편집 반영
|
||||
* @param {Object} block - 블록
|
||||
* @param {string} text - 편집된 텍스트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onSpacerInlineCommit = (block, text) => {
|
||||
if (!String(text ?? '').trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
commitInlineBlockLines(block, [text])
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 입력 중 블록 단축 변환을 처리한다.
|
||||
* @param {Object} block - 문단 블록
|
||||
* @param {string} text - 입력 텍스트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onParagraphLiveInput = (block, text) => {
|
||||
if (String(text ?? '').trim() !== '>') {
|
||||
return
|
||||
}
|
||||
|
||||
pendingFocusLine.value = block.meta.startLine
|
||||
pendingFocusPosition.value = 'start'
|
||||
commitInlineBlockLines(block, ['> '])
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 Enter 분리 결과 줄 배열을 만든다.
|
||||
* @param {string} head - 커서 앞 텍스트
|
||||
* @param {string} tail - 커서 뒤 텍스트
|
||||
* @returns {string[]} 분리된 줄
|
||||
*/
|
||||
const buildParagraphSplitLines = (head, tail) => {
|
||||
const h = String(head ?? '').replace(/\n/g, ' ')
|
||||
const t = String(tail ?? '').replace(/\n/g, ' ')
|
||||
|
||||
if (!t.length) {
|
||||
return [h, '']
|
||||
}
|
||||
|
||||
if (!h.length) {
|
||||
return ['', t]
|
||||
}
|
||||
|
||||
return [h, t]
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 Enter 입력을 블록 단축 입력으로 변환할지 확인한다.
|
||||
* @param {Object} block - 문단 블록
|
||||
* @param {string} before - 커서 앞 텍스트
|
||||
* @param {string} after - 커서 뒤 텍스트
|
||||
* @returns {boolean} 변환 처리 여부
|
||||
*/
|
||||
const applyParagraphShortcutSplit = (block, before, after) => {
|
||||
const head = String(before ?? '').trim()
|
||||
const tail = String(after ?? '')
|
||||
|
||||
if (tail.trim()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (/^```[A-Za-z0-9_-]*$/.test(head)) {
|
||||
const opener = head === '```' ? buildCodeFenceOpener({ language: '', showLineNumbers: false }) : head
|
||||
|
||||
pendingFocusLine.value = block.meta.startLine + 1
|
||||
pendingFocusPosition.value = 'start'
|
||||
commitInlineBlockLines(block, [opener, '', '```'])
|
||||
return true
|
||||
}
|
||||
|
||||
if (head === '!!!' || head === ':::callout') {
|
||||
pendingFocusLine.value = block.meta.startLine + 1
|
||||
pendingFocusPosition.value = 'start'
|
||||
commitInlineBlockLines(block, [
|
||||
buildCalloutOpenerLine({
|
||||
calloutEmojiEnabled: false,
|
||||
calloutEmoji: '💡',
|
||||
calloutBackground: 'blue',
|
||||
title: ''
|
||||
}),
|
||||
'',
|
||||
'',
|
||||
':::'
|
||||
])
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 Enter 분리 — 마크다운에 빈 줄을 넣어 다음 문단을 만든다.
|
||||
* @param {Object} block - 블록
|
||||
* @param {{ before: string, after: string }} payload - 커서 앞·뒤 텍스트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onParagraphSplit = (block, { before, after }) => {
|
||||
const now = Date.now()
|
||||
|
||||
if (now - lastParagraphSplitAt < 120) {
|
||||
return
|
||||
}
|
||||
|
||||
lastParagraphSplitAt = now
|
||||
|
||||
if (applyParagraphShortcutSplit(block, before, after)) {
|
||||
return
|
||||
}
|
||||
|
||||
const replacementLines = buildParagraphSplitLines(before, after)
|
||||
const focusLine = block.meta.startLine + Math.max(replacementLines.length - 1, 1)
|
||||
|
||||
pendingFocusLine.value = focusLine
|
||||
pendingFocusPosition.value = 'start'
|
||||
commitInlineBlockLines(block, replacementLines)
|
||||
}
|
||||
|
||||
/**
|
||||
* 블록 아래에 줄을 삽입한다.
|
||||
* @param {Object} block - 블록
|
||||
|
||||
169
composables/useContentMarkdownParagraphs.js
Normal file
169
composables/useContentMarkdownParagraphs.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { buildCodeFenceOpener } from '../lib/markdown-code-block.js'
|
||||
import { buildCalloutOpenerLine } from '../lib/markdown-callout.js'
|
||||
|
||||
/**
|
||||
* 관리자 라이브 Markdown 문단 편집 이벤트 상태를 만든다.
|
||||
* @param {Object} options - 문단 편집 옵션
|
||||
* @param {import('vue').Ref<number|null>} options.pendingFocusLine - 다음 포커스 줄
|
||||
* @param {import('vue').Ref<'start'|'end'|'auto'>} options.pendingFocusPosition - 다음 포커스 커서 위치
|
||||
* @param {(block: Object, replacementLines: string[]) => void} options.commitInlineBlockLines - 블록 줄 반영 콜백
|
||||
* @returns {Object} 문단 이벤트 핸들러
|
||||
*/
|
||||
export const useContentMarkdownParagraphs = (options) => {
|
||||
/** @type {number} 문단 분리 연속 호출 방지 */
|
||||
let lastParagraphSplitAt = 0
|
||||
|
||||
/**
|
||||
* 문단 줄 반영 후 포커스를 준비한다.
|
||||
* @param {number} line - 줄 번호
|
||||
* @returns {void}
|
||||
*/
|
||||
const prepareParagraphFocus = (line) => {
|
||||
options.pendingFocusLine.value = line
|
||||
options.pendingFocusPosition.value = 'start'
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 인라인 편집 반영
|
||||
* @param {Object} block - 문단 블록
|
||||
* @param {string} text - 편집된 텍스트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onParagraphInlineCommit = (block, text) => {
|
||||
const value = String(text ?? '').replace(/\r/g, '')
|
||||
const lines = value.split('\n').map((line) => line.trimEnd())
|
||||
|
||||
if (lines.length <= 1) {
|
||||
options.commitInlineBlockLines(block, [lines[0] ?? ''])
|
||||
return
|
||||
}
|
||||
|
||||
options.commitInlineBlockLines(block, lines.filter((line) => line.length > 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* 빈 줄(스페이서) 편집 반영
|
||||
* @param {Object} block - 블록
|
||||
* @param {string} text - 편집된 텍스트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onSpacerInlineCommit = (block, text) => {
|
||||
if (!String(text ?? '').trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
options.commitInlineBlockLines(block, [text])
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 입력 중 인용 단축 변환을 처리한다.
|
||||
* @param {Object} block - 문단 블록
|
||||
* @param {string} text - 입력 텍스트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onParagraphLiveInput = (block, text) => {
|
||||
if (String(text ?? '').trim() !== '>') {
|
||||
return
|
||||
}
|
||||
|
||||
prepareParagraphFocus(block.meta.startLine)
|
||||
options.commitInlineBlockLines(block, ['> '])
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 Enter 분리 결과 줄 배열을 만든다.
|
||||
* @param {string} head - 커서 앞 텍스트
|
||||
* @param {string} tail - 커서 뒤 텍스트
|
||||
* @returns {string[]} 분리된 줄
|
||||
*/
|
||||
const buildParagraphSplitLines = (head, tail) => {
|
||||
const h = String(head ?? '').replace(/\n/g, ' ')
|
||||
const t = String(tail ?? '').replace(/\n/g, ' ')
|
||||
|
||||
if (!t.length) {
|
||||
return [h, '']
|
||||
}
|
||||
|
||||
if (!h.length) {
|
||||
return ['', t]
|
||||
}
|
||||
|
||||
return [h, t]
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 Enter 입력을 블록 단축 입력으로 변환할지 확인한다.
|
||||
* @param {Object} block - 문단 블록
|
||||
* @param {string} before - 커서 앞 텍스트
|
||||
* @param {string} after - 커서 뒤 텍스트
|
||||
* @returns {boolean} 변환 처리 여부
|
||||
*/
|
||||
const applyParagraphShortcutSplit = (block, before, after) => {
|
||||
const head = String(before ?? '').trim()
|
||||
const tail = String(after ?? '')
|
||||
|
||||
if (tail.trim()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (/^```[A-Za-z0-9_-]*$/.test(head)) {
|
||||
const opener = head === '```' ? buildCodeFenceOpener({ language: '', showLineNumbers: false }) : head
|
||||
|
||||
prepareParagraphFocus(block.meta.startLine + 1)
|
||||
options.commitInlineBlockLines(block, [opener, '', '```'])
|
||||
return true
|
||||
}
|
||||
|
||||
if (head === '!!!' || head === ':::callout') {
|
||||
prepareParagraphFocus(block.meta.startLine + 1)
|
||||
options.commitInlineBlockLines(block, [
|
||||
buildCalloutOpenerLine({
|
||||
calloutEmojiEnabled: false,
|
||||
calloutEmoji: '💡',
|
||||
calloutBackground: 'blue',
|
||||
title: ''
|
||||
}),
|
||||
'',
|
||||
'',
|
||||
':::'
|
||||
])
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 문단 Enter 분리 — 마크다운에 빈 줄을 넣어 다음 문단을 만든다.
|
||||
* @param {Object} block - 블록
|
||||
* @param {{ before: string, after: string }} payload - 커서 앞·뒤 텍스트
|
||||
* @returns {void}
|
||||
*/
|
||||
const onParagraphSplit = (block, { before, after }) => {
|
||||
const now = Date.now()
|
||||
|
||||
if (now - lastParagraphSplitAt < 120) {
|
||||
return
|
||||
}
|
||||
|
||||
lastParagraphSplitAt = now
|
||||
|
||||
if (applyParagraphShortcutSplit(block, before, after)) {
|
||||
return
|
||||
}
|
||||
|
||||
const replacementLines = buildParagraphSplitLines(before, after)
|
||||
const focusLine = block.meta.startLine + Math.max(replacementLines.length - 1, 1)
|
||||
|
||||
prepareParagraphFocus(focusLine)
|
||||
options.commitInlineBlockLines(block, replacementLines)
|
||||
}
|
||||
|
||||
return {
|
||||
buildParagraphSplitLines,
|
||||
onParagraphInlineCommit,
|
||||
onSpacerInlineCommit,
|
||||
onParagraphLiveInput,
|
||||
onParagraphSplit
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
## 2026-07-27 v1.11.100 — 공개 Markdown 문단 편집 이벤트를 분리한다
|
||||
|
||||
문단 편집은 일반 텍스트 커밋뿐 아니라 빈 줄 스페이서 입력, `>` 인용 변환, 코드·콜아웃 단축 블록 생성까지 담당한다. 이 흐름을 렌더러 안에 남기면 템플릿과 라이브 입력 규칙이 계속 섞이므로 `useContentMarkdownParagraphs`로 분리한다. 렌더러는 문단 편집 컴포넌트 이벤트 연결에 집중하고, 문단 단축 문법과 포커스 예약은 composable에서 테스트 가능한 단위로 유지한다.
|
||||
|
||||
## 2026-07-27 v1.11.99 — 공개 Markdown fenced 블록 편집 이벤트를 분리한다
|
||||
|
||||
코드·콜아웃·토글은 모두 여는 줄과 닫는 줄을 보존하면서 본문을 갱신하고 아래 문단으로 이탈하는 흐름을 공유한다. 이를 `ContentMarkdownRenderer` 안에 각각의 이벤트 함수로 남겨두면 라이브 편집 템플릿과 마크다운 줄 직렬화 책임이 계속 섞인다. `useContentMarkdownFencedBlocks`로 분리해 렌더러는 블록 컴포넌트 이벤트를 연결하고, fenced 블록 문법 보존과 다음 문단 포커스 준비는 composable에서 담당하게 한다.
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
| composables/useContentMarkdownImageDrag.js | 관리자 라이브 Markdown 본문의 단일 이미지·갤러리 드래그 상태, drop target 표시, 병합·삽입·분리·정렬 이벤트 중개 |
|
||||
| composables/useContentMarkdownLiveSelection.js | 관리자 라이브 Markdown 본문의 Selection Bridge, 포커스 복원, line focus/blur, 원문 편집 표시 상태 관리 |
|
||||
| composables/useContentMarkdownFencedBlocks.js | 관리자 라이브 Markdown 코드·콜아웃·토글 fenced 블록 편집 이벤트와 아래 문단 삽입 상태 관리 |
|
||||
| composables/useContentMarkdownParagraphs.js | 관리자 라이브 Markdown 문단·스페이서 편집 이벤트와 문단 Enter 단축 변환 관리 |
|
||||
| composables/useAdminMediaThumbnailRegeneration.js | 관리자 미디어 화면 대표 이미지·페이지 커버·본문 이미지 재생성 가능 여부와 API 호출 상태 |
|
||||
| composables/usePublicSiteSettings.js | 공개 화면의 `/api/site-settings` 조회 key(`site-settings-public`)와 기본 설정값 통일 |
|
||||
|
||||
@@ -139,6 +140,7 @@
|
||||
| tests/useContentMarkdownImageDrag.test.js | 공개 Markdown 라이브 이미지·갤러리 드래그 composable의 병합·삽입·분리·정렬 단위 테스트 |
|
||||
| tests/useContentMarkdownLiveSelection.test.js | 공개 Markdown 라이브 선택 composable의 포커스 요청·예약 포커스 초기화·line focus/blur 단위 테스트 |
|
||||
| tests/useContentMarkdownFencedBlocks.test.js | 공개 Markdown fenced 블록 composable의 코드·콜아웃·토글 아래 문단 삽입 단위 테스트 |
|
||||
| tests/useContentMarkdownParagraphs.test.js | 공개 Markdown 문단 composable의 커밋·분리·인용/코드/콜아웃 단축 변환 단위 테스트 |
|
||||
| tests/markdown-block-parser.test.js | 공개 Markdown 본문 블록 파싱 단위 회귀 테스트 |
|
||||
| tests/markdown-fenced-meta.test.js | Markdown fenced 블록 시작·종료 줄 탐색, 본문 수집, 메타 파싱 단위 회귀 테스트 |
|
||||
| tests/markdown-quote.test.js | Markdown 인용 옵션 파싱과 편집 줄 변환 단위 회귀 테스트 |
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
- 관리자 미디어 업로드 API는 JPG·PNG·WebP 파일을 `/uploads/posts/YYYY/MM/원본파일`에 저장한다. 업로드만으로는 카드 썸네일을 만들지 않고, 해당 이미지가 게시물 대표 이미지로 저장될 때 목록 카드용 WebP 썸네일을 `/uploads/posts/YYYY/MM/thumbs/원본파일명-card.webp`에 생성한다.
|
||||
- 카드 썸네일은 640×360 기준 `cover` 리사이즈와 WebP 품질 82를 사용한다. GIF·동영상·문서 파일은 썸네일을 자동 생성하지 않는다.
|
||||
- 본문 표시용 이미지는 1600px `thumbs/*-body-1600.webp`와 720px `thumbs/*-body-720.webp`를 함께 생성하고, 공개 본문 단일 이미지·갤러리는 `lib/content-image-urls.js`의 `srcset` 계산으로 화면 폭에 맞는 파일을 우선 사용한다. 공개 본문 라이트박스 대상 목록은 `useContentMarkdownBlocks`에서 `lib/content-gallery-images.js` 기준으로 계산하고, 갤러리 행 분할은 `lib/content-gallery-images.js`에서 계산한다. 관리자 라이브 모드의 단일 이미지·갤러리 드래그 상태와 병합·삽입·분리·정렬 이벤트 중개는 `useContentMarkdownImageDrag`에서 관리한다. 라이브 모드의 선택 확장·전체 선택·선택 삭제·포커스 복원·line focus/blur 중개는 `useContentMarkdownLiveSelection`에서 관리한다. 코드·콜아웃·토글 fenced 블록의 라이브 편집 이벤트는 `useContentMarkdownFencedBlocks`에서 관리한다.
|
||||
- 본문 표시용 이미지는 1600px `thumbs/*-body-1600.webp`와 720px `thumbs/*-body-720.webp`를 함께 생성하고, 공개 본문 단일 이미지·갤러리는 `lib/content-image-urls.js`의 `srcset` 계산으로 화면 폭에 맞는 파일을 우선 사용한다. 공개 본문 라이트박스 대상 목록은 `useContentMarkdownBlocks`에서 `lib/content-gallery-images.js` 기준으로 계산하고, 갤러리 행 분할은 `lib/content-gallery-images.js`에서 계산한다. 관리자 라이브 모드의 단일 이미지·갤러리 드래그 상태와 병합·삽입·분리·정렬 이벤트 중개는 `useContentMarkdownImageDrag`에서 관리한다. 라이브 모드의 선택 확장·전체 선택·선택 삭제·포커스 복원·line focus/blur 중개는 `useContentMarkdownLiveSelection`에서 관리한다. 코드·콜아웃·토글 fenced 블록의 라이브 편집 이벤트는 `useContentMarkdownFencedBlocks`에서 관리한다. 문단·스페이서 편집과 `>`·코드·콜아웃 문단 단축 변환은 `useContentMarkdownParagraphs`에서 관리한다.
|
||||
- 라이트박스용 이미지는 2560px `thumbs/*-body-2560.webp`를 생성해 크게 보기 화면에서 우선 사용한다. 별도 원본 보기 UI는 제공하지 않으며, `lib/content-image-urls.js`가 파생 URL을 선택하고 파생 이미지 로드 실패 시에만 원본으로 fallback한다.
|
||||
- 기존 운영 볼륨에 생성된 `thumbs/*-body.webp`, `thumbs/*-lightbox.webp`는 레거시 본문 파생 이미지로 계속 인식해 관리자 미디어의 본문 이미지 탭과 사용처 연결을 유지한다. 새 저장·재생성·백필은 해상도형 파일명만 만든다.
|
||||
- 관리자 미디어의 원본 이미지 사용처는 원본 URL 직접 참조뿐 아니라 해당 원본에서 생성된 카드·페이지 커버·본문 파생 이미지 URL 사용처까지 함께 반영한다. 사용처 계산은 관리자 글 목록 요약 API와 분리된 미디어 참조 전용 조회로 게시물·페이지의 본문과 대표 이미지만 확인한다. 따라서 공개 본문이 `body-1600` 같은 파생 이미지를 사용하더라도 원본은 미사용 삭제 대상에서 제외되고, 본문 파생 이미지도 실제 본문 사용처가 있으면 미사용으로 분류하지 않는다. 원본이 실제로 미사용이라 삭제될 때는 연결된 카드·페이지 커버·본문 자동 생성 이미지도 함께 삭제한다.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- 거대 컴포넌트 책임 분리 진행.
|
||||
- `components/admin/AdminMarkdownEditor.vue`: 미디어 모달, 슬래시 명령, 히스토리·선택 복원, 라이브/소스 전환 로직을 단계적으로 composable 또는 하위 컴포넌트로 분리.
|
||||
- `components/content/ContentMarkdownRenderer.vue`: 남은 문단·목록·인용 편집 이벤트(`onParagraph*`, `onList*`, `onQuote*`)를 도메인별 helper/composable로 추가 분리.
|
||||
- `components/content/ContentMarkdownRenderer.vue`: 남은 목록·인용 편집 이벤트(`onList*`, `onQuote*`)를 도메인별 helper/composable로 추가 분리.
|
||||
- 미디어 카탈로그 전환 코드 수명 정리.
|
||||
- 운영 `MEDIA_CATALOG_MODE=database` 안정화 후 `shadow`/`filesystem` 런타임 분기 축소 가능 여부 검토.
|
||||
- fallback은 운영 안정성에 필요하므로 제거 전 reconcile·로그·테스트 기준을 먼저 정리.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.11.100
|
||||
|
||||
- 공개 Markdown 렌더러의 라이브 문단·스페이서 편집 이벤트와 문단 Enter 단축 변환을 `useContentMarkdownParagraphs` composable로 분리.
|
||||
- 문단 composable의 여러 줄 커밋, 인용 변환, 일반 분리, 코드·콜아웃 단축 변환 단위 테스트 추가.
|
||||
- v1.11 내부 개선 todo에서 완료된 문단 편집 이벤트 분리 범위를 제외하고 남은 블록 이벤트 범위를 목록·인용으로 재정리.
|
||||
|
||||
## v1.11.99
|
||||
|
||||
- 공개 Markdown 렌더러의 코드·콜아웃·토글 fenced 블록 편집 이벤트를 `useContentMarkdownFencedBlocks` composable로 분리.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.99",
|
||||
"version": "1.11.100",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.99",
|
||||
"version": "1.11.100",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.99",
|
||||
"version": "1.11.100",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
91
tests/useContentMarkdownParagraphs.test.js
Normal file
91
tests/useContentMarkdownParagraphs.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { ref } from 'vue'
|
||||
import { useContentMarkdownParagraphs } from '../composables/useContentMarkdownParagraphs.js'
|
||||
|
||||
/**
|
||||
* 테스트용 문단 편집 상태를 만든다.
|
||||
* @returns {{ calls: Object, pendingFocusLine: import('vue').Ref<number|null>, pendingFocusPosition: import('vue').Ref<string>, state: Object }}
|
||||
*/
|
||||
const createParagraphState = () => {
|
||||
const calls = {
|
||||
commits: []
|
||||
}
|
||||
const pendingFocusLine = ref(null)
|
||||
const pendingFocusPosition = ref('auto')
|
||||
const state = useContentMarkdownParagraphs({
|
||||
pendingFocusLine,
|
||||
pendingFocusPosition,
|
||||
commitInlineBlockLines: (block, replacementLines) => {
|
||||
calls.commits.push({ block, replacementLines })
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
calls,
|
||||
pendingFocusLine,
|
||||
pendingFocusPosition,
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
test('문단 인라인 커밋은 여러 줄 입력을 빈 줄 없이 마크다운 줄로 반영한다', () => {
|
||||
const block = { type: 'paragraph', meta: { startLine: 2 } }
|
||||
const { calls, state } = createParagraphState()
|
||||
|
||||
state.onParagraphInlineCommit(block, '첫 줄 \n\n둘째 줄')
|
||||
|
||||
assert.deepEqual(calls.commits[0], {
|
||||
block,
|
||||
replacementLines: ['첫 줄', '둘째 줄']
|
||||
})
|
||||
})
|
||||
|
||||
test('문단 입력 중 > 단독 입력은 인용 마커 줄로 변환한다', () => {
|
||||
const block = { type: 'paragraph', meta: { startLine: 4 } }
|
||||
const { calls, pendingFocusLine, pendingFocusPosition, state } = createParagraphState()
|
||||
|
||||
state.onParagraphLiveInput(block, '>')
|
||||
|
||||
assert.equal(pendingFocusLine.value, 4)
|
||||
assert.equal(pendingFocusPosition.value, 'start')
|
||||
assert.deepEqual(calls.commits[0].replacementLines, ['> '])
|
||||
})
|
||||
|
||||
test('문단 Enter 분리는 커서 앞뒤 텍스트를 두 문단 줄로 나눈다', () => {
|
||||
const block = { type: 'paragraph', meta: { startLine: 10 } }
|
||||
const { calls, pendingFocusLine, pendingFocusPosition, state } = createParagraphState()
|
||||
|
||||
state.onParagraphSplit(block, { before: '앞', after: '뒤' })
|
||||
|
||||
assert.equal(pendingFocusLine.value, 11)
|
||||
assert.equal(pendingFocusPosition.value, 'start')
|
||||
assert.deepEqual(calls.commits[0].replacementLines, ['앞', '뒤'])
|
||||
})
|
||||
|
||||
test('문단 코드 펜스 단축 입력은 코드 블록 줄로 변환한다', () => {
|
||||
const block = { type: 'paragraph', meta: { startLine: 3 } }
|
||||
const { calls, pendingFocusLine, pendingFocusPosition, state } = createParagraphState()
|
||||
|
||||
state.onParagraphSplit(block, { before: '```javascript', after: '' })
|
||||
|
||||
assert.equal(pendingFocusLine.value, 4)
|
||||
assert.equal(pendingFocusPosition.value, 'start')
|
||||
assert.deepEqual(calls.commits[0].replacementLines, ['```javascript', '', '```'])
|
||||
})
|
||||
|
||||
test('문단 콜아웃 단축 입력은 콜아웃 블록 줄로 변환한다', () => {
|
||||
const block = { type: 'paragraph', meta: { startLine: 7 } }
|
||||
const { calls, pendingFocusLine, pendingFocusPosition, state } = createParagraphState()
|
||||
|
||||
state.onParagraphSplit(block, { before: '!!!', after: '' })
|
||||
|
||||
assert.equal(pendingFocusLine.value, 8)
|
||||
assert.equal(pendingFocusPosition.value, 'start')
|
||||
assert.deepEqual(calls.commits[0].replacementLines, [
|
||||
':::callout emoji=none bg=blue',
|
||||
'',
|
||||
'',
|
||||
':::'
|
||||
])
|
||||
})
|
||||
Reference in New Issue
Block a user