Files
sori.studio/lib/markdown-fenced-meta.js

231 lines
5.9 KiB
JavaScript

/**
* fenced 블록 시작 줄 인덱스를 찾는다.
* @param {string[]} lines - 본문 줄 목록
* @param {number} currentLine - 현재 줄
* @param {string} opener - 시작 토큰
* @returns {number} 시작 줄 또는 -1
*/
export const findFencedBlockStart = (lines, currentLine, opener) => {
for (let index = currentLine; index >= 0; index -= 1) {
if ((lines[index] || '').trim() === opener) {
return index
}
if ((lines[index] || '').trim() === ':::') {
break
}
}
return -1
}
/**
* 조건에 맞는 fenced 블록 시작 줄 인덱스를 찾는다.
* @param {string[]} lines - 본문 줄 목록
* @param {number} currentLine - 현재 줄
* @param {(line: string) => boolean} predicate - 시작 줄 판별 함수
* @returns {number} 시작 줄 또는 -1
*/
export const findFencedBlockStartBy = (lines, currentLine, predicate) => {
for (let index = currentLine; index >= 0; index -= 1) {
if (predicate((lines[index] || '').trim())) {
return index
}
if ((lines[index] || '').trim() === ':::') {
break
}
}
return -1
}
/**
* fenced 블록 종료 줄 인덱스를 찾는다.
* @param {string[]} lines - 본문 줄 목록
* @param {number} startLine - 시작 줄
* @returns {number} 종료 줄 또는 -1
*/
export const findFencedBlockEnd = (lines, startLine) => {
for (let index = startLine + 1; index < lines.length; index += 1) {
if ((lines[index] || '').trim() === ':::') {
return index
}
}
return -1
}
/**
* 닫힘 표식까지의 행 목록을 반환한다.
* @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
}