From ee9dc3a31ed78f52bca093039ba2304228a926b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Thu, 12 Feb 2026 12:22:24 +0100 Subject: [PATCH 1/3] (fix) Avoid crash when there is no theme defined. --- lib/files/src/StylesXml.ts | 54 ++++++++++++++++++-------------- lib/files/test/StylesXml.test.ts | 46 +++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/lib/files/src/StylesXml.ts b/lib/files/src/StylesXml.ts index 1fe7d8f2..dd74226f 100644 --- a/lib/files/src/StylesXml.ts +++ b/lib/files/src/StylesXml.ts @@ -174,13 +174,13 @@ export class StylesXml extends XmlFile { async ({ paragraph, text, table, ...style }) => ({ ...style, ppr: await paragraphPropertiesToNode( - paragraph as ParagraphStyle['paragraph'] + paragraph as ParagraphStyle['paragraph'], ), rpr: await textPropertiesToNode( - text as ParagraphStyle['text'] + text as ParagraphStyle['text'], ), tblpr: tablePropertiesToNode( - table as TableStyle['table'] + table as TableStyle['table'], ), conditions: table?.conditions ? await Promise.all( @@ -190,17 +190,17 @@ export class StylesXml extends XmlFile { { ...properties, type: type as TableConditionalTypes, - } - ) - ) - ) + }, + ), + ), + ) : null, - }) - ) + }), + ), ), latentStyles: this.#latentStyles, }, - true + true, ); } @@ -210,7 +210,7 @@ export class StylesXml extends XmlFile { * method throws when the identifier is not unique. */ public add( - properties: Omit & { id?: string } + properties: Omit & { id?: string }, ): string { const id = properties.id || @@ -279,21 +279,21 @@ export class StylesXml extends XmlFile { public static fromDom( dom: Document, location: string, - theme?: ThemeXml + theme?: ThemeXml, ): StylesXml { const instance = new StylesXml(location); const defaultRunProperties = textPropertiesFromNode( evaluateXPathToFirstNode( `/*/${QNS.w}docDefaults/${QNS.w}rPrDefault/${QNS.w}rPr`, - dom - ) + dom, + ), ); const defaultParagraphProperties = paragraphPropertiesFromNode( evaluateXPathToFirstNode( `/*/${QNS.w}docDefaults/${QNS.w}pPrDefault/${QNS.w}pPr`, - dom - ) + dom, + ), ); instance.addDefaults({ @@ -337,7 +337,7 @@ export class StylesXml extends XmlFile { "ppr": ./${QNS.w}pPr, "rpr": ./${QNS.w}rPr }}`, - dom + dom, ).map(({ ppr, rpr, tblpr, tblStylePr, ...json }) => { const runProperties = textPropertiesFromNode(rpr); const instanceFont = @@ -381,20 +381,20 @@ export class StylesXml extends XmlFile { ? { conditions: ( tblStylePr.map( - tableConditionalPropertiesFromNode + tableConditionalPropertiesFromNode, ) as TableConditionalProperties[] ).reduce( (m, { type, ...style }) => Object.assign(m, { [type]: style, }), - {} + {}, ), - } + } : {}), }, }; - }) + }), ); // Warning! Untyped objects @@ -407,7 +407,7 @@ export class StylesXml extends XmlFile { "locked": docxml:st-on-off(@${QNS.w}locked), "semiHidden": docxml:st-on-off(@${QNS.w}semiHidden) }}`, - dom + dom, ).forEach((json) => instance.addLatent(json)); return instance; @@ -418,10 +418,16 @@ export class StylesXml extends XmlFile { */ public static override async fromArchive( archive: Archive, - location: string + location: string, ): Promise { if (archive.hasFile(location)) { - const theme = await ThemeXml.fromArchive(archive); + let theme: ThemeXml | undefined; + try { + theme = await ThemeXml.fromArchive(archive); + } catch (_) { + // no-op + // something happened, the theme document couldn't be read. + } const dom = await archive.readXml(location); return this.fromDom(dom, location, theme); } diff --git a/lib/files/test/StylesXml.test.ts b/lib/files/test/StylesXml.test.ts index c2d32ba7..d658c134 100644 --- a/lib/files/test/StylesXml.test.ts +++ b/lib/files/test/StylesXml.test.ts @@ -1,7 +1,12 @@ import { expect } from 'std/expect'; import { describe, it } from 'std/testing/bdd'; +import { Docx } from '../../Docx.ts'; +import { Paragraph } from '../../components/document/src/Paragraph.ts'; +import { Section } from '../../components/document/src/Section.ts'; +import { Text } from '../../components/document/src/Text.ts'; import { serialize } from '../../utilities/src/dom.ts'; +import { pt } from '../../utilities/src/length.ts'; import { StylesXml } from '../src/StylesXml.ts'; describe('Styles', () => { @@ -28,7 +33,7 @@ describe('Styles', () => { - `.replace(/\n|\t/g, '') + `.replace(/\n|\t/g, ''), ); }); @@ -70,12 +75,12 @@ describe('Styles', () => { - `.replace(/\n|\t/g, '') + `.replace(/\n|\t/g, ''), ); const reparsed = StylesXml.fromDom(node, 'derp').get('test'); expect( - reparsed?.table?.conditions?.lastCol?.cell?.borders?.top + reparsed?.table?.conditions?.lastCol?.cell?.borders?.top, ).toEqual({ type: null, width: null, @@ -83,4 +88,39 @@ describe('Styles', () => { color: '000000', }); }); + + it('can read a document with no theme', async () => { + const docx = Docx.fromNothing(); + const style = docx.document.styles.add({ + id: 'Text', + name: 'Text', + type: 'paragraph', + text: { + fontSize: pt(9), + }, + }); + docx.document.set([ + new Section({}, new Paragraph({ style }, new Text({}, 'foo'))), + ]); + const archive = await docx.toArchive(); + + // word/theme/theme1.xml is the default location for the first theme that is included in a document. + let theme: string | null; + try { + theme = await archive.readText('word/theme/theme1.xml'); + } catch (_) { + theme = null; + } + // It shouldn't be there. + expect(theme).toBeFalsy(); + + // styles.xml is the default location for the first theme that is included in a document. + // This call shouldn't crash if there's no theme. + const styles = await StylesXml.fromArchive(archive, 'styles.xml'); + expect(styles).toBeTruthy(); + + // Same here, shouldn't crash when loading the whole doc. + const fromArchive = Docx.fromArchive(archive); + expect(fromArchive).toBeTruthy(); + }); }); From 4bb212bb4efb585505c503fd948e44bbc3fd44ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Thu, 12 Feb 2026 12:24:46 +0100 Subject: [PATCH 2/3] Fix formatting --- lib/files/src/StylesXml.ts | 42 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/files/src/StylesXml.ts b/lib/files/src/StylesXml.ts index dd74226f..1dcf226e 100644 --- a/lib/files/src/StylesXml.ts +++ b/lib/files/src/StylesXml.ts @@ -174,13 +174,13 @@ export class StylesXml extends XmlFile { async ({ paragraph, text, table, ...style }) => ({ ...style, ppr: await paragraphPropertiesToNode( - paragraph as ParagraphStyle['paragraph'], + paragraph as ParagraphStyle['paragraph'] ), rpr: await textPropertiesToNode( - text as ParagraphStyle['text'], + text as ParagraphStyle['text'] ), tblpr: tablePropertiesToNode( - table as TableStyle['table'], + table as TableStyle['table'] ), conditions: table?.conditions ? await Promise.all( @@ -190,17 +190,17 @@ export class StylesXml extends XmlFile { { ...properties, type: type as TableConditionalTypes, - }, - ), - ), + } + ) + ) ) : null, - }), - ), + }) + ) ), latentStyles: this.#latentStyles, }, - true, + true ); } @@ -210,7 +210,7 @@ export class StylesXml extends XmlFile { * method throws when the identifier is not unique. */ public add( - properties: Omit & { id?: string }, + properties: Omit & { id?: string } ): string { const id = properties.id || @@ -279,21 +279,21 @@ export class StylesXml extends XmlFile { public static fromDom( dom: Document, location: string, - theme?: ThemeXml, + theme?: ThemeXml ): StylesXml { const instance = new StylesXml(location); const defaultRunProperties = textPropertiesFromNode( evaluateXPathToFirstNode( `/*/${QNS.w}docDefaults/${QNS.w}rPrDefault/${QNS.w}rPr`, - dom, - ), + dom + ) ); const defaultParagraphProperties = paragraphPropertiesFromNode( evaluateXPathToFirstNode( `/*/${QNS.w}docDefaults/${QNS.w}pPrDefault/${QNS.w}pPr`, - dom, - ), + dom + ) ); instance.addDefaults({ @@ -337,7 +337,7 @@ export class StylesXml extends XmlFile { "ppr": ./${QNS.w}pPr, "rpr": ./${QNS.w}rPr }}`, - dom, + dom ).map(({ ppr, rpr, tblpr, tblStylePr, ...json }) => { const runProperties = textPropertiesFromNode(rpr); const instanceFont = @@ -381,20 +381,20 @@ export class StylesXml extends XmlFile { ? { conditions: ( tblStylePr.map( - tableConditionalPropertiesFromNode, + tableConditionalPropertiesFromNode ) as TableConditionalProperties[] ).reduce( (m, { type, ...style }) => Object.assign(m, { [type]: style, }), - {}, + {} ), } : {}), }, }; - }), + }) ); // Warning! Untyped objects @@ -407,7 +407,7 @@ export class StylesXml extends XmlFile { "locked": docxml:st-on-off(@${QNS.w}locked), "semiHidden": docxml:st-on-off(@${QNS.w}semiHidden) }}`, - dom, + dom ).forEach((json) => instance.addLatent(json)); return instance; @@ -418,7 +418,7 @@ export class StylesXml extends XmlFile { */ public static override async fromArchive( archive: Archive, - location: string, + location: string ): Promise { if (archive.hasFile(location)) { let theme: ThemeXml | undefined; From 8c6aff517ff897807a4f91fcde83d68229519cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20G=C3=B3mez?= Date: Thu, 12 Feb 2026 12:25:24 +0100 Subject: [PATCH 3/3] Fix formatting again --- lib/files/test/StylesXml.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/files/test/StylesXml.test.ts b/lib/files/test/StylesXml.test.ts index d658c134..fb600734 100644 --- a/lib/files/test/StylesXml.test.ts +++ b/lib/files/test/StylesXml.test.ts @@ -33,7 +33,7 @@ describe('Styles', () => { - `.replace(/\n|\t/g, ''), + `.replace(/\n|\t/g, '') ); }); @@ -75,12 +75,12 @@ describe('Styles', () => { - `.replace(/\n|\t/g, ''), + `.replace(/\n|\t/g, '') ); const reparsed = StylesXml.fromDom(node, 'derp').get('test'); expect( - reparsed?.table?.conditions?.lastCol?.cell?.borders?.top, + reparsed?.table?.conditions?.lastCol?.cell?.borders?.top ).toEqual({ type: null, width: null,