마크다운 표 파싱 로직 분리
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
|||||||
} from '../../lib/markdown-live-edit.js'
|
} from '../../lib/markdown-live-edit.js'
|
||||||
import { buildCodeBlockLines, buildCodeFenceOpener, parseCodeFenceLine } from '../../lib/markdown-code-block.js'
|
import { buildCodeBlockLines, buildCodeFenceOpener, parseCodeFenceLine } from '../../lib/markdown-code-block.js'
|
||||||
import { buildToggleBlockLines, parseToggleOpenerLine } from '../../lib/markdown-toggle.js'
|
import { buildToggleBlockLines, parseToggleOpenerLine } from '../../lib/markdown-toggle.js'
|
||||||
|
import { parseMarkdownTableBlock } from '../../lib/markdown-table.js'
|
||||||
import {
|
import {
|
||||||
buildCalloutOpenerLine,
|
buildCalloutOpenerLine,
|
||||||
CALLOUT_BACKGROUND_OPTIONS,
|
CALLOUT_BACKGROUND_OPTIONS,
|
||||||
@@ -475,90 +476,6 @@ const parseMediaMeta = (raw) => {
|
|||||||
return meta
|
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 - 마크다운 문자열
|
* @param {string} markdown - 마크다운 문자열
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
| lib/markdown-callout.js | 인용 막대 색상·콜아웃 배경색 옵션, 선언 줄 파싱·직렬화 |
|
| lib/markdown-callout.js | 인용 막대 색상·콜아웃 배경색 옵션, 선언 줄 파싱·직렬화 |
|
||||||
| lib/brand-color.js | 사이트 브랜드 컬러 기본값·hex 검증·정규화 |
|
| lib/brand-color.js | 사이트 브랜드 컬러 기본값·hex 검증·정규화 |
|
||||||
| lib/markdown-image.js | 이미지 마크다운 직렬화·파싱, 단독 이미지 URL 판별 |
|
| lib/markdown-image.js | 이미지 마크다운 직렬화·파싱, 단독 이미지 URL 판별 |
|
||||||
|
| 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 에디터 슬래시 명령 목록과 삽입 줄 정의 |
|
||||||
| lib/markdown-inline.js | 공개·관리자 공통 인라인 마크다운 파서, 링크·굵게·기울임·인라인 코드·첨자·인라인 색상 토큰(`::color[텍스트]`) 변환, 링크·굵게·기울임·색상 내부 중첩 인라인 문법과 `<br>` 줄바꿈 처리, 라이브 멀티라인 본문의 기본 불릿/번호 목록 HTML 변환과 contenteditable 목록 마커 기준 커서 offset 계산·직렬화 |
|
| lib/markdown-inline.js | 공개·관리자 공통 인라인 마크다운 파서, 링크·굵게·기울임·인라인 코드·첨자·인라인 색상 토큰(`::color[텍스트]`) 변환, 링크·굵게·기울임·색상 내부 중첩 인라인 문법과 `<br>` 줄바꿈 처리, 라이브 멀티라인 본문의 기본 불릿/번호 목록 HTML 변환과 contenteditable 목록 마커 기준 커서 offset 계산·직렬화 |
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
| tests/multipart-file-upload.test.js | multipart 스트리밍·제한 단위 회귀 테스트 |
|
| tests/multipart-file-upload.test.js | multipart 스트리밍·제한 단위 회귀 테스트 |
|
||||||
| tests/csp-report.test.js | CSP URL 최소화·payload 형식 단위 회귀 테스트 |
|
| tests/csp-report.test.js | CSP URL 최소화·payload 형식 단위 회귀 테스트 |
|
||||||
| tests/media-catalog.test.js | 미디어 URL·역할·참조·LIKE 이스케이프 단위 회귀 테스트 |
|
| tests/media-catalog.test.js | 미디어 URL·역할·참조·LIKE 이스케이프 단위 회귀 테스트 |
|
||||||
|
| tests/markdown-table.test.js | Markdown 표 행·정렬·블록 파싱 단위 회귀 테스트 |
|
||||||
| tests/admin-pagination.test.js | 관리자 페이지네이션 페이지 보정·ellipsis 항목 단위 테스트 |
|
| tests/admin-pagination.test.js | 관리자 페이지네이션 페이지 보정·ellipsis 항목 단위 테스트 |
|
||||||
| tests/playwright/api/core-api.spec.js | 인증·부분 설정 저장·공개 목록·CSP·미디어 reconcile/fallback·세션 무효화 API 통합 10건 |
|
| tests/playwright/api/core-api.spec.js | 인증·부분 설정 저장·공개 목록·CSP·미디어 reconcile/fallback·세션 무효화 API 통합 10건 |
|
||||||
| tests/playwright/e2e/admin-guard-and-settings.spec.js | 설정 카드 독립 저장과 미저장 이탈 취소 브라우저 E2E 2건 |
|
| 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
|
## v1.11.47
|
||||||
|
|
||||||
- 관리자 미디어 화면의 대표 이미지·페이지 커버·본문 이미지 재생성 상태와 API 호출을 `useAdminMediaThumbnailRegeneration` composable로 분리.
|
- 관리자 미디어 화면의 대표 이미지·페이지 커버·본문 이미지 재생성 상태와 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",
|
"name": "sori.studio",
|
||||||
"version": "1.11.47",
|
"version": "1.11.48",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.47",
|
"version": "1.11.48",
|
||||||
"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.47",
|
"version": "1.11.48",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"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