release: 发布 v0.6.1 DOCX 视觉一致性修复
新增通用 CSS 到 OOXML 翻译修复,统一字体、字距、精确行距、段落、列表、表格、引用、代码块与行内代码连续性,不引入按主题 ID 分支。 新增 MdTP Mono 并统一 Serif、Sans、Mono 三字体包的 Chromium 与 DOCX 使用链;字体声明、嵌入部件和 Word/WPS 实际采用均进入硬门禁。 重建封面整页及正文语义块视觉差分,14 套主题、纵横两个方向、五组页边距共 140 个真实场景全部通过,阻断失败和诊断失败均为零。 源码服务、Docker Web API 与实际安装 Desktop 的 red-briefing 导出均包含 5 个字体部件;Word/WPS 原生渲染和逐页复核通过。修复 Docker 构建上下文与运行层复用软链接,并完善 v0.6.1 版本、发行说明和发布归集。 验证:npm test(116 个文件、616 项测试)、npm run typecheck、npm run build、git diff --check 全部通过。Desktop 安装器与 ZIP、Docker v0.6.1 镜像已生成;Windows 产物仍为未签名内部发行。
This commit is contained in:
@@ -442,34 +442,32 @@ function normalizeComputedSlot(
|
||||
style.spacingAfterPt = spacingAfterPt;
|
||||
}
|
||||
}
|
||||
if (context.kind !== "inline") {
|
||||
const padding = {
|
||||
top: length(context, "padding-top", computed.paddingTop, {
|
||||
nonNegative: true
|
||||
}),
|
||||
right: length(context, "padding-right", computed.paddingRight, {
|
||||
nonNegative: true
|
||||
}),
|
||||
bottom: length(context, "padding-bottom", computed.paddingBottom, {
|
||||
nonNegative: true
|
||||
}),
|
||||
left: length(context, "padding-left", computed.paddingLeft, {
|
||||
nonNegative: true
|
||||
})
|
||||
const padding = {
|
||||
top: length(context, "padding-top", computed.paddingTop, {
|
||||
nonNegative: true
|
||||
}),
|
||||
right: length(context, "padding-right", computed.paddingRight, {
|
||||
nonNegative: true
|
||||
}),
|
||||
bottom: length(context, "padding-bottom", computed.paddingBottom, {
|
||||
nonNegative: true
|
||||
}),
|
||||
left: length(context, "padding-left", computed.paddingLeft, {
|
||||
nonNegative: true
|
||||
})
|
||||
};
|
||||
if (
|
||||
padding.top !== undefined &&
|
||||
padding.right !== undefined &&
|
||||
padding.bottom !== undefined &&
|
||||
padding.left !== undefined
|
||||
) {
|
||||
style.paddingPt = {
|
||||
top: padding.top,
|
||||
right: padding.right,
|
||||
bottom: padding.bottom,
|
||||
left: padding.left
|
||||
};
|
||||
if (
|
||||
padding.top !== undefined &&
|
||||
padding.right !== undefined &&
|
||||
padding.bottom !== undefined &&
|
||||
padding.left !== undefined
|
||||
) {
|
||||
style.paddingPt = {
|
||||
top: padding.top,
|
||||
right: padding.right,
|
||||
bottom: padding.bottom,
|
||||
left: padding.left
|
||||
};
|
||||
}
|
||||
}
|
||||
const containingBlockWidthPt = computed.containingBlockWidth
|
||||
? parseCssLengthToPt(computed.containingBlockWidth)
|
||||
@@ -652,7 +650,8 @@ function applyBorderColor(
|
||||
function applyOverrides(
|
||||
slot: DocxStyleSlotName,
|
||||
styleInput: DocxSlotStyleToken,
|
||||
overrides: DocxThemeStyleOverrides
|
||||
overrides: DocxThemeStyleOverrides,
|
||||
replaceExisting: boolean
|
||||
): {
|
||||
style: DocxSlotStyleToken;
|
||||
applied: boolean;
|
||||
@@ -663,7 +662,10 @@ function applyOverrides(
|
||||
key: Key,
|
||||
value: MutableStyle[Key] | undefined
|
||||
) => {
|
||||
if (value !== undefined) {
|
||||
if (
|
||||
value !== undefined &&
|
||||
(replaceExisting || style[key] === undefined)
|
||||
) {
|
||||
style[key] = value;
|
||||
applied = true;
|
||||
}
|
||||
@@ -887,7 +889,10 @@ export function resolveDocxThemeTokens(
|
||||
const override = applyOverrides(
|
||||
definition.name,
|
||||
resolved.style,
|
||||
input.config.overrides
|
||||
input.config.overrides,
|
||||
input.config.mode === "explicit" ||
|
||||
!input.config.legacyPreset ||
|
||||
resolved.source === "preset-fallback"
|
||||
);
|
||||
if (override.applied) {
|
||||
diagnostics.push({
|
||||
@@ -907,6 +912,43 @@ export function resolveDocxThemeTokens(
|
||||
return resolved;
|
||||
}
|
||||
);
|
||||
const blockQuote = slots.find((entry) => entry.slot === "block-quote");
|
||||
const blockQuoteText = slots.find(
|
||||
(entry) => entry.slot === "block-quote-text"
|
||||
);
|
||||
if (blockQuote && blockQuoteText) {
|
||||
const text = blockQuoteText.style;
|
||||
blockQuote.style = {
|
||||
...blockQuote.style,
|
||||
...(text.fontCandidates.length > 0
|
||||
? { fontCandidates: text.fontCandidates }
|
||||
: {}),
|
||||
...(text.fontSizePt !== undefined
|
||||
? { fontSizePt: text.fontSizePt }
|
||||
: {}),
|
||||
...(text.bold !== undefined ? { bold: text.bold } : {}),
|
||||
...(text.italic !== undefined ? { italic: text.italic } : {}),
|
||||
...(text.underline !== undefined
|
||||
? { underline: text.underline }
|
||||
: {}),
|
||||
...(text.strikethrough !== undefined
|
||||
? { strikethrough: text.strikethrough }
|
||||
: {}),
|
||||
...(text.color !== undefined ? { color: text.color } : {}),
|
||||
...(text.lineSpacing !== undefined
|
||||
? { lineSpacing: text.lineSpacing }
|
||||
: {}),
|
||||
...(text.letterSpacingPt !== undefined
|
||||
? { letterSpacingPt: text.letterSpacingPt }
|
||||
: {}),
|
||||
...(text.alignment !== undefined
|
||||
? { alignment: text.alignment }
|
||||
: {}),
|
||||
...(text.firstLineIndentPt !== undefined
|
||||
? { firstLineIndentPt: text.firstLineIndentPt }
|
||||
: {})
|
||||
};
|
||||
}
|
||||
return docxThemeTokenSetSchema.parse({
|
||||
schemaVersion: 1,
|
||||
themeId: snapshot.themeId,
|
||||
|
||||
@@ -34,8 +34,8 @@ export function createDocxStyleProbeMarkup(): string {
|
||||
</p>
|
||||
<ul ${attribute("unordered-list")}><li ${attribute("list-item")}>无序列表</li></ul>
|
||||
<ol ${attribute("ordered-list")}><li>有序列表</li></ol>
|
||||
<blockquote ${attribute("block-quote")}><p>引用内容</p></blockquote>
|
||||
<pre class="md-fences" ${attribute("code-block")}><code>const value = 1;</code></pre>
|
||||
<blockquote ${attribute("block-quote")}><p ${attribute("block-quote-text")}>引用内容</p></blockquote>
|
||||
<pre class="md-fences" ${attribute("code-block")}><code class="language-javascript" ${attribute("code-block-text")}><span class="hljs-keyword" ${attribute("code-token-keyword")}>const</span> <span class="hljs-attr" ${attribute("code-token-attribute")}>key</span> <span class="hljs-operator" ${attribute("code-token-operator")}>=</span> <span class="hljs-string" ${attribute("code-token-string")}>"value"</span>; <span class="hljs-literal" ${attribute("code-token-literal")}>true</span> <span class="hljs-title" ${attribute("code-token-title")}>title</span> <span class="hljs-number" ${attribute("code-token-number")}>1</span> <span class="hljs-comment" ${attribute("code-token-comment")}>// comment</span> <span class="hljs-built_in" ${attribute("code-token-built-in")}>Array</span> <span class="hljs-meta" ${attribute("code-token-meta")}>@meta</span> <span class="hljs-variable" ${attribute("code-token-variable")}>value</span> <span class="hljs-regexp" ${attribute("code-token-regexp")}>/x/</span> <span class="hljs-punctuation" ${attribute("code-token-punctuation")}>{}</span></code></pre>
|
||||
<table ${attribute("table")}>
|
||||
<thead><tr><th ${attribute("table-header")}>表头</th></tr></thead>
|
||||
<tbody><tr><td ${attribute("table-cell")}>单元格</td></tr></tbody>
|
||||
@@ -61,7 +61,7 @@ export function createDocxStyleProbeMarkup(): string {
|
||||
</section>
|
||||
<section class="docx-probe-briefing">
|
||||
<header class="doc-masthead doc-masthead-briefing" ${attribute("briefing-masthead")}>
|
||||
<p class="doc-briefing-masthead">工作简报</p>
|
||||
<p class="doc-briefing-masthead" ${attribute("briefing-masthead-text")}>工作简报</p>
|
||||
<div class="doc-briefing-meta" ${attribute("briefing-meta")}><span>第1期</span><span>示例单位</span><time>2026年7月30日</time></div>
|
||||
</header>
|
||||
<h1 class="doc-title" ${attribute("briefing-title")}>简报标题</h1>
|
||||
|
||||
@@ -118,7 +118,6 @@ body {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
@@ -19,7 +19,22 @@ export const DOCX_STYLE_SLOT_NAMES = [
|
||||
"ordered-list",
|
||||
"list-item",
|
||||
"block-quote",
|
||||
"block-quote-text",
|
||||
"code-block",
|
||||
"code-block-text",
|
||||
"code-token-keyword",
|
||||
"code-token-literal",
|
||||
"code-token-string",
|
||||
"code-token-attribute",
|
||||
"code-token-title",
|
||||
"code-token-number",
|
||||
"code-token-comment",
|
||||
"code-token-built-in",
|
||||
"code-token-meta",
|
||||
"code-token-variable",
|
||||
"code-token-operator",
|
||||
"code-token-regexp",
|
||||
"code-token-punctuation",
|
||||
"table",
|
||||
"table-header",
|
||||
"table-cell",
|
||||
@@ -42,6 +57,7 @@ export const DOCX_STYLE_SLOT_NAMES = [
|
||||
"official-copy-to",
|
||||
"official-printing-row",
|
||||
"briefing-masthead",
|
||||
"briefing-masthead-text",
|
||||
"briefing-meta",
|
||||
"briefing-title",
|
||||
"briefing-contact",
|
||||
@@ -121,7 +137,22 @@ const kinds: Record<DocxStyleSlotName, DocxStyleSlotKind> = {
|
||||
"ordered-list": "list",
|
||||
"list-item": "list",
|
||||
"block-quote": "paragraph",
|
||||
"block-quote-text": "paragraph",
|
||||
"code-block": "paragraph",
|
||||
"code-block-text": "inline",
|
||||
"code-token-keyword": "inline",
|
||||
"code-token-literal": "inline",
|
||||
"code-token-string": "inline",
|
||||
"code-token-attribute": "inline",
|
||||
"code-token-title": "inline",
|
||||
"code-token-number": "inline",
|
||||
"code-token-comment": "inline",
|
||||
"code-token-built-in": "inline",
|
||||
"code-token-meta": "inline",
|
||||
"code-token-variable": "inline",
|
||||
"code-token-operator": "inline",
|
||||
"code-token-regexp": "inline",
|
||||
"code-token-punctuation": "inline",
|
||||
table: "table",
|
||||
"table-header": "table",
|
||||
"table-cell": "table",
|
||||
@@ -144,6 +175,7 @@ const kinds: Record<DocxStyleSlotName, DocxStyleSlotKind> = {
|
||||
"official-copy-to": "structure",
|
||||
"official-printing-row": "structure",
|
||||
"briefing-masthead": "structure",
|
||||
"briefing-masthead-text": "structure",
|
||||
"briefing-meta": "structure",
|
||||
"briefing-title": "structure",
|
||||
"briefing-contact": "structure",
|
||||
|
||||
@@ -175,6 +175,12 @@ describe("DOCX 主题令牌归一化", () => {
|
||||
});
|
||||
expect(findSlot(tokens, "strong").style)
|
||||
.not.toHaveProperty("firstLineIndentPt");
|
||||
expect(findSlot(tokens, "inline-code").style.paddingPt).toEqual({
|
||||
top: 3,
|
||||
right: 6,
|
||||
bottom: 3,
|
||||
left: 6
|
||||
});
|
||||
expect(findSlot(tokens, "table").style.widthPercent).toBe(100);
|
||||
expect(tokens.slots).toHaveLength(DOCX_STYLE_SLOT_NAMES.length);
|
||||
});
|
||||
@@ -204,6 +210,43 @@ describe("DOCX 主题令牌归一化", () => {
|
||||
expect(findSlot(tokens, "table").style.widthPercent).toBe(100);
|
||||
});
|
||||
|
||||
it("引用块组合容器盒模型与内部段落文字样式", () => {
|
||||
const tokens = resolveDocxThemeTokens({
|
||||
snapshot: createSnapshot({
|
||||
"block-quote": {
|
||||
fontSize: "24px",
|
||||
paddingLeft: "20px",
|
||||
borderLeft: "4px solid rgb(122, 122, 122)"
|
||||
},
|
||||
"block-quote-text": {
|
||||
fontFamily: '"Merriweather", serif',
|
||||
fontSize: "16px",
|
||||
fontStyle: "italic",
|
||||
lineHeight: "24px",
|
||||
paddingLeft: "0px",
|
||||
borderLeft: "0px none rgb(17, 34, 51)"
|
||||
}
|
||||
}),
|
||||
config: {
|
||||
mode: "auto",
|
||||
basePreset: "general",
|
||||
overrides: {},
|
||||
legacyPreset: false
|
||||
}
|
||||
});
|
||||
|
||||
expect(findSlot(tokens, "block-quote").style).toMatchObject({
|
||||
fontCandidates: ["Merriweather", "serif"],
|
||||
fontSizePt: 12,
|
||||
italic: true,
|
||||
lineSpacing: 1.5,
|
||||
paddingPt: { left: 15 },
|
||||
borders: {
|
||||
left: { widthPt: 3, color: "#7a7a7a", style: "single" }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("按父级内容区归一化右置百分比结构容器", () => {
|
||||
const tokens = resolveDocxThemeTokens({
|
||||
snapshot: createSnapshot({
|
||||
@@ -356,7 +399,7 @@ describe("DOCX 主题令牌归一化", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("以清单覆盖自动值并在未命中时显式降级", () => {
|
||||
it("旧版清单仅补齐自动值,并在未命中时显式降级", () => {
|
||||
const tokens = resolveDocxThemeTokens({
|
||||
snapshot: createSnapshot({ caption: null }),
|
||||
config: {
|
||||
@@ -376,15 +419,15 @@ describe("DOCX 主题令牌归一化", () => {
|
||||
}
|
||||
});
|
||||
expect(findSlot(tokens, "paragraph")).toMatchObject({
|
||||
source: "manifest-override",
|
||||
source: "computed-css",
|
||||
confidence: "exact",
|
||||
style: {
|
||||
fontSizePt: 11,
|
||||
firstLineIndentPt: 22
|
||||
fontSizePt: 12,
|
||||
firstLineIndentPt: 24
|
||||
}
|
||||
});
|
||||
expect(findSlot(tokens, "hyperlink").style).toMatchObject({
|
||||
color: "#aa0000",
|
||||
color: "#112233",
|
||||
underline: false
|
||||
});
|
||||
expect(findSlot(tokens, "caption")).toMatchObject({
|
||||
@@ -401,6 +444,24 @@ describe("DOCX 主题令牌归一化", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("非旧版自动清单仍可显式覆盖 CSS 计算值", () => {
|
||||
const tokens = resolveDocxThemeTokens({
|
||||
snapshot: createSnapshot(),
|
||||
config: {
|
||||
mode: "auto-with-overrides",
|
||||
basePreset: "formal",
|
||||
legacyPreset: false,
|
||||
overrides: {
|
||||
headings: { color: "#aa0000" }
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(findSlot(tokens, "heading-1")).toMatchObject({
|
||||
source: "manifest-override",
|
||||
style: { color: "#aa0000" }
|
||||
});
|
||||
});
|
||||
|
||||
it("显式模式不读取 CSS,仅使用覆盖与预设降级", () => {
|
||||
const tokens = resolveDocxThemeTokens({
|
||||
snapshot: createSnapshot(),
|
||||
|
||||
@@ -77,6 +77,8 @@ describe("DOCX 标准样式探针", () => {
|
||||
expect(markup).toMatch(
|
||||
/^<article id="write"[^>]*>\s*<h1 data-docx-slot="heading-1">/u
|
||||
);
|
||||
expect(markup).toContain('<code class="language-javascript"');
|
||||
expect(markup).not.toContain('<code class="hljs ');
|
||||
});
|
||||
|
||||
it("拒绝缺失和重复槽位的探针", () => {
|
||||
|
||||
@@ -84,6 +84,7 @@ describe("DOCX 主题样式浏览器运行时", () => {
|
||||
expect(script).toContain("contentPaddingLeft");
|
||||
expect(script).toContain("padding-left: 8px");
|
||||
expect(script).toContain("#write pre.md-fences \\u003e code");
|
||||
expect(script).not.toContain("font-family: inherit");
|
||||
expect(script).toContain("frame.remove()");
|
||||
expect(script).not.toContain("</style><script>x</script>");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user