Files
blogflare/src/lib/hugoParser.ts
T

145 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
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[];
series: string[];
series_weight: number | null;
}
/**
* 解析并转换时间,一旦解析失败或空缺,严格使用当前系统时间
*/
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 (err: any) {
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 parseIntegerField(val: unknown): number | null {
if (val === null || val === undefined || val === '') return null;
const parsed = typeof val === 'number' ? val : Number(String(val).trim());
return Number.isInteger(parsed) ? parsed : null;
}
/**
* 去掉文件名结尾的格式拓展名,提供纯文件名基础
*/
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, any>;
}
}
// 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, any>;
}
}
} catch (err: any) {
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),
series: parseArrayField(meta.series),
series_weight: parseIntegerField(meta.series_weight)
};
}