마크다운 fenced 메타 파싱 분리
This commit is contained in:
@@ -35,6 +35,12 @@ import {
|
|||||||
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 { parseMarkdownTableBlock } from '../../lib/markdown-table.js'
|
||||||
|
import {
|
||||||
|
collectFencedLines,
|
||||||
|
parseBookmarkMeta,
|
||||||
|
parseMediaMeta,
|
||||||
|
parseSignupMeta
|
||||||
|
} from '../../lib/markdown-fenced-meta.js'
|
||||||
import {
|
import {
|
||||||
buildCalloutOpenerLine,
|
buildCalloutOpenerLine,
|
||||||
CALLOUT_BACKGROUND_OPTIONS,
|
CALLOUT_BACKGROUND_OPTIONS,
|
||||||
@@ -303,179 +309,6 @@ const isStandaloneImageUrlLine = (line) => isStandaloneUrlLine(line) && isImageU
|
|||||||
*/
|
*/
|
||||||
const getSpacerHeightClass = (block) => block.meta?.legacy ? 'h-6' : 'h-8'
|
const getSpacerHeightClass = (block) => block.meta?.legacy ? 'h-6' : 'h-8'
|
||||||
|
|
||||||
/**
|
|
||||||
* 닫힘 표식까지의 행 목록을 반환
|
|
||||||
* @param {Array<string>} lines - 전체 마크다운 행
|
|
||||||
* @param {number} startIndex - 본문 시작 인덱스
|
|
||||||
* @returns {{contentLines: Array<string>, endLine: number, nextIndex: number}} 블록 본문과 다음 인덱스
|
|
||||||
*/
|
|
||||||
const collectFencedLines = (lines, startIndex) => {
|
|
||||||
const contentLines = []
|
|
||||||
let index = startIndex
|
|
||||||
|
|
||||||
while (index < lines.length && lines[index].trim() !== ':::') {
|
|
||||||
contentLines.push(lines[index])
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
contentLines,
|
|
||||||
endLine: index < lines.length ? index : Math.max(startIndex - 1, 0),
|
|
||||||
nextIndex: index < lines.length ? index + 1 : lines.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 북마크 fenced 블록 본문에서 URL·제목·설명·썸네일을 파싱한다.
|
|
||||||
* @param {string} raw - fenced 내부 텍스트
|
|
||||||
* @returns {{url: string, title: string, description: string, thumbnail: string}} 북마크 메타
|
|
||||||
*/
|
|
||||||
const parseBookmarkMeta = (raw) => {
|
|
||||||
const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
||||||
const meta = {
|
|
||||||
url: '',
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
thumbnail: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const kv = line.match(/^(\w+)=(.*)$/)
|
|
||||||
|
|
||||||
if (kv) {
|
|
||||||
const key = kv[1].toLowerCase()
|
|
||||||
const val = kv[2].trim()
|
|
||||||
|
|
||||||
if (key === 'url') {
|
|
||||||
meta.url = val
|
|
||||||
} else if (key === 'title') {
|
|
||||||
meta.title = val
|
|
||||||
} else if (key === 'description' || key === 'desc') {
|
|
||||||
meta.description = val
|
|
||||||
} else if (key === 'thumbnail' || key === 'image') {
|
|
||||||
meta.thumbnail = val
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!meta.url && /^https?:\/\//i.test(line)) {
|
|
||||||
meta.url = line
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta.url && !meta.title) {
|
|
||||||
meta.title = line
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta.url && meta.title && !meta.description) {
|
|
||||||
meta.description = line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return meta
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 회원가입(CTA) fenced 블록 본문에서 표시 문구를 파싱한다.
|
|
||||||
* @param {string} raw - fenced 내부 텍스트
|
|
||||||
* @returns {{title: string, description: string, button: string, placeholder: string}} CTA 메타
|
|
||||||
*/
|
|
||||||
const parseSignupMeta = (raw) => {
|
|
||||||
const meta = {
|
|
||||||
title: '뉴스레터에 가입하세요',
|
|
||||||
description: '새 글이 올라오면 받아보실 수 있어요.',
|
|
||||||
button: '구독하기',
|
|
||||||
placeholder: 'you@example.com'
|
|
||||||
}
|
|
||||||
const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const kv = line.match(/^(\w+)=(.*)$/)
|
|
||||||
|
|
||||||
if (!kv) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = kv[1].toLowerCase()
|
|
||||||
const val = kv[2].trim()
|
|
||||||
|
|
||||||
if (key === 'title') {
|
|
||||||
meta.title = val
|
|
||||||
} else if (key === 'description' || key === 'desc') {
|
|
||||||
meta.description = val
|
|
||||||
} else if (key === 'button') {
|
|
||||||
meta.button = val
|
|
||||||
} else if (key === 'placeholder') {
|
|
||||||
meta.placeholder = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return meta
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 미디어 fenced 블록 본문에서 URL과 표시 메타를 파싱한다.
|
|
||||||
* @param {string} raw - fenced 내부 텍스트
|
|
||||||
* @returns {{url: string, title: string, description: string, poster: string, caption: string, fileName: string, size: string}} 미디어 메타
|
|
||||||
*/
|
|
||||||
const parseMediaMeta = (raw) => {
|
|
||||||
const meta = {
|
|
||||||
url: '',
|
|
||||||
title: '',
|
|
||||||
description: '',
|
|
||||||
poster: '',
|
|
||||||
caption: '',
|
|
||||||
fileName: '',
|
|
||||||
size: ''
|
|
||||||
}
|
|
||||||
const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const kv = line.match(/^(\w+)=(.*)$/)
|
|
||||||
|
|
||||||
if (kv) {
|
|
||||||
const key = kv[1].toLowerCase()
|
|
||||||
const val = kv[2].trim()
|
|
||||||
|
|
||||||
if (key === 'url' || key === 'src') {
|
|
||||||
meta.url = val
|
|
||||||
} else if (key === 'title') {
|
|
||||||
meta.title = val
|
|
||||||
} else if (key === 'description' || key === 'desc') {
|
|
||||||
meta.description = val
|
|
||||||
} else if (key === 'poster' || key === 'thumbnail') {
|
|
||||||
meta.poster = val
|
|
||||||
} else if (key === 'caption') {
|
|
||||||
meta.caption = val
|
|
||||||
} else if (key === 'name' || key === 'filename' || key === 'file') {
|
|
||||||
meta.fileName = val
|
|
||||||
} else if (key === 'size') {
|
|
||||||
meta.size = val
|
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!meta.url && (/^https?:\/\//i.test(line) || line.startsWith('/'))) {
|
|
||||||
meta.url = line
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta.url && !meta.title) {
|
|
||||||
meta.title = line
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (meta.url && meta.title && !meta.description) {
|
|
||||||
meta.description = line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return meta
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 마크다운 문자열을 렌더링용 블록 목록으로 변환
|
* 마크다운 문자열을 렌더링용 블록 목록으로 변환
|
||||||
* @param {string} markdown - 마크다운 문자열
|
* @param {string} markdown - 마크다운 문자열
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출 |
|
| lib/markdown-iframe-embed.js | 게시물 본문 iframe 블록의 코드 판별과 안전 속성 추출 |
|
||||||
| lib/markdown-html-block.js | 게시물 본문 HTML 블록의 parser 기반 허용 태그·속성 sanitizer, 실행 URL·인라인 style 제거와 새 창 링크 rel 보정 |
|
| lib/markdown-html-block.js | 게시물 본문 HTML 블록의 parser 기반 허용 태그·속성 sanitizer, 실행 URL·인라인 style 제거와 새 창 링크 rel 보정 |
|
||||||
| lib/markdown-callout.js | 인용 막대 색상·콜아웃 배경색 옵션, 선언 줄 파싱·직렬화 |
|
| lib/markdown-callout.js | 인용 막대 색상·콜아웃 배경색 옵션, 선언 줄 파싱·직렬화 |
|
||||||
|
| lib/markdown-fenced-meta.js | 공개 Markdown fenced 블록 본문 수집과 북마크·회원가입·미디어 메타 파싱 |
|
||||||
| 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-table.js | 공개 Markdown 표 행·구분선·정렬·블록 파싱 |
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
| playwright.config.js | Playwright API/E2E 프로젝트, 단일 worker, 전용 Nuxt 서버·trace 설정 |
|
| playwright.config.js | Playwright API/E2E 프로젝트, 단일 worker, 전용 Nuxt 서버·trace 설정 |
|
||||||
| docker-compose.test.yml | 실행마다 생성·폐기하는 PostgreSQL 16 테스트 DB·볼륨·네트워크 |
|
| docker-compose.test.yml | 실행마다 생성·폐기하는 PostgreSQL 16 테스트 DB·볼륨·네트워크 |
|
||||||
| tests/markdown-html-block.test.js | HTML sanitizer 단위 회귀 테스트 |
|
| tests/markdown-html-block.test.js | HTML sanitizer 단위 회귀 테스트 |
|
||||||
|
| tests/markdown-fenced-meta.test.js | Markdown fenced 블록 본문 수집과 메타 파싱 단위 회귀 테스트 |
|
||||||
| 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 이스케이프 단위 회귀 테스트 |
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v1.11.49
|
||||||
|
|
||||||
|
- 공개 Markdown 렌더러의 fenced 블록 본문 수집과 북마크·회원가입·미디어 메타 파싱을 `lib/markdown-fenced-meta.js`로 분리.
|
||||||
|
- fenced 블록 닫힘 표식 처리와 메타 필드 파싱 단위 테스트를 추가.
|
||||||
|
- `ContentMarkdownRenderer`는 fenced 블록 파싱 결과를 블록 데이터로 연결하는 책임만 남기도록 정리.
|
||||||
|
|
||||||
## v1.11.48
|
## v1.11.48
|
||||||
|
|
||||||
- 공개 Markdown 렌더러의 표 행·구분선·정렬·블록 파싱 helper를 `lib/markdown-table.js`로 분리.
|
- 공개 Markdown 렌더러의 표 행·구분선·정렬·블록 파싱 helper를 `lib/markdown-table.js`로 분리.
|
||||||
|
|||||||
172
lib/markdown-fenced-meta.js
Normal file
172
lib/markdown-fenced-meta.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* 닫힘 표식까지의 행 목록을 반환한다.
|
||||||
|
* @param {Array<string>} lines - 전체 마크다운 행
|
||||||
|
* @param {number} startIndex - 본문 시작 인덱스
|
||||||
|
* @returns {{contentLines: Array<string>, endLine: number, nextIndex: number}} 블록 본문과 다음 인덱스
|
||||||
|
*/
|
||||||
|
export const collectFencedLines = (lines, startIndex) => {
|
||||||
|
const contentLines = []
|
||||||
|
let index = startIndex
|
||||||
|
|
||||||
|
while (index < lines.length && lines[index].trim() !== ':::') {
|
||||||
|
contentLines.push(lines[index])
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentLines,
|
||||||
|
endLine: index < lines.length ? index : Math.max(lines.length - 1, startIndex - 1, 0),
|
||||||
|
nextIndex: index < lines.length ? index + 1 : lines.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 북마크 fenced 블록 본문에서 URL·제목·설명·썸네일을 파싱한다.
|
||||||
|
* @param {string} raw - fenced 내부 텍스트
|
||||||
|
* @returns {{url: string, title: string, description: string, thumbnail: string}} 북마크 메타
|
||||||
|
*/
|
||||||
|
export const parseBookmarkMeta = (raw) => {
|
||||||
|
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean)
|
||||||
|
const meta = {
|
||||||
|
url: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
thumbnail: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const kv = line.match(/^(\w+)=(.*)$/)
|
||||||
|
|
||||||
|
if (kv) {
|
||||||
|
const key = kv[1].toLowerCase()
|
||||||
|
const value = kv[2].trim()
|
||||||
|
|
||||||
|
if (key === 'url') {
|
||||||
|
meta.url = value
|
||||||
|
} else if (key === 'title') {
|
||||||
|
meta.title = value
|
||||||
|
} else if (key === 'description' || key === 'desc') {
|
||||||
|
meta.description = value
|
||||||
|
} else if (key === 'thumbnail' || key === 'image') {
|
||||||
|
meta.thumbnail = value
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!meta.url && /^https?:\/\//i.test(line)) {
|
||||||
|
meta.url = line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.url && !meta.title) {
|
||||||
|
meta.title = line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.url && meta.title && !meta.description) {
|
||||||
|
meta.description = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 회원가입(CTA) fenced 블록 본문에서 표시 문구를 파싱한다.
|
||||||
|
* @param {string} raw - fenced 내부 텍스트
|
||||||
|
* @returns {{title: string, description: string, button: string, placeholder: string}} CTA 메타
|
||||||
|
*/
|
||||||
|
export const parseSignupMeta = (raw) => {
|
||||||
|
const meta = {
|
||||||
|
title: '뉴스레터에 가입하세요',
|
||||||
|
description: '새 글이 올라오면 받아보실 수 있어요.',
|
||||||
|
button: '구독하기',
|
||||||
|
placeholder: 'you@example.com'
|
||||||
|
}
|
||||||
|
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean)
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const kv = line.match(/^(\w+)=(.*)$/)
|
||||||
|
|
||||||
|
if (!kv) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = kv[1].toLowerCase()
|
||||||
|
const value = kv[2].trim()
|
||||||
|
|
||||||
|
if (key === 'title') {
|
||||||
|
meta.title = value
|
||||||
|
} else if (key === 'description' || key === 'desc') {
|
||||||
|
meta.description = value
|
||||||
|
} else if (key === 'button') {
|
||||||
|
meta.button = value
|
||||||
|
} else if (key === 'placeholder') {
|
||||||
|
meta.placeholder = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 미디어 fenced 블록 본문에서 URL과 표시 메타를 파싱한다.
|
||||||
|
* @param {string} raw - fenced 내부 텍스트
|
||||||
|
* @returns {{url: string, title: string, description: string, poster: string, caption: string, fileName: string, size: string}} 미디어 메타
|
||||||
|
*/
|
||||||
|
export const parseMediaMeta = (raw) => {
|
||||||
|
const meta = {
|
||||||
|
url: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
poster: '',
|
||||||
|
caption: '',
|
||||||
|
fileName: '',
|
||||||
|
size: ''
|
||||||
|
}
|
||||||
|
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean)
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const kv = line.match(/^(\w+)=(.*)$/)
|
||||||
|
|
||||||
|
if (kv) {
|
||||||
|
const key = kv[1].toLowerCase()
|
||||||
|
const value = kv[2].trim()
|
||||||
|
|
||||||
|
if (key === 'url' || key === 'src') {
|
||||||
|
meta.url = value
|
||||||
|
} else if (key === 'title') {
|
||||||
|
meta.title = value
|
||||||
|
} else if (key === 'description' || key === 'desc') {
|
||||||
|
meta.description = value
|
||||||
|
} else if (key === 'poster' || key === 'thumbnail') {
|
||||||
|
meta.poster = value
|
||||||
|
} else if (key === 'caption') {
|
||||||
|
meta.caption = value
|
||||||
|
} else if (key === 'name' || key === 'filename' || key === 'file') {
|
||||||
|
meta.fileName = value
|
||||||
|
} else if (key === 'size') {
|
||||||
|
meta.size = value
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!meta.url && (/^https?:\/\//i.test(line) || line.startsWith('/'))) {
|
||||||
|
meta.url = line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.url && !meta.title) {
|
||||||
|
meta.title = line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (meta.url && meta.title && !meta.description) {
|
||||||
|
meta.description = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta
|
||||||
|
}
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.48",
|
"version": "1.11.49",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.48",
|
"version": "1.11.49",
|
||||||
"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.48",
|
"version": "1.11.49",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
100
tests/markdown-fenced-meta.test.js
Normal file
100
tests/markdown-fenced-meta.test.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
import {
|
||||||
|
collectFencedLines,
|
||||||
|
parseBookmarkMeta,
|
||||||
|
parseMediaMeta,
|
||||||
|
parseSignupMeta
|
||||||
|
} from '../lib/markdown-fenced-meta.js'
|
||||||
|
|
||||||
|
test('fenced 블록 닫힘 표식까지의 본문과 다음 줄을 수집한다', () => {
|
||||||
|
assert.deepEqual(collectFencedLines([
|
||||||
|
':::bookmark',
|
||||||
|
'https://example.com',
|
||||||
|
'Example',
|
||||||
|
':::',
|
||||||
|
'다음 문단'
|
||||||
|
], 1), {
|
||||||
|
contentLines: ['https://example.com', 'Example'],
|
||||||
|
endLine: 3,
|
||||||
|
nextIndex: 4
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('닫힘 표식이 없는 fenced 블록도 마지막 본문 범위를 반환한다', () => {
|
||||||
|
assert.deepEqual(collectFencedLines([
|
||||||
|
':::video',
|
||||||
|
'/uploads/video.mp4'
|
||||||
|
], 1), {
|
||||||
|
contentLines: ['/uploads/video.mp4'],
|
||||||
|
endLine: 1,
|
||||||
|
nextIndex: 2
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('북마크 메타는 key=value와 순차 본문을 모두 파싱한다', () => {
|
||||||
|
assert.deepEqual(parseBookmarkMeta([
|
||||||
|
'https://example.com/post',
|
||||||
|
'예시 제목',
|
||||||
|
'예시 설명'
|
||||||
|
].join('\n')), {
|
||||||
|
url: 'https://example.com/post',
|
||||||
|
title: '예시 제목',
|
||||||
|
description: '예시 설명',
|
||||||
|
thumbnail: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(parseBookmarkMeta([
|
||||||
|
'url=https://example.com',
|
||||||
|
'title=제목',
|
||||||
|
'desc=설명',
|
||||||
|
'image=/thumb.webp'
|
||||||
|
].join('\n')), {
|
||||||
|
url: 'https://example.com',
|
||||||
|
title: '제목',
|
||||||
|
description: '설명',
|
||||||
|
thumbnail: '/thumb.webp'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('회원가입 메타는 기본값을 유지하며 전달된 필드만 바꾼다', () => {
|
||||||
|
assert.deepEqual(parseSignupMeta('title=구독\nbutton=시작하기'), {
|
||||||
|
title: '구독',
|
||||||
|
description: '새 글이 올라오면 받아보실 수 있어요.',
|
||||||
|
button: '시작하기',
|
||||||
|
placeholder: 'you@example.com'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('미디어 메타는 key=value와 순차 본문을 모두 파싱한다', () => {
|
||||||
|
assert.deepEqual(parseMediaMeta([
|
||||||
|
'/uploads/video.mp4',
|
||||||
|
'영상 제목',
|
||||||
|
'영상 설명'
|
||||||
|
].join('\n')), {
|
||||||
|
url: '/uploads/video.mp4',
|
||||||
|
title: '영상 제목',
|
||||||
|
description: '영상 설명',
|
||||||
|
poster: '',
|
||||||
|
caption: '',
|
||||||
|
fileName: '',
|
||||||
|
size: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(parseMediaMeta([
|
||||||
|
'src=/uploads/audio.mp3',
|
||||||
|
'title=오디오',
|
||||||
|
'thumbnail=/poster.webp',
|
||||||
|
'caption=캡션',
|
||||||
|
'filename=sound.mp3',
|
||||||
|
'size=3MB'
|
||||||
|
].join('\n')), {
|
||||||
|
url: '/uploads/audio.mp3',
|
||||||
|
title: '오디오',
|
||||||
|
description: '',
|
||||||
|
poster: '/poster.webp',
|
||||||
|
caption: '캡션',
|
||||||
|
fileName: 'sound.mp3',
|
||||||
|
size: '3MB'
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user