diff options
| author | Michael Williamson <mike@zwobble.org> | 2025-09-11 15:07:41 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2025-09-11 15:07:41 +0100 |
| commit | cee62eea4e52484a473128f342e2a497e32261b7 (patch) | |
| tree | c2d38753571a59f38c337edaac76d185a7e3601d | |
| parent | 660da135c950031097269f5d610e771698f99eb3 (diff) | |
Handle numbering levels defined without an index
| -rw-r--r-- | NEWS | 2 | ||||
| -rw-r--r-- | lib/docx/body-reader.js | 7 | ||||
| -rw-r--r-- | lib/docx/numbering-xml.js | 29 | ||||
| -rw-r--r-- | test/docx/body-reader.tests.js | 6 | ||||
| -rw-r--r-- | test/docx/numbering-xml.tests.js | 63 |
5 files changed, 99 insertions, 8 deletions
@@ -4,6 +4,8 @@ * Support disabling external file accesses using the externalFileAccess option. +* Handle numbering levels defined without an index. + # 1.10.0 * Add "Heading" and "Body" styles, as found in documents created by Apple Pages, diff --git a/lib/docx/body-reader.js b/lib/docx/body-reader.js index 5cdb4ef..826424b 100644 --- a/lib/docx/body-reader.js +++ b/lib/docx/body-reader.js @@ -670,6 +670,13 @@ function readNumberingProperties(styleId, element, numbering) { } } + // Some malformed documents define numbering levels without an index, and + // reference the numbering using a w:numPr element without a w:ilvl child. + // To handle such cases, we assume a level of 0 as a fallback. + if (numId !== undefined) { + return numbering.findLevel(numId, "0"); + } + return null; } diff --git a/lib/docx/numbering-xml.js b/lib/docx/numbering-xml.js index 7e368ac..c41477e 100644 --- a/lib/docx/numbering-xml.js +++ b/lib/docx/numbering-xml.js @@ -64,18 +64,37 @@ function readAbstractNums(root) { function readAbstractNum(element) { var levels = {}; + + // Some malformed documents define numbering levels without an index, and + // reference the numbering using a w:numPr element without a w:ilvl child. + // To handle such cases, we assume a level of 0 as a fallback. + var levelWithoutIndex = null; + element.getElementsByTagName("w:lvl").forEach(function(levelElement) { var levelIndex = levelElement.attributes["w:ilvl"]; var numFmt = levelElement.firstOrEmpty("w:numFmt").attributes["w:val"]; + var isOrdered = numFmt !== "bullet"; var paragraphStyleId = levelElement.firstOrEmpty("w:pStyle").attributes["w:val"]; - levels[levelIndex] = { - isOrdered: numFmt !== "bullet", - level: levelIndex, - paragraphStyleId: paragraphStyleId - }; + if (levelIndex === undefined) { + levelWithoutIndex = { + isOrdered: isOrdered, + level: "0", + paragraphStyleId: paragraphStyleId + }; + } else { + levels[levelIndex] = { + isOrdered: isOrdered, + level: levelIndex, + paragraphStyleId: paragraphStyleId + }; + } }); + if (levels["0"] === undefined && levelWithoutIndex !== null) { + levels["0"] = levelWithoutIndex; + } + var numStyleLink = element.firstOrEmpty("w:numStyleLink").attributes["w:val"]; return {levels: levels, numStyleLink: numStyleLink}; diff --git a/test/docx/body-reader.tests.js b/test/docx/body-reader.tests.js index 4fbea03..de7af2d 100644 --- a/test/docx/body-reader.tests.js +++ b/test/docx/body-reader.tests.js @@ -223,17 +223,17 @@ test("numbering properties are converted to numbering at specified level", funct assert.deepEqual(numberingLevel, {level: "1", isOrdered: true}); }); -test("numbering properties are ignored if w:ilvl is missing", function() { +test("when numbering properties are missing w:ilvl then level of 0 is assumed", function() { var numberingPropertiesXml = new XmlElement("w:numPr", {}, [ new XmlElement("w:numId", {"w:val": "42"}) ]); var numbering = new NumberingMap({ - findLevel: {"42": {"1": {isOrdered: true, level: "1"}}} + findLevel: {"42": {"0": {isOrdered: true, level: "0"}}} }); var numberingLevel = _readNumberingProperties(null, numberingPropertiesXml, numbering); - assert.equal(numberingLevel, null); + assert.deepEqual(numberingLevel, {level: "0", isOrdered: true}); }); test("numbering properties are ignored if w:numId is missing", function() { diff --git a/test/docx/numbering-xml.tests.js b/test/docx/numbering-xml.tests.js index a648349..004e9ab 100644 --- a/test/docx/numbering-xml.tests.js +++ b/test/docx/numbering-xml.tests.js @@ -45,6 +45,69 @@ test('w:num element referencing non-existent w:abstractNumId is ignored', functi duck.assertThat(numbering.findLevel("47", "0"), duck.equalTo(null)); }); +test('given no other levels with index of 0 when level is missing w:ilvl then level index is 0', function() { + var numbering = readNumberingXml( + new XmlElement("w:numbering", {}, [ + new XmlElement("w:abstractNum", {"w:abstractNumId": "42"}, [ + new XmlElement("w:lvl", {}, [ + new XmlElement("w:numFmt", {"w:val": "decimal"}) + ]) + ]), + new XmlElement("w:num", {"w:numId": "47"}, [ + new XmlElement("w:abstractNumId", {"w:val": "42"}) + ]) + ]), + {styles: stylesReader.defaultStyles} + ); + duck.assertThat(numbering.findLevel("47", "0"), duck.hasProperties({ + isOrdered: true + })); +}); + +test('given previous other level with index of 0 when level is missing w:ilvl then level is ignored', function() { + var numbering = readNumberingXml( + new XmlElement("w:numbering", {}, [ + new XmlElement("w:abstractNum", {"w:abstractNumId": "42"}, [ + new XmlElement("w:lvl", {"w:ilvl": "0"}, [ + new XmlElement("w:numFmt", {"w:val": "bullet"}) + ]), + new XmlElement("w:lvl", {}, [ + new XmlElement("w:numFmt", {"w:val": "decimal"}) + ]) + ]), + new XmlElement("w:num", {"w:numId": "47"}, [ + new XmlElement("w:abstractNumId", {"w:val": "42"}) + ]) + ]), + {styles: stylesReader.defaultStyles} + ); + duck.assertThat(numbering.findLevel("47", "0"), duck.hasProperties({ + isOrdered: false + })); +}); + +test('given subsequent other level with index of 0 when level is missing w:ilvl then level is ignored', function() { + var numbering = readNumberingXml( + new XmlElement("w:numbering", {}, [ + new XmlElement("w:abstractNum", {"w:abstractNumId": "42"}, [ + new XmlElement("w:lvl", {}, [ + new XmlElement("w:numFmt", {"w:val": "decimal"}) + ]), + new XmlElement("w:lvl", {"w:ilvl": "0"}, [ + new XmlElement("w:numFmt", {"w:val": "bullet"}) + ]) + ]), + new XmlElement("w:num", {"w:numId": "47"}, [ + new XmlElement("w:abstractNumId", {"w:val": "42"}) + ]) + ]), + {styles: stylesReader.defaultStyles} + ); + duck.assertThat(numbering.findLevel("47", "0"), duck.hasProperties({ + isOrdered: false + })); +}); + test('when level is missing w:numFmt then level is ordered', function() { var numbering = readNumberingXml( new XmlElement("w:numbering", {}, [ |
