新增能力: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 产物未配置代码签名。
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import { inflateSync } from "node:zlib";
|
||
import { describe, expect, it } from "vitest";
|
||
import { sampleMarkdown } from "../src/sample-markdown";
|
||
|
||
function readPngDimensions(base64: string) {
|
||
const png = Buffer.from(base64, "base64");
|
||
expect(png.subarray(0, 8)).toEqual(
|
||
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
|
||
);
|
||
const width = png.readUInt32BE(16);
|
||
const height = png.readUInt32BE(20);
|
||
const bitDepth = png[24] ?? 0;
|
||
const colorType = png[25] ?? 0;
|
||
const samplesPerPixel =
|
||
colorType === 0 || colorType === 3
|
||
? 1
|
||
: colorType === 2
|
||
? 3
|
||
: colorType === 4
|
||
? 2
|
||
: 4;
|
||
const idatParts: Buffer[] = [];
|
||
let offset = 8;
|
||
while (offset + 12 <= png.length) {
|
||
const length = png.readUInt32BE(offset);
|
||
const type = png.toString("ascii", offset + 4, offset + 8);
|
||
if (type === "IDAT") {
|
||
idatParts.push(
|
||
png.subarray(offset + 8, offset + 8 + length)
|
||
);
|
||
}
|
||
offset += 12 + length;
|
||
}
|
||
const decoded = inflateSync(Buffer.concat(idatParts));
|
||
const rowBytes = Math.ceil(
|
||
(width * bitDepth * samplesPerPixel) / 8
|
||
);
|
||
expect(decoded.length).toBe((rowBytes + 1) * height);
|
||
return { width, height };
|
||
}
|
||
|
||
describe("默认示例 Markdown", () => {
|
||
it("仅保留一张网络图片并提供三种 Base64 图片尺寸", () => {
|
||
expect(
|
||
sampleMarkdown.match(/https?:\/\/[^\s)]+/gu)
|
||
).toHaveLength(1);
|
||
expect(
|
||
sampleMarkdown.match(/data:image\/png;base64,/gu)
|
||
).toHaveLength(3);
|
||
expect(sampleMarkdown).toContain("1600×900");
|
||
expect(sampleMarkdown).toContain("900×1800");
|
||
expect(sampleMarkdown).toContain("1200×880");
|
||
});
|
||
|
||
it("所有内嵌 PNG 均可完整解压且尺寸符合描述", () => {
|
||
const images = Array.from(
|
||
sampleMarkdown.matchAll(
|
||
/data:image\/png;base64,([A-Za-z0-9+/=]+)/gu
|
||
),
|
||
(match) => readPngDimensions(match[1] ?? "")
|
||
);
|
||
expect(images).toEqual([
|
||
{ width: 1600, height: 900 },
|
||
{ width: 900, height: 1800 },
|
||
{ width: 1200, height: 880 }
|
||
]);
|
||
});
|
||
|
||
it("覆盖 Mermaid、ECharts 分档和串行媒体场景", () => {
|
||
expect(
|
||
sampleMarkdown.match(/```mermaid/gu)
|
||
).toHaveLength(3);
|
||
expect(
|
||
sampleMarkdown.match(/```echarts/gu)
|
||
).toHaveLength(4);
|
||
expect(sampleMarkdown).toContain("height: 40mm");
|
||
expect(sampleMarkdown).toContain("height: 58mm");
|
||
expect(sampleMarkdown).toContain("height: 85mm");
|
||
expect(sampleMarkdown).toContain("## 串行媒体块");
|
||
});
|
||
});
|