mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-13 06:19:46 +08:00
105339cf3f
- Remove quotes from CSS custom property regex character class so values containing quotes are fully stripped - grace/simple themes now layer default CSS before their own rules - Add tests for quoted property stripping and theme layering
175 lines
5.5 KiB
TypeScript
175 lines
5.5 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import test, { type TestContext } from "node:test";
|
|
|
|
import { COLOR_PRESETS, FONT_FAMILY_MAP } from "./constants.ts";
|
|
import {
|
|
buildMarkdownDocumentMeta,
|
|
formatTimestamp,
|
|
renderMarkdownDocument,
|
|
resolveColorToken,
|
|
resolveFontFamilyToken,
|
|
resolveMarkdownStyle,
|
|
resolveRenderOptions,
|
|
} from "./document.ts";
|
|
|
|
function escapeRegExp(value: string): string {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`);
|
|
}
|
|
|
|
function findInlineStyle(html: string, tagName: string, text: string): string {
|
|
const pattern = new RegExp(
|
|
`<${tagName}[^>]*style="([^"]*)"[^>]*>${escapeRegExp(text)}</${tagName}>`,
|
|
);
|
|
const match = html.match(pattern);
|
|
assert.ok(match, `Expected inline style for <${tagName}>${text}</${tagName}>`);
|
|
return match![1]!;
|
|
}
|
|
|
|
function useCwd(t: TestContext, cwd: string): void {
|
|
const previous = process.cwd();
|
|
process.chdir(cwd);
|
|
t.after(() => {
|
|
process.chdir(previous);
|
|
});
|
|
}
|
|
|
|
async function makeTempDir(prefix: string): Promise<string> {
|
|
return fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
|
}
|
|
|
|
test("document token resolvers map known presets and allow passthrough values", () => {
|
|
assert.equal(resolveColorToken("green"), COLOR_PRESETS.green);
|
|
assert.equal(resolveColorToken("#123456"), "#123456");
|
|
assert.equal(resolveColorToken(), undefined);
|
|
|
|
assert.equal(resolveFontFamilyToken("mono"), FONT_FAMILY_MAP.mono);
|
|
assert.equal(resolveFontFamilyToken("Custom Font"), "Custom Font");
|
|
assert.equal(resolveFontFamilyToken(), undefined);
|
|
});
|
|
|
|
test("formatTimestamp uses compact sortable datetime output", () => {
|
|
const date = new Date("2026-03-13T21:04:05.000Z");
|
|
const pad = (value: number) => String(value).padStart(2, "0");
|
|
const expected = `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
|
|
date.getDate(),
|
|
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
|
|
assert.equal(formatTimestamp(date), expected);
|
|
});
|
|
|
|
test("buildMarkdownDocumentMeta prefers frontmatter and falls back to markdown title and summary", () => {
|
|
const metaFromYaml = buildMarkdownDocumentMeta(
|
|
"# Markdown Title\n\nBody summary paragraph that should be ignored.",
|
|
{
|
|
title: `" YAML Title "`,
|
|
author: "'Baoyu'",
|
|
summary: `" YAML Summary "`,
|
|
},
|
|
"fallback",
|
|
);
|
|
|
|
assert.deepEqual(metaFromYaml, {
|
|
title: "YAML Title",
|
|
author: "Baoyu",
|
|
description: "YAML Summary",
|
|
});
|
|
|
|
const metaFromMarkdown = buildMarkdownDocumentMeta(
|
|
`## “Markdown Title”\n\nThis is the first body paragraph that should become the summary because it is long enough.`,
|
|
{},
|
|
"fallback",
|
|
);
|
|
|
|
assert.equal(metaFromMarkdown.title, "Markdown Title");
|
|
assert.match(metaFromMarkdown.description ?? "", /^This is the first body paragraph/);
|
|
});
|
|
|
|
test("resolveMarkdownStyle merges theme defaults with explicit overrides", () => {
|
|
const style = resolveMarkdownStyle({
|
|
theme: "modern",
|
|
primaryColor: "#112233",
|
|
fontFamily: "Custom Sans",
|
|
});
|
|
|
|
assert.equal(style.primaryColor, "#112233");
|
|
assert.equal(style.fontFamily, "Custom Sans");
|
|
assert.equal(style.fontSize, "15px");
|
|
assert.equal(style.containerBg, "rgba(250, 249, 245, 1)");
|
|
});
|
|
|
|
test("resolveRenderOptions loads workspace EXTEND settings and lets explicit options win", async (t) => {
|
|
const root = await makeTempDir("baoyu-md-render-options-");
|
|
useCwd(t, root);
|
|
|
|
const extendPath = path.join(
|
|
root,
|
|
".baoyu-skills",
|
|
"baoyu-markdown-to-html",
|
|
"EXTEND.md",
|
|
);
|
|
await fs.mkdir(path.dirname(extendPath), { recursive: true });
|
|
await fs.writeFile(
|
|
extendPath,
|
|
`---
|
|
default_theme: modern
|
|
default_color: green
|
|
default_font_family: mono
|
|
default_font_size: 17
|
|
default_code_theme: nord
|
|
mac_code_block: false
|
|
show_line_number: true
|
|
cite: true
|
|
count: true
|
|
legend: title-alt
|
|
keep_title: true
|
|
---
|
|
`,
|
|
);
|
|
|
|
const fromExtend = resolveRenderOptions();
|
|
assert.equal(fromExtend.theme, "modern");
|
|
assert.equal(fromExtend.primaryColor, COLOR_PRESETS.green);
|
|
assert.equal(fromExtend.fontFamily, FONT_FAMILY_MAP.mono);
|
|
assert.equal(fromExtend.fontSize, "17px");
|
|
assert.equal(fromExtend.codeTheme, "nord");
|
|
assert.equal(fromExtend.isMacCodeBlock, false);
|
|
assert.equal(fromExtend.isShowLineNumber, true);
|
|
assert.equal(fromExtend.citeStatus, true);
|
|
assert.equal(fromExtend.countStatus, true);
|
|
assert.equal(fromExtend.legend, "title-alt");
|
|
assert.equal(fromExtend.keepTitle, true);
|
|
|
|
const explicit = resolveRenderOptions({
|
|
theme: "simple",
|
|
fontSize: "18px",
|
|
keepTitle: false,
|
|
});
|
|
assert.equal(explicit.theme, "simple");
|
|
assert.equal(explicit.fontSize, "18px");
|
|
assert.equal(explicit.keepTitle, false);
|
|
});
|
|
|
|
test("renderMarkdownDocument layers default rules into grace theme before CSS inlining", async () => {
|
|
const { html } = await renderMarkdownDocument(
|
|
`## Section\n\nParagraph with **bold** text.`,
|
|
{ keepTitle: true, theme: "grace" },
|
|
);
|
|
|
|
const h2Style = findInlineStyle(html, "h2", "Section");
|
|
assert.match(h2Style, /background: #92617E/);
|
|
assert.match(h2Style, /box-shadow: 0 4px 6px rgba\(0, 0, 0, 0\.1\)/);
|
|
|
|
const pMatch = html.match(/<p[^>]*style="([^"]*)"[^>]*>/);
|
|
assert.ok(pMatch, "Expected inline style on <p> tag");
|
|
assert.match(pMatch![1]!, /color:/);
|
|
|
|
const strongPattern = /<strong[^>]*style="([^"]*)"[^>]*>bold<\/strong>/;
|
|
const strongMatch = html.match(strongPattern);
|
|
assert.ok(strongMatch, "Expected inline style for <strong>bold</strong>");
|
|
assert.match(strongMatch![1]!, /font-weight:/);
|
|
});
|