diff --git a/src/saxes.ts b/src/saxes.ts index f19231af..dd4623f5 100644 --- a/src/saxes.ts +++ b/src/saxes.ts @@ -180,6 +180,7 @@ export const EVENTS = [ "doctype", "comment", "opentagstart", + "attribute", "opentag", "closetag", "cdata", @@ -194,6 +195,7 @@ const EVENT_NAME_TO_HANDLER_NAME: Record = { doctype: "doctypeHandler", comment: "commentHandler", opentagstart: "openTagStartHandler", + attribute: "attributeHandler", opentag: "openTagHandler", closetag: "closeTagHandler", cdata: "cdataHandler", @@ -241,6 +243,17 @@ export type CommentHandler = (comment: string) => void; */ export type OpenTagStartHandler = (tag: StartTagForOptions) => void; +export type AttributeEventForOptions = + O extends { xmlns: true } ? SaxesAttributeNSIncomplete : + O extends { xmlns?: false | undefined } ? SaxesAttributePlain : + SaxesAttribute; + +/** + * Event handler for attributes. + */ +export type AttributeHandler = + (attribute: AttributeEventForOptions) => void; + /** * Event handler for an open tag. This is called when the open tag is * complete. (We've encountered the ">" that ends the open tag.) The default @@ -294,6 +307,7 @@ export type EventNameToHandler = { "doctype": DoctypeHandler; "comment": CommentHandler; "opentagstart": OpenTagStartHandler; + "attribute": AttributeHandler; "opentag": OpenTagHandler; "closetag": CloseTagHandler; "cdata": CDataHandler; @@ -333,20 +347,17 @@ export interface SaxesAttributeNS { } /** - * This is an alias for SaxesAttributeNS which will eventually be removed in a - * future major version. - * - * @deprecated + * This is an attribute, as recorded by a parser which parses namespaces but + * prior to the URI being resolvable. This is what is passed to the attribute + * event handler. */ -export type SaxesAttribute = SaxesAttributeNS; +export type SaxesAttributeNSIncomplete = Exclude; /** * This interface defines the structure of attributes when the parser is * NOT processing namespaces (created with ``xmlns: false``). - * - * This is not exported because this structure is used only internally. */ -interface SaxesAttributePlain { +export interface SaxesAttributePlain { /** * The attribute's name. */ @@ -356,6 +367,11 @@ interface SaxesAttributePlain { value: string; } +/** + * A saxes attribute, with or without namespace information. + */ +export type SaxesAttribute = SaxesAttributeNS | SaxesAttributePlain; + /** * This are the fields that MAY be present on a complete tag. */ @@ -544,7 +560,7 @@ export type TagForOptions = export type StartTagForOptions = O extends { xmlns: true } ? SaxesStartTagNS : - O extends { xmlns: false | undefined } ? SaxesStartTagPlain : + O extends { xmlns?: false | undefined } ? SaxesStartTagPlain : SaxesStartTag; export class SaxesParser { @@ -582,7 +598,7 @@ export class SaxesParser { private prevI!: number; private carriedFromPrevious?: string; private forbiddenState!: number; - private attribList!: (SaxesAttributeNS | SaxesAttributePlain)[]; + private attribList!: (SaxesAttributeNSIncomplete | SaxesAttributePlain)[]; private state!: number; private reportedTextBeforeRoot!: boolean; private reportedTextAfterRoot!: boolean; @@ -611,6 +627,7 @@ export class SaxesParser { private errorHandler?: ErrorHandler; private endHandler?: EndHandler; private readyHandler?: ReadyHandler; + private attributeHandler?: AttributeHandler; /** * Indicates whether or not the parser is closed. If ``true``, wait for @@ -2087,8 +2104,7 @@ export class SaxesParser { let { i: start } = this; // eslint-disable-next-line no-constant-condition while (true) { - const code = this.getCode(); - switch (code) { + switch (this.getCode()) { case q: this.pushAttrib(this.name, this.text + chunk.slice(start, this.prevI)); @@ -2359,7 +2375,10 @@ export class SaxesParser { private pushAttribNS(name: string, value: string): void { const { prefix, local } = this.qname(name); - this.attribList.push({ name, prefix, local, value, uri: undefined }); + const attr = { name, prefix, local, value }; + this.attribList.push(attr); + // eslint-disable-next-line no-unused-expressions + this.attributeHandler?.(attr as AttributeEventForOptions); if (prefix === "xmlns") { const trimmed = value.trim(); if (this.currentXMLVersion === "1.0" && trimmed === "") { @@ -2376,7 +2395,10 @@ export class SaxesParser { } private pushAttribPlain(name: string, value: string): void { - this.attribList.push({ name, value }); + const attr = { name, value }; + this.attribList.push(attr); + // eslint-disable-next-line no-unused-expressions + this.attributeHandler?.(attr as AttributeEventForOptions); } /** @@ -2493,7 +2515,7 @@ export class SaxesParser { const seen = new Set(); // Note: do not apply default ns to attributes: // http://www.w3.org/TR/REC-xml-names/#defaulting - for (const attr of attribList as SaxesAttributeNS[]) { + for (const attr of attribList as SaxesAttributeNSIncomplete[]) { const { name, prefix, local } = attr; let uri; let eqname; diff --git a/test/attribute-name.ts b/test/attribute-name.ts index dff24bd4..2ba4fcdf 100644 --- a/test/attribute-name.ts +++ b/test/attribute-name.ts @@ -5,6 +5,12 @@ test({ xml: "", expect: [ ["opentagstart", { name: "root", attributes: {}, ns: {} }], + ["attribute", { + name: "length", + value: "12345", + prefix: "", + local: "length", + }], [ "opentag", { diff --git a/test/attribute-no-space.ts b/test/attribute-no-space.ts index d9f8ea17..9aa66b86 100644 --- a/test/attribute-no-space.ts +++ b/test/attribute-no-space.ts @@ -6,7 +6,15 @@ test({ xml: "", expect: [ ["opentagstart", { name: "root", attributes: {} }], + ["attribute", { + name: "attr1", + value: "first", + }], ["error", "1:20: no whitespace between attributes."], + ["attribute", { + name: "attr2", + value: "second", + }], ["opentag", { name: "root", attributes: { @@ -35,6 +43,14 @@ test({ name: "root", attributes: {}, }], + ["attribute", { + name: "attr1", + value: "first", + }], + ["attribute", { + name: "attr2", + value: "second", + }], ["opentag", { name: "root", attributes: { @@ -63,6 +79,14 @@ test({ name: "root", attributes: {}, }], + ["attribute", { + name: "attr1", + value: "first", + }], + ["attribute", { + name: "attr2", + value: "second", + }], ["opentag", { name: "root", attributes: { @@ -91,6 +115,14 @@ test({ name: "root", attributes: {}, }], + ["attribute", { + name: "attr1", + value: "first", + }], + ["attribute", { + name: "attr2", + value: "second", + }], ["opentag", { name: "root", attributes: { diff --git a/test/attribute-normalization.ts b/test/attribute-normalization.ts index bd35684b..cb20f732 100644 --- a/test/attribute-normalization.ts +++ b/test/attribute-normalization.ts @@ -11,6 +11,14 @@ test({ name: "root", attributes: {}, }], + ["attribute", { + name: "attr1", + value: "\r \n\t", + }], + ["attribute", { + name: "attr2", + value: " a b", + }], ["opentag", { name: "root", attributes: { attr1: "\r \n\t", attr2: " a b" }, diff --git a/test/attribute-unquoted.ts b/test/attribute-unquoted.ts index 6a46707f..e29ecf6f 100644 --- a/test/attribute-unquoted.ts +++ b/test/attribute-unquoted.ts @@ -7,6 +7,12 @@ test({ expect: [ ["opentagstart", { name: "root", attributes: {}, ns: {} }], ["error", "1:14: unquoted attribute value."], + ["attribute", { + name: "length", + value: "12345", + prefix: "", + local: "length", + }], ["opentag", { name: "root", attributes: { diff --git a/test/bom.ts b/test/bom.ts index 71647be7..5bec73ea 100644 --- a/test/bom.ts +++ b/test/bom.ts @@ -17,6 +17,7 @@ test({ xml: "\uFEFF

\uFEFFStarts and ends with BOM\uFEFF

", expect: [ ["opentagstart", { name: "P", attributes: {} }], + ["attribute", { name: "BOM", value: "\uFEFF" }], ["opentag", { name: "P", attributes: { BOM: "\uFEFF" }, diff --git a/test/cdata.ts b/test/cdata.ts index 0673577c..832ea3ca 100644 --- a/test/cdata.ts +++ b/test/cdata.ts @@ -16,6 +16,7 @@ test({ name: "cdata end in attribute", expect: [ ["opentagstart", { name: "r", attributes: {} }], + ["attribute", { name: "foo", value: "]]>" }], ["opentag", { name: "r", attributes: { diff --git a/test/duplicate-attribute.ts b/test/duplicate-attribute.ts index 7e55e911..0070e865 100644 --- a/test/duplicate-attribute.ts +++ b/test/duplicate-attribute.ts @@ -8,6 +8,8 @@ test({ name: "span", attributes: {}, }], + ["attribute", { name: "id", value: "hello" }], + ["attribute", { name: "id", value: "there" }], ["error", "1:28: duplicate attribute: id."], ["opentag", { name: "span", diff --git a/test/eol-handling.ts b/test/eol-handling.ts index 50ef5b25..5172efde 100644 --- a/test/eol-handling.ts +++ b/test/eol-handling.ts @@ -22,6 +22,10 @@ describe("eol handling", () => { const expect = [ ["text", "\n\n"], ["opentagstart", { name: "moo", attributes: {} }], + ["attribute", { + name: "a", + value: "12 3", + }], ["opentag", { name: "moo", attributes: { @@ -142,6 +146,14 @@ SYSTEM ["processinginstruction", { target: "fnord", body: "" }], ["text", "\n"], ["opentagstart", { name: "moo", attributes: {} }], + ["attribute", { + name: "a", + value: "12 3", + }], + ["attribute", { + name: "b", + value: " z ", + }], ["opentag", { name: "moo", attributes: { @@ -160,6 +172,10 @@ abc ["processinginstruction", { target: "fnord", body: "" }], ["text", "\n"], ["opentagstart", { name: "abc", attributes: {} }], + ["attribute", { + name: "a", + value: "bc", + }], ["opentag", { name: "abc", attributes: { diff --git a/test/issue-47.ts b/test/issue-47.ts index 9e6f4d17..b41f5215 100644 --- a/test/issue-47.ts +++ b/test/issue-47.ts @@ -6,6 +6,7 @@ test({ xml: "", expect: [ ["opentagstart", { name: "a", attributes: {} }], + ["attribute", { name: "href", value: "query.svc?x=1&y=2&z=3" }], ["opentag", { name: "a", attributes: { href: "query.svc?x=1&y=2&z=3" }, diff --git a/test/opentagstart.ts b/test/opentagstart.ts index a99f6a5f..61a1009f 100644 --- a/test/opentagstart.ts +++ b/test/opentagstart.ts @@ -13,6 +13,15 @@ describe("openstarttag", () => { attributes: {}, }, ], + [ + "attribute", + { + name: "length", + value: "12345", + prefix: "", + local: "length", + }, + ], [ "opentag", { @@ -70,6 +79,13 @@ describe("openstarttag", () => { attributes: {}, }, ], + [ + "attribute", + { + name: "length", + value: "12345", + }, + ], [ "opentag", { diff --git a/test/testutil.ts b/test/testutil.ts index a1dcb372..3d624843 100644 --- a/test/testutil.ts +++ b/test/testutil.ts @@ -1,5 +1,6 @@ import { expect } from "chai"; -import { EVENTS, SaxesOptions, SaxesParser } from "../build/dist/saxes"; +import { EventName, EVENTS, SaxesOptions, + SaxesParser } from "../build/dist/saxes"; export interface TestOptions { xml?: string | string[]; @@ -8,14 +9,15 @@ export interface TestOptions { expect: any[]; fn?: (parser: SaxesParser<{}>) => void; opt?: SaxesOptions; + events?: EventName[]; } export function test(options: TestOptions): void { - const { xml, name, expect: expected, fn } = options; + const { xml, name, expect: expected, fn, events } = options; it(name, () => { const parser = new SaxesParser(options.opt); let expectedIx = 0; - for (const ev of EVENTS) { + for (const ev of events ?? EVENTS) { // eslint-disable-next-line @typescript-eslint/no-explicit-any, no-loop-func parser.on(ev, (n: any) => { if (process.env.DEBUG !== undefined) { @@ -49,9 +51,7 @@ export function test(options: TestOptions): void { } } - if (fn !== undefined) { - fn(parser); - } + fn?.(parser); expect(expectedIx).to.equal(expected.length); }); diff --git a/test/trailing-attribute-no-value.ts b/test/trailing-attribute-no-value.ts index 78df17b9..b02f65ba 100644 --- a/test/trailing-attribute-no-value.ts +++ b/test/trailing-attribute-no-value.ts @@ -6,6 +6,7 @@ test({ expect: [ ["opentagstart", { name: "root", attributes: {} }], ["error", "1:13: attribute without value."], + ["attribute", { name: "attrib", value: "attrib" }], ["opentag", { name: "root", attributes: { attrib: "attrib" }, isSelfClosing: false }], ["closetag", diff --git a/test/xml-internal-entities.ts b/test/xml-internal-entities.ts index a3f09551..5f6301f5 100644 --- a/test/xml-internal-entities.ts +++ b/test/xml-internal-entities.ts @@ -56,31 +56,10 @@ for (const entity in entitiesToTest) { test({ name: "xml internal entities", expect: [ - [ - "opentagstart", - { - name: "a", - attributes: {}, - }, - ], ...attributeErrors, - [ - "opentag", - { - name: "a", - attributes: myAttributes, - isSelfClosing: true, - }, - ], - [ - "closetag", - { - name: "a", - attributes: myAttributes, - isSelfClosing: true, - }, - ], ], + // We only care about errrors for this test. + events: ["error"], fn(parser: SaxesParser): void { Object.assign(parser.ENTITIES, ENTITIES); parser.write(`${xmlStart}/>`).close(); diff --git a/test/xmlns-issue-41.ts b/test/xmlns-issue-41.ts index 22266271..4447599b 100644 --- a/test/xmlns-issue-41.ts +++ b/test/xmlns-issue-41.ts @@ -2,6 +2,25 @@ import { test } from "./testutil"; const expect = [ ["opentagstart", { name: "parent", attributes: {}, ns: {} }], + [ + "attribute", + { + name: "xmlns:a", + local: "a", + prefix: "xmlns", + // eslint-disable-next-line @typescript-eslint/tslint/config + value: "http://ATTRIBUTE", + }, + ], + [ + "attribute", + { + name: "a:attr", + local: "attr", + prefix: "a", + value: "value", + }, + ], [ "opentag", { @@ -75,12 +94,23 @@ const xmls = [ "", "", ]; +// Take the first expect array and create a new one with elements at indexes 1 +// and 2 swapped. +const expect2 = expect.slice(); +const tmp = expect2[1]; +// eslint-disable-next-line prefer-destructuring +expect2[1] = expect2[2]; +expect2[2] = tmp; +const expects = [ + expect, + expect2, +]; describe("issue 41", () => { xmls.forEach((x, i) => { test({ name: `order ${i}`, xml: x, - expect, + expect: expects[i], opt: { xmlns: true, }, diff --git a/test/xmlns-rebinding.ts b/test/xmlns-rebinding.ts index fcdb1528..723a6c14 100644 --- a/test/xmlns-rebinding.ts +++ b/test/xmlns-rebinding.ts @@ -17,6 +17,42 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "xmlns:x", + value: "x1", + prefix: "xmlns", + local: "x", + }, + ], + [ + "attribute", + { + name: "xmlns:y", + value: "y1", + prefix: "xmlns", + local: "y", + }, + ], + [ + "attribute", + { + name: "x:a", + value: "x1", + prefix: "x", + local: "a", + }, + ], + [ + "attribute", + { + name: "y:a", + value: "y1", + prefix: "y", + local: "a", + }, + ], [ "opentag", { @@ -71,6 +107,15 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "xmlns:x", + value: "x2", + prefix: "xmlns", + local: "x", + }, + ], [ "opentag", { @@ -102,6 +147,24 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "x:a", + value: "x2", + prefix: "x", + local: "a", + }, + ], + [ + "attribute", + { + name: "y:a", + value: "y1", + prefix: "y", + local: "a", + }, + ], [ "opentag", { @@ -187,6 +250,24 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "x:a", + value: "x1", + prefix: "x", + local: "a", + }, + ], + [ + "attribute", + { + name: "y:a", + value: "y1", + prefix: "y", + local: "a", + }, + ], [ "opentag", { diff --git a/test/xmlns-strict.ts b/test/xmlns-strict.ts index c5253970..f7ebe5f0 100644 --- a/test/xmlns-strict.ts +++ b/test/xmlns-strict.ts @@ -41,6 +41,15 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "attr", + value: "normal", + prefix: "", + local: "attr", + }, + ], [ "opentag", { @@ -89,6 +98,15 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "xmlns", + value: "uri:default", + prefix: "", + local: "xmlns", + }, + ], [ "opentag", { @@ -120,6 +138,15 @@ test({ attributes: {}, }, ], + [ + "attribute", + { + name: "attr", + value: "normal", + prefix: "", + local: "attr", + }, + ], [ "opentag", { @@ -191,6 +218,15 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "xmlns:a", + value: "uri:nsa", + prefix: "xmlns", + local: "a", + }, + ], [ "opentag", { @@ -222,6 +258,15 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "attr", + value: "normal", + prefix: "", + local: "attr", + }, + ], [ "opentag", { @@ -270,6 +315,15 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "a:attr", + value: "namespaced", + prefix: "a", + local: "attr", + }, + ], [ "opentag", { diff --git a/test/xmlns-unbound.ts b/test/xmlns-unbound.ts index 9130a497..86161a70 100644 --- a/test/xmlns-unbound.ts +++ b/test/xmlns-unbound.ts @@ -63,6 +63,15 @@ describe("xmlns unbound prefixes", () => { ns: {}, }, ], + [ + "attribute", + { + name: "xmlns:unbound", + value: "someuri", + prefix: "xmlns", + local: "unbound", + }, + ], [ "opentag", { @@ -127,6 +136,15 @@ describe("xmlns unbound prefixes", () => { ns: {}, }, ], + [ + "attribute", + { + name: "unbound:attr", + value: "value", + prefix: "unbound", + local: "attr", + }, + ], [ "error", "1:28: unbound namespace prefix: \"unbound\".", diff --git a/test/xmlns-xml-default-ns.ts b/test/xmlns-xml-default-ns.ts index 13160a19..ac8a1234 100644 --- a/test/xmlns-xml-default-ns.ts +++ b/test/xmlns-xml-default-ns.ts @@ -30,6 +30,25 @@ test({ ns: {}, }, ], + [ + "attribute", + { + name: "xmlns", + // eslint-disable-next-line @typescript-eslint/tslint/config + value: "http://foo", + prefix: "", + local: "xmlns", + }, + ], + [ + "attribute", + { + name: "attr", + value: "bar", + prefix: "", + local: "attr", + }, + ], [ "opentag", { diff --git a/test/xmlns-xml-default-prefix.ts b/test/xmlns-xml-default-prefix.ts index 31dfca5b..c66b47fe 100644 --- a/test/xmlns-xml-default-prefix.ts +++ b/test/xmlns-xml-default-prefix.ts @@ -55,6 +55,15 @@ describe("xml default prefix", () => { ns: {}, }, ], + [ + "attribute", + { + name: "xml:lang", + local: "lang", + prefix: "xml", + value: "en", + }, + ], [ "opentag", { @@ -113,6 +122,15 @@ describe("xml default prefix", () => { ns: {}, }, ], + [ + "attribute", + { + name: "xmlns:xml", + local: "xml", + prefix: "xmlns", + value: "ERROR", + }, + ], [ "error", "1:27: xml prefix must be bound to \