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("", idx)
+ ? "?>"
+ : 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('');
+});