From ccb007ab0c15890424dffbff1852e27accaf8140 Mon Sep 17 00:00:00 2001 From: Tomas Zijdemans Date: Thu, 2 Jul 2026 08:58:08 +0200 Subject: [PATCH 1/3] remove dead types --- xml/_parser.ts | 2 +- xml/_tokenizer.ts | 51 +++++++++++++- xml/_tokenizer_test.ts | 3 +- xml/types.ts | 151 ++--------------------------------------- 4 files changed, 56 insertions(+), 151 deletions(-) diff --git a/xml/_parser.ts b/xml/_parser.ts index 31ebec49f675..565184618c9c 100644 --- a/xml/_parser.ts +++ b/xml/_parser.ts @@ -16,9 +16,9 @@ import type { ParseStreamOptions, XmlAttributeIterator, XmlEventCallbacks, - XmlTokenCallbacks, } from "./types.ts"; import { XmlSyntaxError } from "./types.ts"; +import type { XmlTokenCallbacks } from "./_tokenizer.ts"; import { decodeEntities } from "./_entities.ts"; import { validateNamespaceBinding, diff --git a/xml/_tokenizer.ts b/xml/_tokenizer.ts index 5f0e5e8ad080..89af09554c77 100644 --- a/xml/_tokenizer.ts +++ b/xml/_tokenizer.ts @@ -11,9 +11,12 @@ */ import { + type XmlContentCallback, + type XmlDeclarationCallback, + type XmlDoctypeCallback, type XmlPosition, + type XmlProcessingInstructionCallback, XmlSyntaxError, - type XmlTokenCallbacks, } from "./types.ts"; import { isIllegalXmlLiteralChar, @@ -24,6 +27,52 @@ import { } from "./_common.ts"; import { isNameChar, isNameStartChar } from "./_name_chars.ts"; +/** + * Callbacks for tokenizer output - enables zero-allocation token emission. + * + * Instead of creating token objects, the tokenizer invokes these callbacks + * directly with primitive values. + */ +export interface XmlTokenCallbacks { + /** Called when a start tag opens (e.g., `` or `/>`). */ + onStartTagClose?(selfClosing: boolean): void; + + /** Called when an end tag is encountered (e.g., ``). */ + onEndTag?(name: string, line: number, column: number, offset: number): void; + + /** Called for text content between tags. */ + onText?: XmlContentCallback; + + /** Called for CDATA sections. */ + onCData?: XmlContentCallback; + + /** Called for XML comments. */ + onComment?: XmlContentCallback; + + /** Called for processing instructions. */ + onProcessingInstruction?: XmlProcessingInstructionCallback; + + /** Called for XML declarations. */ + onDeclaration?: XmlDeclarationCallback; + + /** Called for DOCTYPE declarations. */ + onDoctype?: XmlDoctypeCallback; + + /** Called for internal entity declarations in the DTD. */ + onEntityDeclaration?(name: string, value: string): void; +} + /** Options for the XML tokenizer. */ interface XmlTokenizerOptions { /** diff --git a/xml/_tokenizer_test.ts b/xml/_tokenizer_test.ts index 5aec5a063d15..dbc7d9de1e1b 100644 --- a/xml/_tokenizer_test.ts +++ b/xml/_tokenizer_test.ts @@ -1,8 +1,7 @@ // Copyright 2018-2026 the Deno authors. MIT license. import { assertEquals, assertThrows } from "@std/assert"; -import { XmlTokenizer } from "./_tokenizer.ts"; -import type { XmlTokenCallbacks } from "./types.ts"; +import { type XmlTokenCallbacks, XmlTokenizer } from "./_tokenizer.ts"; import { XmlSyntaxError } from "./types.ts"; /** Token type for testing - recreates the old token structure for assertions. */ diff --git a/xml/types.ts b/xml/types.ts index eb1552aa4408..d8edd4d5e3b5 100644 --- a/xml/types.ts +++ b/xml/types.ts @@ -110,93 +110,8 @@ export interface XmlName { } /** - * An XML attribute with its qualified name and value. - */ -export interface XmlAttribute { - /** The qualified name of the attribute. */ - readonly name: XmlName; - /** The decoded attribute value. */ - readonly value: string; -} - -// ============================================================================ -// Event Types (for streaming parser) -// ============================================================================ - -/** - * Event emitted when an element start tag is encountered. - */ -export interface XmlStartElementEvent extends XmlPosition { - /** The event type discriminant. */ - readonly type: "start_element"; - /** The qualified name of the element. */ - readonly name: XmlName; - /** The attributes on the element. */ - readonly attributes: ReadonlyArray; - /** Whether this is a self-closing tag (``). */ - readonly selfClosing: boolean; -} - -/** - * Event emitted when an element end tag is encountered. - */ -export interface XmlEndElementEvent extends XmlPosition { - /** The event type discriminant. */ - readonly type: "end_element"; - /** The qualified name of the element. */ - readonly name: XmlName; -} - -/** - * Event emitted for text content. - */ -export interface XmlTextEvent extends XmlPosition { - /** The event type discriminant. */ - readonly type: "text"; - /** The decoded text content. */ - readonly text: string; -} - -/** - * Event emitted for CDATA sections. - * - * @see {@link https://www.w3.org/TR/xml/#sec-cdata-sect | XML 1.0 §2.7 CDATA Sections} - */ -export interface XmlCDataEvent extends XmlPosition { - /** The event type discriminant. */ - readonly type: "cdata"; - /** The raw CDATA content (not entity-decoded). */ - readonly text: string; -} - -/** - * Event emitted for comments. - * - * @see {@link https://www.w3.org/TR/xml/#sec-comments | XML 1.0 §2.5 Comments} - */ -export interface XmlCommentEvent extends XmlPosition { - /** The event type discriminant. */ - readonly type: "comment"; - /** The comment text (excluding ``). */ - readonly text: string; -} - -/** - * Event emitted for processing instructions. - * - * @see {@link https://www.w3.org/TR/xml/#sec-pi | XML 1.0 §2.6 Processing Instructions} - */ -export interface XmlProcessingInstructionEvent extends XmlPosition { - /** The event type discriminant. */ - readonly type: "processing_instruction"; - /** The PI target (e.g., "xml-stylesheet"). */ - readonly target: string; - /** The PI content after the target. */ - readonly content: string; -} - -/** - * Event emitted for the XML declaration. + * The XML declaration of a document, exposed as + * {@linkcode XmlDocument.declaration}. * * @see {@link https://www.w3.org/TR/xml/#sec-prolog-dtd | XML 1.0 §2.8 Prolog} */ @@ -211,18 +126,6 @@ export interface XmlDeclarationEvent extends XmlPosition { readonly standalone?: "yes" | "no"; } -/** - * Discriminated union of all XML events emitted by the streaming parser. - */ -export type XmlEvent = - | XmlStartElementEvent - | XmlEndElementEvent - | XmlTextEvent - | XmlCDataEvent - | XmlCommentEvent - | XmlProcessingInstructionEvent - | XmlDeclarationEvent; - /** * Base options shared by parsing functions. */ @@ -451,53 +354,7 @@ export type XmlProcessingInstructionCallback = ( ) => void; /** - * Callbacks for tokenizer output - enables zero-allocation token emission. - * - * Instead of creating token objects, the tokenizer invokes these callbacks - * directly with primitive values. - */ -export interface XmlTokenCallbacks { - /** Called when a start tag opens (e.g., `` or `/>`). */ - onStartTagClose?(selfClosing: boolean): void; - - /** Called when an end tag is encountered (e.g., ``). */ - onEndTag?(name: string, line: number, column: number, offset: number): void; - - /** Called for text content between tags. */ - onText?: XmlContentCallback; - - /** Called for CDATA sections. */ - onCData?: XmlContentCallback; - - /** Called for XML comments. */ - onComment?: XmlContentCallback; - - /** Called for processing instructions. */ - onProcessingInstruction?: XmlProcessingInstructionCallback; - - /** Called for XML declarations. */ - onDeclaration?: XmlDeclarationCallback; - - /** Called for DOCTYPE declarations. */ - onDoctype?: XmlDoctypeCallback; - - /** Called for internal entity declarations in the DTD. */ - onEntityDeclaration?(name: string, value: string): void; -} - -/** - * Reusable attribute accessor that avoids allocating XmlAttribute arrays. + * Reusable attribute accessor that avoids allocating attribute object arrays. * * Instead of creating an array of attribute objects, attributes are accessed * by index through this interface. The implementation reuses internal arrays @@ -531,7 +388,7 @@ export interface XmlAttributeIterator { /** * Callbacks for event-level output - enables zero-allocation event emission. * - * The parser invokes these callbacks instead of creating XmlEvent objects. + * The parser invokes these callbacks instead of creating event objects. */ export interface XmlEventCallbacks { /** Called for XML declarations. */ From d8e1518cc7f4f4ee9f91312dc48cb2ed7c01ea41 Mon Sep 17 00:00:00 2001 From: Tomas Zijdemans Date: Thu, 2 Jul 2026 09:00:23 +0200 Subject: [PATCH 2/3] fix docs --- xml/types.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/xml/types.ts b/xml/types.ts index d8edd4d5e3b5..0cb77456ab83 100644 --- a/xml/types.ts +++ b/xml/types.ts @@ -15,7 +15,7 @@ export interface XmlPosition { readonly line: number; /** Column number (1-indexed). */ readonly column: number; - /** Byte offset in the input. */ + /** Character offset in the input string (in UTF-16 code units, 0-indexed). */ readonly offset: number; } @@ -59,7 +59,8 @@ export class XmlSyntaxError extends SyntaxError { */ readonly column: number; /** - * The byte offset where the error occurred. + * The character offset in the input string where the error occurred + * (in UTF-16 code units, 0-indexed). * * @example Usage * ```ts @@ -287,7 +288,11 @@ export interface XmlElement { readonly type: "element"; /** The qualified name of the element. */ readonly name: XmlName; - /** Attribute lookup by local name. */ + /** + * Attribute values keyed by the raw qualified attribute name as written in + * the document, including any namespace prefix (e.g. `"id"`, `"xlink:href"`, + * `"xmlns"`, `"xmlns:ns"`). + */ readonly attributes: Readonly>; /** The child nodes of this element. */ readonly children: ReadonlyArray; From e064d2317fe3fc1a936a69078f649c3badf2ecb7 Mon Sep 17 00:00:00 2001 From: Tomas Zijdemans Date: Thu, 2 Jul 2026 09:21:31 +0200 Subject: [PATCH 3/3] clarify --- xml/_parse_sync.ts | 4 ++-- xml/_parse_sync_test.ts | 12 ++++++++++ xml/parse_stream_test.ts | 47 ++++++++++++++++++++++++++++++++++++++++ xml/stringify.ts | 4 ++-- xml/types.ts | 29 +++++++++++++++++++------ xml/types_test.ts | 13 +++++++++++ 6 files changed, 98 insertions(+), 11 deletions(-) diff --git a/xml/_parse_sync.ts b/xml/_parse_sync.ts index 9bea7c759181..6f4dd77beec5 100644 --- a/xml/_parse_sync.ts +++ b/xml/_parse_sync.ts @@ -17,7 +17,7 @@ import type { ParseOptions, XmlCDataNode, XmlCommentNode, - XmlDeclarationEvent, + XmlDeclaration, XmlDocument, XmlElement, XmlName, @@ -114,7 +114,7 @@ export function parseSync(xml: string, options?: ParseOptions): XmlDocument { // Tree building state const stack: MutableElement[] = []; let root: MutableElement | undefined; - let declaration: XmlDeclarationEvent | undefined; + let declaration: XmlDeclaration | undefined; let rootClosed = false; // Track whether root element has been closed // Namespace tracking (lazy initialization for performance) diff --git a/xml/_parse_sync_test.ts b/xml/_parse_sync_test.ts index 775ddb47227e..7940d3d7cdc9 100644 --- a/xml/_parse_sync_test.ts +++ b/xml/_parse_sync_test.ts @@ -415,6 +415,18 @@ Deno.test("parseSync() with trackPosition false reports zero positions", () => { throw new Error("Expected XmlSyntaxError"); }); +Deno.test("parseSync() with trackPosition false omits position from error message", () => { + try { + parseSync("", { trackPosition: false }); + } catch (e) { + if (e instanceof XmlSyntaxError) { + assertEquals(e.message, "Unclosed element "); + return; + } + } + throw new Error("Expected XmlSyntaxError"); +}); + Deno.test("parseSync() extracts standalone yes from declaration", () => { // Tests standalone attribute extraction const doc = parseSync(''); diff --git a/xml/parse_stream_test.ts b/xml/parse_stream_test.ts index c97da3bf29f3..37edc2859a2b 100644 --- a/xml/parse_stream_test.ts +++ b/xml/parse_stream_test.ts @@ -168,6 +168,53 @@ Deno.test("parseXmlStream() handles declaration", async () => { assertEquals(encoding, "UTF-8"); }); +Deno.test("parseXmlStream() invokes onDoctype when disallowDoctype is false", async () => { + const xml = ''; + const stream = ReadableStream.from([xml]); + + const doctypes: Array<{ + name: string; + publicId: string | undefined; + systemId: string | undefined; + }> = []; + + await parseXmlStream( + stream, + { + onDoctype(name, publicId, systemId) { + doctypes.push({ name, publicId, systemId }); + }, + }, + { disallowDoctype: false }, + ); + + assertEquals(doctypes, [{ + name: "html", + publicId: "-//W3C//DTD XHTML 1.0//EN", + systemId: "http://www.w3.org/TR/xhtml1/DTD/xhtml1.dtd", + }]); +}); + +Deno.test("parseXmlStream() rejects DOCTYPE by default without invoking onDoctype", async () => { + const xml = ""; + const stream = ReadableStream.from([xml]); + + let called = false; + + await assertRejects( + () => + parseXmlStream(stream, { + onDoctype() { + called = true; + }, + }), + XmlSyntaxError, + "DOCTYPE", + ); + assertEquals(called, false); +}); + Deno.test("parseXmlStream() ignores whitespace when configured", async () => { const xml = "\n \n"; const stream = ReadableStream.from([xml]); diff --git a/xml/stringify.ts b/xml/stringify.ts index 751ae52c5cd4..cb4ebc747f86 100644 --- a/xml/stringify.ts +++ b/xml/stringify.ts @@ -9,7 +9,7 @@ import type { StringifyOptions, - XmlDeclarationEvent, + XmlDeclaration, XmlDocument, XmlElement, XmlNode, @@ -64,7 +64,7 @@ export function stringify( } /** Serializes an XML declaration to a string. */ -function serializeDeclaration(decl: XmlDeclarationEvent): string { +function serializeDeclaration(decl: XmlDeclaration): string { const encoding = decl.encoding !== undefined ? ` encoding="${decl.encoding}"` : ""; diff --git a/xml/types.ts b/xml/types.ts index 0cb77456ab83..e7c985996cd7 100644 --- a/xml/types.ts +++ b/xml/types.ts @@ -33,7 +33,8 @@ export interface XmlPosition { */ export class XmlSyntaxError extends SyntaxError { /** - * The line number where the error occurred (1-indexed). + * The line number where the error occurred (1-indexed), or `0` when + * position tracking is disabled. * * @example Usage * ```ts @@ -46,7 +47,8 @@ export class XmlSyntaxError extends SyntaxError { */ readonly line: number; /** - * The column number where the error occurred (1-indexed). + * The column number where the error occurred (1-indexed), or `0` when + * position tracking is disabled. * * @example Usage * ```ts @@ -76,11 +78,18 @@ export class XmlSyntaxError extends SyntaxError { /** * Constructs a new XmlSyntaxError. * + * The position is appended to the message, unless `position.line` is `0` + * (the sentinel used when position tracking is disabled). + * * @param message The error message describing the syntax issue. * @param position The position in the XML source where the error occurred. */ constructor(message: string, position: XmlPosition) { - super(`${message} at line ${position.line}, column ${position.column}`); + super( + position.line === 0 + ? message + : `${message} at line ${position.line}, column ${position.column}`, + ); this.name = "XmlSyntaxError"; this.line = position.line; this.column = position.column; @@ -116,8 +125,8 @@ export interface XmlName { * * @see {@link https://www.w3.org/TR/xml/#sec-prolog-dtd | XML 1.0 §2.8 Prolog} */ -export interface XmlDeclarationEvent extends XmlPosition { - /** The event type discriminant. */ +export interface XmlDeclaration extends XmlPosition { + /** The type discriminant. */ readonly type: "declaration"; /** The XML version as declared in the document (e.g. `"1.0"` or `"1.1"`). */ readonly version: string; @@ -312,7 +321,7 @@ export type XmlNode = */ export interface XmlDocument { /** The XML declaration, if present. */ - readonly declaration?: XmlDeclarationEvent; + readonly declaration?: XmlDeclaration; /** The root element of the document. */ readonly root: XmlElement; } @@ -399,7 +408,13 @@ export interface XmlEventCallbacks { /** Called for XML declarations. */ onDeclaration?: XmlDeclarationCallback; - /** Called for DOCTYPE declarations. */ + /** + * Called for DOCTYPE declarations. + * + * Only invoked when the {@linkcode BaseParseOptions.disallowDoctype} + * option is set to `false`; with the default (`true`), a DOCTYPE + * declaration throws an {@linkcode XmlSyntaxError} instead. + */ onDoctype?: XmlDoctypeCallback; /** diff --git a/xml/types_test.ts b/xml/types_test.ts index 97c105ebf594..b7b1a005351f 100644 --- a/xml/types_test.ts +++ b/xml/types_test.ts @@ -112,6 +112,19 @@ Deno.test("XmlSyntaxError contains position information", () => { assertEquals(error.message, "Test error at line 5, column 10"); }); +Deno.test("XmlSyntaxError omits position from message when line is 0", () => { + const error = new XmlSyntaxError("Test error", { + line: 0, + column: 0, + offset: 0, + }); + + assertEquals(error.message, "Test error"); + assertEquals(error.line, 0); + assertEquals(error.column, 0); + assertEquals(error.offset, 0); +}); + Deno.test("XmlSyntaxError is instanceof SyntaxError", () => { const error = new XmlSyntaxError("Test", { line: 1, column: 1, offset: 0 }); assertEquals(error instanceof SyntaxError, true);