마크다운 표 파싱 로직 분리
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
} from '../../lib/markdown-live-edit.js'
|
||||
import { buildCodeBlockLines, buildCodeFenceOpener, parseCodeFenceLine } from '../../lib/markdown-code-block.js'
|
||||
import { buildToggleBlockLines, parseToggleOpenerLine } from '../../lib/markdown-toggle.js'
|
||||
import { parseMarkdownTableBlock } from '../../lib/markdown-table.js'
|
||||
import {
|
||||
buildCalloutOpenerLine,
|
||||
CALLOUT_BACKGROUND_OPTIONS,
|
||||
@@ -475,90 +476,6 @@ const parseMediaMeta = (raw) => {
|
||||
return meta
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 행 후보인지 확인한다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {boolean} 표 행 후보 여부
|
||||
*/
|
||||
const isMarkdownTableRow = (line) => {
|
||||
const value = String(line || '').trim()
|
||||
return value.startsWith('|') && value.endsWith('|') && value.split('|').length >= 4
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 구분선 행인지 확인한다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {boolean} 구분선 여부
|
||||
*/
|
||||
const isMarkdownTableSeparator = (line) => {
|
||||
if (!isMarkdownTableRow(line)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return splitMarkdownTableRow(line).every((cell) => /^:?-{3,}:?$/.test(cell.trim()))
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 행을 셀 배열로 나눈다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {string[]} 셀 목록
|
||||
*/
|
||||
const splitMarkdownTableRow = (line) => String(line || '')
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim().replace(/\\\|/g, '|'))
|
||||
|
||||
/**
|
||||
* 표 정렬 행에서 셀 정렬 값을 파싱한다.
|
||||
* @param {string} cell - 구분선 셀
|
||||
* @returns {'left'|'center'|'right'} 정렬 값
|
||||
*/
|
||||
const parseMarkdownTableAlignment = (cell) => {
|
||||
const value = String(cell || '').trim()
|
||||
|
||||
if (value.startsWith(':') && value.endsWith(':')) {
|
||||
return 'center'
|
||||
}
|
||||
|
||||
if (value.endsWith(':')) {
|
||||
return 'right'
|
||||
}
|
||||
|
||||
return 'left'
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 블록을 파싱한다.
|
||||
* @param {string[]} lines - 전체 마크다운 줄
|
||||
* @param {number} startIndex - 표 시작 후보 줄
|
||||
* @returns {{headers: string[], alignments: string[], rows: string[][], endLine: number, nextIndex: number}|null} 표 메타
|
||||
*/
|
||||
const parseMarkdownTableBlock = (lines, startIndex) => {
|
||||
if (!isMarkdownTableRow(lines[startIndex]) || !isMarkdownTableSeparator(lines[startIndex + 1])) {
|
||||
return null
|
||||
}
|
||||
|
||||
const headers = splitMarkdownTableRow(lines[startIndex])
|
||||
const alignments = splitMarkdownTableRow(lines[startIndex + 1]).map(parseMarkdownTableAlignment)
|
||||
const rows = []
|
||||
let index = startIndex + 2
|
||||
|
||||
while (index < lines.length && isMarkdownTableRow(lines[index]) && !isMarkdownTableSeparator(lines[index])) {
|
||||
rows.push(splitMarkdownTableRow(lines[index]))
|
||||
index += 1
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
alignments,
|
||||
rows,
|
||||
endLine: index - 1,
|
||||
nextIndex: index
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 문자열을 렌더링용 블록 목록으로 변환
|
||||
* @param {string} markdown - 마크다운 문자열
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
| lib/markdown-callout.js | 인용 막대 색상·콜아웃 배경색 옵션, 선언 줄 파싱·직렬화 |
|
||||
| lib/brand-color.js | 사이트 브랜드 컬러 기본값·hex 검증·정규화 |
|
||||
| lib/markdown-image.js | 이미지 마크다운 직렬화·파싱, 단독 이미지 URL 판별 |
|
||||
| lib/markdown-table.js | 공개 Markdown 표 행·구분선·정렬·블록 파싱 |
|
||||
| lib/markdown-toc.js | 공개 게시글 TOC용 H1~H3 제목 추출과 앵커 ID 생성 |
|
||||
| lib/markdown-slash-commands.js | 관리자 Markdown-first 에디터 슬래시 명령 목록과 삽입 줄 정의 |
|
||||
| lib/markdown-inline.js | 공개·관리자 공통 인라인 마크다운 파서, 링크·굵게·기울임·인라인 코드·첨자·인라인 색상 토큰(`::color[텍스트]`) 변환, 링크·굵게·기울임·색상 내부 중첩 인라인 문법과 `<br>` 줄바꿈 처리, 라이브 멀티라인 본문의 기본 불릿/번호 목록 HTML 변환과 contenteditable 목록 마커 기준 커서 offset 계산·직렬화 |
|
||||
@@ -107,6 +108,7 @@
|
||||
| tests/multipart-file-upload.test.js | multipart 스트리밍·제한 단위 회귀 테스트 |
|
||||
| tests/csp-report.test.js | CSP URL 최소화·payload 형식 단위 회귀 테스트 |
|
||||
| tests/media-catalog.test.js | 미디어 URL·역할·참조·LIKE 이스케이프 단위 회귀 테스트 |
|
||||
| tests/markdown-table.test.js | Markdown 표 행·정렬·블록 파싱 단위 회귀 테스트 |
|
||||
| tests/admin-pagination.test.js | 관리자 페이지네이션 페이지 보정·ellipsis 항목 단위 테스트 |
|
||||
| tests/playwright/api/core-api.spec.js | 인증·부분 설정 저장·공개 목록·CSP·미디어 reconcile/fallback·세션 무효화 API 통합 10건 |
|
||||
| tests/playwright/e2e/admin-guard-and-settings.spec.js | 설정 카드 독립 저장과 미저장 이탈 취소 브라우저 E2E 2건 |
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.11.48
|
||||
|
||||
- 공개 Markdown 렌더러의 표 행·구분선·정렬·블록 파싱 helper를 `lib/markdown-table.js`로 분리.
|
||||
- 표 파싱 단위 테스트를 추가해 정렬과 이스케이프된 파이프 문자 처리를 회귀 확인.
|
||||
- `ContentMarkdownRenderer`는 표 렌더링 데이터 소비에 집중하도록 내부 파싱 책임을 축소.
|
||||
|
||||
## v1.11.47
|
||||
|
||||
- 관리자 미디어 화면의 대표 이미지·페이지 커버·본문 이미지 재생성 상태와 API 호출을 `useAdminMediaThumbnailRegeneration` composable로 분리.
|
||||
|
||||
112
lib/markdown-table.js
Normal file
112
lib/markdown-table.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 마크다운 표 행 후보인지 확인한다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {boolean} 표 행 후보 여부
|
||||
*/
|
||||
export const isMarkdownTableRow = (line) => {
|
||||
const value = String(line || '').trim()
|
||||
return value.startsWith('|') && value.endsWith('|') && value.split('|').length >= 4
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 행을 셀 배열로 나눈다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {string[]} 셀 목록
|
||||
*/
|
||||
export const splitMarkdownTableRow = (line) => {
|
||||
const value = String(line || '').trim().replace(/^\|/, '').replace(/\|$/, '')
|
||||
const cells = []
|
||||
let cell = ''
|
||||
let escaped = false
|
||||
|
||||
for (const character of value) {
|
||||
if (escaped) {
|
||||
cell += character === '|' ? '|' : `\\${character}`
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (character === '\\') {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (character === '|') {
|
||||
cells.push(cell.trim())
|
||||
cell = ''
|
||||
continue
|
||||
}
|
||||
|
||||
cell += character
|
||||
}
|
||||
|
||||
if (escaped) {
|
||||
cell += '\\'
|
||||
}
|
||||
|
||||
cells.push(cell.trim())
|
||||
|
||||
return cells
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 구분선 행인지 확인한다.
|
||||
* @param {string} line - 원본 줄
|
||||
* @returns {boolean} 구분선 여부
|
||||
*/
|
||||
export const isMarkdownTableSeparator = (line) => {
|
||||
if (!isMarkdownTableRow(line)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return splitMarkdownTableRow(line).every((cell) => /^:?-{3,}:?$/.test(cell.trim()))
|
||||
}
|
||||
|
||||
/**
|
||||
* 표 정렬 행에서 셀 정렬 값을 파싱한다.
|
||||
* @param {string} cell - 구분선 셀
|
||||
* @returns {'left'|'center'|'right'} 정렬 값
|
||||
*/
|
||||
export const parseMarkdownTableAlignment = (cell) => {
|
||||
const value = String(cell || '').trim()
|
||||
|
||||
if (value.startsWith(':') && value.endsWith(':')) {
|
||||
return 'center'
|
||||
}
|
||||
|
||||
if (value.endsWith(':')) {
|
||||
return 'right'
|
||||
}
|
||||
|
||||
return 'left'
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 표 블록을 파싱한다.
|
||||
* @param {string[]} lines - 전체 마크다운 줄
|
||||
* @param {number} startIndex - 표 시작 후보 줄
|
||||
* @returns {{headers: string[], alignments: string[], rows: string[][], endLine: number, nextIndex: number}|null} 표 메타
|
||||
*/
|
||||
export const parseMarkdownTableBlock = (lines, startIndex) => {
|
||||
if (!isMarkdownTableRow(lines[startIndex]) || !isMarkdownTableSeparator(lines[startIndex + 1])) {
|
||||
return null
|
||||
}
|
||||
|
||||
const headers = splitMarkdownTableRow(lines[startIndex])
|
||||
const alignments = splitMarkdownTableRow(lines[startIndex + 1]).map(parseMarkdownTableAlignment)
|
||||
const rows = []
|
||||
let index = startIndex + 2
|
||||
|
||||
while (index < lines.length && isMarkdownTableRow(lines[index]) && !isMarkdownTableSeparator(lines[index])) {
|
||||
rows.push(splitMarkdownTableRow(lines[index]))
|
||||
index += 1
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
alignments,
|
||||
rows,
|
||||
endLine: index - 1,
|
||||
nextIndex: index
|
||||
}
|
||||
}
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.47",
|
||||
"version": "1.11.48",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.47",
|
||||
"version": "1.11.48",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.11.47",
|
||||
"version": "1.11.48",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
50
tests/markdown-table.test.js
Normal file
50
tests/markdown-table.test.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
isMarkdownTableRow,
|
||||
isMarkdownTableSeparator,
|
||||
parseMarkdownTableAlignment,
|
||||
parseMarkdownTableBlock,
|
||||
splitMarkdownTableRow
|
||||
} from '../lib/markdown-table.js'
|
||||
|
||||
test('마크다운 표 행과 구분선을 판별한다', () => {
|
||||
assert.equal(isMarkdownTableRow('| 제목 | 값 |'), true)
|
||||
assert.equal(isMarkdownTableRow('제목 | 값'), false)
|
||||
assert.equal(isMarkdownTableSeparator('| --- | :---: | ---: |'), true)
|
||||
assert.equal(isMarkdownTableSeparator('| 제목 | 값 |'), false)
|
||||
})
|
||||
|
||||
test('마크다운 표 셀과 정렬 값을 파싱한다', () => {
|
||||
assert.deepEqual(splitMarkdownTableRow('| A | B\\|C |'), ['A', 'B|C'])
|
||||
assert.equal(parseMarkdownTableAlignment('---'), 'left')
|
||||
assert.equal(parseMarkdownTableAlignment(':---:'), 'center')
|
||||
assert.equal(parseMarkdownTableAlignment('---:'), 'right')
|
||||
})
|
||||
|
||||
test('마크다운 표 블록을 메타 정보로 변환한다', () => {
|
||||
assert.deepEqual(parseMarkdownTableBlock([
|
||||
'| 이름 | 수량 | 비고 |',
|
||||
'| :--- | ---: | :---: |',
|
||||
'| 사과 | 2 | 빨강 |',
|
||||
'| 배 | 10 | 노랑 |',
|
||||
'',
|
||||
'다음 문단'
|
||||
], 0), {
|
||||
headers: ['이름', '수량', '비고'],
|
||||
alignments: ['left', 'right', 'center'],
|
||||
rows: [
|
||||
['사과', '2', '빨강'],
|
||||
['배', '10', '노랑']
|
||||
],
|
||||
endLine: 3,
|
||||
nextIndex: 4
|
||||
})
|
||||
})
|
||||
|
||||
test('구분선이 없으면 표 블록으로 처리하지 않는다', () => {
|
||||
assert.equal(parseMarkdownTableBlock([
|
||||
'| 이름 | 수량 |',
|
||||
'| 사과 | 2 |'
|
||||
], 0), null)
|
||||
})
|
||||
Reference in New Issue
Block a user