release: 发布 v0.6.2 DOCX 真实文档修复
新增能力:将 DOCX 发布验收拆分为四套独立 140,支持真实语料冻结、指纹复用、失败与基础设施错误独立统计,并为表格换行、全 JSON 围栏、代码连续性和长文档分页建立通用门禁。 问题修复:冻结 Paged.js 分片前的逻辑表格列轨并传递打印几何,统一 Markdown 表格换行、代码、段落与 OOXML 翻译;改进 PDF 文本流排序、语义块映射、颜色与栅格比较,消除窄字符重叠和跨行范围符号误报。 兼容与部署:版本统一为 0.6.2;正式 Docker 镜像内置固定 Chromium、Pandoc 3.9.0.2 和 Serif/Sans/Mono 字体;Desktop NSIS 与 ZIP 继续直接内置字体,无需系统字体安装。 验证结果:合成基线与长庆严格 280/280,M4N 140/140;健康数据残余误报 6/115(5.22%),均核查为重复表头自动对齐/取样误报且基础设施错误为 0。全项目测试、类型检查、生产构建和 git diff --check 通过;正式 Docker、NSIS、ZIP、离线镜像、部署包、清单及 SHA-256 均已生成并校验。
This commit is contained in:
@@ -23,6 +23,7 @@ import hljs from "highlight.js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import type { Options as MarkdownItOptions } from "markdown-it";
|
||||
import type Renderer from "markdown-it/lib/renderer.mjs";
|
||||
import type StateInline from "markdown-it/lib/rules_inline/state_inline.mjs";
|
||||
import type Token from "markdown-it/lib/token.mjs";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { renderDocumentStructure } from "./render-document-structure.js";
|
||||
@@ -49,6 +50,7 @@ export interface RenderMarkdownOptions {
|
||||
export interface RenderedMarkdown
|
||||
extends Omit<RenderedMarkdownDocument, "rendererVersion"> {
|
||||
rendererVersion: typeof RENDERER_VERSION;
|
||||
markdownBody: string;
|
||||
}
|
||||
|
||||
const markdownOptions: MarkdownItOptions = {
|
||||
@@ -93,6 +95,104 @@ const markdown = new MarkdownIt(markdownOptions)
|
||||
trust: false
|
||||
});
|
||||
|
||||
const tableBreakMarkupPattern = /^(?:<br>|<br\/>|<br \/>)/iu;
|
||||
|
||||
function tableBreakCandidateRule(
|
||||
state: StateInline,
|
||||
silent: boolean
|
||||
): boolean {
|
||||
const match = state.src.slice(state.pos).match(tableBreakMarkupPattern);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
if (!silent) {
|
||||
const token = state.push("table_break_candidate", "", 0);
|
||||
token.content = match[0];
|
||||
}
|
||||
state.pos += match[0].length;
|
||||
return true;
|
||||
}
|
||||
|
||||
markdown.inline.ruler.before(
|
||||
"html_inline",
|
||||
"table_break_candidate",
|
||||
tableBreakCandidateRule
|
||||
);
|
||||
markdown.core.ruler.after("inline", "scope_table_breaks", (state) => {
|
||||
let tableCellDepth = 0;
|
||||
for (const token of state.tokens) {
|
||||
if (token.type === "td_open" || token.type === "th_open") {
|
||||
tableCellDepth += 1;
|
||||
continue;
|
||||
}
|
||||
if (token.type === "td_close" || token.type === "th_close") {
|
||||
tableCellDepth = Math.max(0, tableCellDepth - 1);
|
||||
continue;
|
||||
}
|
||||
if (token.type !== "inline") {
|
||||
continue;
|
||||
}
|
||||
for (const child of token.children ?? []) {
|
||||
if (child.type === "table_break_candidate" && tableCellDepth > 0) {
|
||||
child.type = "table_break";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
markdown.renderer.rules.table_break_candidate = (tokens, index) =>
|
||||
markdown.utils.escapeHtml(tokens[index]?.content ?? "");
|
||||
markdown.renderer.rules.table_break = () => "<br>";
|
||||
|
||||
const markdownAlertTitles = new Map([
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["important", "Important"],
|
||||
["warning", "Warning"],
|
||||
["caution", "Caution"]
|
||||
]);
|
||||
|
||||
markdown.core.ruler.after("scope_table_breaks", "github_alerts", (state) => {
|
||||
for (let index = 0; index < state.tokens.length - 2; index += 1) {
|
||||
const blockquote = state.tokens[index];
|
||||
const paragraph = state.tokens[index + 1];
|
||||
const inline = state.tokens[index + 2];
|
||||
if (
|
||||
blockquote?.type !== "blockquote_open" ||
|
||||
paragraph?.type !== "paragraph_open" ||
|
||||
inline?.type !== "inline"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const [marker, bodyBreak] = inline.children ?? [];
|
||||
const match = marker?.type === "text"
|
||||
? marker.content.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]$/iu)
|
||||
: undefined;
|
||||
if (!marker || !match) {
|
||||
continue;
|
||||
}
|
||||
const alertType = match[1]?.toLowerCase();
|
||||
const title = alertType ? markdownAlertTitles.get(alertType) : undefined;
|
||||
if (!alertType || !title) {
|
||||
continue;
|
||||
}
|
||||
blockquote.attrJoin("class", `md-alert md-alert-${alertType}`);
|
||||
marker.type = "markdown_alert_title";
|
||||
marker.content = title;
|
||||
marker.meta = { alertType };
|
||||
if (bodyBreak?.type === "softbreak") {
|
||||
bodyBreak.type = "markdown_alert_body_break";
|
||||
}
|
||||
}
|
||||
});
|
||||
markdown.renderer.rules.markdown_alert_title = (tokens, index) => {
|
||||
const token = tokens[index];
|
||||
const alertType = typeof token?.meta?.alertType === "string"
|
||||
? token.meta.alertType
|
||||
: "note";
|
||||
return `<span class="md-alert-text md-alert-text-${escapeAttribute(alertType)}">${markdown.utils.escapeHtml(token?.content ?? "")}</span>`;
|
||||
};
|
||||
markdown.renderer.rules.markdown_alert_body_break = () => "</p>\n<p>";
|
||||
|
||||
const defaultValidateLink = markdown.validateLink.bind(markdown);
|
||||
markdown.validateLink = (href) =>
|
||||
defaultValidateLink(href) ||
|
||||
@@ -141,6 +241,13 @@ markdown.renderer.rules.paragraph_open = (
|
||||
if (standaloneImage) {
|
||||
return '<figure class="md-document-image-block">\n';
|
||||
}
|
||||
const inlineToken = tokens[index + 1];
|
||||
if (
|
||||
inlineToken?.type === "inline" &&
|
||||
(inlineToken.children ?? []).some((child) => child.type === "code_inline")
|
||||
) {
|
||||
tokens[index]?.attrJoin("class", "md-inline-code-paragraph");
|
||||
}
|
||||
return defaultParagraphOpenRenderer
|
||||
? defaultParagraphOpenRenderer(
|
||||
tokens,
|
||||
@@ -367,6 +474,7 @@ export function renderMarkdown(
|
||||
|
||||
return {
|
||||
rendererVersion: RENDERER_VERSION,
|
||||
markdownBody: parsed.content,
|
||||
articleHtml,
|
||||
bodyHtml,
|
||||
metadata,
|
||||
|
||||
Reference in New Issue
Block a user