Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions xml/_parse_sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
ParseOptions,
XmlCDataNode,
XmlCommentNode,
XmlDeclarationEvent,
XmlDeclaration,
XmlDocument,
XmlElement,
XmlName,
Expand Down Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions xml/_parse_sync_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<root><unclosed>", { trackPosition: false });
} catch (e) {
if (e instanceof XmlSyntaxError) {
assertEquals(e.message, "Unclosed element <unclosed>");
return;
}
}
throw new Error("Expected XmlSyntaxError");
});

Deno.test("parseSync() extracts standalone yes from declaration", () => {
// Tests standalone attribute extraction
const doc = parseSync('<?xml version="1.0" standalone="yes"?><root/>');
Expand Down
2 changes: 1 addition & 1 deletion xml/_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 50 additions & 1 deletion xml/_tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
*/

import {
type XmlContentCallback,
type XmlDeclarationCallback,
type XmlDoctypeCallback,
type XmlPosition,
type XmlProcessingInstructionCallback,
XmlSyntaxError,
type XmlTokenCallbacks,
} from "./types.ts";
import {
isIllegalXmlLiteralChar,
Expand All @@ -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., `<element`). */
onStartTagOpen?(
name: string,
line: number,
column: number,
offset: number,
): void;

/** Called for each attribute in a start tag. */
onAttribute?(name: string, value: string): void;

/** Called when a start tag closes (e.g., `>` or `/>`). */
onStartTagClose?(selfClosing: boolean): void;

/** Called when an end tag is encountered (e.g., `</element>`). */
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 {
/**
Expand Down
3 changes: 1 addition & 2 deletions xml/_tokenizer_test.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down
47 changes: 47 additions & 0 deletions xml/parse_stream_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0//EN" ' +
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1.dtd"><html/>';
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 = "<!DOCTYPE root><root/>";
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 = "<root>\n <item/>\n</root>";
const stream = ReadableStream.from([xml]);
Expand Down
4 changes: 2 additions & 2 deletions xml/stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import type {
StringifyOptions,
XmlDeclarationEvent,
XmlDeclaration,
XmlDocument,
XmlElement,
XmlNode,
Expand Down Expand Up @@ -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}"`
: "";
Expand Down
Loading
Loading