release: 发布 v0.5.0
新增共享 Preview Engine,统一 Web 连续预览、快速分页、Playwright PDF 与 Electron PDF;实现稳定前缀复用和修改位置后的增量分页,保留媒体块按文档顺序串行回填与单次重排。 完善跨端链接与桌面文档工作流:Web 受控处理锚点和 HTTP/HTTPS 外链;Desktop 支持本地路径、file URI、系统协议、多窗口、同文件单例、Markdown 当前或新窗口打开,以及聚焦时外部文件变化提示。 统一四套内置主题名称并默认使用 Typora Github;修复连续预览双滚动条、ECharts 尺寸、PDF 本地链接、围栏代码块 Typora DOM 与重复行内样式;桌面发行链强制完整重建内嵌 Web,避免安装包携带陈旧资源。 发布 Web/Compose 与 Windows NSIS/ZIP:镜像 yixiong/md-to-pdf:v0.5.0 已健康部署;NSIS SHA-256 为 60992D1FDCA513F46346C78478537EB4159D8C0E76B41ECF3CDC25BE77707D92,ZIP SHA-256 为 D74F82293FB67126E583546CBA894569EFC9A0B1B6343FAC648CCC95F1D188D8,本机安装版已升级至 v0.5.0。 验证:全项目 238 项测试通过,类型检查、生产构建和 git diff --check 通过;Web 快速/连续/精确预览、Compose、Desktop 多窗口、窗口状态、文件关联、链接与代码块均完成真实环境验收。
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
findCommonPrefixLength,
|
||||
mountContinuousRenderStage
|
||||
} from "../src/continuous-preview.js";
|
||||
|
||||
describe("连续预览增量边界", () => {
|
||||
it("从首个变化块开始替换后缀", () => {
|
||||
expect(
|
||||
findCommonPrefixLength(
|
||||
["标题", "段落 A", "图表", "结尾"],
|
||||
["标题", "段落 A", "新图表", "新结尾"]
|
||||
)
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it("处理新增、删除和完全相同的文档", () => {
|
||||
expect(findCommonPrefixLength(["A"], ["A", "B"])).toBe(1);
|
||||
expect(findCommonPrefixLength(["A", "B"], ["A"])).toBe(1);
|
||||
expect(findCommonPrefixLength(["A", "B"], ["A", "B"])).toBe(2);
|
||||
expect(findCommonPrefixLength(["A"], ["B"])).toBe(0);
|
||||
});
|
||||
|
||||
it("将变化后缀挂载到真实 DOM 测量且提交时不保留包装层", () => {
|
||||
const root = document.createElement("div");
|
||||
const article = document.createElement("article");
|
||||
article.id = "write";
|
||||
article.className = "theme-document";
|
||||
const suffix = document.createDocumentFragment();
|
||||
const heading = document.createElement("h2");
|
||||
heading.textContent = "图表";
|
||||
const chart = document.createElement("figure");
|
||||
chart.className = "md-echarts";
|
||||
suffix.append(heading, chart);
|
||||
document.body.append(root);
|
||||
|
||||
const stage = mountContinuousRenderStage(
|
||||
root,
|
||||
article,
|
||||
suffix
|
||||
);
|
||||
|
||||
expect(stage.article.isConnected).toBe(true);
|
||||
expect(stage.article.parentElement).toBe(root);
|
||||
expect(stage.article.id).toBe("write");
|
||||
expect(stage.article.className).toBe("theme-document");
|
||||
expect(stage.article.dataset.continuousRenderStage).toBe("true");
|
||||
expect(Array.from(stage.article.children)).toEqual([
|
||||
heading,
|
||||
chart
|
||||
]);
|
||||
|
||||
const rendered = stage.takeContent();
|
||||
expect(Array.from(rendered.children)).toEqual([heading, chart]);
|
||||
expect(stage.article.childNodes).toHaveLength(0);
|
||||
|
||||
stage.dispose();
|
||||
expect(stage.article.isConnected).toBe(false);
|
||||
expect(root.childNodes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { defaultExportConfig } from "@md-to-pdf/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fitDocumentImagesToPage } from "../src/document-image-fit.js";
|
||||
|
||||
describe("Markdown 图片单页适配", () => {
|
||||
it("标记独立图片段落并将超高图片缩放到单页", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML =
|
||||
'<p> <img class="md-document-image" src="data:image/png;base64,AA=="> </p>';
|
||||
const image = root.querySelector("img")!;
|
||||
Object.defineProperties(image, {
|
||||
naturalWidth: { value: 1000 },
|
||||
naturalHeight: { value: 3000 }
|
||||
});
|
||||
|
||||
fitDocumentImagesToPage(root, defaultExportConfig);
|
||||
|
||||
expect(image.parentElement?.classList).toContain(
|
||||
"md-document-image-block"
|
||||
);
|
||||
expect(image.dataset.pageHeightFitted).toBe("true");
|
||||
expect(image.parentElement?.dataset.pageHeightFitted).toBe("true");
|
||||
expect(
|
||||
image.parentElement?.style.getPropertyValue("margin-block")
|
||||
).toBe("0");
|
||||
expect(Number.parseFloat(image.style.height)).toBeCloseTo(
|
||||
1000.5748,
|
||||
3
|
||||
);
|
||||
expect(Number.parseFloat(image.style.width)).toBeCloseTo(
|
||||
333.5249,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it("普通横图保持主题控制的尺寸", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML =
|
||||
'<p><img class="md-document-image" src="data:image/png;base64,AA=="></p>';
|
||||
const image = root.querySelector("img")!;
|
||||
Object.defineProperties(image, {
|
||||
naturalWidth: { value: 1600 },
|
||||
naturalHeight: { value: 900 }
|
||||
});
|
||||
|
||||
fitDocumentImagesToPage(root, defaultExportConfig);
|
||||
|
||||
expect(image.dataset.pageHeightFitted).toBeUndefined();
|
||||
expect(image.style.height).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { defaultExportConfig } from "@md-to-pdf/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fitOversizedEChartsToPage } from "../src/echarts-page-fit.js";
|
||||
|
||||
describe("ECharts 单页高度适配", () => {
|
||||
it("将超高 SVG 和宿主同步缩放到单页内容区", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<h2 style="margin: 10px 0">大型图表</h2>
|
||||
<figure class="md-echarts">
|
||||
<div class="md-echarts-host" style="height: 2000px">
|
||||
<svg width="1000" height="2000"></svg>
|
||||
</div>
|
||||
</figure>
|
||||
`;
|
||||
|
||||
const figure = root.querySelector<HTMLElement>(".md-echarts");
|
||||
const title = root.querySelector<HTMLElement>("h2");
|
||||
const host = root.querySelector<HTMLElement>(
|
||||
".md-echarts-host"
|
||||
);
|
||||
const svg = root.querySelector<SVGSVGElement>("svg");
|
||||
if (title) {
|
||||
title.getBoundingClientRect = () => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 700,
|
||||
bottom: 40,
|
||||
width: 700,
|
||||
height: 40,
|
||||
toJSON: () => ({})
|
||||
});
|
||||
}
|
||||
fitOversizedEChartsToPage(root, defaultExportConfig);
|
||||
|
||||
expect(figure?.dataset.pageHeightFitted).toBe("true");
|
||||
expect(title?.style.breakAfter).toBe("avoid-page");
|
||||
expect(figure?.style.breakAfter).toBe("page");
|
||||
expect(figure?.style.pageBreakAfter).toBe("always");
|
||||
expect(figure?.nextElementSibling).toHaveProperty(
|
||||
"dataset.mediaPageBreak",
|
||||
"true"
|
||||
);
|
||||
expect(Number.parseFloat(host?.style.height ?? "0")).toBeCloseTo(
|
||||
960.5748,
|
||||
3
|
||||
);
|
||||
expect(host?.style.aspectRatio).toBe("");
|
||||
expect(svg?.getAttribute("viewBox")).toBe("0 0 1000 2000");
|
||||
expect(Number.parseFloat(svg?.style.height ?? "0")).toBeCloseTo(
|
||||
960.5748,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it("普通高度图表不改变作者设置的宿主高度", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<figure class="md-echarts">
|
||||
<div class="md-echarts-host" style="height: 300px">
|
||||
<svg viewBox="0 0 1000 400"></svg>
|
||||
</div>
|
||||
</figure>
|
||||
`;
|
||||
|
||||
fitOversizedEChartsToPage(root, defaultExportConfig);
|
||||
|
||||
const figure = root.querySelector<HTMLElement>(".md-echarts");
|
||||
const host = root.querySelector<HTMLElement>(
|
||||
".md-echarts-host"
|
||||
);
|
||||
expect(figure?.dataset.pageHeightFitted).toBeUndefined();
|
||||
expect(host?.style.height).toBe("300px");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
assignPagedSourceReferences,
|
||||
createIncrementalPaginationPlan,
|
||||
findSourceNodePageIndex
|
||||
} from "../src/incremental-pagination.js";
|
||||
|
||||
function createSourceNode(reference: string) {
|
||||
const node = document.createElement("section");
|
||||
node.dataset.ref = reference;
|
||||
return node;
|
||||
}
|
||||
|
||||
function createPage(
|
||||
references: string[],
|
||||
startReference?: string
|
||||
) {
|
||||
const element = document.createElement("article");
|
||||
for (const reference of references) {
|
||||
element.append(createSourceNode(reference));
|
||||
}
|
||||
return {
|
||||
element,
|
||||
startToken: startReference
|
||||
? {
|
||||
node: createSourceNode(startReference),
|
||||
offset: 0
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
describe("快速预览增量分页计划", () => {
|
||||
it("从变化块所在页的前一页开始续排", () => {
|
||||
const sourceNodes = [
|
||||
createSourceNode("a"),
|
||||
createSourceNode("b"),
|
||||
createSourceNode("c"),
|
||||
createSourceNode("d")
|
||||
];
|
||||
const pages = [
|
||||
createPage(["a"]),
|
||||
createPage(["b"], "b"),
|
||||
createPage(["c"], "c"),
|
||||
createPage(["d"], "d")
|
||||
];
|
||||
|
||||
expect(
|
||||
createIncrementalPaginationPlan(
|
||||
2,
|
||||
sourceNodes.length,
|
||||
sourceNodes,
|
||||
pages
|
||||
)
|
||||
).toEqual({
|
||||
prefixLength: 2,
|
||||
invalidationPageIndex: 1,
|
||||
startToken: pages[1]?.startToken
|
||||
});
|
||||
});
|
||||
|
||||
it("尾部新增时以旧文档最后一个块定位并回退一页", () => {
|
||||
const sourceNodes = [
|
||||
createSourceNode("a"),
|
||||
createSourceNode("b")
|
||||
];
|
||||
const pages = [
|
||||
createPage(["a"]),
|
||||
createPage(["b"], "b")
|
||||
];
|
||||
|
||||
expect(
|
||||
createIncrementalPaginationPlan(
|
||||
2,
|
||||
sourceNodes.length,
|
||||
sourceNodes,
|
||||
pages
|
||||
)?.invalidationPageIndex
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it("无法验证边界时拒绝使用旧分页缓存", () => {
|
||||
const sourceNodes = [createSourceNode("a")];
|
||||
const pages = [createPage(["other"])];
|
||||
|
||||
expect(
|
||||
createIncrementalPaginationPlan(
|
||||
1,
|
||||
sourceNodes.length,
|
||||
sourceNodes,
|
||||
pages
|
||||
)
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
createIncrementalPaginationPlan(
|
||||
0,
|
||||
sourceNodes.length,
|
||||
sourceNodes,
|
||||
pages
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("通过 data-ref 在分页结果中反查源节点页码", () => {
|
||||
const pages = [
|
||||
createPage(["a"]),
|
||||
createPage(["b", "c"])
|
||||
];
|
||||
expect(
|
||||
findSourceNodePageIndex(pages, createSourceNode("c"))
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it("只为新节点补充分页引用并保留既有引用", () => {
|
||||
const root = document.createElement("article");
|
||||
root.dataset.ref = "existing-root";
|
||||
root.id = "write";
|
||||
const child = document.createElement("p");
|
||||
child.id = "paragraph";
|
||||
root.append(child);
|
||||
let sequence = 0;
|
||||
|
||||
assignPagedSourceReferences(root, () => {
|
||||
sequence += 1;
|
||||
return `generated-${sequence}`;
|
||||
});
|
||||
|
||||
expect(root.dataset.ref).toBe("existing-root");
|
||||
expect(root.dataset.id).toBe("write");
|
||||
expect(child.dataset.ref).toBe("generated-1");
|
||||
expect(child.dataset.id).toBe("paragraph");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyMediaBackfillCandidates,
|
||||
calculateMediaBackfillScale,
|
||||
findMediaBackfillCandidates,
|
||||
getMediaBackfillIdsInDocumentOrder,
|
||||
prepareMediaBackfillBlocks
|
||||
} from "../src/media-page-backfill.js";
|
||||
import { getPrecedingMediaTitleHeight } from "../src/diagram-page-fit.js";
|
||||
|
||||
function rect(width: number, height: number) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: width,
|
||||
bottom: height,
|
||||
width,
|
||||
height,
|
||||
toJSON: () => ({})
|
||||
};
|
||||
}
|
||||
|
||||
function positionedRect(
|
||||
top: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
return {
|
||||
...rect(width, height),
|
||||
y: top,
|
||||
top,
|
||||
bottom: top + height
|
||||
};
|
||||
}
|
||||
|
||||
describe("媒体分页空白回填", () => {
|
||||
it("全页适配时保留前置标题高度并避免标题孤行", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<h2 style="margin: 10px 0 12px">媒体标题</h2>
|
||||
<div class="mermaid"></div>
|
||||
`;
|
||||
const title = root.querySelector<HTMLElement>("h2")!;
|
||||
const block = root.querySelector<HTMLElement>(".mermaid")!;
|
||||
title.getBoundingClientRect = () =>
|
||||
positionedRect(100, 600, 40);
|
||||
|
||||
expect(getPrecedingMediaTitleHeight(block)).toBe(40);
|
||||
expect(title.style.getPropertyValue("break-after")).toBe(
|
||||
"avoid-page"
|
||||
);
|
||||
expect(title.style.getPropertyPriority("break-after")).toBe(
|
||||
"important"
|
||||
);
|
||||
});
|
||||
|
||||
it("按限宽后的基准高度计算额外缩放率", () => {
|
||||
expect(
|
||||
calculateMediaBackfillScale({
|
||||
remainingHeight: 420,
|
||||
fixedHeight: 30,
|
||||
baselineVisualHeight: 430
|
||||
})
|
||||
).toBeCloseTo(388 / 430, 6);
|
||||
});
|
||||
|
||||
it("缩小超过阈值或无需缩小时不回填", () => {
|
||||
expect(
|
||||
calculateMediaBackfillScale({
|
||||
remainingHeight: 300,
|
||||
fixedHeight: 30,
|
||||
baselineVisualHeight: 430
|
||||
})
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
calculateMediaBackfillScale({
|
||||
remainingHeight: 500,
|
||||
fixedHeight: 30,
|
||||
baselineVisualHeight: 430
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("记录图片限宽后的实际基准尺寸", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<figure class="md-document-image-block">
|
||||
<img class="md-document-image">
|
||||
<figcaption>标题</figcaption>
|
||||
</figure>
|
||||
`;
|
||||
const image = root.querySelector("img")!;
|
||||
image.getBoundingClientRect = () => rect(640, 480);
|
||||
|
||||
expect(prepareMediaBackfillBlocks(root)).toBe(1);
|
||||
const figure = root.querySelector<HTMLElement>("figure")!;
|
||||
expect(figure.dataset.mediaBackfillKind).toBe("image");
|
||||
expect(figure.dataset.mediaBaselineWidth).toBe("640");
|
||||
expect(figure.dataset.mediaBaselineHeight).toBe("480");
|
||||
});
|
||||
|
||||
it("保持媒体元素在源文档中的串行处理顺序", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<figure data-media-backfill-id="media-2"></figure>
|
||||
<div><figure data-media-backfill-id="media-1"></figure></div>
|
||||
<figure></figure>
|
||||
`;
|
||||
|
||||
expect(getMediaBackfillIdsInDocumentOrder(root)).toEqual([
|
||||
"media-2",
|
||||
"media-1"
|
||||
]);
|
||||
});
|
||||
|
||||
it("根据上一页剩余区域生成一次回填候选", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<div class="pagedjs_page">
|
||||
<div class="pagedjs_page_content">
|
||||
<div
|
||||
class="pagedjs_page_content_flow"
|
||||
data-ref="previous-content"
|
||||
>上一页内容</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagedjs_page">
|
||||
<div class="pagedjs_page_content">
|
||||
<h2 data-ref="media-title">图片标题</h2>
|
||||
<figure
|
||||
class="md-document-image-block"
|
||||
data-media-backfill-id="media-1"
|
||||
data-media-baseline-width="600"
|
||||
data-media-baseline-height="400"
|
||||
>
|
||||
<img class="md-document-image">
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const pageContents = root.querySelectorAll<HTMLElement>(
|
||||
".pagedjs_page_content"
|
||||
);
|
||||
const previousFlow =
|
||||
pageContents[0]!.firstElementChild as HTMLElement;
|
||||
const title = root.querySelector<HTMLElement>("h2")!;
|
||||
const block = root.querySelector<HTMLElement>("figure")!;
|
||||
const image = root.querySelector<HTMLImageElement>("img")!;
|
||||
pageContents[0]!.getBoundingClientRect = () =>
|
||||
positionedRect(100, 700, 1_000);
|
||||
previousFlow.getBoundingClientRect = () =>
|
||||
positionedRect(100, 700, 550);
|
||||
pageContents[1]!.getBoundingClientRect = () =>
|
||||
positionedRect(100, 700, 1_000);
|
||||
title.getBoundingClientRect = () =>
|
||||
positionedRect(100, 600, 40);
|
||||
block.getBoundingClientRect = () =>
|
||||
positionedRect(150, 600, 330);
|
||||
image.getBoundingClientRect = () =>
|
||||
positionedRect(150, 600, 300);
|
||||
|
||||
expect(findMediaBackfillCandidates(root)).toEqual([
|
||||
{ id: "media-1", scale: 0.92 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("显式分页的媒体块不会回填到上一页", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<div class="pagedjs_page">
|
||||
<div class="pagedjs_page_content"></div>
|
||||
</div>
|
||||
<div class="pagedjs_page">
|
||||
<div class="pagedjs_page_content">
|
||||
<h2 data-ref="media-title" style="break-before: page">
|
||||
图片标题
|
||||
</h2>
|
||||
<figure
|
||||
class="md-document-image-block"
|
||||
data-ref="media-block"
|
||||
data-media-backfill-id="media-1"
|
||||
data-media-baseline-width="600"
|
||||
data-media-baseline-height="400"
|
||||
>
|
||||
<img class="md-document-image">
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const pageContents = root.querySelectorAll<HTMLElement>(
|
||||
".pagedjs_page_content"
|
||||
);
|
||||
const title = root.querySelector<HTMLElement>("h2")!;
|
||||
const block = root.querySelector<HTMLElement>("figure")!;
|
||||
const image = root.querySelector<HTMLImageElement>("img")!;
|
||||
pageContents[0]!.getBoundingClientRect = () =>
|
||||
positionedRect(100, 700, 1_000);
|
||||
pageContents[1]!.getBoundingClientRect = () =>
|
||||
positionedRect(100, 700, 1_000);
|
||||
title.getBoundingClientRect = () =>
|
||||
positionedRect(100, 600, 40);
|
||||
block.getBoundingClientRect = () =>
|
||||
positionedRect(150, 600, 330);
|
||||
image.getBoundingClientRect = () =>
|
||||
positionedRect(150, 600, 300);
|
||||
|
||||
expect(findMediaBackfillCandidates(root)).toEqual([]);
|
||||
});
|
||||
|
||||
it("忽略媒体之前其他内容的显式分页规则", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<div class="pagedjs_page">
|
||||
<div class="pagedjs_page_content">
|
||||
<div data-ref="previous-content">上一页内容</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pagedjs_page">
|
||||
<div class="pagedjs_page_content">
|
||||
<h1 data-ref="earlier-heading" style="break-before: page">
|
||||
本页章节
|
||||
</h1>
|
||||
<h2 data-ref="media-title">图片标题</h2>
|
||||
<figure
|
||||
class="md-document-image-block"
|
||||
data-ref="media-block"
|
||||
data-media-backfill-id="media-1"
|
||||
data-media-baseline-width="600"
|
||||
data-media-baseline-height="400"
|
||||
>
|
||||
<img class="md-document-image">
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const pageContents = root.querySelectorAll<HTMLElement>(
|
||||
".pagedjs_page_content"
|
||||
);
|
||||
const previousFlow = root.querySelector<HTMLElement>(
|
||||
"[data-ref='previous-content']"
|
||||
)!;
|
||||
const earlierHeading = root.querySelector<HTMLElement>("h1")!;
|
||||
const title = root.querySelector<HTMLElement>("h2")!;
|
||||
const block = root.querySelector<HTMLElement>("figure")!;
|
||||
const image = root.querySelector<HTMLImageElement>("img")!;
|
||||
pageContents[0]!.getBoundingClientRect = () =>
|
||||
positionedRect(100, 700, 1_000);
|
||||
previousFlow.getBoundingClientRect = () =>
|
||||
positionedRect(100, 700, 550);
|
||||
pageContents[1]!.getBoundingClientRect = () =>
|
||||
positionedRect(100, 700, 1_000);
|
||||
earlierHeading.getBoundingClientRect = () =>
|
||||
positionedRect(100, 600, 40);
|
||||
title.getBoundingClientRect = () =>
|
||||
positionedRect(150, 600, 40);
|
||||
block.getBoundingClientRect = () =>
|
||||
positionedRect(200, 600, 330);
|
||||
image.getBoundingClientRect = () =>
|
||||
positionedRect(200, 600, 300);
|
||||
|
||||
expect(findMediaBackfillCandidates(root)).toEqual([
|
||||
{ id: "media-1", scale: 0.92 }
|
||||
]);
|
||||
});
|
||||
|
||||
it("将候选缩放应用到图片和 ECharts 主体", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML = `
|
||||
<h2>图片标题保持原尺寸</h2>
|
||||
<figure
|
||||
class="md-document-image-block"
|
||||
data-media-backfill-id="media-1"
|
||||
data-media-backfill-kind="image"
|
||||
data-media-baseline-width="800"
|
||||
data-media-baseline-height="600"
|
||||
>
|
||||
<img class="md-document-image">
|
||||
</figure>
|
||||
<figure
|
||||
class="md-echarts"
|
||||
data-media-backfill-id="media-2"
|
||||
data-media-backfill-kind="echarts"
|
||||
data-media-baseline-width="700"
|
||||
data-media-baseline-height="500"
|
||||
>
|
||||
<div class="md-echarts-host"><svg></svg></div>
|
||||
</figure>
|
||||
<div
|
||||
class="mermaid"
|
||||
data-media-backfill-id="media-3"
|
||||
data-media-backfill-kind="mermaid"
|
||||
data-media-baseline-width="600"
|
||||
data-media-baseline-height="400"
|
||||
>
|
||||
<svg></svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
expect(
|
||||
applyMediaBackfillCandidates(root, [
|
||||
{ id: "media-1", scale: 0.9 },
|
||||
{ id: "media-2", scale: 0.88 },
|
||||
{ id: "media-3", scale: 0.9 }
|
||||
])
|
||||
).toBe(3);
|
||||
const title = root.querySelector<HTMLElement>("h2")!;
|
||||
const image = root.querySelector<HTMLImageElement>(
|
||||
"img.md-document-image"
|
||||
)!;
|
||||
const host = root.querySelector<HTMLElement>(".md-echarts-host")!;
|
||||
const svg = root.querySelector<SVGSVGElement>("svg")!;
|
||||
const mermaidSvg = root.querySelector<SVGSVGElement>(
|
||||
".mermaid svg"
|
||||
)!;
|
||||
expect(title.getAttribute("style")).toBeNull();
|
||||
expect(image.style.width).toBe("720px");
|
||||
expect(image.style.height).toBe("540px");
|
||||
expect(host.style.width).toBe("616px");
|
||||
expect(host.style.height).toBe("440px");
|
||||
expect(svg.style.width).toBe("616px");
|
||||
expect(svg.style.height).toBe("440px");
|
||||
expect(mermaidSvg.style.width).toBe("540px");
|
||||
expect(mermaidSvg.style.height).toBe("360px");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { defaultExportConfig } from "@md-to-pdf/core";
|
||||
import {
|
||||
createMermaidSiteConfig,
|
||||
MERMAID_SECURE_CONFIG_KEYS
|
||||
} from "../src/mermaid-config.js";
|
||||
|
||||
describe("Mermaid 站点配置", () => {
|
||||
it("默认使用 default 主题和 classic 外观", () => {
|
||||
const config = createMermaidSiteConfig(defaultExportConfig.mermaid);
|
||||
expect(config.theme).toBe("default");
|
||||
expect(config.look).toBe("classic");
|
||||
expect(config.securityLevel).toBe("strict");
|
||||
expect(config.startOnLoad).toBe(false);
|
||||
expect(config.flowchart?.wrappingWidth).toBe(320);
|
||||
});
|
||||
|
||||
it("锁定安全限制和原始主题 CSS", () => {
|
||||
expect(MERMAID_SECURE_CONFIG_KEYS).toEqual(
|
||||
expect.arrayContaining([
|
||||
"secure",
|
||||
"securityLevel",
|
||||
"startOnLoad",
|
||||
"maxTextSize",
|
||||
"maxEdges",
|
||||
"suppressErrorRendering",
|
||||
"themeCSS"
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("每次创建独立的 secure 配置数组", () => {
|
||||
const first = createMermaidSiteConfig(defaultExportConfig.mermaid);
|
||||
const second = createMermaidSiteConfig(defaultExportConfig.mermaid);
|
||||
expect(first.secure).toEqual(second.secure);
|
||||
expect(first.secure).not.toBe(second.secure);
|
||||
});
|
||||
|
||||
it("应用导出配置中的布局、主题、外观和字体", () => {
|
||||
const config = createMermaidSiteConfig({
|
||||
layout: "elk",
|
||||
theme: "forest",
|
||||
look: "handDrawn",
|
||||
fontFamily: "Microsoft YaHei, sans-serif"
|
||||
});
|
||||
|
||||
expect(config).toMatchObject({
|
||||
layout: "elk",
|
||||
theme: "forest",
|
||||
look: "handDrawn",
|
||||
fontFamily: "Microsoft YaHei, sans-serif"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { defaultExportConfig } from "@md-to-pdf/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
calculateMermaidPageFit,
|
||||
getPageContentDimensions,
|
||||
parseSvgViewBox
|
||||
} from "../src/mermaid-page-fit.js";
|
||||
|
||||
describe("Mermaid 单页高度适配", () => {
|
||||
it("解析 SVG viewBox 尺寸", () => {
|
||||
expect(parseSvgViewBox("0 0 1200 2400")).toEqual({
|
||||
width: 1200,
|
||||
height: 2400
|
||||
});
|
||||
expect(parseSvgViewBox("0,0,800,600")).toEqual({
|
||||
width: 800,
|
||||
height: 600
|
||||
});
|
||||
expect(parseSvgViewBox("0 0 0 600")).toBeUndefined();
|
||||
expect(parseSvgViewBox(null)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("使用纸张和页边距计算正文区域", () => {
|
||||
const dimensions = getPageContentDimensions(defaultExportConfig);
|
||||
|
||||
expect(dimensions.width).toBeCloseTo(672.7559, 3);
|
||||
expect(dimensions.height).toBeCloseTo(1001.5748, 3);
|
||||
});
|
||||
|
||||
it("宽度适配后未超高时保持普通 Mermaid 布局", () => {
|
||||
const fit = calculateMermaidPageFit(
|
||||
{ width: 1600, height: 900 },
|
||||
{ width: 672, height: 1000 }
|
||||
);
|
||||
|
||||
expect(fit).toEqual({
|
||||
width: 672,
|
||||
height: 378,
|
||||
scaled: false
|
||||
});
|
||||
});
|
||||
|
||||
it("宽度适配后超高时按页面高度等比例缩小", () => {
|
||||
const fit = calculateMermaidPageFit(
|
||||
{ width: 1000, height: 2000 },
|
||||
{ width: 672, height: 1000 }
|
||||
);
|
||||
|
||||
expect(fit.scaled).toBe(true);
|
||||
expect(fit.height).toBe(999);
|
||||
expect(fit.width).toBe(499.5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderMermaidDefinitions } from "../src/mermaid-renderer.js";
|
||||
|
||||
describe("Mermaid 独立图表渲染", () => {
|
||||
it("单个图表失败后继续渲染后续图表", async () => {
|
||||
const renderDefinition = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("语法错误"))
|
||||
.mockResolvedValueOnce({ svg: "<svg>第二张图</svg>" });
|
||||
|
||||
const outcomes = await renderMermaidDefinitions(
|
||||
["invalid", "flowchart LR\nA --> B"],
|
||||
renderDefinition
|
||||
);
|
||||
|
||||
expect(renderDefinition).toHaveBeenCalledTimes(2);
|
||||
expect(outcomes[0]).toMatchObject({
|
||||
success: false,
|
||||
error: expect.any(Error)
|
||||
});
|
||||
expect(outcomes[1]).toEqual({
|
||||
success: true,
|
||||
svg: "<svg>第二张图</svg>"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createSvgDataUrl,
|
||||
getLockedMermaidBoxSize,
|
||||
getMermaidImageAlt,
|
||||
resolveMermaidOutputMode
|
||||
} from "../src/mermaid-static-image.js";
|
||||
|
||||
describe("Mermaid 静态 SVG 图片", () => {
|
||||
it("生成无需外部请求的 UTF-8 Data URL", () => {
|
||||
const url = createSvgDataUrl(
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"><text>中文 & A</text></svg>'
|
||||
);
|
||||
|
||||
expect(url).toMatch(/^data:image\/svg\+xml;charset=utf-8,/);
|
||||
expect(decodeURIComponent(url.split(",")[1] ?? "")).toContain(
|
||||
"<text>中文 & A</text>"
|
||||
);
|
||||
});
|
||||
|
||||
it("保留渲染后的精确小数尺寸", () => {
|
||||
expect(
|
||||
getLockedMermaidBoxSize({
|
||||
width: 742.375,
|
||||
height: 386.625
|
||||
})
|
||||
).toEqual({
|
||||
width: 742.375,
|
||||
height: 386.625
|
||||
});
|
||||
expect(
|
||||
getLockedMermaidBoxSize({
|
||||
width: 0,
|
||||
height: 386.625
|
||||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("优先使用无障碍标签并回退到 SVG 标题", () => {
|
||||
expect(getMermaidImageAlt(" 数据链路 ", "备用标题")).toBe(
|
||||
"数据链路"
|
||||
);
|
||||
expect(getMermaidImageAlt(null, " 数据关系图 ")).toBe(
|
||||
"数据关系图"
|
||||
);
|
||||
expect(getMermaidImageAlt(" ", null)).toBe("Mermaid 图表");
|
||||
});
|
||||
|
||||
it("默认使用静态 SVG 并允许内部切回内联模式", () => {
|
||||
expect(resolveMermaidOutputMode("")).toBe("svg-image");
|
||||
expect(resolveMermaidOutputMode("?target=pdf")).toBe("svg-image");
|
||||
expect(
|
||||
resolveMermaidOutputMode(
|
||||
"?target=pdf&mermaid-output=inline-svg"
|
||||
)
|
||||
).toBe("inline-svg");
|
||||
expect(
|
||||
resolveMermaidOutputMode("?mermaid-output=unknown")
|
||||
).toBe("svg-image");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveBreakTokenElement } from "../src/paged-break-token.js";
|
||||
|
||||
describe("Paged.js 分页断点", () => {
|
||||
it("直接返回元素断点", () => {
|
||||
const element = {
|
||||
nodeType: 1,
|
||||
parentElement: null
|
||||
} as unknown as Element;
|
||||
|
||||
expect(resolveBreakTokenElement(element)).toBe(element);
|
||||
});
|
||||
|
||||
it("文本断点使用父元素继续查找", () => {
|
||||
const parentElement = {} as Element;
|
||||
const textNode = {
|
||||
nodeType: 3,
|
||||
parentElement
|
||||
} as unknown as Node;
|
||||
|
||||
expect(resolveBreakTokenElement(textNode)).toBe(parentElement);
|
||||
});
|
||||
|
||||
it("安全跳过空断点和无父元素节点", () => {
|
||||
const detachedTextNode = {
|
||||
nodeType: 3,
|
||||
parentElement: null
|
||||
} as unknown as Node;
|
||||
|
||||
expect(resolveBreakTokenElement(undefined)).toBeUndefined();
|
||||
expect(
|
||||
resolveBreakTokenElement(detachedTextNode)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
import { defaultExportConfig } from "@md-to-pdf/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPagedMediaCss,
|
||||
continuousDocumentGeometryCss,
|
||||
createPagedPreviewRenderRequest,
|
||||
documentBaseCss,
|
||||
documentGeometryCss,
|
||||
documentInteractionCss,
|
||||
formatPageNumber,
|
||||
isPagedPreviewFrameMessage,
|
||||
isPagedPreviewRenderRequest,
|
||||
PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
type PagedPreviewPayload
|
||||
} from "../src/paged-preview.js";
|
||||
|
||||
const payload: PagedPreviewPayload = {
|
||||
articleHtml: '<article id="write"><h1>分页测试</h1></article>',
|
||||
fileName: "分页测试.md",
|
||||
metadata: {
|
||||
title: "分页测试",
|
||||
author: "",
|
||||
subject: "",
|
||||
keywords: [],
|
||||
language: "zh-CN"
|
||||
},
|
||||
features: [],
|
||||
themeCss: "#write { color: #333; }",
|
||||
exportConfig: defaultExportConfig
|
||||
};
|
||||
|
||||
describe("分页预览协议", () => {
|
||||
it("生成真实纸张尺寸和页边距 CSS", () => {
|
||||
const css = buildPagedMediaCss(defaultExportConfig, payload);
|
||||
|
||||
expect(css).toContain("size: 210mm 297mm");
|
||||
expect(css).toContain("margin: 16mm 16mm");
|
||||
expect(css).toContain("@bottom-center");
|
||||
expect(css).not.toContain("counter-reset: page");
|
||||
expect(css).toContain("#write thead");
|
||||
expect(css).toContain("display: table-header-group");
|
||||
expect(css).toContain("break-inside: avoid");
|
||||
expect(css).toContain(".pagedjs_pages");
|
||||
expect(css).toContain("padding: 34px 0");
|
||||
expect(css).toContain('html[data-render-target="preview"]');
|
||||
expect(css).toContain("overflow-x: hidden");
|
||||
expect(css).toContain("overflow-y: hidden");
|
||||
expect(css).not.toContain("scrollbar-color");
|
||||
expect(css).toContain('html[data-render-target="pdf"]');
|
||||
expect(css).toContain("height: auto !important");
|
||||
expect(css).toContain("overflow: visible !important");
|
||||
expect(css).toContain("padding: 0");
|
||||
expect(css).toContain("break-after: page");
|
||||
expect(css).not.toContain("@media print");
|
||||
});
|
||||
|
||||
it("安全替换页眉变量并生成三栏页边距盒", () => {
|
||||
const css = buildPagedMediaCss(
|
||||
{
|
||||
...defaultExportConfig,
|
||||
header: {
|
||||
...defaultExportConfig.header,
|
||||
enabled: true,
|
||||
showDivider: true,
|
||||
left: {
|
||||
enabled: true,
|
||||
content: '${title} — ${filename} "测试"'
|
||||
},
|
||||
center: {
|
||||
enabled: true,
|
||||
content: "${author}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
...payload,
|
||||
metadata: {
|
||||
...payload.metadata,
|
||||
author: "内网团队"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(css).toContain("@top-left");
|
||||
expect(css).toContain("@top-center");
|
||||
expect(css).toContain("@top-right");
|
||||
expect(css).toContain(
|
||||
'content: "分页测试 — 分页测试.md \\"测试\\""'
|
||||
);
|
||||
expect(css).toContain('content: "内网团队"');
|
||||
expect(css).toContain("border-bottom: 0.2mm solid currentColor");
|
||||
});
|
||||
|
||||
it("支持自定义页码模板和起始页码", () => {
|
||||
const css = buildPagedMediaCss(
|
||||
{
|
||||
...defaultExportConfig,
|
||||
footer: {
|
||||
...defaultExportConfig.footer,
|
||||
format: "custom",
|
||||
template: "第 ${page} / ${pages} 页",
|
||||
alignment: "right",
|
||||
startFrom: 5
|
||||
}
|
||||
},
|
||||
payload
|
||||
);
|
||||
|
||||
expect(css).toContain("@bottom-right");
|
||||
expect(
|
||||
formatPageNumber(
|
||||
{
|
||||
...defaultExportConfig.footer,
|
||||
format: "custom",
|
||||
template: "第 ${page} / ${pages} 页",
|
||||
startFrom: 5
|
||||
},
|
||||
2,
|
||||
8
|
||||
)
|
||||
).toBe("第 7 / 8 页");
|
||||
});
|
||||
|
||||
it("逐页递增内置页码格式但保持真实总页数", () => {
|
||||
expect(
|
||||
formatPageNumber(defaultExportConfig.footer, 0, 5)
|
||||
).toBe("1 / 5");
|
||||
expect(
|
||||
formatPageNumber(defaultExportConfig.footer, 1, 5)
|
||||
).toBe("2 / 5");
|
||||
expect(
|
||||
formatPageNumber(
|
||||
{
|
||||
...defaultExportConfig.footer,
|
||||
format: "chinese-page-total",
|
||||
startFrom: 5
|
||||
},
|
||||
3,
|
||||
5
|
||||
)
|
||||
).toBe("第 8 页 / 共 5 页");
|
||||
expect(
|
||||
formatPageNumber(
|
||||
{
|
||||
...defaultExportConfig.footer,
|
||||
format: "dash-page"
|
||||
},
|
||||
4,
|
||||
5
|
||||
)
|
||||
).toBe("- 5 -");
|
||||
});
|
||||
|
||||
it("在主题 CSS 之后强制分页画布保持透明", () => {
|
||||
expect(documentGeometryCss).toContain(
|
||||
"background: transparent !important"
|
||||
);
|
||||
});
|
||||
|
||||
it("避免 Typora 围栏容器重复应用行内代码盒模型", () => {
|
||||
expect(documentBaseCss).toContain(
|
||||
"#write pre.md-fences > code"
|
||||
);
|
||||
expect(documentBaseCss).toContain("background: transparent;");
|
||||
expect(documentBaseCss).toContain("font-size: inherit;");
|
||||
expect(documentBaseCss).not.toMatch(
|
||||
/#write pre\.md-fences > code\s*\{[^}]*!important/su
|
||||
);
|
||||
});
|
||||
|
||||
it("以低优先级兜底样式保留链接识别和焦点反馈", () => {
|
||||
expect(documentInteractionCss).toContain(":where(#write a[href])");
|
||||
expect(documentInteractionCss).toContain(
|
||||
"text-decoration-line: underline;"
|
||||
);
|
||||
expect(documentInteractionCss).toContain(":focus-visible");
|
||||
expect(documentInteractionCss).toContain("--md-accent");
|
||||
expect(documentInteractionCss).not.toContain("!important");
|
||||
});
|
||||
|
||||
it("连续预览由内容高度驱动且不创建内部滚动容器", () => {
|
||||
expect(continuousDocumentGeometryCss).toContain(
|
||||
"overflow: hidden !important"
|
||||
);
|
||||
expect(continuousDocumentGeometryCss).toContain(
|
||||
"min-height: 0 !important"
|
||||
);
|
||||
expect(continuousDocumentGeometryCss).toContain(
|
||||
'[data-continuous-render-stage="true"]'
|
||||
);
|
||||
expect(continuousDocumentGeometryCss).toContain(
|
||||
"position: fixed !important"
|
||||
);
|
||||
expect(continuousDocumentGeometryCss).not.toContain("100vh");
|
||||
});
|
||||
|
||||
it("生成并识别分页渲染请求", () => {
|
||||
const request = createPagedPreviewRenderRequest(7, payload);
|
||||
|
||||
expect(request).toEqual({
|
||||
scope: PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
type: "render",
|
||||
requestId: 7,
|
||||
layout: "paged",
|
||||
payload
|
||||
});
|
||||
expect(isPagedPreviewRenderRequest(request)).toBe(true);
|
||||
expect(
|
||||
createPagedPreviewRenderRequest(
|
||||
8,
|
||||
payload,
|
||||
"continuous"
|
||||
).layout
|
||||
).toBe("continuous");
|
||||
expect(
|
||||
isPagedPreviewRenderRequest({
|
||||
...request,
|
||||
requestId: "7"
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("只接受分页 iframe 协议消息", () => {
|
||||
expect(
|
||||
isPagedPreviewFrameMessage({
|
||||
scope: PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
type: "link",
|
||||
href: "https://example.com"
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPagedPreviewFrameMessage({
|
||||
scope: PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
type: "link",
|
||||
href: ""
|
||||
})
|
||||
).toBe(false);
|
||||
expect(
|
||||
isPagedPreviewFrameMessage({
|
||||
scope: PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
type: "rendered",
|
||||
requestId: 3,
|
||||
layout: "paged",
|
||||
pageCount: 2,
|
||||
contentHeight: 2400,
|
||||
echartsErrors: [],
|
||||
mermaidErrors: []
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
isPagedPreviewFrameMessage({
|
||||
scope: PAGED_PREVIEW_MESSAGE_SCOPE,
|
||||
type: "rendered",
|
||||
requestId: 3,
|
||||
layout: "paged",
|
||||
pageCount: 2,
|
||||
echartsErrors: [],
|
||||
mermaidErrors: []
|
||||
})
|
||||
).toBe(false);
|
||||
expect(
|
||||
isPagedPreviewFrameMessage({
|
||||
scope: "other",
|
||||
type: "ready"
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolvePagedRenderTarget } from "../src/paged-render-target.js";
|
||||
|
||||
describe("分页文档渲染目标", () => {
|
||||
it("默认使用网页预览目标", () => {
|
||||
expect(resolvePagedRenderTarget("")).toBe("preview");
|
||||
expect(resolvePagedRenderTarget("?target=unknown")).toBe("preview");
|
||||
});
|
||||
|
||||
it("识别 PDF 渲染目标", () => {
|
||||
expect(resolvePagedRenderTarget("?target=pdf")).toBe("pdf");
|
||||
expect(resolvePagedRenderTarget("?foo=1&target=pdf")).toBe("pdf");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { preparePdfDocumentLinks } from "../src/pdf-document-links.js";
|
||||
|
||||
describe("PDF 本地链接准备", () => {
|
||||
it("只改写本地链接并保留网络、协议和锚点", () => {
|
||||
const root = document.createElement("article");
|
||||
root.innerHTML = [
|
||||
'<a href="../README.md">本地</a>',
|
||||
'<a href="file:///C:/docs/a.md">文件</a>',
|
||||
'<a href="https://example.com">网络</a>',
|
||||
'<a href="mailto:a@example.com">邮件</a>',
|
||||
'<a href="#chapter">锚点</a>'
|
||||
].join("");
|
||||
|
||||
expect(preparePdfDocumentLinks(root)).toBe(2);
|
||||
const hrefs = Array.from(root.querySelectorAll("a")).map(
|
||||
(link) => link.getAttribute("href")
|
||||
);
|
||||
expect(hrefs[0]).toContain(
|
||||
"https://mdpdf.local.invalid/document-link?"
|
||||
);
|
||||
expect(hrefs[1]).toContain(
|
||||
"https://mdpdf.local.invalid/document-link?"
|
||||
);
|
||||
expect(hrefs.slice(2)).toEqual([
|
||||
"https://example.com",
|
||||
"mailto:a@example.com",
|
||||
"#chapter"
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { enablePrintMediaForPreview } from "../src/preview-styles.js";
|
||||
|
||||
describe("打印媒体预览", () => {
|
||||
it("将打印媒体规则转换为屏幕预览规则", () => {
|
||||
const css = `
|
||||
html { font-size: 16px; }
|
||||
@media print {
|
||||
html { font-size: 13px; }
|
||||
}
|
||||
`;
|
||||
|
||||
expect(enablePrintMediaForPreview(css)).toContain("@media screen {");
|
||||
expect(enablePrintMediaForPreview(css)).toContain(
|
||||
"html { font-size: 13px; }"
|
||||
);
|
||||
});
|
||||
|
||||
it("支持 only print 和带条件的打印媒体规则", () => {
|
||||
const css = [
|
||||
"@media only print { body { color: black; } }",
|
||||
"@media print and (color) { body { background: white; } }"
|
||||
].join("\n");
|
||||
|
||||
expect(enablePrintMediaForPreview(css)).toBe(
|
||||
[
|
||||
"@media only screen { body { color: black; } }",
|
||||
"@media screen and (color) { body { background: white; } }"
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
it("不改变普通屏幕媒体规则", () => {
|
||||
const css = "@media screen and (min-width: 800px) { body { margin: 0; } }";
|
||||
expect(enablePrintMediaForPreview(css)).toBe(css);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user