diff --git a/CHANGELOG.md b/CHANGELOG.md index 13dd986..1c24c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +### Changed + +- Format files whose `` has an internal subset (`[ ... ]`) instead of failing to parse; the subset is printed as written. + ## [3.4.2] - 2025-07-08 ### Changed diff --git a/src/parser.js b/src/parser.js index 6845471..e47d6a0 100644 --- a/src/parser.js +++ b/src/parser.js @@ -16,6 +16,56 @@ function createError(message, options) { return Object.assign(error, options); } +// The @xml-tools/parser grammar has no rule for the internal subset of a +// DOCTYPE (``). Find its `[ ... ]` range so it can be +// blanked out before parsing and printed back verbatim. +function findInternalSubset(text) { + let idx = 0; + + // Skip whitespace, processing instructions and comments before the DOCTYPE. + for (;;) { + while (/\s/.test(text[idx] ?? "")) idx += 1; + + const close = text.startsWith("" + : text.startsWith("" + : null; + const end = close ? text.indexOf(close, idx) : -1; + + if (end === -1) break; + idx = end + close.length; + } + + if (!text.startsWith("") return null; + } else if (text.startsWith("", idx); + if (commentEnd === -1) return null; + idx = commentEnd + 2; + } else if (char === "]") { + return { start, end: idx }; + } + } + + return null; +} + function simplifyCST(node) { switch (node.name) { case "attribute": { @@ -68,6 +118,7 @@ function simplifyCST(node) { DocType: DocType[0].image, Name: Name[0].image, externalID: externalID ? simplifyCST(externalID[0]) : null, + internalSubset: null, CLOSE: CLOSE[0].image, location: node.location }; @@ -166,7 +217,14 @@ function simplifyCST(node) { const parser = { parse(text) { - const { lexErrors, parseErrors, cst } = xmlToolsParse(text); + const subset = findInternalSubset(text); + const { lexErrors, parseErrors, cst } = xmlToolsParse( + subset + ? text.slice(0, subset.start) + + text.slice(subset.start, subset.end + 1).replace(/[^\r\n]/g, " ") + + text.slice(subset.end + 1) + : text + ); // If there are any lexical errors, throw the first of them as an error. if (lexErrors.length > 0) { @@ -200,7 +258,16 @@ const parser = { } // Otherwise return the CST. - return simplifyCST(cst); + const document = simplifyCST(cst); + + if (subset) { + document.docTypeDecl.internalSubset = text.slice( + subset.start + 1, + subset.end + ); + } + + return document; }, astFormat: "xml", locStart(node) { diff --git a/src/printer.js b/src/printer.js index 753ba61..c970800 100644 --- a/src/printer.js +++ b/src/printer.js @@ -184,13 +184,17 @@ function printContent(path, opts, print) { } function printDocTypeDecl(path, opts, print) { - const { DocType, Name, externalID, CLOSE } = path.node; + const { DocType, Name, externalID, internalSubset, CLOSE } = path.node; const parts = [DocType, " ", Name]; if (externalID) { parts.push(" ", print("externalID")); } + if (internalSubset !== null) { + parts.push(" [", doc.utils.replaceEndOfLine(internalSubset), "]"); + } + return group([...parts, CLOSE]); } diff --git a/test/__snapshots__/format.test.js.snap b/test/__snapshots__/format.test.js.snap index 5a89e2a..3af777e 100644 --- a/test/__snapshots__/format.test.js.snap +++ b/test/__snapshots__/format.test.js.snap @@ -1,5 +1,15 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`DOCTYPE with an internal subset 1`] = ` +" +"> + +]> +&a; +" +`; + exports[`bracketSameLine => true 1`] = ` " double", async () => { expect(formatted).toMatchSnapshot(); }); + +test("DOCTYPE with an internal subset", async () => { + const content = ` +"> + +]> +&a; +`; + + const formatted = await format(content); + expect(formatted).toMatchSnapshot(); + expect(await format(formatted)).toBe(formatted); +}); diff --git a/test/parser.test.js b/test/parser.test.js index 20a126d..68dc533 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -10,3 +10,11 @@ test("parseError", () => { expect(() => parser.parse(" { + const { docTypeDecl } = parser.parse( + ']>\n&a;' + ); + + expect(docTypeDecl.internalSubset).toBe(''); +});