feat: 新增 Hugo Markdown 头部元数据解析器

This commit is contained in:
seahi committed 2026-08-05 16:16:24 +08:00
1 parent c8a95499f7
commit 60caa08228
1 file changed
+125
+125
View File
@@ -0,0 +1,125 @@
import { parse as parseToml } from 'smol-toml';
import { parse as parseYaml } from 'yaml';
export interface HugoPostData {
title: string;
slug: string;
content: string;
cover: string;
published_at: string;
updated_at: string;
categories: string[];
tags: string[];
}
/**
* 解析并转换时间,一旦解析失败或空缺,严格使用当前系统时间
*/
function parseOrCurrentDate(val: unknown): string {
if (!val) return new Date().toISOString();
try {
const dateObj = new Date(val as string | number | Date);
if (isNaN(dateObj.getTime())) {
return new Date().toISOString();
}
return dateObj.toISOString();
} catch {
return new Date().toISOString();
}
}
/**
* 清洗分类和标签列表(同时支持数组与单字符格式)
*/
function parseArrayField(val: unknown): string[] {
if (Array.isArray(val)) {
return val.map((item) => String(item).trim()).filter(Boolean);
}
if (typeof val === 'string' && val.trim() !== '') {
return [val.trim()];
}
return [];
}
/**
* 去掉文件名结尾的格式拓展名,提供纯文件名基础
*/
function stripExtension(filename: string): string {
return filename.replace(/\.(md|markdown|txt)$/i, '').trim();
}
/**
* Hugo Markdown 头部元数据及正文解析器
* 同时原生兼容 TOML (+++) 与 YAML (---) 语法,自动过滤并丢弃本项目无端对应参数
*/
export function parseHugoMarkdown(rawContent: string, fileName = ''): HugoPostData {
// 剔除可能存在的 UTF-8 BOM
const text = rawContent.replace(/^\uFEFF/, '').trim();
const defaultName = fileName ? stripExtension(fileName) : '新文章';
let meta: Record<string, unknown> = {};
let bodyContent = text;
try {
// 1. 优先检识 TOML (以 +++ 开头包络)
if (text.startsWith('+++')) {
const endIndex = text.indexOf('\n+++', 3);
if (endIndex !== -1) {
const frontmatter = text.substring(3, endIndex).trim();
bodyContent = text.substring(endIndex + 4).trim();
meta = parseToml(frontmatter) as Record<string, unknown>;
}
}
// 2. 检识常规 YAML (以 --- 开头包络)
else if (text.startsWith('---')) {
const endIndex = text.indexOf('\n---', 3);
if (endIndex !== -1) {
const frontmatter = text.substring(3, endIndex).trim();
bodyContent = text.substring(endIndex + 4).trim();
meta = parseYaml(frontmatter) as Record<string, unknown>;
}
}
} catch (err) {
console.error('解析 Markdown 头部参数时出现异常,直接回货底稿正文并对时差退回初始系统时刻:', err);
// 当解析受卡出错,空字典依然不阻挠对下方日期与稿费正文字面的顺利捕拾
}
// 取值并完成映射:除以下对应参数处,诸如 draft/twemoji/lightgallery 等全在空载转储里随水蒸丢!
const title = typeof meta.title === 'string' && meta.title.trim() !== '' ? meta.title.trim() : defaultName;
let slug = typeof meta.slug === 'string' ? meta.slug.trim() : '';
if (!slug) {
// 没有声明明确的 slug 时,由名目精简转化出基础链接字符串
slug = defaultName
.toLowerCase()
.replace(/[^\w\u4e00-\u9fa5\s-]/g, '')
.replace(/\s+/g, '-');
if (!slug) {
slug = 'post-' + Date.now();
}
}
// 特色图片字段识别(首挑 featuredImage,也兼备处理 cover/image)
const cover = typeof meta.featuredImage === 'string'
? meta.featuredImage.trim()
: typeof meta.cover === 'string'
? meta.cover.trim()
: typeof meta.image === 'string'
? meta.image.trim()
: '';
// 时间字段转化:严格按需求约定“一旦转换错漏或是全无,采用目前时刻填满”
const published_at = parseOrCurrentDate(meta.date || meta.published_at);
const updated_at = parseOrCurrentDate(meta.lastmod || meta.updated_at || meta.date || meta.published_at);
return {
title: title || '无名小贴',
slug,
content: bodyContent,
cover,
published_at,
updated_at,
categories: parseArrayField(meta.categories || meta.category),
tags: parseArrayField(meta.tags || meta.tag)
};
}