Skip to content

Commit a4736c8

Browse files
caio-pizzolaldrinjensonclaude
authored
fix: handle null declaration in DocxExporter to prevent export crash (#2578)
* fix: handle null declaration in DocxExporter to prevent export crash When exporting a SuperDoc to DOCX, if the converter's declaration is null (e.g., for documents created/edited via Yjs collaboration without raw XML import), the export crashes with: TypeError: Cannot read properties of null (reading 'attributes'). Add optional chaining and a standard XML declaration fallback (version="1.0" encoding="UTF-8" standalone="yes") so export succeeds when converter.declaration is null or undefined. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(super-converter): extract DEFAULT_XML_DECLARATION to shared constant Move the inline XML declaration fallback to constants.js so exporter.js and citation-sources.js share one source of truth. Collapse duplicate null/undefined test cases into it.each. --------- Co-authored-by: aldrinjenson <aldrin@athenaintelligence.ai> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b65488d commit a4736c8

4 files changed

Lines changed: 35 additions & 13 deletions

File tree

packages/super-editor/src/core/super-converter/citation-sources.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { v4 as uuidv4 } from 'uuid';
22
import { resolveOpcTargetPath } from './helpers.js';
3+
import { DEFAULT_XML_DECLARATION } from './constants.js';
34

45
export const BIBLIOGRAPHY_NAMESPACE_URI = 'http://schemas.openxmlformats.org/officeDocument/2006/bibliography';
56
export const CUSTOM_XML_RELATIONSHIP_TYPE =
@@ -12,14 +13,6 @@ const DEFAULT_SELECTED_STYLE = '/APA.XSL';
1213
const DEFAULT_STYLE_NAME = 'APA';
1314
const DEFAULT_VERSION = '6';
1415

15-
const DEFAULT_XML_DECLARATION = Object.freeze({
16-
attributes: Object.freeze({
17-
version: '1.0',
18-
encoding: 'UTF-8',
19-
standalone: 'yes',
20-
}),
21-
});
22-
2316
const API_TO_OOXML_SOURCE_TYPE = Object.freeze({
2417
book: 'Book',
2518
journalArticle: 'JournalArticle',

packages/super-editor/src/core/super-converter/constants.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ export const COMMENT_FILE_BASENAMES = [
1111
'commentsExtensible.xml',
1212
];
1313

14+
/** Standard XML declaration used for all OOXML parts */
15+
export const DEFAULT_XML_DECLARATION = Object.freeze({
16+
attributes: Object.freeze({
17+
version: '1.0',
18+
encoding: 'UTF-8',
19+
standalone: 'yes',
20+
}),
21+
});
22+
1423
// Comment-related relationship types (used for pruning stale rels on export)
1524
export const COMMENT_RELATIONSHIP_TYPES = new Set([
1625
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',

packages/super-editor/src/core/super-converter/exporter.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { translateVectorShape, translateShapeGroup } from '@converter/v3/handler
3939
import { translator as wTextTranslator } from '@converter/v3/handlers/w/t';
4040
import { translator as wFootnoteReferenceTranslator } from './v3/handlers/w/footnoteReference/footnoteReference-translator.js';
4141
import { carbonCopy } from '@core/utilities/carbonCopy.js';
42+
import { DEFAULT_XML_DECLARATION } from './constants.js';
4243

4344
const DEFAULT_SECTION_PROPS_TWIPS = Object.freeze({
4445
pageSize: Object.freeze({ width: '12240', height: '15840' }),
@@ -586,11 +587,7 @@ export class DocxExporter {
586587

587588
#generate_xml_as_list(data, debug = false) {
588589
const json = JSON.parse(JSON.stringify(data));
589-
const declaration = this.converter.declaration?.attributes ?? {
590-
version: '1.0',
591-
encoding: 'UTF-8',
592-
standalone: 'yes',
593-
};
590+
const declaration = this.converter.declaration?.attributes ?? DEFAULT_XML_DECLARATION.attributes;
594591
const xmlTag = `<?xml${Object.entries(declaration)
595592
.map(([key, value]) => ` ${key}="${value}"`)
596593
.join('')}?>`;

packages/super-editor/src/tests/export/docxExporter.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,29 @@ describe('DocxExporter', () => {
4141
expect(xml).toContain('Format=&lt;&lt;NUM&gt;&gt;_&lt;&lt;VER&gt;&gt;');
4242
});
4343

44+
it.each([
45+
{ label: 'null', stub: { declaration: null } },
46+
{ label: 'undefined', stub: {} },
47+
])('uses default XML declaration when converter.declaration is $label', ({ stub }) => {
48+
const exporter = new DocxExporter(stub);
49+
50+
const data = {
51+
name: 'w:document',
52+
attributes: {},
53+
elements: [
54+
{
55+
name: 'w:t',
56+
elements: [{ type: 'text', text: 'Hello' }],
57+
},
58+
],
59+
};
60+
61+
const xml = exporter.schemaToXml(data);
62+
63+
expect(xml).toContain('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>');
64+
expect(xml).toContain('<w:t>Hello</w:t>');
65+
});
66+
4467
it('encodes all ampersands in text nodes including entity-like sequences', () => {
4568
const exporter = new DocxExporter(createConverterStub());
4669

0 commit comments

Comments
 (0)