新增能力:Web 与 Desktop 支持新建和保存 Markdown;桌面端注册 .md/.markdown 打开程序,支持单实例文件转交、自定义主题目录以及窗口位置、尺寸和最大化状态的延迟原子持久化与屏幕越界修正。 界面与兼容性:保留打开 Markdown 为独立主操作,将新建、保存和主题目录收纳到更多菜单;Web 移除本地素材目录入口并明确默认不支持本地素材;新文档默认使用一级标题或首行合法化生成文件名,可不保存直接导出 PDF。 渲染修复:图片、Mermaid 和 ECharts 按文档顺序串行执行媒体分页回填,每个媒体元素至多重排一次;图片以限宽后的高度作为缩放基准,只缩放媒体主体并保持标题尺寸;ECharts 冻结为 SVG 图片后参与统一分页。 示例与文档:默认示例仅保留一张网络图片,增加有效 Base64 横图、竖图、边界图片及多尺寸 Mermaid/ECharts;更新根 README、Desktop README、部署文档、四个 packages README、PROGRESS 和 AGENTS 发布规范;登记 v0.4.6 超链接导航问题。 部署与验证:构建并运行 yixiong/md-to-pdf:v0.4.5 正式镜像,Compose 健康且 PDF 冒烟无 Mermaid/ECharts 错误;生成 NSIS 安装包和免安装 ZIP,本机已升级到 NSIS v0.4.5 并清理旧 Squirrel 安装;202 项测试、类型检查、生产构建和 git diff --check 全部通过。 发布产物:Setup.exe SHA-256 9CBD6362E15AA745185805E753D5A7749434E0E142E6CE0F66463E6A19468F1D;ZIP SHA-256 97B2D44E72F626A3396E5AB5B330FFB7F9BECAF945A1707808C876940A82E850;当前 Windows 产物未配置代码签名。
111 lines
2.9 KiB
TypeScript
111 lines
2.9 KiB
TypeScript
const fallbackBaseName = "未命名文档";
|
|
const windowsReservedName =
|
|
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu;
|
|
|
|
function stripMarkdownFormatting(value: string) {
|
|
return value
|
|
.replace(/!\[([^\]]*)\]\([^)]*\)/gu, "$1")
|
|
.replace(/\[([^\]]+)\]\([^)]*\)/gu, "$1")
|
|
.replace(/<\/?[A-Za-z][^>]*>/gu, " ")
|
|
.replace(/[`*_~]/gu, "")
|
|
.replace(/^\s{0,3}(?:>\s*|[-+*]\s+|\d+[.)]\s+)/u, "")
|
|
.trim();
|
|
}
|
|
|
|
export function legalizeMarkdownBaseName(value: string) {
|
|
const normalized = stripMarkdownFormatting(value)
|
|
.normalize("NFKC")
|
|
.replace(/[^\p{L}\p{N}\s_-]+/gu, " ")
|
|
.replace(/\s+/gu, " ")
|
|
.trim()
|
|
.replace(/[ .]+$/u, "");
|
|
const shortened = Array.from(normalized).slice(0, 100).join("").trim();
|
|
if (!shortened) {
|
|
return fallbackBaseName;
|
|
}
|
|
return windowsReservedName.test(shortened)
|
|
? `文档 ${shortened}`
|
|
: shortened;
|
|
}
|
|
|
|
export function deriveMarkdownBaseName(markdown: string) {
|
|
const lines = markdown.replace(/^\uFEFF/u, "").split(/\r?\n/u);
|
|
let inFrontMatter = lines[0]?.trim() === "---";
|
|
let inFence = false;
|
|
let fenceMarker = "";
|
|
let fenceLength = 0;
|
|
let firstBodyLine = "";
|
|
|
|
for (let index = inFrontMatter ? 1 : 0; index < lines.length; index += 1) {
|
|
const line = lines[index] ?? "";
|
|
const trimmed = line.trim();
|
|
if (inFrontMatter) {
|
|
if (trimmed === "---" || trimmed === "...") {
|
|
inFrontMatter = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (inFence) {
|
|
const closingFence = trimmed.match(/^(`+|~+)\s*$/u)?.[1];
|
|
if (
|
|
closingFence?.[0] === fenceMarker &&
|
|
closingFence.length >= fenceLength
|
|
) {
|
|
inFence = false;
|
|
fenceMarker = "";
|
|
fenceLength = 0;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const fence = trimmed.match(/^(`{3,}|~{3,})/u)?.[1];
|
|
if (fence) {
|
|
if (!inFence) {
|
|
inFence = true;
|
|
fenceMarker = fence[0] ?? "";
|
|
fenceLength = fence.length;
|
|
}
|
|
continue;
|
|
}
|
|
if (inFence || !trimmed) {
|
|
continue;
|
|
}
|
|
|
|
const heading = line.match(/^\s{0,3}#\s+(.+?)\s*#*\s*$/u)?.[1];
|
|
if (heading) {
|
|
return legalizeMarkdownBaseName(heading);
|
|
}
|
|
if (!firstBodyLine) {
|
|
firstBodyLine = line;
|
|
}
|
|
}
|
|
|
|
return legalizeMarkdownBaseName(firstBodyLine);
|
|
}
|
|
|
|
export function createMarkdownFileName(markdown: string) {
|
|
return `${deriveMarkdownBaseName(markdown)}.md`;
|
|
}
|
|
|
|
export function ensureMarkdownFileName(fileName: string, markdown: string) {
|
|
const trimmed = fileName.trim();
|
|
if (!trimmed) {
|
|
return createMarkdownFileName(markdown);
|
|
}
|
|
return /\.(?:md|markdown)$/iu.test(trimmed)
|
|
? trimmed
|
|
: `${trimmed}.md`;
|
|
}
|
|
|
|
export function downloadMarkdown(markdown: string, fileName: string) {
|
|
const url = URL.createObjectURL(
|
|
new Blob([markdown], { type: "text/markdown;charset=utf-8" })
|
|
);
|
|
const anchor = document.createElement("a");
|
|
anchor.href = url;
|
|
anchor.download = fileName;
|
|
anchor.click();
|
|
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
}
|