From 105339cf3f9e30768cebeeee8f8fbc04069261f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Fri, 20 Mar 2026 23:17:04 -0500 Subject: [PATCH] fix(baoyu-md): fix CSS custom property regex for quoted values and add theme layering - 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 --- packages/baoyu-md/src/document.test.ts | 34 +++++++++++++++++++ packages/baoyu-md/src/html-builder.test.ts | 11 ++++++ packages/baoyu-md/src/html-builder.ts | 14 ++++---- packages/baoyu-md/src/themes.ts | 13 ++++++- .../vendor/baoyu-md/src/document.test.ts | 34 +++++++++++++++++++ .../vendor/baoyu-md/src/html-builder.test.ts | 11 ++++++ .../vendor/baoyu-md/src/html-builder.ts | 14 ++++---- .../scripts/vendor/baoyu-md/src/themes.ts | 13 ++++++- .../vendor/baoyu-md/src/document.test.ts | 34 +++++++++++++++++++ .../vendor/baoyu-md/src/html-builder.test.ts | 11 ++++++ .../vendor/baoyu-md/src/html-builder.ts | 14 ++++---- .../scripts/vendor/baoyu-md/src/themes.ts | 13 ++++++- .../vendor/baoyu-md/src/document.test.ts | 34 +++++++++++++++++++ .../vendor/baoyu-md/src/html-builder.test.ts | 11 ++++++ .../vendor/baoyu-md/src/html-builder.ts | 14 ++++---- .../scripts/vendor/baoyu-md/src/themes.ts | 13 ++++++- 16 files changed, 256 insertions(+), 32 deletions(-) diff --git a/packages/baoyu-md/src/document.test.ts b/packages/baoyu-md/src/document.test.ts index c188acc..bfa6533 100644 --- a/packages/baoyu-md/src/document.test.ts +++ b/packages/baoyu-md/src/document.test.ts @@ -9,12 +9,26 @@ 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)}`, + ); + const match = html.match(pattern); + assert.ok(match, `Expected inline style for <${tagName}>${text}`); + return match![1]!; +} + function useCwd(t: TestContext, cwd: string): void { const previous = process.cwd(); process.chdir(cwd); @@ -138,3 +152,23 @@ keep_title: true 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(/]*style="([^"]*)"[^>]*>/); + assert.ok(pMatch, "Expected inline style on

tag"); + assert.match(pMatch![1]!, /color:/); + + const strongPattern = /]*style="([^"]*)"[^>]*>bold<\/strong>/; + const strongMatch = html.match(strongPattern); + assert.ok(strongMatch, "Expected inline style for bold"); + assert.match(strongMatch![1]!, /font-weight:/); +}); diff --git a/packages/baoyu-md/src/html-builder.test.ts b/packages/baoyu-md/src/html-builder.test.ts index 2cab343..b68de0d 100644 --- a/packages/baoyu-md/src/html-builder.test.ts +++ b/packages/baoyu-md/src/html-builder.test.ts @@ -59,6 +59,17 @@ test("normalizeCssText and normalizeInlineCss replace variables and strip declar assert.doesNotMatch(normalizedHtml, /var\(--md-primary-color\)/); }); +test("normalizeInlineCss removes quoted custom property values without leaving fragments behind", () => { + const normalizedHtml = normalizeInlineCss( + ``, + DEFAULT_STYLE, + ); + + assert.match(normalizedHtml, /style=" color: #0F4C81"/); + assert.doesNotMatch(normalizedHtml, /Courier New/); + assert.doesNotMatch(normalizedHtml, /--md-font-family/); +}); + test("HTML structure helpers hoist nested lists and remove the first heading", () => { const nestedList = `

`; assert.equal( diff --git a/packages/baoyu-md/src/html-builder.ts b/packages/baoyu-md/src/html-builder.ts index 3ca61a0..d27e03a 100644 --- a/packages/baoyu-md/src/html-builder.ts +++ b/packages/baoyu-md/src/html-builder.ts @@ -100,13 +100,13 @@ export function normalizeCssText(cssText: string, style: StyleConfig = DEFAULT_S .replace(/var\(--md-accent-color\)/g, style.accentColor) .replace(/var\(--md-container-bg\)/g, style.containerBg) .replace(/hsl\(var\(--foreground\)\)/g, "#3f3f3f") - .replace(/--md-primary-color:\s*[^;"']+;?/g, "") - .replace(/--md-font-family:\s*[^;"']+;?/g, "") - .replace(/--md-font-size:\s*[^;"']+;?/g, "") - .replace(/--blockquote-background:\s*[^;"']+;?/g, "") - .replace(/--md-accent-color:\s*[^;"']+;?/g, "") - .replace(/--md-container-bg:\s*[^;"']+;?/g, "") - .replace(/--foreground:\s*[^;"']+;?/g, ""); + .replace(/--md-primary-color:\s*[^;]+;?/g, "") + .replace(/--md-font-family:\s*[^;]+;?/g, "") + .replace(/--md-font-size:\s*[^;]+;?/g, "") + .replace(/--blockquote-background:\s*[^;]+;?/g, "") + .replace(/--md-accent-color:\s*[^;]+;?/g, "") + .replace(/--md-container-bg:\s*[^;]+;?/g, "") + .replace(/--foreground:\s*[^;]+;?/g, ""); } export function normalizeInlineCss(html: string, style: StyleConfig = DEFAULT_STYLE): string { diff --git a/packages/baoyu-md/src/themes.ts b/packages/baoyu-md/src/themes.ts index 9c3dbeb..6767ad3 100644 --- a/packages/baoyu-md/src/themes.ts +++ b/packages/baoyu-md/src/themes.ts @@ -6,6 +6,7 @@ import type { ThemeName } from "./types.js"; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); export const THEME_DIR = path.resolve(SCRIPT_DIR, "themes"); const FALLBACK_THEMES: ThemeName[] = ["default", "grace", "simple"]; +const THEMES_EXTENDING_DEFAULT = new Set(["grace", "simple"]); function stripOutputScope(cssContent: string): string { let css = cssContent; @@ -41,6 +42,7 @@ export function loadThemeCss(theme: ThemeName): { themeCss: string; } { const basePath = path.join(THEME_DIR, "base.css"); + const defaultThemePath = path.join(THEME_DIR, "default.css"); const themePath = path.join(THEME_DIR, `${theme}.css`); if (!fs.existsSync(basePath)) { @@ -51,9 +53,18 @@ export function loadThemeCss(theme: ThemeName): { throw new Error(`Missing theme CSS for "${theme}": ${themePath}`); } + const layeredThemeCss: string[] = []; + if (theme !== "default" && THEMES_EXTENDING_DEFAULT.has(theme)) { + if (!fs.existsSync(defaultThemePath)) { + throw new Error(`Missing default theme CSS: ${defaultThemePath}`); + } + layeredThemeCss.push(fs.readFileSync(defaultThemePath, "utf-8")); + } + layeredThemeCss.push(fs.readFileSync(themePath, "utf-8")); + return { baseCss: fs.readFileSync(basePath, "utf-8"), - themeCss: fs.readFileSync(themePath, "utf-8"), + themeCss: layeredThemeCss.join("\n"), }; } diff --git a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/document.test.ts b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/document.test.ts index c188acc..bfa6533 100644 --- a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/document.test.ts +++ b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/document.test.ts @@ -9,12 +9,26 @@ 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)}`, + ); + const match = html.match(pattern); + assert.ok(match, `Expected inline style for <${tagName}>${text}`); + return match![1]!; +} + function useCwd(t: TestContext, cwd: string): void { const previous = process.cwd(); process.chdir(cwd); @@ -138,3 +152,23 @@ keep_title: true 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(/]*style="([^"]*)"[^>]*>/); + assert.ok(pMatch, "Expected inline style on

tag"); + assert.match(pMatch![1]!, /color:/); + + const strongPattern = /]*style="([^"]*)"[^>]*>bold<\/strong>/; + const strongMatch = html.match(strongPattern); + assert.ok(strongMatch, "Expected inline style for bold"); + assert.match(strongMatch![1]!, /font-weight:/); +}); diff --git a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts index 2cab343..b68de0d 100644 --- a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts +++ b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.test.ts @@ -59,6 +59,17 @@ test("normalizeCssText and normalizeInlineCss replace variables and strip declar assert.doesNotMatch(normalizedHtml, /var\(--md-primary-color\)/); }); +test("normalizeInlineCss removes quoted custom property values without leaving fragments behind", () => { + const normalizedHtml = normalizeInlineCss( + ``, + DEFAULT_STYLE, + ); + + assert.match(normalizedHtml, /style=" color: #0F4C81"/); + assert.doesNotMatch(normalizedHtml, /Courier New/); + assert.doesNotMatch(normalizedHtml, /--md-font-family/); +}); + test("HTML structure helpers hoist nested lists and remove the first heading", () => { const nestedList = `

  • Parent
    • Child
`; assert.equal( diff --git a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.ts b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.ts index 3ca61a0..d27e03a 100644 --- a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.ts +++ b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/html-builder.ts @@ -100,13 +100,13 @@ export function normalizeCssText(cssText: string, style: StyleConfig = DEFAULT_S .replace(/var\(--md-accent-color\)/g, style.accentColor) .replace(/var\(--md-container-bg\)/g, style.containerBg) .replace(/hsl\(var\(--foreground\)\)/g, "#3f3f3f") - .replace(/--md-primary-color:\s*[^;"']+;?/g, "") - .replace(/--md-font-family:\s*[^;"']+;?/g, "") - .replace(/--md-font-size:\s*[^;"']+;?/g, "") - .replace(/--blockquote-background:\s*[^;"']+;?/g, "") - .replace(/--md-accent-color:\s*[^;"']+;?/g, "") - .replace(/--md-container-bg:\s*[^;"']+;?/g, "") - .replace(/--foreground:\s*[^;"']+;?/g, ""); + .replace(/--md-primary-color:\s*[^;]+;?/g, "") + .replace(/--md-font-family:\s*[^;]+;?/g, "") + .replace(/--md-font-size:\s*[^;]+;?/g, "") + .replace(/--blockquote-background:\s*[^;]+;?/g, "") + .replace(/--md-accent-color:\s*[^;]+;?/g, "") + .replace(/--md-container-bg:\s*[^;]+;?/g, "") + .replace(/--foreground:\s*[^;]+;?/g, ""); } export function normalizeInlineCss(html: string, style: StyleConfig = DEFAULT_STYLE): string { diff --git a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/themes.ts b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/themes.ts index 9c3dbeb..6767ad3 100644 --- a/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/themes.ts +++ b/skills/baoyu-markdown-to-html/scripts/vendor/baoyu-md/src/themes.ts @@ -6,6 +6,7 @@ import type { ThemeName } from "./types.js"; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); export const THEME_DIR = path.resolve(SCRIPT_DIR, "themes"); const FALLBACK_THEMES: ThemeName[] = ["default", "grace", "simple"]; +const THEMES_EXTENDING_DEFAULT = new Set(["grace", "simple"]); function stripOutputScope(cssContent: string): string { let css = cssContent; @@ -41,6 +42,7 @@ export function loadThemeCss(theme: ThemeName): { themeCss: string; } { const basePath = path.join(THEME_DIR, "base.css"); + const defaultThemePath = path.join(THEME_DIR, "default.css"); const themePath = path.join(THEME_DIR, `${theme}.css`); if (!fs.existsSync(basePath)) { @@ -51,9 +53,18 @@ export function loadThemeCss(theme: ThemeName): { throw new Error(`Missing theme CSS for "${theme}": ${themePath}`); } + const layeredThemeCss: string[] = []; + if (theme !== "default" && THEMES_EXTENDING_DEFAULT.has(theme)) { + if (!fs.existsSync(defaultThemePath)) { + throw new Error(`Missing default theme CSS: ${defaultThemePath}`); + } + layeredThemeCss.push(fs.readFileSync(defaultThemePath, "utf-8")); + } + layeredThemeCss.push(fs.readFileSync(themePath, "utf-8")); + return { baseCss: fs.readFileSync(basePath, "utf-8"), - themeCss: fs.readFileSync(themePath, "utf-8"), + themeCss: layeredThemeCss.join("\n"), }; } diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/document.test.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/document.test.ts index c188acc..bfa6533 100644 --- a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/document.test.ts +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/document.test.ts @@ -9,12 +9,26 @@ 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)}`, + ); + const match = html.match(pattern); + assert.ok(match, `Expected inline style for <${tagName}>${text}`); + return match![1]!; +} + function useCwd(t: TestContext, cwd: string): void { const previous = process.cwd(); process.chdir(cwd); @@ -138,3 +152,23 @@ keep_title: true 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(/]*style="([^"]*)"[^>]*>/); + assert.ok(pMatch, "Expected inline style on

tag"); + assert.match(pMatch![1]!, /color:/); + + const strongPattern = /]*style="([^"]*)"[^>]*>bold<\/strong>/; + const strongMatch = html.match(strongPattern); + assert.ok(strongMatch, "Expected inline style for bold"); + assert.match(strongMatch![1]!, /font-weight:/); +}); diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts index 2cab343..b68de0d 100644 --- a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.test.ts @@ -59,6 +59,17 @@ test("normalizeCssText and normalizeInlineCss replace variables and strip declar assert.doesNotMatch(normalizedHtml, /var\(--md-primary-color\)/); }); +test("normalizeInlineCss removes quoted custom property values without leaving fragments behind", () => { + const normalizedHtml = normalizeInlineCss( + ``, + DEFAULT_STYLE, + ); + + assert.match(normalizedHtml, /style=" color: #0F4C81"/); + assert.doesNotMatch(normalizedHtml, /Courier New/); + assert.doesNotMatch(normalizedHtml, /--md-font-family/); +}); + test("HTML structure helpers hoist nested lists and remove the first heading", () => { const nestedList = `

  • Parent
    • Child
`; assert.equal( diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.ts index 3ca61a0..d27e03a 100644 --- a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.ts +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/html-builder.ts @@ -100,13 +100,13 @@ export function normalizeCssText(cssText: string, style: StyleConfig = DEFAULT_S .replace(/var\(--md-accent-color\)/g, style.accentColor) .replace(/var\(--md-container-bg\)/g, style.containerBg) .replace(/hsl\(var\(--foreground\)\)/g, "#3f3f3f") - .replace(/--md-primary-color:\s*[^;"']+;?/g, "") - .replace(/--md-font-family:\s*[^;"']+;?/g, "") - .replace(/--md-font-size:\s*[^;"']+;?/g, "") - .replace(/--blockquote-background:\s*[^;"']+;?/g, "") - .replace(/--md-accent-color:\s*[^;"']+;?/g, "") - .replace(/--md-container-bg:\s*[^;"']+;?/g, "") - .replace(/--foreground:\s*[^;"']+;?/g, ""); + .replace(/--md-primary-color:\s*[^;]+;?/g, "") + .replace(/--md-font-family:\s*[^;]+;?/g, "") + .replace(/--md-font-size:\s*[^;]+;?/g, "") + .replace(/--blockquote-background:\s*[^;]+;?/g, "") + .replace(/--md-accent-color:\s*[^;]+;?/g, "") + .replace(/--md-container-bg:\s*[^;]+;?/g, "") + .replace(/--foreground:\s*[^;]+;?/g, ""); } export function normalizeInlineCss(html: string, style: StyleConfig = DEFAULT_STYLE): string { diff --git a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/themes.ts b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/themes.ts index 9c3dbeb..6767ad3 100644 --- a/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/themes.ts +++ b/skills/baoyu-post-to-wechat/scripts/vendor/baoyu-md/src/themes.ts @@ -6,6 +6,7 @@ import type { ThemeName } from "./types.js"; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); export const THEME_DIR = path.resolve(SCRIPT_DIR, "themes"); const FALLBACK_THEMES: ThemeName[] = ["default", "grace", "simple"]; +const THEMES_EXTENDING_DEFAULT = new Set(["grace", "simple"]); function stripOutputScope(cssContent: string): string { let css = cssContent; @@ -41,6 +42,7 @@ export function loadThemeCss(theme: ThemeName): { themeCss: string; } { const basePath = path.join(THEME_DIR, "base.css"); + const defaultThemePath = path.join(THEME_DIR, "default.css"); const themePath = path.join(THEME_DIR, `${theme}.css`); if (!fs.existsSync(basePath)) { @@ -51,9 +53,18 @@ export function loadThemeCss(theme: ThemeName): { throw new Error(`Missing theme CSS for "${theme}": ${themePath}`); } + const layeredThemeCss: string[] = []; + if (theme !== "default" && THEMES_EXTENDING_DEFAULT.has(theme)) { + if (!fs.existsSync(defaultThemePath)) { + throw new Error(`Missing default theme CSS: ${defaultThemePath}`); + } + layeredThemeCss.push(fs.readFileSync(defaultThemePath, "utf-8")); + } + layeredThemeCss.push(fs.readFileSync(themePath, "utf-8")); + return { baseCss: fs.readFileSync(basePath, "utf-8"), - themeCss: fs.readFileSync(themePath, "utf-8"), + themeCss: layeredThemeCss.join("\n"), }; } diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/document.test.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/document.test.ts index c188acc..bfa6533 100644 --- a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/document.test.ts +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/document.test.ts @@ -9,12 +9,26 @@ 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)}`, + ); + const match = html.match(pattern); + assert.ok(match, `Expected inline style for <${tagName}>${text}`); + return match![1]!; +} + function useCwd(t: TestContext, cwd: string): void { const previous = process.cwd(); process.chdir(cwd); @@ -138,3 +152,23 @@ keep_title: true 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(/]*style="([^"]*)"[^>]*>/); + assert.ok(pMatch, "Expected inline style on

tag"); + assert.match(pMatch![1]!, /color:/); + + const strongPattern = /]*style="([^"]*)"[^>]*>bold<\/strong>/; + const strongMatch = html.match(strongPattern); + assert.ok(strongMatch, "Expected inline style for bold"); + assert.match(strongMatch![1]!, /font-weight:/); +}); diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.test.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.test.ts index 2cab343..b68de0d 100644 --- a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.test.ts +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.test.ts @@ -59,6 +59,17 @@ test("normalizeCssText and normalizeInlineCss replace variables and strip declar assert.doesNotMatch(normalizedHtml, /var\(--md-primary-color\)/); }); +test("normalizeInlineCss removes quoted custom property values without leaving fragments behind", () => { + const normalizedHtml = normalizeInlineCss( + ``, + DEFAULT_STYLE, + ); + + assert.match(normalizedHtml, /style=" color: #0F4C81"/); + assert.doesNotMatch(normalizedHtml, /Courier New/); + assert.doesNotMatch(normalizedHtml, /--md-font-family/); +}); + test("HTML structure helpers hoist nested lists and remove the first heading", () => { const nestedList = `

  • Parent
    • Child
`; assert.equal( diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.ts index 3ca61a0..d27e03a 100644 --- a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.ts +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/html-builder.ts @@ -100,13 +100,13 @@ export function normalizeCssText(cssText: string, style: StyleConfig = DEFAULT_S .replace(/var\(--md-accent-color\)/g, style.accentColor) .replace(/var\(--md-container-bg\)/g, style.containerBg) .replace(/hsl\(var\(--foreground\)\)/g, "#3f3f3f") - .replace(/--md-primary-color:\s*[^;"']+;?/g, "") - .replace(/--md-font-family:\s*[^;"']+;?/g, "") - .replace(/--md-font-size:\s*[^;"']+;?/g, "") - .replace(/--blockquote-background:\s*[^;"']+;?/g, "") - .replace(/--md-accent-color:\s*[^;"']+;?/g, "") - .replace(/--md-container-bg:\s*[^;"']+;?/g, "") - .replace(/--foreground:\s*[^;"']+;?/g, ""); + .replace(/--md-primary-color:\s*[^;]+;?/g, "") + .replace(/--md-font-family:\s*[^;]+;?/g, "") + .replace(/--md-font-size:\s*[^;]+;?/g, "") + .replace(/--blockquote-background:\s*[^;]+;?/g, "") + .replace(/--md-accent-color:\s*[^;]+;?/g, "") + .replace(/--md-container-bg:\s*[^;]+;?/g, "") + .replace(/--foreground:\s*[^;]+;?/g, ""); } export function normalizeInlineCss(html: string, style: StyleConfig = DEFAULT_STYLE): string { diff --git a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/themes.ts b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/themes.ts index 9c3dbeb..6767ad3 100644 --- a/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/themes.ts +++ b/skills/baoyu-post-to-weibo/scripts/vendor/baoyu-md/src/themes.ts @@ -6,6 +6,7 @@ import type { ThemeName } from "./types.js"; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); export const THEME_DIR = path.resolve(SCRIPT_DIR, "themes"); const FALLBACK_THEMES: ThemeName[] = ["default", "grace", "simple"]; +const THEMES_EXTENDING_DEFAULT = new Set(["grace", "simple"]); function stripOutputScope(cssContent: string): string { let css = cssContent; @@ -41,6 +42,7 @@ export function loadThemeCss(theme: ThemeName): { themeCss: string; } { const basePath = path.join(THEME_DIR, "base.css"); + const defaultThemePath = path.join(THEME_DIR, "default.css"); const themePath = path.join(THEME_DIR, `${theme}.css`); if (!fs.existsSync(basePath)) { @@ -51,9 +53,18 @@ export function loadThemeCss(theme: ThemeName): { throw new Error(`Missing theme CSS for "${theme}": ${themePath}`); } + const layeredThemeCss: string[] = []; + if (theme !== "default" && THEMES_EXTENDING_DEFAULT.has(theme)) { + if (!fs.existsSync(defaultThemePath)) { + throw new Error(`Missing default theme CSS: ${defaultThemePath}`); + } + layeredThemeCss.push(fs.readFileSync(defaultThemePath, "utf-8")); + } + layeredThemeCss.push(fs.readFileSync(themePath, "utf-8")); + return { baseCss: fs.readFileSync(basePath, "utf-8"), - themeCss: fs.readFileSync(themePath, "utf-8"), + themeCss: layeredThemeCss.join("\n"), }; }