mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-13 22:29:48 +08:00
Merge pull request #86 from JimLiu/codex/baoyu-md-shared
refactor: share markdown renderer via baoyu-md
This commit is contained in:
+3
-1
@@ -27,7 +27,9 @@ Release hooks are configured via `.releaserc.yml`. This repo does not stage a se
|
|||||||
|
|
||||||
`packages/` is the **only** source of truth. Never edit `skills/*/scripts/vendor/` directly.
|
`packages/` is the **only** source of truth. Never edit `skills/*/scripts/vendor/` directly.
|
||||||
|
|
||||||
Current package: `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 6 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`, `baoyu-url-to-markdown`).
|
Current packages:
|
||||||
|
- `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 6 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`, `baoyu-url-to-markdown`)
|
||||||
|
- `baoyu-md` (shared Markdown rendering and placeholder pipeline), consumed by 3 skills (`baoyu-markdown-to-html`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`)
|
||||||
|
|
||||||
**How it works**: Sync script copies packages into each consuming skill's `vendor/` directory and rewrites dependency specs to `file:./vendor/<name>`. Vendor copies are committed to git, making skills self-contained.
|
**How it works**: Sync script copies packages into each consuming skill's `vendor/` directory and rewrites dependency specs to `file:./vendor/<name>`. Vendor copies are committed to git, making skills self-contained.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"name": "baoyu-md",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"front-matter": "^4.0.2",
|
"front-matter": "^4.0.2",
|
||||||
@@ -12,7 +12,7 @@ export function printUsage(): void {
|
|||||||
console.error(
|
console.error(
|
||||||
[
|
[
|
||||||
"Usage:",
|
"Usage:",
|
||||||
" npx tsx src/md/render.ts <markdown_file> [options]",
|
" npx tsx render.ts <markdown_file> [options]",
|
||||||
"",
|
"",
|
||||||
"Options:",
|
"Options:",
|
||||||
` --theme <name> Theme (${THEME_NAMES.join(", ")})`,
|
` --theme <name> Theme (${THEME_NAMES.join(", ")})`,
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { Lexer } from "marked";
|
||||||
|
|
||||||
|
export type FrontmatterFields = Record<string, string>;
|
||||||
|
|
||||||
|
export function parseFrontmatter(content: string): {
|
||||||
|
frontmatter: FrontmatterFields;
|
||||||
|
body: string;
|
||||||
|
} {
|
||||||
|
const match = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||||
|
if (!match) {
|
||||||
|
return { frontmatter: {}, body: content };
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontmatter: FrontmatterFields = {};
|
||||||
|
const lines = match[1]!.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
const colonIdx = line.indexOf(":");
|
||||||
|
if (colonIdx <= 0) continue;
|
||||||
|
|
||||||
|
const key = line.slice(0, colonIdx).trim();
|
||||||
|
const value = line.slice(colonIdx + 1).trim();
|
||||||
|
frontmatter[key] = stripWrappingQuotes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { frontmatter, body: match[2]! };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeFrontmatter(frontmatter: FrontmatterFields): string {
|
||||||
|
const entries = Object.entries(frontmatter);
|
||||||
|
if (entries.length === 0) return "";
|
||||||
|
return `---\n${entries.map(([key, value]) => `${key}: ${value}`).join("\n")}\n---\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripWrappingQuotes(value: string): string {
|
||||||
|
if (!value) return value;
|
||||||
|
|
||||||
|
const doubleQuoted = value.startsWith('"') && value.endsWith('"');
|
||||||
|
const singleQuoted = value.startsWith("'") && value.endsWith("'");
|
||||||
|
const cjkDoubleQuoted = value.startsWith("\u201c") && value.endsWith("\u201d");
|
||||||
|
const cjkSingleQuoted = value.startsWith("\u2018") && value.endsWith("\u2019");
|
||||||
|
|
||||||
|
if (doubleQuoted || singleQuoted || cjkDoubleQuoted || cjkSingleQuoted) {
|
||||||
|
return value.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFrontmatterString(value: unknown): string | undefined {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return stripWrappingQuotes(value);
|
||||||
|
}
|
||||||
|
if (typeof value === "number" || typeof value === "boolean") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickFirstString(
|
||||||
|
frontmatter: Record<string, unknown>,
|
||||||
|
keys: string[],
|
||||||
|
): string | undefined {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = toFrontmatterString(frontmatter[key]);
|
||||||
|
if (value) return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTitleFromMarkdown(markdown: string): string {
|
||||||
|
const tokens = Lexer.lex(markdown, { gfm: true, breaks: true });
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (token.type !== "heading" || (token.depth !== 1 && token.depth !== 2)) continue;
|
||||||
|
return stripWrappingQuotes(token.text);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractSummaryFromBody(body: string, maxLen: number): string {
|
||||||
|
const lines = body.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
if (trimmed.startsWith("#")) continue;
|
||||||
|
if (trimmed.startsWith("![")) continue;
|
||||||
|
if (trimmed.startsWith(">")) continue;
|
||||||
|
if (trimmed.startsWith("-") || trimmed.startsWith("*")) continue;
|
||||||
|
if (/^\d+\./.test(trimmed)) continue;
|
||||||
|
if (trimmed.startsWith("```")) continue;
|
||||||
|
|
||||||
|
const cleanText = trimmed
|
||||||
|
.replace(/\*\*(.+?)\*\*/g, "$1")
|
||||||
|
.replace(/\*(.+?)\*/g, "$1")
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||||
|
.replace(/`([^`]+)`/g, "$1");
|
||||||
|
|
||||||
|
if (cleanText.length > 20) {
|
||||||
|
if (cleanText.length <= maxLen) return cleanText;
|
||||||
|
return `${cleanText.slice(0, maxLen - 3)}...`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { ReadTimeResults } from "reading-time";
|
||||||
|
|
||||||
|
import {
|
||||||
|
COLOR_PRESETS,
|
||||||
|
DEFAULT_STYLE,
|
||||||
|
FONT_FAMILY_MAP,
|
||||||
|
THEME_STYLE_DEFAULTS,
|
||||||
|
} from "./constants.js";
|
||||||
|
import {
|
||||||
|
extractSummaryFromBody,
|
||||||
|
extractTitleFromMarkdown,
|
||||||
|
pickFirstString,
|
||||||
|
stripWrappingQuotes,
|
||||||
|
} from "./content.js";
|
||||||
|
import { loadExtendConfig } from "./extend-config.js";
|
||||||
|
import {
|
||||||
|
buildCss,
|
||||||
|
buildHtmlDocument,
|
||||||
|
inlineCss,
|
||||||
|
loadCodeThemeCss,
|
||||||
|
modifyHtmlStructure,
|
||||||
|
normalizeInlineCss,
|
||||||
|
removeFirstHeading,
|
||||||
|
} from "./html-builder.js";
|
||||||
|
import { initRenderer, postProcessHtml, renderMarkdown } from "./renderer.js";
|
||||||
|
import { loadThemeCss, normalizeThemeCss } from "./themes.js";
|
||||||
|
import type { HtmlDocumentMeta, IOpts, StyleConfig, ThemeName } from "./types.js";
|
||||||
|
|
||||||
|
export interface RenderMarkdownDocumentOptions {
|
||||||
|
codeTheme?: string;
|
||||||
|
countStatus?: boolean;
|
||||||
|
citeStatus?: boolean;
|
||||||
|
defaultTitle?: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
isMacCodeBlock?: boolean;
|
||||||
|
isShowLineNumber?: boolean;
|
||||||
|
keepTitle?: boolean;
|
||||||
|
legend?: string;
|
||||||
|
primaryColor?: string;
|
||||||
|
theme?: ThemeName;
|
||||||
|
themeMode?: IOpts["themeMode"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderMarkdownDocumentResult {
|
||||||
|
contentHtml: string;
|
||||||
|
html: string;
|
||||||
|
meta: HtmlDocumentMeta;
|
||||||
|
readingTime: ReadTimeResults;
|
||||||
|
style: StyleConfig;
|
||||||
|
yamlData: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveColorToken(value?: string): string | undefined {
|
||||||
|
if (!value) return undefined;
|
||||||
|
return COLOR_PRESETS[value] ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFontFamilyToken(value?: string): string | undefined {
|
||||||
|
if (!value) return undefined;
|
||||||
|
return FONT_FAMILY_MAP[value] ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTimestamp(date = new Date()): string {
|
||||||
|
const pad = (value: number) => String(value).padStart(2, "0");
|
||||||
|
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
|
||||||
|
date.getDate(),
|
||||||
|
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMarkdownDocumentMeta(
|
||||||
|
markdown: string,
|
||||||
|
yamlData: Record<string, unknown>,
|
||||||
|
defaultTitle = "document",
|
||||||
|
): HtmlDocumentMeta {
|
||||||
|
const title = pickFirstString(yamlData, ["title"])
|
||||||
|
|| extractTitleFromMarkdown(markdown)
|
||||||
|
|| defaultTitle;
|
||||||
|
const author = pickFirstString(yamlData, ["author"]);
|
||||||
|
const description = pickFirstString(yamlData, ["description", "summary"])
|
||||||
|
|| extractSummaryFromBody(markdown, 120);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: stripWrappingQuotes(title),
|
||||||
|
author: author ? stripWrappingQuotes(author) : undefined,
|
||||||
|
description: description ? stripWrappingQuotes(description) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMarkdownStyle(options: RenderMarkdownDocumentOptions = {}): StyleConfig {
|
||||||
|
const theme = options.theme ?? "default";
|
||||||
|
const themeDefaults = THEME_STYLE_DEFAULTS[theme] ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...DEFAULT_STYLE,
|
||||||
|
...themeDefaults,
|
||||||
|
...(options.primaryColor !== undefined ? { primaryColor: options.primaryColor } : {}),
|
||||||
|
...(options.fontFamily !== undefined ? { fontFamily: options.fontFamily } : {}),
|
||||||
|
...(options.fontSize !== undefined ? { fontSize: options.fontSize } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRenderOptions(
|
||||||
|
options: RenderMarkdownDocumentOptions = {},
|
||||||
|
): RenderMarkdownDocumentOptions {
|
||||||
|
const extendConfig = loadExtendConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
codeTheme: options.codeTheme ?? extendConfig.default_code_theme ?? "github",
|
||||||
|
countStatus: options.countStatus ?? extendConfig.count ?? false,
|
||||||
|
citeStatus: options.citeStatus ?? extendConfig.cite ?? false,
|
||||||
|
defaultTitle: options.defaultTitle,
|
||||||
|
fontFamily: options.fontFamily ?? resolveFontFamilyToken(extendConfig.default_font_family ?? undefined),
|
||||||
|
fontSize: options.fontSize ?? extendConfig.default_font_size ?? undefined,
|
||||||
|
isMacCodeBlock: options.isMacCodeBlock ?? extendConfig.mac_code_block ?? true,
|
||||||
|
isShowLineNumber: options.isShowLineNumber ?? extendConfig.show_line_number ?? false,
|
||||||
|
keepTitle: options.keepTitle ?? extendConfig.keep_title ?? false,
|
||||||
|
legend: options.legend ?? extendConfig.legend ?? "alt",
|
||||||
|
primaryColor: options.primaryColor ?? resolveColorToken(extendConfig.default_color ?? undefined),
|
||||||
|
theme: options.theme ?? extendConfig.default_theme ?? "default",
|
||||||
|
themeMode: options.themeMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderMarkdownDocument(
|
||||||
|
markdown: string,
|
||||||
|
options: RenderMarkdownDocumentOptions = {},
|
||||||
|
): Promise<RenderMarkdownDocumentResult> {
|
||||||
|
const resolvedOptions = resolveRenderOptions(options);
|
||||||
|
const theme = resolvedOptions.theme ?? "default";
|
||||||
|
const codeTheme = resolvedOptions.codeTheme ?? "github";
|
||||||
|
const style = resolveMarkdownStyle(resolvedOptions);
|
||||||
|
|
||||||
|
const { baseCss, themeCss } = loadThemeCss(theme);
|
||||||
|
const css = normalizeThemeCss(buildCss(baseCss, themeCss, style));
|
||||||
|
const codeThemeCss = loadCodeThemeCss(codeTheme);
|
||||||
|
|
||||||
|
const renderer = initRenderer({
|
||||||
|
citeStatus: resolvedOptions.citeStatus ?? false,
|
||||||
|
countStatus: resolvedOptions.countStatus ?? false,
|
||||||
|
isMacCodeBlock: resolvedOptions.isMacCodeBlock ?? true,
|
||||||
|
isShowLineNumber: resolvedOptions.isShowLineNumber ?? false,
|
||||||
|
legend: resolvedOptions.legend ?? "alt",
|
||||||
|
themeMode: resolvedOptions.themeMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { yamlData, markdownContent, readingTime } = renderer.parseFrontMatterAndContent(markdown);
|
||||||
|
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(markdown, renderer);
|
||||||
|
|
||||||
|
let contentHtml = postProcessHtml(baseHtml, readingTimeResult, renderer);
|
||||||
|
if (!(resolvedOptions.keepTitle ?? false)) {
|
||||||
|
contentHtml = removeFirstHeading(contentHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = buildMarkdownDocumentMeta(
|
||||||
|
markdownContent,
|
||||||
|
yamlData as Record<string, unknown>,
|
||||||
|
resolvedOptions.defaultTitle,
|
||||||
|
);
|
||||||
|
const html = buildHtmlDocument(meta, css, contentHtml, codeThemeCss);
|
||||||
|
const inlinedHtml = normalizeInlineCss(await inlineCss(html), style);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentHtml,
|
||||||
|
html: modifyHtmlStructure(inlinedHtml),
|
||||||
|
meta,
|
||||||
|
readingTime,
|
||||||
|
style,
|
||||||
|
yamlData: yamlData as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderMarkdownFileToHtml(
|
||||||
|
inputPath: string,
|
||||||
|
options: RenderMarkdownDocumentOptions = {},
|
||||||
|
): Promise<RenderMarkdownDocumentResult & {
|
||||||
|
backupPath?: string;
|
||||||
|
outputPath: string;
|
||||||
|
}> {
|
||||||
|
const markdown = fs.readFileSync(inputPath, "utf-8");
|
||||||
|
const outputPath = path.resolve(
|
||||||
|
path.dirname(inputPath),
|
||||||
|
`${path.basename(inputPath, path.extname(inputPath))}.html`,
|
||||||
|
);
|
||||||
|
const result = await renderMarkdownDocument(markdown, {
|
||||||
|
...options,
|
||||||
|
defaultTitle: options.defaultTitle ?? path.basename(outputPath, ".html"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let backupPath: string | undefined;
|
||||||
|
if (fs.existsSync(outputPath)) {
|
||||||
|
backupPath = `${outputPath}.bak-${formatTimestamp()}`;
|
||||||
|
fs.renameSync(outputPath, backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, result.html, "utf-8");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
backupPath,
|
||||||
|
outputPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import http from "node:http";
|
||||||
|
import https from "node:https";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export interface ImagePlaceholder {
|
||||||
|
originalPath: string;
|
||||||
|
placeholder: string;
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolvedImageInfo extends ImagePlaceholder {
|
||||||
|
localPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceMarkdownImagesWithPlaceholders(
|
||||||
|
markdown: string,
|
||||||
|
placeholderPrefix: string,
|
||||||
|
): {
|
||||||
|
images: ImagePlaceholder[];
|
||||||
|
markdown: string;
|
||||||
|
} {
|
||||||
|
const images: ImagePlaceholder[] = [];
|
||||||
|
let imageCounter = 0;
|
||||||
|
|
||||||
|
const rewritten = markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, src) => {
|
||||||
|
const placeholder = `${placeholderPrefix}${++imageCounter}`;
|
||||||
|
images.push({
|
||||||
|
alt,
|
||||||
|
originalPath: src,
|
||||||
|
placeholder,
|
||||||
|
});
|
||||||
|
return placeholder;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { images, markdown: rewritten };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getImageExtension(urlOrPath: string): string {
|
||||||
|
const match = urlOrPath.match(/\.(jpg|jpeg|png|gif|webp)(\?|$)/i);
|
||||||
|
return match ? match[1]!.toLowerCase() : "png";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadFile(url: string, destPath: string): Promise<void> {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
const protocol = url.startsWith("https://") ? https : http;
|
||||||
|
const file = fs.createWriteStream(destPath);
|
||||||
|
|
||||||
|
const request = protocol.get(url, { headers: { "User-Agent": "Mozilla/5.0" } }, (response) => {
|
||||||
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||||
|
const redirectUrl = response.headers.location;
|
||||||
|
if (redirectUrl) {
|
||||||
|
file.close();
|
||||||
|
fs.unlinkSync(destPath);
|
||||||
|
void downloadFile(redirectUrl, destPath).then(resolve).catch(reject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
file.close();
|
||||||
|
fs.unlinkSync(destPath);
|
||||||
|
reject(new Error(`Failed to download: ${response.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.pipe(file);
|
||||||
|
file.on("finish", () => {
|
||||||
|
file.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", (error) => {
|
||||||
|
file.close();
|
||||||
|
fs.unlink(destPath, () => {});
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.setTimeout(30_000, () => {
|
||||||
|
request.destroy();
|
||||||
|
reject(new Error("Download timeout"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveImagePath(
|
||||||
|
imagePath: string,
|
||||||
|
baseDir: string,
|
||||||
|
tempDir: string,
|
||||||
|
logLabel = "baoyu-md",
|
||||||
|
): Promise<string> {
|
||||||
|
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
||||||
|
const hash = createHash("md5").update(imagePath).digest("hex").slice(0, 8);
|
||||||
|
const ext = getImageExtension(imagePath);
|
||||||
|
const localPath = path.join(tempDir, `remote_${hash}.${ext}`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(localPath)) {
|
||||||
|
console.error(`[${logLabel}] Downloading: ${imagePath}`);
|
||||||
|
await downloadFile(imagePath, localPath);
|
||||||
|
}
|
||||||
|
return localPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = path.isAbsolute(imagePath)
|
||||||
|
? imagePath
|
||||||
|
: path.resolve(baseDir, imagePath);
|
||||||
|
return resolveLocalWithFallback(resolved, logLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveContentImages(
|
||||||
|
images: ImagePlaceholder[],
|
||||||
|
baseDir: string,
|
||||||
|
tempDir: string,
|
||||||
|
logLabel = "baoyu-md",
|
||||||
|
): Promise<ResolvedImageInfo[]> {
|
||||||
|
const resolved: ResolvedImageInfo[] = [];
|
||||||
|
|
||||||
|
for (const image of images) {
|
||||||
|
resolved.push({
|
||||||
|
...image,
|
||||||
|
localPath: await resolveImagePath(image.originalPath, baseDir, tempDir, logLabel),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLocalWithFallback(resolved: string, logLabel: string): string {
|
||||||
|
if (fs.existsSync(resolved)) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(resolved);
|
||||||
|
const base = ext ? resolved.slice(0, -ext.length) : resolved;
|
||||||
|
const alternatives = [
|
||||||
|
`${base}.webp`,
|
||||||
|
`${base}.jpg`,
|
||||||
|
`${base}.jpeg`,
|
||||||
|
`${base}.png`,
|
||||||
|
`${base}.gif`,
|
||||||
|
`${base}_original.png`,
|
||||||
|
`${base}_original.jpg`,
|
||||||
|
].filter((candidate) => candidate !== resolved);
|
||||||
|
|
||||||
|
for (const alternative of alternatives) {
|
||||||
|
if (!fs.existsSync(alternative)) continue;
|
||||||
|
console.error(
|
||||||
|
`[${logLabel}] Image fallback: ${path.basename(resolved)} -> ${path.basename(alternative)}`,
|
||||||
|
);
|
||||||
|
return alternative;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export * from "./cli.js";
|
||||||
|
export * from "./constants.js";
|
||||||
|
export * from "./content.js";
|
||||||
|
export * from "./document.js";
|
||||||
|
export * from "./extend-config.js";
|
||||||
|
export * from "./html-builder.js";
|
||||||
|
export * from "./images.js";
|
||||||
|
export * from "./renderer.js";
|
||||||
|
export * from "./themes.js";
|
||||||
|
export * from "./types.js";
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
|
||||||
|
import path from "node:path";
|
||||||
|
import { parseArgs, printUsage } from "./cli.js";
|
||||||
|
import { renderMarkdownFileToHtml } from "./document.js";
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const options = parseArgs(process.argv.slice(2));
|
||||||
|
if (!options) {
|
||||||
|
printUsage();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputPath = path.resolve(process.cwd(), options.inputPath);
|
||||||
|
if (!inputPath.toLowerCase().endsWith(".md")) {
|
||||||
|
console.error("Input file must end with .md");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await renderMarkdownFileToHtml(inputPath, {
|
||||||
|
codeTheme: options.codeTheme,
|
||||||
|
countStatus: options.countStatus,
|
||||||
|
citeStatus: options.citeStatus,
|
||||||
|
fontFamily: options.fontFamily,
|
||||||
|
fontSize: options.fontSize,
|
||||||
|
isMacCodeBlock: options.isMacCodeBlock,
|
||||||
|
isShowLineNumber: options.isShowLineNumber,
|
||||||
|
keepTitle: options.keepTitle,
|
||||||
|
legend: options.legend,
|
||||||
|
primaryColor: options.primaryColor,
|
||||||
|
theme: options.theme,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.backupPath) {
|
||||||
|
console.log(`Backup created: ${result.backupPath}`);
|
||||||
|
}
|
||||||
|
console.log(`HTML written: ${result.outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user