fix: Ensure necessary namespaces are in patched doc (#2698)

* refactor: Extract timestamp properties

In preparation for reworking DocumentAttributes.

* refactor: Consolidate namespaces

* fix: Ensure necessary namespaces are in patched doc

Fixes #2697

* Fix tsc and ESLint errors

* Fix CSpell

* Add a test to fix code coverage failure
This commit is contained in:
Josh Kelley
2025-01-06 17:19:00 -05:00
committed by GitHub
parent 7b9b474484
commit ff37f3b460
10 changed files with 129 additions and 155 deletions

View File

@ -8,11 +8,14 @@
// words - list of words to be always considered correct // words - list of words to be always considered correct
"words": [ "words": [
"Abjad", "Abjad",
"aink",
"aiueo", "aiueo",
"ATLEAST", "ATLEAST",
"chosung", "chosung",
"clippy", "clippy",
"datas", "datas",
"dcmitype",
"dcterms",
"docsify", "docsify",
"dolan", "dolan",
"execa", "execa",
@ -32,6 +35,7 @@
"panose", "panose",
"rels", "rels",
"rsid", "rsid",
"sdtdh",
"twip", "twip",
"twips", "twips",
"Xmlable", "Xmlable",

View File

@ -65,5 +65,5 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Dependencies - name: Install Dependencies
run: npm ci --force run: npm ci --force
- name: Prettier - name: CSpell
run: npm run cspell run: npm run cspell

View File

@ -2,7 +2,7 @@ import { FontOptions } from "@file/fonts/font-table";
import { ICommentsOptions } from "@file/paragraph/run/comment-run"; import { ICommentsOptions } from "@file/paragraph/run/comment-run";
import { IHyphenationOptions } from "@file/settings"; import { IHyphenationOptions } from "@file/settings";
import { ICompatibilityOptions } from "@file/settings/compatibility"; import { ICompatibilityOptions } from "@file/settings/compatibility";
import { StringContainer, XmlComponent } from "@file/xml-components"; import { StringContainer, XmlAttributeComponent, XmlComponent } from "@file/xml-components";
import { dateTimeValue } from "@util/values"; import { dateTimeValue } from "@util/values";
import { ICustomPropertyOptions } from "../custom-properties"; import { ICustomPropertyOptions } from "../custom-properties";
@ -75,15 +75,7 @@ export type IPropertiesOptions = {
export class CoreProperties extends XmlComponent { export class CoreProperties extends XmlComponent {
public constructor(options: Omit<IPropertiesOptions, "sections">) { public constructor(options: Omit<IPropertiesOptions, "sections">) {
super("cp:coreProperties"); super("cp:coreProperties");
this.root.push( this.root.push(new DocumentAttributes(["cp", "dc", "dcterms", "dcmitype", "xsi"]));
new DocumentAttributes({
cp: "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
dc: "http://purl.org/dc/elements/1.1/",
dcterms: "http://purl.org/dc/terms/",
dcmitype: "http://purl.org/dc/dcmitype/",
xsi: "http://www.w3.org/2001/XMLSchema-instance",
}),
);
if (options.title) { if (options.title) {
this.root.push(new StringContainer("dc:title", options.title)); this.root.push(new StringContainer("dc:title", options.title));
} }
@ -110,11 +102,15 @@ export class CoreProperties extends XmlComponent {
} }
} }
class TimestampElementProperties extends XmlAttributeComponent<{ readonly type: string }> {
protected readonly xmlKeys = { type: "xsi:type" };
}
class TimestampElement extends XmlComponent { class TimestampElement extends XmlComponent {
public constructor(name: string) { public constructor(name: string) {
super(name); super(name);
this.root.push( this.root.push(
new DocumentAttributes({ new TimestampElementProperties({
type: "dcterms:W3CDTF", type: "dcterms:W3CDTF",
}), }),
); );

View File

@ -1,89 +1,60 @@
import { XmlAttributeComponent } from "@file/xml-components"; import { AttributeMap, XmlAttributeComponent } from "@file/xml-components";
/* cSpell:disable */ /* cSpell:disable */
export type IDocumentAttributesProperties = { export const DocumentAttributeNamespaces = {
readonly wpc?: string; wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas",
readonly mc?: string; mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
readonly o?: string; o: "urn:schemas-microsoft-com:office:office",
readonly r?: string; r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
readonly m?: string; m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
readonly v?: string; v: "urn:schemas-microsoft-com:vml",
readonly wp14?: string; wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
readonly wp?: string; wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
readonly w10?: string; w10: "urn:schemas-microsoft-com:office:word",
readonly w?: string; w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
readonly w14?: string; w14: "http://schemas.microsoft.com/office/word/2010/wordml",
readonly w15?: string; w15: "http://schemas.microsoft.com/office/word/2012/wordml",
readonly wpg?: string; wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
readonly wpi?: string; wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
readonly wne?: string; wne: "http://schemas.microsoft.com/office/word/2006/wordml",
readonly wps?: string; wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
readonly Ignorable?: string; cp: "http://schemas.openxmlformats.org/package/2006/metadata/core-properties",
readonly cp?: string; dc: "http://purl.org/dc/elements/1.1/",
readonly dc?: string; dcterms: "http://purl.org/dc/terms/",
readonly dcterms?: string; dcmitype: "http://purl.org/dc/dcmitype/",
readonly dcmitype?: string; xsi: "http://www.w3.org/2001/XMLSchema-instance",
readonly xsi?: string; cx: "http://schemas.microsoft.com/office/drawing/2014/chartex",
readonly type?: string; cx1: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex",
readonly cx?: string; cx2: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex",
readonly cx1?: string; cx3: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex",
readonly cx2?: string; cx4: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex",
readonly cx3?: string; cx5: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex",
readonly cx4?: string; cx6: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex",
readonly cx5?: string; cx7: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex",
readonly cx6?: string; cx8: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex",
readonly cx7?: string; aink: "http://schemas.microsoft.com/office/drawing/2016/ink",
readonly cx8?: string; am3d: "http://schemas.microsoft.com/office/drawing/2017/model3d",
readonly aink?: string; w16cex: "http://schemas.microsoft.com/office/word/2018/wordml/cex",
readonly am3d?: string; w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid",
readonly w16cex?: string; w16: "http://schemas.microsoft.com/office/word/2018/wordml",
readonly w16cid?: string; w16sdtdh: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash",
readonly w16?: string; w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex",
readonly w16sdtdh?: string;
readonly w16se?: string;
}; };
/* cSpell:enable */ /* cSpell:enable */
export type DocumentAttributeNamespace = keyof typeof DocumentAttributeNamespaces;
export type IDocumentAttributesProperties = Partial<Record<DocumentAttributeNamespace, string>> & {
readonly Ignorable?: string;
};
export class DocumentAttributes extends XmlAttributeComponent<IDocumentAttributesProperties> { export class DocumentAttributes extends XmlAttributeComponent<IDocumentAttributesProperties> {
protected readonly xmlKeys = { protected readonly xmlKeys = {
wpc: "xmlns:wpc",
mc: "xmlns:mc",
o: "xmlns:o",
r: "xmlns:r",
m: "xmlns:m",
v: "xmlns:v",
wp14: "xmlns:wp14",
wp: "xmlns:wp",
w10: "xmlns:w10",
w: "xmlns:w",
w14: "xmlns:w14",
w15: "xmlns:w15",
wpg: "xmlns:wpg",
wpi: "xmlns:wpi",
wne: "xmlns:wne",
wps: "xmlns:wps",
Ignorable: "mc:Ignorable", Ignorable: "mc:Ignorable",
cp: "xmlns:cp", ...Object.fromEntries(Object.keys(DocumentAttributeNamespaces).map((key) => [key, `xmlns:${key}`])),
dc: "xmlns:dc", } as AttributeMap<IDocumentAttributesProperties>;
dcterms: "xmlns:dcterms",
dcmitype: "xmlns:dcmitype", public constructor(ns: readonly DocumentAttributeNamespace[], Ignorable?: string) {
xsi: "xmlns:xsi", super({ Ignorable, ...Object.fromEntries(ns.map((n) => [n, DocumentAttributeNamespaces[n]])) });
type: "xsi:type", }
cx: "xmlns:cx",
cx1: "xmlns:cx1",
cx2: "xmlns:cx2",
cx3: "xmlns:cx3",
cx4: "xmlns:cx4",
cx5: "xmlns:cx5",
cx6: "xmlns:cx6",
cx7: "xmlns:cx7",
cx8: "xmlns:cx8",
aink: "xmlns:aink",
am3d: "xmlns:am3d",
w16cex: "xmlns:w16cex",
w16cid: "xmlns:w16cid",
w16: "xmlns:w16",
w16sdtdh: "xmlns:w16sdtdh",
w16se: "xmlns:w16se",
};
} }

View File

@ -37,41 +37,43 @@ export class Document extends XmlComponent {
public constructor(options: IDocumentOptions) { public constructor(options: IDocumentOptions) {
super("w:document"); super("w:document");
this.root.push( this.root.push(
new DocumentAttributes({ new DocumentAttributes(
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", [
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006", "wpc",
o: "urn:schemas-microsoft-com:office:office", "mc",
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships", "o",
m: "http://schemas.openxmlformats.org/officeDocument/2006/math", "r",
v: "urn:schemas-microsoft-com:vml", "m",
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing", "v",
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", "wp14",
w10: "urn:schemas-microsoft-com:office:word", "wp",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main", "w10",
w14: "http://schemas.microsoft.com/office/word/2010/wordml", "w",
w15: "http://schemas.microsoft.com/office/word/2012/wordml", "w14",
wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup", "w15",
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk", "wpg",
wne: "http://schemas.microsoft.com/office/word/2006/wordml", "wpi",
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape", "wne",
cx: "http://schemas.microsoft.com/office/drawing/2014/chartex", "wps",
cx1: "http://schemas.microsoft.com/office/drawing/2015/9/8/chartex", "cx",
cx2: "http://schemas.microsoft.com/office/drawing/2015/10/21/chartex", "cx1",
cx3: "http://schemas.microsoft.com/office/drawing/2016/5/9/chartex", "cx2",
cx4: "http://schemas.microsoft.com/office/drawing/2016/5/10/chartex", "cx3",
cx5: "http://schemas.microsoft.com/office/drawing/2016/5/11/chartex", "cx4",
cx6: "http://schemas.microsoft.com/office/drawing/2016/5/12/chartex", "cx5",
cx7: "http://schemas.microsoft.com/office/drawing/2016/5/13/chartex", "cx6",
cx8: "http://schemas.microsoft.com/office/drawing/2016/5/14/chartex", "cx7",
aink: "http://schemas.microsoft.com/office/drawing/2016/ink", "cx8",
am3d: "http://schemas.microsoft.com/office/drawing/2017/model3d", "aink",
w16cex: "http://schemas.microsoft.com/office/word/2018/wordml/cex", "am3d",
w16cid: "http://schemas.microsoft.com/office/word/2016/wordml/cid", "w16cex",
w16: "http://schemas.microsoft.com/office/word/2018/wordml", "w16cid",
w16sdtdh: "http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash", "w16",
w16se: "http://schemas.microsoft.com/office/word/2015/wordml/symex", "w16sdtdh",
Ignorable: "w14 w15 wp14", "w16se",
}), ],
"w14 w15 wp14",
),
); );
this.body = new Body(); this.body = new Body();
if (options.background) { if (options.background) {

View File

@ -37,25 +37,10 @@ export class Numbering extends XmlComponent {
public constructor(options: INumberingOptions) { public constructor(options: INumberingOptions) {
super("w:numbering"); super("w:numbering");
this.root.push( this.root.push(
new DocumentAttributes({ new DocumentAttributes(
wpc: "http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas", ["wpc", "mc", "o", "r", "m", "v", "wp14", "wp", "w10", "w", "w14", "w15", "wpg", "wpi", "wne", "wps"],
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006", "w14 w15 wp14",
o: "urn:schemas-microsoft-com:office:office", ),
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
m: "http://schemas.openxmlformats.org/officeDocument/2006/math",
v: "urn:schemas-microsoft-com:vml",
wp14: "http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing",
wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
w10: "urn:schemas-microsoft-com:office:word",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
wpg: "http://schemas.microsoft.com/office/word/2010/wordprocessingGroup",
wpi: "http://schemas.microsoft.com/office/word/2010/wordprocessingInk",
wne: "http://schemas.microsoft.com/office/word/2006/wordml",
wps: "http://schemas.microsoft.com/office/word/2010/wordprocessingShape",
Ignorable: "w14 w15 wp14",
}),
); );
const abstractNumbering = new AbstractNumbering(this.abstractNumUniqueNumericId(), [ const abstractNumbering = new AbstractNumbering(this.abstractNumUniqueNumericId(), [

View File

@ -144,6 +144,10 @@ describe("External styles factory", () => {
expect(() => new ExternalStylesFactory().newInstance(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><foo/>`)).to.throw( expect(() => new ExternalStylesFactory().newInstance(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><foo/>`)).to.throw(
"can not find styles element", "can not find styles element",
); );
expect(() => new ExternalStylesFactory().newInstance(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`)).to.throw(
"can not find styles element",
);
}); });
it("should parse styles elements", () => { it("should parse styles elements", () => {

View File

@ -38,14 +38,7 @@ export type IDefaultStylesOptions = {
export class DefaultStylesFactory { export class DefaultStylesFactory {
public newInstance(options: IDefaultStylesOptions = {}): IStylesOptions { public newInstance(options: IDefaultStylesOptions = {}): IStylesOptions {
const documentAttributes = new DocumentAttributes({ const documentAttributes = new DocumentAttributes(["mc", "r", "w", "w14", "w15"], "w14 w15");
mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
w14: "http://schemas.microsoft.com/office/word/2010/wordml",
w15: "http://schemas.microsoft.com/office/word/2012/wordml",
Ignorable: "w14 w15",
});
return { return {
initialStyles: documentAttributes, initialStyles: documentAttributes,
importedStyles: [ importedStyles: [

View File

@ -1,7 +1,7 @@
import { BaseXmlComponent, IContext } from "./base"; import { BaseXmlComponent, IContext } from "./base";
import { IXmlAttribute, IXmlableObject } from "./xmlable-object"; import { IXmlAttribute, IXmlableObject } from "./xmlable-object";
type AttributeMap<T> = Record<keyof T, string>; export type AttributeMap<T> = Record<keyof T, string>;
export type AttributeData = Record<string, boolean | number | string>; export type AttributeData = Record<string, boolean | number | string>;
export type AttributePayload<T> = { readonly [P in keyof T]: { readonly key: string; readonly value: T[P] } }; export type AttributePayload<T> = { readonly [P in keyof T]: { readonly key: string; readonly value: T[P] } };

View File

@ -2,6 +2,7 @@ import JSZip from "jszip";
import { Element, js2xml } from "xml-js"; import { Element, js2xml } from "xml-js";
import { ImageReplacer } from "@export/packer/image-replacer"; import { ImageReplacer } from "@export/packer/image-replacer";
import { DocumentAttributeNamespaces } from "@file/document";
import { IViewWrapper } from "@file/document-wrapper"; import { IViewWrapper } from "@file/document-wrapper";
import { File } from "@file/file"; import { File } from "@file/file";
import { FileChild } from "@file/file-child"; import { FileChild } from "@file/file-child";
@ -100,6 +101,24 @@ export const patchDocument = async <T extends PatchDocumentOutputType = PatchDoc
} }
const json = toJson(await value.async("text")); const json = toJson(await value.async("text"));
if (key === "word/document.xml") {
const document = json.elements?.find((i) => i.name === "w:document");
if (document) {
// We could check all namespaces from Document, but we'll instead
// check only those that may be used by our element types.
// eslint-disable-next-line functional/immutable-data
document.attributes = document.attributes ?? {};
for (const ns of ["mc", "wp", "r", "w15", "m"] as const) {
// eslint-disable-next-line functional/immutable-data
document.attributes[`xmlns:${ns}`] = DocumentAttributeNamespaces[ns];
}
// eslint-disable-next-line functional/immutable-data
document.attributes["mc:Ignorable"] = `${document.attributes["mc:Ignorable"] || ""} w15`.trim();
}
}
if (key.startsWith("word/") && !key.endsWith(".xml.rels")) { if (key.startsWith("word/") && !key.endsWith(".xml.rels")) {
const context: IContext = { const context: IContext = {
file, file,