Files
blog/editor-api/test/frontmatter-roundtrip.mjs
T
zqlit c217d20c30 feat(editor): 在线编辑文章(Worker 前端 + 轻量 docker 后端)
后端(新增 editor-api/,零 npm 依赖,只用 node 内置模块):
- 只做文章相关:列表/读取/新建/保存/删除/图片上传/git 状态·发布·同步
- front matter 往返保真:未改动的块按字节照抄,CRLF/块标量/引号写法都不动
- 列表用目录名当 id,slug 撞名不再静默改错文件(返回 409 列候选)
- 鉴权只有一条路:X-Editor-Token(只存在 Worker 侧,浏览器拿不到)
- 端口只绑 127.0.0.1,由宿主机 nginx 反代出去

部署(新增 deploy/editor-api/bootstrap.sh):
- 一条命令在新机器上完成 克隆仓库→写 .env→起容器→健康检查
- 状态全在两个目录(/srv/blog 仓库工作区 + /srv/editor-api 配置),
  迁移 = 复制目录或在新机重跑本脚本,容器本身无状态

Cloudflare Worker 侧(blog-admin):
- src/routes/editor.ts:鉴权 + 反代,浏览器只跟 Worker 说话
- 管理面板新增「文章编辑」:两栏布局 + 快捷插入面板(13 项短代码,
  与 write-server 的 ShortcutPanel 一致)+ 底部 草稿/保存/发布
- 系统设置改为 schema 驱动的表单,且**以运行时实际生效的配置为准**
  (frontend_conf/captcha/moderator/ip_region/site_default + KV human_check),
  修复「表单显示一套、评论系统跑另一套」的脱节问题

验证:tsc 0 错;front matter 往返 130/130;保存往返 130/130;
API e2e 44/44;无头 Chrome UI e2e 14/14
2026-10-04 21:16:06 +08:00

94 lines
2.8 KiB
JavaScript
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.
/**
* front matter 解析器回归测试:拿仓库里全部真实文章跑无损往返。
*
* node test/frontmatter-roundtrip.mjs [contentDir]
*
* 断言三件事:
* 1. split → join 能**逐字节**还原原文件(说明切分没吃掉任何字符)
* 2. parse → stringify → parse 后对象与首次解析**深相等**(说明丢了不信息)
* 3. 记录所有解析失败的样本,便于判断是否需要补语法
*/
import fs from 'node:fs';
import path from 'node:path';
import { splitFrontMatter, joinFrontMatter, parse, stringify, deepEqual } from '../src/frontmatter.mjs';
const root = process.argv[2] || path.resolve(import.meta.dirname, '../../content/posts');
function walk(dir, out = []) {
for (const name of fs.readdirSync(dir)) {
const p = path.join(dir, name);
const st = fs.statSync(p);
if (st.isDirectory()) walk(p, out);
else if (name.endsWith('.md')) out.push(p);
}
return out;
}
const files = walk(root);
let ok = 0;
const byteFail = [];
const parseFail = [];
const roundFail = [];
for (const f of files) {
const text = fs.readFileSync(f, 'utf8');
const rel = path.relative(root, f);
const { raw, body, eol } = splitFrontMatter(text);
if (raw == null) {
parseFail.push([rel, '没有 front matter']);
continue;
}
// 1. 逐字节还原(含原始换行风格)
if (joinFrontMatter(raw, body, eol) !== text) byteFail.push(rel);
// 2. 解析 + 往返
let first;
try {
first = parse(raw);
} catch (e) {
parseFail.push([rel, e.message]);
continue;
}
let second;
try {
second = parse(stringify(first));
} catch (e) {
roundFail.push([rel, 'stringify/parse 失败: ' + e.message]);
continue;
}
if (!deepEqual(first, second)) {
const diff = Object.keys(first).concat(Object.keys(second)).filter(
(k, i, a) => a.indexOf(k) === i && !deepEqual(first[k], second[k]),
);
roundFail.push([rel, '字段不一致: ' + diff.join(', ')]);
continue;
}
ok++;
}
const total = files.length;
console.log('样本总数 :', total);
console.log('解析 + 往返通过 :', ok);
console.log('切分不还原 :', byteFail.length);
console.log('解析失败 :', parseFail.length);
console.log('往返不一致 :', roundFail.length);
const show = (title, arr) => {
if (!arr.length) return;
console.log('\n--- ' + title + ' ---');
for (const [f, why] of arr.slice(0, 12)) console.log(' ' + f + ' || ' + why);
if (arr.length > 12) console.log(' … 另有 ' + (arr.length - 12) + ' 条');
};
show('切分不还原(必须为 0)', byteFail.map((f) => [f, 'join != 原文']));
show('解析失败', parseFail);
show('往返不一致', roundFail);
const bad = byteFail.length + parseFail.length + roundFail.length;
process.exit(bad === 0 ? 0 : 1);