From 9b1fe203ae92415072271eba2503c55436962c54 Mon Sep 17 00:00:00 2001 From: Tomas Zijdemans Date: Thu, 8 Jan 2026 22:00:13 +0100 Subject: [PATCH 1/3] feat(xml): add XML module with streaming parser, DOM-style parser, and serialization --- .github/workflows/title.yml | 1 + browser-compat.tsconfig.json | 1 + deno.json | 4 +- import_map.json | 2 +- xml/_common.ts | 66 + xml/_entities.ts | 178 + xml/_entities_test.ts | 307 + xml/_parse_sync.ts | 492 + xml/_parse_sync_test.ts | 302 + xml/_parser.ts | 316 + xml/_parser_test.ts | 436 + xml/_tokenizer.ts | 1062 ++ xml/_tokenizer_test.ts | 1236 ++ xml/deno.json | 11 + xml/mod.ts | 37 + xml/parse.ts | 174 + xml/parse_stream.ts | 161 + xml/parse_stream_test.ts | 333 + xml/parse_test.ts | 257 + xml/stringify.ts | 256 + xml/stringify_test.ts | 600 + xml/testdata/cdata-edge-cases.xml | 53 + xml/testdata/cdata.xml | 22 + xml/testdata/comments-edge-cases.xml | 48 + xml/testdata/deep-nesting.xml | 35 + xml/testdata/doctype-internal.xml | 22 + xml/testdata/doctype.xml | 16 + xml/testdata/edge-cases.xml | 43 + xml/testdata/entities.xml | 22 + xml/testdata/gpx.xml | 50 + xml/testdata/large.xml | 11003 ++++++++++++++++ .../malformed/double-dash-comment.xml | 5 + xml/testdata/malformed/invalid-char.xml | 4 + xml/testdata/malformed/mismatched.xml | 5 + xml/testdata/malformed/unclosed.xml | 7 + xml/testdata/mixed-content.xml | 25 + xml/testdata/namespaced.xml | 19 + xml/testdata/processing-instructions.xml | 24 + xml/testdata/rss.xml | 27 + xml/testdata/simple.xml | 11 + xml/testdata/sitemap.xml | 41 + xml/testdata/soap-envelope.xml | 25 + xml/testdata/svg.xml | 33 + xml/testdata/unicode.xml | 36 + xml/types.ts | 483 + xml/types_test.ts | 148 + 46 files changed, 18437 insertions(+), 2 deletions(-) create mode 100644 xml/_common.ts create mode 100644 xml/_entities.ts create mode 100644 xml/_entities_test.ts create mode 100644 xml/_parse_sync.ts create mode 100644 xml/_parse_sync_test.ts create mode 100644 xml/_parser.ts create mode 100644 xml/_parser_test.ts create mode 100644 xml/_tokenizer.ts create mode 100644 xml/_tokenizer_test.ts create mode 100644 xml/deno.json create mode 100644 xml/mod.ts create mode 100644 xml/parse.ts create mode 100644 xml/parse_stream.ts create mode 100644 xml/parse_stream_test.ts create mode 100644 xml/parse_test.ts create mode 100644 xml/stringify.ts create mode 100644 xml/stringify_test.ts create mode 100644 xml/testdata/cdata-edge-cases.xml create mode 100644 xml/testdata/cdata.xml create mode 100644 xml/testdata/comments-edge-cases.xml create mode 100644 xml/testdata/deep-nesting.xml create mode 100644 xml/testdata/doctype-internal.xml create mode 100644 xml/testdata/doctype.xml create mode 100644 xml/testdata/edge-cases.xml create mode 100644 xml/testdata/entities.xml create mode 100644 xml/testdata/gpx.xml create mode 100644 xml/testdata/large.xml create mode 100644 xml/testdata/malformed/double-dash-comment.xml create mode 100644 xml/testdata/malformed/invalid-char.xml create mode 100644 xml/testdata/malformed/mismatched.xml create mode 100644 xml/testdata/malformed/unclosed.xml create mode 100644 xml/testdata/mixed-content.xml create mode 100644 xml/testdata/namespaced.xml create mode 100644 xml/testdata/processing-instructions.xml create mode 100644 xml/testdata/rss.xml create mode 100644 xml/testdata/simple.xml create mode 100644 xml/testdata/sitemap.xml create mode 100644 xml/testdata/soap-envelope.xml create mode 100644 xml/testdata/svg.xml create mode 100644 xml/testdata/unicode.xml create mode 100644 xml/types.ts create mode 100644 xml/types_test.ts diff --git a/.github/workflows/title.yml b/.github/workflows/title.yml index 2b60efe2118c..a16e167a8321 100644 --- a/.github/workflows/title.yml +++ b/.github/workflows/title.yml @@ -76,4 +76,5 @@ jobs: ulid(/unstable)? uuid(/unstable)? webgpu(/unstable)? + xml(/unstable)? yaml(/unstable)? diff --git a/browser-compat.tsconfig.json b/browser-compat.tsconfig.json index 67f1c4f91b22..ef4f01edc9fb 100644 --- a/browser-compat.tsconfig.json +++ b/browser-compat.tsconfig.json @@ -48,6 +48,7 @@ "./ulid", "./uuid", "./webgpu", + "./xml", "./yaml" ] } diff --git a/deno.json b/deno.json index 2be179b1ba3b..07e24075e9a8 100644 --- a/deno.json +++ b/deno.json @@ -39,7 +39,8 @@ "_tools/node_test_runner", "http/testdata", "fs/testdata", - "dotenv/testdata" + "dotenv/testdata", + "xml/testdata" ], "lint": { "rules": { @@ -94,6 +95,7 @@ "./ulid", "./uuid", "./webgpu", + "./xml", "./yaml" ] } diff --git a/import_map.json b/import_map.json index 6f68dd6653ce..077208a7d2b0 100644 --- a/import_map.json +++ b/import_map.json @@ -5,7 +5,6 @@ "npm:/typescript": "npm:typescript@5.8.2", "automation/": "https://raw.githubusercontent.com/denoland/automation/0.10.0/", "graphviz": "npm:node-graphviz@^0.1.1", - "@std/assert": "jsr:@std/assert@^1.0.16", "@std/async": "jsr:@std/async@^1.0.16", "@std/bytes": "jsr:@std/bytes@^1.0.6", @@ -46,6 +45,7 @@ "@std/ulid": "jsr:@std/ulid@^1.0.0", "@std/uuid": "jsr:@std/uuid@^1.1.0", "@std/webgpu": "jsr:@std/webgpu@^0.224.9", + "@std/xml": "jsr:@std/xml@^0.0.1", "@std/yaml": "jsr:@std/yaml@^1.0.10" } } diff --git a/xml/_common.ts b/xml/_common.ts new file mode 100644 index 000000000000..4a5d1d73d8e0 --- /dev/null +++ b/xml/_common.ts @@ -0,0 +1,66 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +// This module is browser compatible. + +/** + * Internal shared utilities for the XML module. + * + * @module + */ + +import type { XmlName } from "./types.ts"; + +/** + * Line ending normalization pattern per XML 1.0 §2.11. + * Converts \r\n and standalone \r to \n. + */ +export const LINE_ENDING_RE = /\r\n?/g; + +/** + * Whitespace-only test per XML 1.0 §2.3. + * Uses explicit [ \t\r\n] instead of \s to match XML spec exactly: + * S ::= (#x20 | #x9 | #xD | #xA)+ + */ +export const WHITESPACE_ONLY_RE = /^[ \t\r\n]*$/; + +/** + * XML declaration version attribute pattern. + * Matches both single and double quoted values. + */ +export const VERSION_RE = /version\s*=\s*(?:"([^"]+)"|'([^']+)')/; + +/** + * XML declaration encoding attribute pattern. + * Matches both single and double quoted values. + */ +export const ENCODING_RE = /encoding\s*=\s*(?:"([^"]+)"|'([^']+)')/; + +/** + * XML declaration standalone attribute pattern. + * Matches both single and double quoted values, restricted to "yes" or "no". + */ +export const STANDALONE_RE = /standalone\s*=\s*(?:"(yes|no)"|'(yes|no)')/; + +/** + * Parses a qualified XML name into its prefix and local parts. + * + * @example Usage + * ```ts + * import { parseName } from "./_common.ts"; + * + * parseName("ns:element"); // { prefix: "ns", local: "element" } + * parseName("element"); // { local: "element" } + * ``` + * + * @param name The raw name string (e.g., "ns:element" or "element") + * @returns An XmlName object with local and optional prefix + */ +export function parseName(name: string): XmlName { + const colonIndex = name.indexOf(":"); + if (colonIndex === -1) { + return { local: name }; + } + return { + prefix: name.slice(0, colonIndex), + local: name.slice(colonIndex + 1), + }; +} diff --git a/xml/_entities.ts b/xml/_entities.ts new file mode 100644 index 000000000000..9f77267df25e --- /dev/null +++ b/xml/_entities.ts @@ -0,0 +1,178 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +// This module is browser compatible. + +/** + * Internal module for XML entity encoding and decoding. + * + * @module + */ + +/** + * The five predefined XML entities per XML 1.0 §4.6. + * Using const assertion for precise typing. + */ +const NAMED_ENTITIES = { + lt: "<", + gt: ">", + amp: "&", + apos: "'", + quot: '"', +} as const; + +/** + * Reverse mapping for encoding special characters. + */ +const CHAR_TO_ENTITY = { + "<": "<", + ">": ">", + "&": "&", + "'": "'", + '"': """, +} as const; + +/** + * Extended mapping for attribute value encoding (includes whitespace). + */ +const ATTR_CHAR_MAP: Record = { + "<": "<", + ">": ">", + "&": "&", + "'": "'", + '"': """, + "\t": " ", + "\n": " ", + "\r": " ", +}; + +// Hoisted regex patterns for performance +const ENTITY_RE = /&([a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+);/g; +const SPECIAL_CHARS_RE = /[<>&'"]/g; +const ATTR_ENCODE_RE = /[<>&'"\t\n\r]/g; + +/** + * Pattern to detect bare `&` not followed by a valid reference. + * Valid references are: &name; or &#digits; or &#xhexdigits; + */ +const BARE_AMPERSAND_RE = /&(?![a-zA-Z][a-zA-Z0-9]*;|#[0-9]+;|#x[0-9a-fA-F]+;)/; + +/** + * Checks if a code point is a valid XML 1.0 Char per §2.2. + * + * Per the specification: + * Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + * + * This excludes: + * - NULL (#x0) + * - Control characters #x1-#x8, #xB-#xC, #xE-#x1F + * - Surrogate pairs #xD800-#xDFFF (handled separately) + * - Non-characters #xFFFE-#xFFFF + * + * @see {@link https://www.w3.org/TR/xml/#charsets | XML 1.0 §2.2 Characters} + */ +function isValidXmlChar(codePoint: number): boolean { + return ( + codePoint === 0x9 || + codePoint === 0xA || + codePoint === 0xD || + (codePoint >= 0x20 && codePoint <= 0xD7FF) || + (codePoint >= 0xE000 && codePoint <= 0xFFFD) || + (codePoint >= 0x10000 && codePoint <= 0x10FFFF) + ); +} + +/** + * Options for entity decoding. + */ +export interface DecodeEntityOptions { + /** + * If true, throws an error on invalid bare `&` characters. + * Per XML 1.0 §3.1, `&` must be escaped as `&` unless it starts + * a valid entity or character reference. + * + * @default false + */ + readonly strict?: boolean; +} + +/** + * Decodes XML entities in a string. + * + * Handles the five predefined entities (§4.6) and numeric character + * references (§4.1) per the XML 1.0 specification. + * + * @param text The text containing XML entities to decode. + * @param options Decoding options. + * @returns The text with entities decoded. + */ +export function decodeEntities( + text: string, + options?: DecodeEntityOptions, +): string { + // Fast path: no ampersand means no entities to decode + if (!text.includes("&")) return text; + + if (options?.strict) { + const match = BARE_AMPERSAND_RE.exec(text); + if (match) { + throw new Error( + `Invalid bare '&' at position ${match.index}: ` + + `entity references must be &name; or &#num; or &#xHex;`, + ); + } + } + + return text.replace(ENTITY_RE, (match, entity: string) => { + if (entity.startsWith("#x")) { + // Hexadecimal character reference + const codePoint = parseInt(entity.slice(2), 16); + // Invalid per XML 1.0 §4.1 WFC: Legal Character - must match Char production + if (!isValidXmlChar(codePoint)) { + return match; + } + return String.fromCodePoint(codePoint); + } + if (entity.startsWith("#")) { + // Decimal character reference + const codePoint = parseInt(entity.slice(1), 10); + // Invalid per XML 1.0 §4.1 WFC: Legal Character - must match Char production + if (!isValidXmlChar(codePoint)) { + return match; + } + return String.fromCodePoint(codePoint); + } + // Named entity + if (entity in NAMED_ENTITIES) { + return NAMED_ENTITIES[entity as keyof typeof NAMED_ENTITIES]; + } + // Unknown entity - return as-is + return match; + }); +} + +/** + * Encodes special characters as XML entities. + * + * @param text The text to encode. + * @returns The text with special characters encoded as entities. + */ +export function encodeEntities(text: string): string { + // Fast path: no special characters means nothing to encode + if (!/[<>&'"]/.test(text)) return text; + return text.replace( + SPECIAL_CHARS_RE, + (char) => CHAR_TO_ENTITY[char as keyof typeof CHAR_TO_ENTITY], + ); +} + +/** + * Encodes special characters for use in XML attribute values. + * Encodes whitespace characters that would be normalized per XML 1.0 §3.3.3. + * + * @param value The attribute value to encode. + * @returns The encoded attribute value. + */ +export function encodeAttributeValue(value: string): string { + // Fast path: no special characters means nothing to encode + if (!/[<>&'"\t\n\r]/.test(value)) return value; + return value.replace(ATTR_ENCODE_RE, (c) => ATTR_CHAR_MAP[c]!); +} diff --git a/xml/_entities_test.ts b/xml/_entities_test.ts new file mode 100644 index 000000000000..4ac01ce96f0a --- /dev/null +++ b/xml/_entities_test.ts @@ -0,0 +1,307 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +import { assertEquals, assertThrows } from "@std/assert"; +import { + decodeEntities, + encodeAttributeValue, + encodeEntities, +} from "./_entities.ts"; + +// ============================================================================= +// decodeEntities tests +// ============================================================================= + +Deno.test("decodeEntities() decodes predefined entities", () => { + assertEquals(decodeEntities("<"), "<"); + assertEquals(decodeEntities(">"), ">"); + assertEquals(decodeEntities("&"), "&"); + assertEquals(decodeEntities("'"), "'"); + assertEquals(decodeEntities("""), '"'); +}); + +Deno.test("decodeEntities() decodes multiple entities in a string", () => { + assertEquals( + decodeEntities("<hello> & "world""), + ' & "world"', + ); +}); + +Deno.test("decodeEntities() decodes decimal character references", () => { + assertEquals(decodeEntities("<"), "<"); + assertEquals(decodeEntities(">"), ">"); + assertEquals(decodeEntities("&"), "&"); + assertEquals(decodeEntities("'"), "'"); + assertEquals(decodeEntities("""), '"'); + assertEquals(decodeEntities("A"), "A"); + assertEquals(decodeEntities("€"), "€"); +}); + +Deno.test("decodeEntities() decodes hexadecimal character references", () => { + assertEquals(decodeEntities("<"), "<"); + assertEquals(decodeEntities("<"), "<"); + assertEquals(decodeEntities(">"), ">"); + assertEquals(decodeEntities("&"), "&"); + assertEquals(decodeEntities("'"), "'"); + assertEquals(decodeEntities("""), '"'); + assertEquals(decodeEntities("A"), "A"); + assertEquals(decodeEntities("€"), "€"); +}); + +Deno.test("decodeEntities() leaves unknown entities unchanged", () => { + assertEquals(decodeEntities("&unknown;"), "&unknown;"); + assertEquals(decodeEntities(" "), " "); +}); + +Deno.test("decodeEntities() handles text without entities", () => { + assertEquals(decodeEntities("hello world"), "hello world"); + assertEquals(decodeEntities(""), ""); +}); + +Deno.test("decodeEntities() handles incomplete entity-like patterns", () => { + assertEquals(decodeEntities("a & b"), "a & b"); + assertEquals(decodeEntities("foo &bar"), "foo &bar"); + assertEquals(decodeEntities("&;"), "&;"); +}); + +Deno.test("decodeEntities() handles invalid code points gracefully", () => { + // Code points exceeding max Unicode (0x10FFFF) are returned as-is + assertEquals(decodeEntities("�"), "�"); + assertEquals(decodeEntities("�"), "�"); + assertEquals(decodeEntities("�"), "�"); +}); + +Deno.test("decodeEntities() handles max valid Unicode code point", () => { + // 0x10FFFF is the maximum valid Unicode code point + assertEquals(decodeEntities("􏿿"), "\u{10FFFF}"); + assertEquals(decodeEntities("􏿿"), "\u{10FFFF}"); +}); + +Deno.test("decodeEntities() handles leading zeros in character references", () => { + assertEquals(decodeEntities("A"), "A"); + assertEquals(decodeEntities("A"), "A"); + assertEquals(decodeEntities("A"), "A"); +}); + +Deno.test("decodeEntities() handles consecutive entities", () => { + assertEquals(decodeEntities("&&"), "&&"); + assertEquals(decodeEntities("<>"), "<>"); + assertEquals(decodeEntities("ABC"), "ABC"); +}); + +// ============================================================================= +// decodeEntities strict mode tests +// ============================================================================= + +Deno.test("decodeEntities() strict mode throws on bare &", () => { + assertThrows( + () => decodeEntities("foo&bar", { strict: true }), + Error, + "Invalid bare '&' at position 3", + ); +}); + +Deno.test("decodeEntities() strict mode throws on & at end", () => { + assertThrows( + () => decodeEntities("trailing&", { strict: true }), + Error, + "Invalid bare '&' at position 8", + ); +}); + +Deno.test("decodeEntities() strict mode throws on & followed by space", () => { + assertThrows( + () => decodeEntities("a & b", { strict: true }), + Error, + "Invalid bare '&' at position 2", + ); +}); + +Deno.test("decodeEntities() strict mode allows valid entity references", () => { + // Should not throw + assertEquals(decodeEntities("&", { strict: true }), "&"); + assertEquals(decodeEntities("<>", { strict: true }), "<>"); + assertEquals(decodeEntities("A", { strict: true }), "A"); + assertEquals(decodeEntities("A", { strict: true }), "A"); +}); + +Deno.test("decodeEntities() strict mode allows unknown named entities", () => { + // Unknown named entities like   are valid syntax (just not decoded) + assertEquals(decodeEntities(" ", { strict: true }), " "); + assertEquals(decodeEntities("&foo;", { strict: true }), "&foo;"); +}); + +Deno.test("decodeEntities() lenient mode (default) passes through bare &", () => { + // Default behavior: pass through + assertEquals(decodeEntities("foo&bar"), "foo&bar"); + assertEquals(decodeEntities("a & b"), "a & b"); + assertEquals(decodeEntities("trailing&"), "trailing&"); +}); + +Deno.test("decodeEntities() strict mode detects & in attribute-like values", () => { + // Common case: URL query strings in attributes + assertThrows( + () => decodeEntities("http://example.com?a=1&b=2", { strict: true }), + Error, + "Invalid bare '&'", + ); +}); + +// ============================================================================= +// encodeEntities tests +// ============================================================================= + +Deno.test("encodeEntities() encodes special characters", () => { + assertEquals(encodeEntities("<"), "<"); + assertEquals(encodeEntities(">"), ">"); + assertEquals(encodeEntities("&"), "&"); + assertEquals(encodeEntities("'"), "'"); + assertEquals(encodeEntities('"'), """); +}); + +Deno.test("encodeEntities() encodes multiple special characters", () => { + assertEquals( + encodeEntities(' & "world"'), + "<hello> & "world"", + ); +}); + +Deno.test("encodeEntities() leaves regular text unchanged", () => { + assertEquals(encodeEntities("hello world"), "hello world"); + assertEquals(encodeEntities(""), ""); +}); + +Deno.test("encodeEntities() passes through Unicode unchanged", () => { + assertEquals(encodeEntities("héllo wörld"), "héllo wörld"); + assertEquals(encodeEntities("日本語"), "日本語"); + assertEquals(encodeEntities("emoji: 🎉"), "emoji: 🎉"); +}); + +// ============================================================================= +// encodeAttributeValue tests +// ============================================================================= + +Deno.test("encodeAttributeValue() encodes special characters", () => { + assertEquals(encodeAttributeValue("<>&'\""), "<>&'""); +}); + +Deno.test("encodeAttributeValue() encodes whitespace per XML 1.0 §3.3.3", () => { + assertEquals(encodeAttributeValue("a\tb"), "a b"); + assertEquals(encodeAttributeValue("line1\nline2"), "line1 line2"); + assertEquals(encodeAttributeValue("line1\rline2"), "line1 line2"); + assertEquals(encodeAttributeValue("line1\r\nline2"), "line1 line2"); +}); + +Deno.test("encodeAttributeValue() handles combined cases", () => { + assertEquals( + encodeAttributeValue(''), + "<value with "special" chars>", + ); +}); + +// ============================================================================= +// Round-trip tests +// ============================================================================= + +Deno.test("decodeEntities and encodeEntities are inverse operations for basic entities", () => { + const original = ' & "world"'; + const encoded = encodeEntities(original); + const decoded = decodeEntities(encoded); + assertEquals(decoded, original); +}); + +// ============================================================================= +// Additional edge case tests +// ============================================================================= + +Deno.test("decodeEntities() handles empty hex reference", () => { + // &#x; is not a valid pattern, should pass through + assertEquals(decodeEntities("&#x;"), "&#x;"); +}); + +Deno.test("decodeEntities() handles invalid hex digits", () => { + // G is not a hex digit, should pass through + assertEquals(decodeEntities("&#xGG;"), "&#xGG;"); + assertEquals(decodeEntities("&#xZZ;"), "&#xZZ;"); +}); + +Deno.test("decodeEntities() handles empty decimal reference", () => { + // &#; is not valid + assertEquals(decodeEntities("&#;"), "&#;"); +}); + +Deno.test("decodeEntities() handles surrogate code points gracefully", () => { + // U+D800-U+DFFF are surrogate pairs, invalid as single code points in Unicode + // String.fromCodePoint(0xD800) throws RangeError + // Our implementation returns the original reference for invalid surrogates + assertEquals(decodeEntities("�"), "�"); + assertEquals(decodeEntities("�"), "�"); + assertEquals(decodeEntities("�"), "�"); + // Decimal surrogates (55296 = 0xD800, 57343 = 0xDFFF) + assertEquals(decodeEntities("�"), "�"); + assertEquals(decodeEntities("�"), "�"); +}); + +Deno.test("decodeEntities() rejects invalid XML characters per §2.2", () => { + // XML 1.0 §4.1 WFC: Legal Character - char refs must match Char production + // Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] + + // NULL (#x0) is invalid + assertEquals(decodeEntities("�"), "�"); + assertEquals(decodeEntities("�"), "�"); + + // Control characters #x1-#x8 are invalid + assertEquals(decodeEntities(""), ""); + assertEquals(decodeEntities(""), ""); + assertEquals(decodeEntities(""), ""); + assertEquals(decodeEntities(""), ""); + + // #xB and #xC are invalid (but #x9, #xA, #xD are valid) + assertEquals(decodeEntities(" "), " "); + assertEquals(decodeEntities(" "), " "); + assertEquals(decodeEntities(" "), " "); + assertEquals(decodeEntities(" "), " "); + + // Control characters #xE-#x1F are invalid + assertEquals(decodeEntities(""), ""); + assertEquals(decodeEntities(""), ""); + assertEquals(decodeEntities(""), ""); + assertEquals(decodeEntities(""), ""); + + // #xFFFE and #xFFFF are invalid + assertEquals(decodeEntities("￾"), "￾"); + assertEquals(decodeEntities("￿"), "￿"); + assertEquals(decodeEntities("￾"), "￾"); + assertEquals(decodeEntities("￿"), "￿"); +}); + +Deno.test("decodeEntities() allows valid XML whitespace characters", () => { + // #x9 (tab), #xA (newline), #xD (carriage return) are valid + assertEquals(decodeEntities(" "), "\t"); + assertEquals(decodeEntities(" "), "\t"); + assertEquals(decodeEntities(" "), "\n"); + assertEquals(decodeEntities(" "), "\n"); + assertEquals(decodeEntities(" "), "\r"); + assertEquals(decodeEntities(" "), "\r"); +}); + +Deno.test("decodeEntities() handles mixed valid and invalid references", () => { + assertEquals(decodeEntities("<&invalid;>"), "<&invalid;>"); + assertEquals(decodeEntities("&�&"), "&�&"); +}); + +Deno.test("decodeEntities() handles entity at start and end", () => { + assertEquals(decodeEntities("<text>"), ""); + assertEquals(decodeEntities("&"), "&"); +}); + +Deno.test("encodeEntities() handles all special chars adjacent", () => { + assertEquals(encodeEntities("<>&'\""), "<>&'""); +}); + +Deno.test("encodeAttributeValue() handles empty string", () => { + assertEquals(encodeAttributeValue(""), ""); +}); + +Deno.test("encodeAttributeValue() handles only whitespace", () => { + assertEquals(encodeAttributeValue("\t\n\r"), " "); +}); diff --git a/xml/_parse_sync.ts b/xml/_parse_sync.ts new file mode 100644 index 000000000000..67f97f3a762c --- /dev/null +++ b/xml/_parse_sync.ts @@ -0,0 +1,492 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +// This module is browser compatible. + +/** + * Internal synchronous XML parser for non-streaming use. + * + * This module provides a high-performance single-pass parser that directly + * builds the XML tree without intermediate tokens or events. It is used by + * the `parse()` function for parsing complete XML strings. + * + * For streaming parsing, use {@linkcode XmlParseStream} from `parse_stream.ts`. + * + * @module + */ + +import type { + ParseOptions, + XmlCDataNode, + XmlCommentNode, + XmlDeclarationEvent, + XmlDocument, + XmlElement, + XmlName, + XmlNode, + XmlTextNode, +} from "./types.ts"; +import { XmlSyntaxError } from "./types.ts"; +import { decodeEntities } from "./_entities.ts"; +import { + ENCODING_RE, + LINE_ENDING_RE, + parseName, + STANDALONE_RE, + VERSION_RE, + WHITESPACE_ONLY_RE, +} from "./_common.ts"; + +/** Internal mutable type for building the tree. */ +type MutableElement = { + type: "element"; + name: XmlName; + attributes: Record; + children: XmlNode[]; +}; + +/** + * Synchronous single-pass XML parser. + * + * Directly builds the XML tree without intermediate tokens or events, + * providing significant performance improvements over the streaming parser + * for non-streaming use cases. + * + * @param xml The XML string to parse. + * @param options Options to control parsing behavior. + * @returns The parsed document. + * @throws {XmlSyntaxError} If the XML is malformed. + */ +export function parseSync(xml: string, options?: ParseOptions): XmlDocument { + const ignoreWhitespace = options?.ignoreWhitespace ?? false; + const ignoreComments = options?.ignoreComments ?? false; + + // Normalize line endings (XML 1.0 §2.11) + const input = xml.includes("\r") ? xml.replace(LINE_ENDING_RE, "\n") : xml; + const len = input.length; + + // Parser state + let pos = 0; + let line = 1; + let col = 1; + + // Tree building state + const stack: MutableElement[] = []; + let root: MutableElement | undefined; + let declaration: XmlDeclarationEvent | undefined; + + /** + * Throws a syntax error at the current position. + */ + function error(message: string): never { + throw new XmlSyntaxError(message, { line, column: col, offset: pos }); + } + + /** + * Advances the position by one character, updating line/column. + */ + function advance(): void { + if (input[pos] === "\n") { + line++; + col = 1; + } else { + col++; + } + pos++; + } + + /** + * Skips XML whitespace characters. + */ + function skipWhitespace(): void { + while (pos < len) { + const code = input.charCodeAt(pos); + if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) { + advance(); + } else { + break; + } + } + } + + /** + * Reads an XML name (element or attribute name). + */ + function readName(): string { + const start = pos; + while (pos < len) { + const code = input.charCodeAt(pos); + // NameChar: a-z, A-Z, 0-9, _, :, ., -, or >127 (non-ASCII) + if ( + (code >= 97 && code <= 122) || // a-z + (code >= 65 && code <= 90) || // A-Z + (code >= 48 && code <= 57) || // 0-9 + code === 95 || // _ + code === 58 || // : + code === 46 || // . + code === 45 || // - + code > 127 // non-ASCII + ) { + pos++; + col++; + } else { + break; + } + } + return input.slice(start, pos); + } + + /** + * Reads a quoted attribute value and normalizes it per XML 1.0 §3.3.3. + */ + function readQuotedValue(): string { + const quote = input[pos]; + if (quote !== '"' && quote !== "'") { + error("Expected quote to start attribute value"); + } + advance(); + const start = pos; + while (pos < len && input[pos] !== quote) { + if (input[pos] === "<") { + error("'<' not allowed in attribute value"); + } + advance(); + } + if (pos >= len) { + error("Unterminated attribute value"); + } + const raw = input.slice(start, pos); + advance(); // closing quote + + // Normalize whitespace (§3.3.3) and decode entities + return decodeEntities(raw.replace(/[\t\n]/g, " ")); + } + + /** + * Reads text content until the next '<'. + */ + function readText(): string { + const start = pos; + while (pos < len && input[pos] !== "<") { + advance(); + } + return decodeEntities(input.slice(start, pos)); + } + + /** + * Adds a text node to the current element. + */ + function addTextNode(text: string): void { + if (text.length === 0) return; // Fast path: empty string check first (36x faster) + if (ignoreWhitespace && WHITESPACE_ONLY_RE.test(text)) return; + const node: XmlTextNode = { type: "text", text }; + if (stack.length > 0) { + stack[stack.length - 1]!.children.push(node); + } + } + + /** + * Adds a CDATA node to the current element. + */ + function addCDataNode(text: string): void { + const node: XmlCDataNode = { type: "cdata", text }; + if (stack.length > 0) { + stack[stack.length - 1]!.children.push(node); + } + } + + /** + * Adds a comment node to the current element (if not ignored). + */ + function addCommentNode(text: string): void { + if (ignoreComments) return; + const node: XmlCommentNode = { type: "comment", text }; + if (stack.length > 0) { + stack[stack.length - 1]!.children.push(node); + } + } + + // Main parsing loop + while (pos < len) { + // Handle text content first (early continue) + if (input[pos] !== "<") { + const text = readText(); + addTextNode(text); + continue; + } + + advance(); + + if (pos >= len) { + error("Unexpected end of input after '<'"); + } + + const c = input[pos]!; + + // End tag: + if (c === "/") { + advance(); + const name = readName(); + skipWhitespace(); + if (input[pos] !== ">") { + error("Expected '>' in end tag"); + } + advance(); + + const expected = stack.pop(); + if (!expected) { + error(`Unexpected closing tag `); + } + + const parsedName = parseName(name); + if ( + expected.name.local !== parsedName.local || + expected.name.prefix !== parsedName.prefix + ) { + const expectedFull = expected.name.prefix + ? `${expected.name.prefix}:${expected.name.local}` + : expected.name.local; + error( + `Mismatched closing tag: expected but found `, + ); + } + continue; + } + + // Comment, CDATA, or DOCTYPE + if (c === "!") { + advance(); + + // Comment: + if (pos + 1 < len && input[pos] === "-" && input[pos + 1] === "-") { + pos += 2; + col += 2; + const start = pos; + + // Use indexOf for fast delimiter search (92x faster for large comments) + const endIdx = input.indexOf("-->", pos); + if (endIdx === -1) { + // Advance to end for accurate error position + while (pos < len) advance(); + error("Unterminated comment"); + } + + // Update line/col by scanning for newlines in the comment + const content = input.slice(start, endIdx); + for (let i = 0; i < content.length; i++) { + if (content.charCodeAt(i) === 10) { // \n + line++; + col = 1; + } else { + col++; + } + } + + addCommentNode(content); + pos = endIdx + 3; + col += 3; + continue; + } + + // CDATA: + if (pos + 6 < len && input.slice(pos, pos + 7) === "[CDATA[") { + pos += 7; + col += 7; + const start = pos; + + // Use indexOf for fast delimiter search (92x faster for large CDATA) + const endIdx = input.indexOf("]]>", pos); + if (endIdx === -1) { + while (pos < len) advance(); + error("Unterminated CDATA section"); + } + + // Update line/col by scanning for newlines + const content = input.slice(start, endIdx); + for (let i = 0; i < content.length; i++) { + if (content.charCodeAt(i) === 10) { + line++; + col = 1; + } else { + col++; + } + } + + addCDataNode(content); + pos = endIdx + 3; + col += 3; + continue; + } + + // DOCTYPE: + if (pos + 6 < len && input.slice(pos, pos + 7) === "DOCTYPE") { + pos += 7; + col += 7; + + // Skip DOCTYPE content (we don't use it for tree building) + while (pos < len && input[pos] !== ">") { + if (input[pos] === "[") { + // Internal subset - skip until matching ] + let depth = 1; + advance(); + while (pos < len && depth > 0) { + if (input[pos] === "[") depth++; + else if (input[pos] === "]") depth--; + advance(); + } + } else { + advance(); + } + } + if (pos < len) advance(); // '>' + continue; + } + + error("Unsupported markup declaration"); + } + + // Processing instruction or XML declaration: + if (c === "?") { + advance(); + const target = readName(); + const contentStart = pos; + + // Use indexOf for fast delimiter search + const endIdx = input.indexOf("?>", pos); + if (endIdx === -1) { + while (pos < len) advance(); + error("Unterminated processing instruction"); + } + + // Update line/col by scanning for newlines + for (let i = pos; i < endIdx; i++) { + if (input.charCodeAt(i) === 10) { + line++; + col = 1; + } else { + col++; + } + } + + const content = input.slice(contentStart, endIdx).trim(); + pos = endIdx + 2; + col += 2; + + // Direct comparison (6x faster than toLowerCase) + if (target === "xml" || target === "XML") { + // XML declaration + const versionMatch = VERSION_RE.exec(content); + const encodingMatch = ENCODING_RE.exec(content); + const standaloneMatch = STANDALONE_RE.exec(content); + + declaration = { + type: "declaration", + version: versionMatch?.[1] ?? versionMatch?.[2] ?? "1.0", + line: 1, + column: 1, + offset: 0, + ...(encodingMatch && { + encoding: encodingMatch[1] ?? encodingMatch[2], + }), + ...(standaloneMatch && { + standalone: (standaloneMatch[1] ?? standaloneMatch[2]) as + | "yes" + | "no", + }), + }; + } + // Other PIs are ignored for tree building (consistent with current behavior) + continue; + } + + // Start tag: or + const name = readName(); + if (name === "") { + error(`Unexpected character '${c}' after '<'`); + } + + const elementName = parseName(name); + const attributes: Record = {}; + let selfClosing = false; + + // Read attributes + while (true) { + skipWhitespace(); + if (pos >= len) { + error("Unexpected end of input in start tag"); + } + + const ch = input[pos]!; + + if (ch === ">") { + advance(); + break; + } + + if (ch === "/") { + advance(); + if (input[pos] !== ">") { + error("Expected '>' after '/' in self-closing tag"); + } + advance(); + selfClosing = true; + break; + } + + // Read attribute + const attrName = readName(); + if (attrName === "") { + error(`Unexpected character '${ch}' in start tag`); + } + + skipWhitespace(); + if (input[pos] !== "=") { + error("Expected '=' after attribute name"); + } + advance(); + skipWhitespace(); + + const attrValue = readQuotedValue(); + const parsedAttrName = parseName(attrName); + attributes[parsedAttrName.local] = attrValue; + } + + // Create element + const element: MutableElement = { + type: "element", + name: elementName, + attributes, + children: [], + }; + + if (stack.length > 0) { + stack[stack.length - 1]!.children.push(element as XmlElement); + } else if (!root) { + root = element; + } + + // Only push non-self-closing elements to stack + if (!selfClosing) { + stack.push(element); + } + } + + // Check for unclosed elements + if (stack.length > 0) { + const unclosed = stack[stack.length - 1]!; + const name = unclosed.name.prefix + ? `${unclosed.name.prefix}:${unclosed.name.local}` + : unclosed.name.local; + error(`Unclosed element <${name}>`); + } + + if (!root) { + throw new XmlSyntaxError( + "No root element found in XML document", + { line: 1, column: 1, offset: 0 }, + ); + } + + return { + ...(declaration !== undefined && { declaration }), + root: root as XmlElement, + }; +} diff --git a/xml/_parse_sync_test.ts b/xml/_parse_sync_test.ts new file mode 100644 index 000000000000..dcab4e731e38 --- /dev/null +++ b/xml/_parse_sync_test.ts @@ -0,0 +1,302 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +import { assertEquals, assertThrows } from "@std/assert"; +import { parseSync } from "./_parse_sync.ts"; +import { XmlSyntaxError } from "./types.ts"; + +// ============================================================================= +// Line Ending Normalization (XML 1.0 §2.11) +// ============================================================================= + +Deno.test("parseSync() normalizes CRLF line endings", () => { + const doc = parseSync("\r\n \r\n", { + ignoreWhitespace: true, + }); + + assertEquals(doc.root.children.length, 1); +}); + +Deno.test("parseSync() normalizes CR line endings", () => { + const doc = parseSync("\r \r", { + ignoreWhitespace: true, + }); + + assertEquals(doc.root.children.length, 1); +}); + +// ============================================================================= +// Element Name Character Ranges +// ============================================================================= + +Deno.test("parseSync() handles uppercase element names", () => { + const doc = parseSync(""); + + assertEquals(doc.root.name.local, "ROOT"); + assertEquals(doc.root.children.length, 1); + if (doc.root.children[0]!.type === "element") { + assertEquals(doc.root.children[0]!.name.local, "ITEM"); + } +}); + +// ============================================================================= +// XML Declaration Variations +// ============================================================================= + +Deno.test("parseSync() handles declaration with single quotes", () => { + const doc = parseSync(""); + + assertEquals(doc.declaration?.version, "1.0"); + assertEquals(doc.declaration?.encoding, "UTF-8"); +}); + +Deno.test("parseSync() handles declaration with standalone", () => { + const doc = parseSync(''); + + assertEquals(doc.declaration?.version, "1.0"); + assertEquals(doc.declaration?.standalone, "yes"); +}); + +Deno.test("parseSync() handles declaration with all attributes single-quoted", () => { + const doc = parseSync( + "", + ); + + assertEquals(doc.declaration?.version, "1.0"); + assertEquals(doc.declaration?.encoding, "UTF-8"); + assertEquals(doc.declaration?.standalone, "no"); +}); + +// ============================================================================= +// DOCTYPE Handling +// ============================================================================= + +Deno.test("parseSync() handles DOCTYPE with internal subset", () => { + const doc = parseSync(` + ]>`); + + assertEquals(doc.root.name.local, "root"); +}); + +Deno.test("parseSync() handles DOCTYPE with nested brackets in internal subset", () => { + // This tests the nested bracket depth tracking (line 313: if (input[pos] === "[") depth++) + const doc = parseSync(` + + ]>`); + + assertEquals(doc.root.name.local, "root"); +}); + +// ============================================================================= +// Empty Text Node Handling +// ============================================================================= + +Deno.test("parseSync() handles adjacent tags without text between them", () => { + // This tests the empty text check (line 179: if (text.length === 0) return) + const doc = parseSync(""); + + assertEquals(doc.root.children.length, 2); + if (doc.root.children[0]!.type === "element") { + assertEquals(doc.root.children[0]!.name.local, "a"); + } + if (doc.root.children[1]!.type === "element") { + assertEquals(doc.root.children[1]!.name.local, "b"); + } +}); + +// ============================================================================= +// XML Declaration Edge Cases +// ============================================================================= + +Deno.test("parseSync() handles declaration with only encoding (no version)", () => { + // This tests the fallback to "1.0" when version is missing (line 346) + const doc = parseSync(''); + + assertEquals(doc.declaration?.version, "1.0"); + assertEquals(doc.declaration?.encoding, "UTF-8"); +}); + +// ============================================================================= +// Error Handling: End of Input +// ============================================================================= + +Deno.test("parseSync() throws on unexpected end after <", () => { + assertThrows( + () => parseSync("<"), + XmlSyntaxError, + "Unexpected end of input", + ); +}); + +Deno.test("parseSync() throws on unexpected end in start tag", () => { + // Input ends after whitespace in tag, before any attribute or closing + assertThrows( + () => parseSync(" in end tag", () => { + assertThrows( + () => parseSync(" { + assertThrows( + () => parseSync(""), + XmlSyntaxError, + "Unexpected closing tag", + ); +}); + +Deno.test("parseSync() throws on mismatched closing tag", () => { + assertThrows( + () => parseSync(""), + XmlSyntaxError, + "Mismatched closing tag", + ); +}); + +Deno.test("parseSync() throws on mismatched namespaced closing tag", () => { + assertThrows( + () => parseSync(""), + XmlSyntaxError, + "Mismatched closing tag: expected ", + ); +}); + +// ============================================================================= +// Error Handling: Unclosed Elements +// ============================================================================= + +Deno.test("parseSync() throws on unclosed element", () => { + assertThrows( + () => parseSync(""), + XmlSyntaxError, + "Unclosed element", + ); +}); + +Deno.test("parseSync() throws on unclosed namespaced element", () => { + assertThrows( + () => parseSync(""), + XmlSyntaxError, + "Unclosed element ", + ); +}); + +// ============================================================================= +// Error Handling: Unterminated Constructs +// ============================================================================= + +Deno.test("parseSync() throws on unterminated comment", () => { + assertThrows( + () => parseSync("text", + { ignoreComments: true }, + ); + + assertEquals(events.length, 3); + assertEquals(events[0]!.type, "start_element"); + assertEquals(events[1]!.type, "text"); + assertEquals(events[2]!.type, "end_element"); +}); + +Deno.test("parse() filters PIs when ignoreProcessingInstructions is true", async () => { + const events = await collectEvents( + "", + { ignoreProcessingInstructions: true }, + ); + + assertEquals(events.length, 2); + assertEquals(events[0]!.type, "start_element"); + assertEquals(events[1]!.type, "end_element"); +}); + +Deno.test("parse() ignores whitespace-only text when option is set", async () => { + const events = await collectEvents( + " \n ", + { ignoreWhitespace: true }, + ); + + assertEquals(events.length, 2); + assertEquals(events[0]!.type, "start_element"); + assertEquals(events[1]!.type, "end_element"); +}); + +Deno.test("parse() preserves whitespace by default", async () => { + const events = await collectEvents(" \n "); + + assertEquals(events.length, 3); + assertEquals(events[1]!.type, "text"); + if (events[1]!.type === "text") { + assertEquals(events[1]!.text, " \n "); + } +}); + +Deno.test("parse() preserves text with whitespace and content", async () => { + const events = await collectEvents( + " hello ", + { ignoreWhitespace: true }, + ); + + assertEquals(events.length, 3); + assertEquals(events[1]!.type, "text"); + if (events[1]!.type === "text") { + assertEquals(events[1]!.text, " hello "); + } +}); + +Deno.test("parse() handles multiple options together", async () => { + const events = await collectEvents( + ` + + + +`, + { + ignoreWhitespace: true, + ignoreComments: true, + ignoreProcessingInstructions: true, + coerceCDataToText: true, + }, + ); + + assertEquals(events.length, 3); + assertEquals(events[0]!.type, "start_element"); + assertEquals(events[1]!.type, "text"); + assertEquals(events[2]!.type, "end_element"); +}); + +// ============================================================================= +// Well-formedness Validation (Parser-specific) +// ============================================================================= + +Deno.test("parse() throws on mismatched closing tag", async () => { + await assertRejects( + () => collectEvents(""), + XmlSyntaxError, + "Mismatched closing tag: expected but found ", + ); +}); + +Deno.test("parse() throws on unexpected closing tag", async () => { + await assertRejects( + () => collectEvents(""), + XmlSyntaxError, + "Unexpected closing tag with no matching opening tag", + ); +}); + +Deno.test("parse() throws on unclosed element", async () => { + await assertRejects( + () => collectEvents(""), + XmlSyntaxError, + "Unclosed element ", + ); +}); + +Deno.test("parse() throws on deeply nested unclosed element", async () => { + await assertRejects( + () => collectEvents(""), + XmlSyntaxError, + "Unclosed element ", + ); +}); + +Deno.test("parse() throws on multiple top-level elements after self-closing", async () => { + await assertRejects( + () => collectEvents(""), + XmlSyntaxError, + "Unexpected closing tag ", + ); +}); + +// ============================================================================= +// Position Tracking (Parser-specific - verifies propagation) +// ============================================================================= + +Deno.test("parse() tracks position for start elements", async () => { + const events = await collectEvents(""); + + assertEquals(events[0]!.type, "start_element"); + if (events[0]!.type === "start_element") { + assertEquals(events[0]!.line, 1); + assertEquals(events[0]!.column, 1); + assertEquals(events[0]!.offset, 0); + } +}); + +Deno.test("parse() tracks position on multiple lines", async () => { + const events = await collectEvents("\n \n"); + + const child = events.find( + (e) => e.type === "start_element" && e.name.local === "child", + ); + assertEquals(child!.type, "start_element"); + if (child!.type === "start_element") { + assertEquals(child!.line, 2); + assertEquals(child!.column, 3); + } +}); + +Deno.test("parse() tracks position for declaration", async () => { + const events = await collectEvents(''); + + assertEquals(events[0]!.type, "declaration"); + if (events[0]!.type === "declaration") { + assertEquals(events[0]!.line, 1); + assertEquals(events[0]!.column, 1); + assertEquals(events[0]!.offset, 0); + } +}); + +// ============================================================================= +// Additional coverage: Declaration with encoding and standalone +// ============================================================================= + +Deno.test("parse() propagates declaration encoding", async () => { + const events = await collectEvents( + '', + ); + + assertEquals(events[0]!.type, "declaration"); + if (events[0]!.type === "declaration") { + assertEquals(events[0]!.encoding, "UTF-8"); + } +}); + +Deno.test("parse() propagates declaration standalone", async () => { + const events = await collectEvents( + '', + ); + + assertEquals(events[0]!.type, "declaration"); + if (events[0]!.type === "declaration") { + assertEquals(events[0]!.standalone, "yes"); + } +}); + +// ============================================================================= +// Additional coverage: Processing instruction events (not filtered) +// ============================================================================= + +Deno.test("parse() emits processing instruction events by default", async () => { + const events = await collectEvents( + '', + ); + + assertEquals(events[0]!.type, "processing_instruction"); + if (events[0]!.type === "processing_instruction") { + assertEquals(events[0]!.target, "xml-stylesheet"); + assertEquals(events[0]!.content, 'href="style.xsl"'); + assertEquals(events[0]!.line, 1); + assertEquals(events[0]!.column, 1); + assertEquals(events[0]!.offset, 0); + } +}); + +Deno.test("parse() emits multiple processing instructions", async () => { + const events = await collectEvents( + "", + ); + + const pis = events.filter((e) => e.type === "processing_instruction"); + assertEquals(pis.length, 2); +}); + +// ============================================================================= +// Additional coverage: DOCTYPE handling +// ============================================================================= + +Deno.test("parse() silently consumes DOCTYPE", async () => { + const events = await collectEvents(""); + + // DOCTYPE should not appear in events - only start_element and end_element + assertEquals(events.length, 2); + assertEquals(events[0]!.type, "start_element"); + assertEquals(events[1]!.type, "end_element"); +}); diff --git a/xml/_tokenizer.ts b/xml/_tokenizer.ts new file mode 100644 index 000000000000..a05b4ddb5022 --- /dev/null +++ b/xml/_tokenizer.ts @@ -0,0 +1,1062 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +// This module is browser compatible. + +/** + * Internal XML tokenizer module. + * + * Implements a streaming state machine that tokenizes XML input, + * handling chunk boundaries and tracking position information. + * + * @module + */ + +import { type XmlPosition, XmlSyntaxError } from "./types.ts"; +import { + ENCODING_RE, + LINE_ENDING_RE, + STANDALONE_RE, + VERSION_RE, +} from "./_common.ts"; + +/** Position information for tokens. Re-exported from types.ts for convenience. */ +export type TokenPosition = XmlPosition; + +/** Token types emitted by the tokenizer. */ +export type XmlToken = + | { type: "start_tag_open"; name: string; position: TokenPosition } + | { type: "attribute"; name: string; value: string } + | { type: "start_tag_close"; selfClosing: boolean } + | { type: "end_tag"; name: string; position: TokenPosition } + | { type: "text"; content: string; position: TokenPosition } + | { type: "cdata"; content: string; position: TokenPosition } + | { type: "comment"; content: string; position: TokenPosition } + | { + type: "processing_instruction"; + target: string; + content: string; + position: TokenPosition; + } + | { + type: "declaration"; + version: string; + encoding?: string; + standalone?: "yes" | "no"; + position: TokenPosition; + } + | { + type: "doctype"; + name: string; + publicId?: string; + systemId?: string; + position: TokenPosition; + }; + +/** Tokenizer state machine states. */ +const State = { + /** Waiting for < or text content */ + INITIAL: 0, + /** Just saw <, determining tag type */ + TAG_OPEN: 1, + /** Reading element name after < */ + TAG_NAME: 2, + /** Reading or attributes */ + AFTER_TAG_NAME: 4, + /** Reading attribute name */ + ATTRIBUTE_NAME: 5, + /** After attribute name, expecting = */ + AFTER_ATTRIBUTE_NAME: 6, + /** After =, expecting quote */ + BEFORE_ATTRIBUTE_VALUE: 7, + /** Reading attribute value (double quoted) */ + ATTRIBUTE_VALUE_DOUBLE: 8, + /** Reading attribute value (single quoted) */ + ATTRIBUTE_VALUE_SINGLE: 9, + /** Inside */ + CDATA: 10, + /** Inside */ + COMMENT: 11, + /** Reading PI target name */ + PI_TARGET: 12, + /** Reading PI content */ + PI_CONTENT: 13, + /** After */ + AFTER_END_TAG_NAME: 17, + /** Reading */ + DOCTYPE_AFTER_NAME: 20, + /** Reading PUBLIC keyword */ + DOCTYPE_PUBLIC: 21, + /** Reading public ID literal */ + DOCTYPE_PUBLIC_ID: 22, + /** After public ID, expecting system ID */ + DOCTYPE_AFTER_PUBLIC_ID: 23, + /** Reading SYSTEM keyword */ + DOCTYPE_SYSTEM: 24, + /** Reading system ID literal */ + DOCTYPE_SYSTEM_ID: 25, + /** Inside internal subset [...] */ + DOCTYPE_INTERNAL_SUBSET: 26, + /** Inside quoted string in internal subset */ + DOCTYPE_INTERNAL_SUBSET_STRING: 27, + /** After / in start tag, expecting > for self-closing */ + EXPECT_SELF_CLOSE_GT: 28, + /** Inside comment, seen one - */ + COMMENT_DASH: 29, + /** Inside comment, seen -- (expecting > or spec violation) */ + COMMENT_DASH_DASH: 30, + /** Inside CDATA, seen one ] */ + CDATA_BRACKET: 31, + /** Inside CDATA, seen ]] */ + CDATA_BRACKET_BRACKET: 32, + /** Inside PI target, seen ? (expecting > for empty PI) */ + PI_TARGET_QUESTION: 33, + /** Inside PI content, seen ? */ + PI_QUESTION: 34, +} as const; + +type State = typeof State[keyof typeof State]; + +// NOTE: These patterns cover the ASCII subset of XML 1.0 NameStartChar/NameChar. +// The full spec (Production [4], [4a]) includes many Unicode ranges: +// NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | ... +// NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | ... +// +// For pragmatic reasons, we use `charCodeAt(0) > 127` as a fallback for non-ASCII. +// This is OVERLY PERMISSIVE: it accepts some characters the spec excludes (e.g., +// U+00D7 multiplication sign, U+00F7 division sign, U+037E Greek question mark). +// However, invalid names are rare in real documents, and a fully compliant regex +// would be ~200+ characters. This trade-off is acceptable for a non-validating parser. + +/** + * Tokenizes an async iterable of XML string chunks. + * + * Line endings are normalized per XML 1.0 §2.11. + * Yields arrays of tokens (one array per input chunk) for better performance. + * + * @param source Async iterable of XML string chunks. + * @yields Arrays of XML tokens, batched per input chunk. + */ +export async function* tokenize( + source: AsyncIterable, +): AsyncGenerator { + let buffer = ""; + let bufferIndex = 0; + let state: State = State.INITIAL; + let line = 1; + let column = 1; + let offset = 0; + + // Position of current token start + let tokenLine = 1; + let tokenColumn = 1; + let tokenOffset = 0; + + // Slice-based accumulators: track start index + partial for cross-chunk + // Text content (highest frequency) + let textStartIdx = -1; + let textPartial = ""; + // CDATA content (high frequency for product feeds) + let cdataStartIdx = -1; + let cdataPartial = ""; + // Attribute values (medium frequency) + let attrStartIdx = -1; + let attrPartial = ""; + + // Traditional accumulators for short/infrequent strings + let tagName = ""; + let attrName = ""; + let piTarget = ""; + let piContent = ""; + let commentContent = ""; + let cdataCheck = ""; + + // DOCTYPE accumulators + let doctypeCheck = ""; + let doctypeName = ""; + let doctypePublicId = ""; + let doctypeSystemId = ""; + let doctypeQuoteChar = ""; + let doctypeBracketDepth = 0; + + // For tracking text start position + let textStartLine = 1; + let textStartColumn = 1; + let textStartOffset = 0; + + function saveTokenPosition(): void { + tokenLine = line; + tokenColumn = column; + tokenOffset = offset; + } + + function getTokenPosition(): TokenPosition { + return { line: tokenLine, column: tokenColumn, offset: tokenOffset }; + } + + function error(message: string): never { + throw new XmlSyntaxError(message, { line, column, offset }); + } + + // Optimized character checks using charCode (4x faster than regex) + function isNameStartChar(c: string): boolean { + const code = c.charCodeAt(0); + return (code >= 97 && code <= 122) || // a-z + (code >= 65 && code <= 90) || // A-Z + code === 95 || code === 58 || // _ : + code > 127; // non-ASCII + } + + function isNameChar(c: string): boolean { + const code = c.charCodeAt(0); + return (code >= 97 && code <= 122) || // a-z + (code >= 65 && code <= 90) || // A-Z + (code >= 48 && code <= 57) || // 0-9 + code === 95 || code === 58 || // _ : + code === 46 || code === 45 || // . - + code > 127; // non-ASCII + } + + function isWhitespace(c: string): boolean { + const code = c.charCodeAt(0); + return code === 32 || code === 9 || code === 10 || code === 13; // space, tab, LF, CR + } + + /** Flush text content using slice, returns tokens instead of yielding */ + function flushTextTokens(): XmlToken[] { + if (textStartIdx !== -1) { + const content = textPartial + buffer.slice(textStartIdx, bufferIndex); + textStartIdx = -1; + textPartial = ""; + if (content.length > 0) { + return [{ + type: "text", + content, + position: { + line: textStartLine, + column: textStartColumn, + offset: textStartOffset, + }, + }]; + } + } + return []; + } + + /** Flush text and emit to batch */ + function flushText(): void { + for (const token of flushTextTokens()) { + emit(token); + } + } + + /** Get attribute value using slice */ + function getAttrValue(): string { + const value = attrPartial + buffer.slice(attrStartIdx, bufferIndex); + attrStartIdx = -1; + attrPartial = ""; + return value; + } + + /** Save partial content before buffer reset (for cross-chunk handling) */ + function savePartialsBeforeReset(): void { + if (textStartIdx !== -1) { + textPartial += buffer.slice(textStartIdx, bufferIndex); + textStartIdx = 0; // Will continue from start of new buffer + } + if (cdataStartIdx !== -1) { + cdataPartial += buffer.slice(cdataStartIdx, bufferIndex); + cdataStartIdx = 0; + } + if (attrStartIdx !== -1) { + attrPartial += buffer.slice(attrStartIdx, bufferIndex); + attrStartIdx = 0; + } + } + + function advance(): void { + if (buffer[bufferIndex] === "\n") { + line++; + column = 1; + } else { + column++; + } + offset++; + bufferIndex++; + } + + // Normalize line endings: \r\n and \r → \n + function normalizeLineEndings(chunk: string): string { + // Fast path: skip regex if no carriage returns (common case) + return chunk.includes("\r") ? chunk.replace(LINE_ENDING_RE, "\n") : chunk; + } + + // Batch tokens per chunk for reduced async overhead + let tokenBatch: XmlToken[] = []; + + /** Push token to current batch instead of yielding directly */ + function emit(token: XmlToken): void { + tokenBatch.push(token); + } + + for await (const chunk of source) { + // Save any partial content before resetting buffer + savePartialsBeforeReset(); + // Reset buffer: keep unprocessed portion + new chunk + buffer = buffer.slice(bufferIndex) + normalizeLineEndings(chunk); + bufferIndex = 0; + + while (bufferIndex < buffer.length) { + // SAFETY: bufferIndex < buffer.length is checked above + const c = buffer[bufferIndex]!; + + switch (state) { + case State.INITIAL: { + if (c === "<") { + flushText(); + saveTokenPosition(); + advance(); + state = State.TAG_OPEN; + } else { + // Track text start index for slice (instead of += per char) + if (textStartIdx === -1) { + textStartLine = line; + textStartColumn = column; + textStartOffset = offset; + textStartIdx = bufferIndex; + } + advance(); + } + break; + } + + case State.TAG_OPEN: { + if (c === "/") { + advance(); + tagName = ""; + state = State.END_TAG_NAME; + } else if (c === "!") { + advance(); + state = State.MARKUP_DECLARATION; + } else if (c === "?") { + advance(); + piTarget = ""; + state = State.PI_TARGET; + } else if (isNameStartChar(c)) { + tagName = c; + advance(); + state = State.TAG_NAME; + } else { + error(`Unexpected character '${c}' after '<'`); + } + break; + } + + case State.TAG_NAME: { + if (isNameChar(c)) { + tagName += c; + advance(); + } else if (isWhitespace(c)) { + emit({ + type: "start_tag_open", + name: tagName, + position: getTokenPosition(), + }); + advance(); + state = State.AFTER_TAG_NAME; + } else if (c === ">") { + emit({ + type: "start_tag_open", + name: tagName, + position: getTokenPosition(), + }); + emit({ type: "start_tag_close", selfClosing: false }); + advance(); + state = State.INITIAL; + } else if (c === "/") { + emit({ + type: "start_tag_open", + name: tagName, + position: getTokenPosition(), + }); + advance(); + state = State.EXPECT_SELF_CLOSE_GT; + } else { + error(`Unexpected character '${c}' in tag name`); + } + break; + } + + case State.AFTER_TAG_NAME: { + if (isWhitespace(c)) { + advance(); + } else if (c === ">") { + emit({ type: "start_tag_close", selfClosing: false }); + advance(); + state = State.INITIAL; + } else if (c === "/") { + advance(); + state = State.EXPECT_SELF_CLOSE_GT; + } else if (isNameStartChar(c)) { + attrName = c; + advance(); + state = State.ATTRIBUTE_NAME; + } else { + error(`Unexpected character '${c}' after tag name`); + } + break; + } + + case State.ATTRIBUTE_NAME: { + if (isNameChar(c)) { + attrName += c; + advance(); + } else if (isWhitespace(c)) { + advance(); + state = State.AFTER_ATTRIBUTE_NAME; + } else if (c === "=") { + advance(); + state = State.BEFORE_ATTRIBUTE_VALUE; + } else { + error(`Unexpected character '${c}' in attribute name`); + } + break; + } + + case State.AFTER_ATTRIBUTE_NAME: { + if (isWhitespace(c)) { + advance(); + } else if (c === "=") { + advance(); + state = State.BEFORE_ATTRIBUTE_VALUE; + } else { + error(`Expected '=' after attribute name`); + } + break; + } + + case State.BEFORE_ATTRIBUTE_VALUE: { + if (isWhitespace(c)) { + advance(); + } else if (c === '"') { + advance(); + attrStartIdx = bufferIndex; // Start tracking after quote + attrPartial = ""; + state = State.ATTRIBUTE_VALUE_DOUBLE; + } else if (c === "'") { + advance(); + attrStartIdx = bufferIndex; + attrPartial = ""; + state = State.ATTRIBUTE_VALUE_SINGLE; + } else { + error(`Expected quote to start attribute value`); + } + break; + } + + case State.ATTRIBUTE_VALUE_DOUBLE: { + if (c === '"') { + emit({ type: "attribute", name: attrName, value: getAttrValue() }); + advance(); + state = State.AFTER_TAG_NAME; + } else if (c === "<") { + error(`'<' not allowed in attribute value`); + } else { + advance(); + } + break; + } + + case State.ATTRIBUTE_VALUE_SINGLE: { + if (c === "'") { + emit({ type: "attribute", name: attrName, value: getAttrValue() }); + advance(); + state = State.AFTER_TAG_NAME; + } else if (c === "<") { + error(`'<' not allowed in attribute value`); + } else { + advance(); + } + break; + } + + case State.END_TAG_NAME: { + if (tagName === "" && isNameStartChar(c)) { + tagName = c; + advance(); + } else if (isNameChar(c)) { + tagName += c; + advance(); + } else if (isWhitespace(c)) { + advance(); + state = State.AFTER_END_TAG_NAME; + } else if (c === ">") { + emit({ + type: "end_tag", + name: tagName, + position: getTokenPosition(), + }); + advance(); + state = State.INITIAL; + } else { + error(`Unexpected character '${c}' in end tag`); + } + break; + } + + case State.AFTER_END_TAG_NAME: { + if (isWhitespace(c)) { + advance(); + } else if (c === ">") { + emit({ + type: "end_tag", + name: tagName, + position: getTokenPosition(), + }); + advance(); + state = State.INITIAL; + } else { + error(`Unexpected character '${c}' in end tag`); + } + break; + } + + case State.EXPECT_SELF_CLOSE_GT: { + if (c === ">") { + emit({ type: "start_tag_close", selfClosing: true }); + advance(); + state = State.INITIAL; + } else { + error(`Expected '>' after '/' in self-closing tag`); + } + break; + } + + case State.MARKUP_DECLARATION: { + if (c === "-") { + advance(); + state = State.COMMENT_START; + } else if (c === "[") { + advance(); + cdataCheck = ""; + state = State.CDATA_START; + } else if (c === "D") { + // Likely DOCTYPE + doctypeCheck = "D"; + advance(); + state = State.DOCTYPE_START; + } else { + error(`Unsupported markup declaration`); + } + break; + } + + case State.DOCTYPE_START: { + doctypeCheck += c; + advance(); + if (doctypeCheck === "DOCTYPE") { + doctypeName = ""; + doctypePublicId = ""; + doctypeSystemId = ""; + state = State.DOCTYPE_NAME; + } else if (!"DOCTYPE".startsWith(doctypeCheck)) { + error(`Expected DOCTYPE, got ") { + emit({ + type: "doctype", + name: doctypeName, + position: getTokenPosition(), + }); + advance(); + state = State.INITIAL; + } else if (c === "[") { + advance(); + doctypeBracketDepth = 1; + state = State.DOCTYPE_INTERNAL_SUBSET; + } else if ( + isNameChar(c) || (doctypeName === "" && isNameStartChar(c)) + ) { + doctypeName += c; + advance(); + } else { + error(`Unexpected character '${c}' in DOCTYPE name`); + } + break; + } + + case State.DOCTYPE_AFTER_NAME: { + if (isWhitespace(c)) { + advance(); + } else if (c === ">") { + emit({ + type: "doctype", + name: doctypeName, + ...(doctypePublicId && { publicId: doctypePublicId }), + ...(doctypeSystemId && { systemId: doctypeSystemId }), + position: getTokenPosition(), + }); + advance(); + state = State.INITIAL; + } else if (c === "[") { + advance(); + doctypeBracketDepth = 1; + state = State.DOCTYPE_INTERNAL_SUBSET; + } else if (c === "P") { + doctypeCheck = "P"; + advance(); + state = State.DOCTYPE_PUBLIC; + } else if (c === "S") { + doctypeCheck = "S"; + advance(); + state = State.DOCTYPE_SYSTEM; + } else { + error(`Unexpected character '${c}' in DOCTYPE`); + } + break; + } + + case State.DOCTYPE_PUBLIC: { + doctypeCheck += c; + advance(); + if (doctypeCheck === "PUBLIC") { + state = State.DOCTYPE_PUBLIC_ID; + } else if (!"PUBLIC".startsWith(doctypeCheck)) { + error(`Expected PUBLIC, got ${doctypeCheck}`); + } + break; + } + + case State.DOCTYPE_PUBLIC_ID: { + if (isWhitespace(c)) { + advance(); + } else if (c === '"' || c === "'") { + doctypeQuoteChar = c; + doctypePublicId = ""; + advance(); + // Read until closing quote + while ( + bufferIndex < buffer.length && + buffer[bufferIndex] !== doctypeQuoteChar + ) { + doctypePublicId += buffer[bufferIndex]; + advance(); + } + if (bufferIndex >= buffer.length) { + // Need more data + continue; + } + advance(); // closing quote + state = State.DOCTYPE_AFTER_PUBLIC_ID; + } else { + error(`Expected quote to start public ID`); + } + break; + } + + case State.DOCTYPE_AFTER_PUBLIC_ID: { + if (isWhitespace(c)) { + advance(); + } else if (c === '"' || c === "'") { + doctypeQuoteChar = c; + doctypeSystemId = ""; + advance(); + // Read until closing quote + while ( + bufferIndex < buffer.length && + buffer[bufferIndex] !== doctypeQuoteChar + ) { + doctypeSystemId += buffer[bufferIndex]; + advance(); + } + if (bufferIndex >= buffer.length) { + continue; + } + advance(); // closing quote + state = State.DOCTYPE_AFTER_NAME; + } else if (c === ">") { + // PUBLIC without system ID (unusual but possible) + emit({ + type: "doctype", + name: doctypeName, + publicId: doctypePublicId, + position: getTokenPosition(), + }); + advance(); + state = State.INITIAL; + } else { + error(`Expected system ID or '>' after public ID`); + } + break; + } + + case State.DOCTYPE_SYSTEM: { + doctypeCheck += c; + advance(); + if (doctypeCheck === "SYSTEM") { + state = State.DOCTYPE_SYSTEM_ID; + } else if (!"SYSTEM".startsWith(doctypeCheck)) { + error(`Expected SYSTEM, got ${doctypeCheck}`); + } + break; + } + + case State.DOCTYPE_SYSTEM_ID: { + if (isWhitespace(c)) { + advance(); + } else if (c === '"' || c === "'") { + doctypeQuoteChar = c; + doctypeSystemId = ""; + advance(); + // Read until closing quote + while ( + bufferIndex < buffer.length && + buffer[bufferIndex] !== doctypeQuoteChar + ) { + doctypeSystemId += buffer[bufferIndex]; + advance(); + } + if (bufferIndex >= buffer.length) { + continue; + } + advance(); // closing quote + state = State.DOCTYPE_AFTER_NAME; + } else { + error(`Expected quote to start system ID`); + } + break; + } + + case State.DOCTYPE_INTERNAL_SUBSET: { + // Skip internal subset content, tracking bracket depth + if (c === "]") { + doctypeBracketDepth--; + advance(); + if (doctypeBracketDepth === 0) { + state = State.DOCTYPE_AFTER_NAME; + } + } else if (c === "[") { + doctypeBracketDepth++; + advance(); + } else if (c === '"' || c === "'") { + // Skip quoted strings (may contain brackets) + doctypeQuoteChar = c; + advance(); + state = State.DOCTYPE_INTERNAL_SUBSET_STRING; + } else { + advance(); + } + break; + } + + case State.DOCTYPE_INTERNAL_SUBSET_STRING: { + if (c === doctypeQuoteChar) { + advance(); + state = State.DOCTYPE_INTERNAL_SUBSET; + } else { + advance(); + } + break; + } + + case State.COMMENT_START: { + if (c === "-") { + advance(); + commentContent = ""; + state = State.COMMENT; + } else { + error(`Expected '-' to start comment`); + } + break; + } + + case State.COMMENT: { + // Per XML 1.0 §2.5, comments use the grammar: + // Comment ::= '' + // This means '--' MUST NOT appear inside comments. + // We use sub-states to handle chunk boundaries correctly. + if (c === "-") { + advance(); + state = State.COMMENT_DASH; + } else { + commentContent += c; + advance(); + } + break; + } + + case State.COMMENT_DASH: { + // We've seen one '-' in comment content + if (c === "-") { + // Now we've seen '--', expecting '>' or spec violation + advance(); + state = State.COMMENT_DASH_DASH; + } else { + // Single dash is valid content, add it and continue + commentContent += "-"; + commentContent += c; + advance(); + state = State.COMMENT; + } + break; + } + + case State.COMMENT_DASH_DASH: { + // We've seen '--', expecting '>' + if (c === ">") { + // Valid comment end + emit({ + type: "comment", + content: commentContent, + position: getTokenPosition(), + }); + advance(); + state = State.INITIAL; + } else if (c === "-") { + // "---..." case: first '--' violates spec, but we're lenient + // Treat the first dash as content, stay in COMMENT_DASH_DASH + // (we still have '--' pending from the second and third dashes) + commentContent += "-"; + advance(); + // state stays COMMENT_DASH_DASH + } else { + // '--' followed by non-'>': spec violation (lenient mode) + // Per XML 1.0 §2.5: '--' MUST NOT occur within comments + // We include it in content for error recovery + commentContent += "--"; + commentContent += c; + advance(); + state = State.COMMENT; + } + break; + } + + case State.CDATA_START: { + cdataCheck += c; + advance(); + if (cdataCheck === "CDATA[") { + cdataStartIdx = bufferIndex; // Start tracking after CDATA[ + cdataPartial = ""; + state = State.CDATA; + } else if (!"CDATA[".startsWith(cdataCheck)) { + error(`Expected 'CDATA[' after ' across chunk boundaries + if (c === "]") { + // Save content up to this point (excluding the ]) + cdataPartial += buffer.slice(cdataStartIdx, bufferIndex); + advance(); + cdataStartIdx = bufferIndex; // Reset start for potential continuation + state = State.CDATA_BRACKET; + } else { + advance(); + } + break; + } + + case State.CDATA_BRACKET: { + // We've seen one ] in CDATA content + if (c === "]") { + advance(); + cdataStartIdx = bufferIndex; + state = State.CDATA_BRACKET_BRACKET; + } else { + // Single ] is valid content, add it back + cdataPartial += "]"; + // cdataStartIdx already points to current position + advance(); + state = State.CDATA; + } + break; + } + + case State.CDATA_BRACKET_BRACKET: { + // We've seen ]], expecting > or more content + if (c === ">") { + // Emit CDATA with content (]] is NOT included) + emit({ + type: "cdata", + content: cdataPartial, + position: getTokenPosition(), + }); + cdataStartIdx = -1; + cdataPartial = ""; + advance(); + state = State.INITIAL; + } else if (c === "]") { + // "...]]]..." - first ] is content, still have ]] pending + cdataPartial += "]"; + advance(); + cdataStartIdx = bufferIndex; + // state stays CDATA_BRACKET_BRACKET, pendingBrackets stays 2 + } else { + // ]] followed by non->, ]] is content, continue + cdataPartial += "]]"; + // cdataStartIdx already at current position + advance(); + state = State.CDATA; + } + break; + } + + case State.PI_TARGET: { + if (isNameChar(c)) { + piTarget += c; + advance(); + } else if (isWhitespace(c)) { + advance(); + piContent = ""; + state = State.PI_CONTENT; + } else if (c === "?") { + // Possible end of PI with empty content, use sub-state for chunk safety + advance(); + state = State.PI_TARGET_QUESTION; + } else { + error( + `Unexpected character '${c}' in processing instruction target`, + ); + } + break; + } + + case State.PI_TARGET_QUESTION: { + // We've seen ? after PI target (no whitespace), expecting > + if (c === ">") { + // Empty PI content + if (piTarget.toLowerCase() === "xml") { + emit(createDeclaration("", getTokenPosition())); + } else { + emit({ + type: "processing_instruction", + target: piTarget, + content: "", + position: getTokenPosition(), + }); + } + advance(); + state = State.INITIAL; + } else { + error( + `Expected '>' after '?' in processing instruction, got '${c}'`, + ); + } + break; + } + + case State.PI_CONTENT: { + // Use sub-state to handle ?> across chunk boundaries + if (c === "?") { + advance(); + state = State.PI_QUESTION; + } else { + piContent += c; + advance(); + } + break; + } + + case State.PI_QUESTION: { + // We've seen ? in PI content, check for > + if (c === ">") { + if (piTarget.toLowerCase() === "xml") { + emit(createDeclaration(piContent, getTokenPosition())); + } else { + emit({ + type: "processing_instruction", + target: piTarget, + content: piContent.trim(), + position: getTokenPosition(), + }); + } + advance(); + state = State.INITIAL; + } else if (c === "?") { + // "??" - first ? is content, stay waiting for > + piContent += "?"; + advance(); + // state stays PI_QUESTION + } else { + // ? followed by non->, add to content + piContent += "?"; + piContent += c; + advance(); + state = State.PI_CONTENT; + } + break; + } + } + } + + // Yield batch at end of each chunk (reduces async overhead 3000x) + if (tokenBatch.length > 0) { + yield tokenBatch; + tokenBatch = []; + } + } + + // Flush remaining text and yield final batch + for (const token of flushTextTokens()) { + emit(token); + } + if (tokenBatch.length > 0) { + yield tokenBatch; + } + + // Check for incomplete state + if (state !== State.INITIAL) { + error(`Unexpected end of input`); + } +} + +/** + * Create XML declaration token. + * + * Uses alternation in regex to enforce matching quotes (single or double). + * Per XML 1.0 spec, `version='1.0"` (mismatched quotes) is invalid. + */ +function createDeclaration( + content: string, + position: TokenPosition, +): XmlToken { + const versionMatch = VERSION_RE.exec(content); + const encodingMatch = ENCODING_RE.exec(content); + const standaloneMatch = STANDALONE_RE.exec(content); + + const version = versionMatch?.[1] ?? versionMatch?.[2] ?? "1.0"; + const encoding = encodingMatch?.[1] ?? encodingMatch?.[2]; + const standalone = standaloneMatch?.[1] ?? standaloneMatch?.[2]; + + return { + type: "declaration", + version, + ...(encoding && { encoding }), + ...(standalone && { standalone: standalone as "yes" | "no" }), + position, + }; +} diff --git a/xml/_tokenizer_test.ts b/xml/_tokenizer_test.ts new file mode 100644 index 000000000000..172d6d7e147d --- /dev/null +++ b/xml/_tokenizer_test.ts @@ -0,0 +1,1236 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +import { assertEquals, assertRejects } from "@std/assert"; +import { tokenize, type XmlToken } from "./_tokenizer.ts"; +import { XmlSyntaxError } from "./types.ts"; + +/** Helper to collect tokens from a string (flattens batches). */ +async function collectTokens(xml: string): Promise { + const tokens: XmlToken[] = []; + for await (const batch of tokenize(toAsyncIterable(xml))) { + tokens.push(...batch); + } + return tokens; +} + +/** Convert string to async iterable. */ +async function* toAsyncIterable(s: string): AsyncIterable { + yield s; +} + +/** Type predicate for filtering tokens by type. */ +function isTokenType( + type: T, +): (token: XmlToken) => token is Extract { + return (token): token is Extract => + token.type === type; +} + +// ============================================================================= +// Basic element tests +// ============================================================================= + +Deno.test("tokenize() handles simple element", async () => { + const tokens = await collectTokens(""); + assertEquals(tokens.length, 3); + assertEquals(tokens[0]?.type, "start_tag_open"); + assertEquals((tokens[0] as { name: string }).name, "root"); + assertEquals(tokens[1]?.type, "start_tag_close"); + assertEquals(tokens[2]?.type, "end_tag"); + assertEquals((tokens[2] as { name: string }).name, "root"); +}); + +Deno.test("tokenize() handles self-closing element", async () => { + const tokens = await collectTokens(""); + assertEquals(tokens.length, 2); + assertEquals(tokens[0]?.type, "start_tag_open"); + assertEquals((tokens[0] as { name: string }).name, "item"); + assertEquals(tokens[1]?.type, "start_tag_close"); + assertEquals((tokens[1] as { selfClosing: boolean }).selfClosing, true); +}); + +Deno.test("tokenize() handles self-closing element with space", async () => { + const tokens = await collectTokens(""); + assertEquals(tokens.length, 2); + assertEquals(tokens[0]?.type, "start_tag_open"); + assertEquals(tokens[1]?.type, "start_tag_close"); + assertEquals((tokens[1] as { selfClosing: boolean }).selfClosing, true); +}); + +Deno.test("tokenize() handles nested elements", async () => { + const tokens = await collectTokens(""); + assertEquals(tokens.length, 6); + assertEquals((tokens[0] as { name: string }).name, "a"); + assertEquals((tokens[2] as { name: string }).name, "b"); + assertEquals((tokens[4] as { name: string }).name, "b"); + assertEquals((tokens[5] as { name: string }).name, "a"); +}); + +// ============================================================================= +// Attribute tests +// ============================================================================= + +Deno.test("tokenize() handles single attribute", async () => { + const tokens = await collectTokens(''); + assertEquals(tokens.length, 3); + assertEquals(tokens[1]?.type, "attribute"); + assertEquals((tokens[1] as { name: string }).name, "id"); + assertEquals((tokens[1] as { value: string }).value, "123"); +}); + +Deno.test("tokenize() handles multiple attributes", async () => { + const tokens = await collectTokens(''); + const attrs = tokens.filter(isTokenType("attribute")); + assertEquals(attrs.length, 2); + assertEquals(attrs[0]?.name, "id"); + assertEquals(attrs[1]?.name, "name"); +}); + +Deno.test("tokenize() handles single-quoted attributes", async () => { + const tokens = await collectTokens(""); + const attr = tokens.find((t) => t.type === "attribute"); + assertEquals((attr as { value: string }).value, "123"); +}); + +Deno.test("tokenize() handles attributes with entities", async () => { + const tokens = await collectTokens(''); + const attr = tokens.find((t) => t.type === "attribute"); + // Tokenizer does NOT decode entities - that's the parser's job + assertEquals((attr as { value: string }).value, "a<b"); +}); + +Deno.test("tokenize() handles namespaced attributes", async () => { + const tokens = await collectTokens(''); + const attr = tokens.find((t) => t.type === "attribute"); + assertEquals((attr as { name: string }).name, "xml:lang"); +}); + +// ============================================================================= +// Text content tests +// ============================================================================= + +Deno.test("tokenize() handles text content", async () => { + const tokens = await collectTokens("Hello World"); + const text = tokens.find((t) => t.type === "text"); + assertEquals((text as { content: string }).content, "Hello World"); +}); + +Deno.test("tokenize() handles text with entities", async () => { + const tokens = await collectTokens("<test>"); + const text = tokens.find((t) => t.type === "text"); + // Tokenizer does NOT decode entities + assertEquals((text as { content: string }).content, "<test>"); +}); + +Deno.test("tokenize() handles multiple text nodes", async () => { + const tokens = await collectTokens("onetwo"); + const texts = tokens.filter(isTokenType("text")); + assertEquals(texts.length, 2); + assertEquals(texts[0]?.content, "one"); + assertEquals(texts[1]?.content, "two"); +}); + +// ============================================================================= +// CDATA tests +// ============================================================================= + +Deno.test("tokenize() handles CDATA section", async () => { + const tokens = await collectTokens("]]>"); + const cdata = tokens.find((t) => t.type === "cdata"); + assertEquals((cdata as { content: string }).content, "" }], + }; + + assertEquals( + stringify(element), + "<script>&</script>", + ); +}); + +Deno.test("stringify() preserves whitespace in text", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "text", text: " spaces " }], + }; + + assertEquals(stringify(element), " spaces "); +}); + +// ============================================================================= +// CDATA Sections +// ============================================================================= + +Deno.test("stringify() serializes CDATA section", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "cdata", text: "xml" }], + }; + + assertEquals(stringify(element), "xml]]>"); +}); + +Deno.test("stringify() does not encode entities in CDATA", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "cdata", text: "& stays &" }], + }; + + assertEquals( + stringify(element), + "", + ); +}); + +// ============================================================================= +// Comments +// ============================================================================= + +Deno.test("stringify() serializes comments", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "comment", text: " comment " }], + }; + + assertEquals(stringify(element), ""); +}); + +// ============================================================================= +// Mixed Content +// ============================================================================= + +Deno.test("stringify() serializes mixed content", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [ + { type: "text", text: "text" }, + { + type: "element", + name: { local: "child" }, + attributes: {}, + children: [], + }, + { type: "text", text: "more" }, + ], + }; + + assertEquals(stringify(element), "textmore"); +}); + +// ============================================================================= +// XML Declaration +// ============================================================================= + +Deno.test("stringify() includes XML declaration from document", () => { + const doc: XmlDocument = { + declaration: { + type: "declaration", + version: "1.0", + line: 1, + column: 1, + offset: 0, + }, + root: { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [], + }, + }; + + assertEquals(stringify(doc), ''); +}); + +Deno.test("stringify() includes encoding in declaration", () => { + const doc: XmlDocument = { + declaration: { + type: "declaration", + version: "1.0", + encoding: "UTF-8", + line: 1, + column: 1, + offset: 0, + }, + root: { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [], + }, + }; + + assertEquals(stringify(doc), ''); +}); + +Deno.test("stringify() includes standalone in declaration", () => { + const doc: XmlDocument = { + declaration: { + type: "declaration", + version: "1.0", + standalone: "yes", + line: 1, + column: 1, + offset: 0, + }, + root: { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [], + }, + }; + + assertEquals(stringify(doc), ''); +}); + +Deno.test("stringify() omits declaration when option is false", () => { + const doc: XmlDocument = { + declaration: { + type: "declaration", + version: "1.0", + line: 1, + column: 1, + offset: 0, + }, + root: { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [], + }, + }; + + assertEquals(stringify(doc, { declaration: false }), ""); +}); + +Deno.test("stringify() handles document without declaration", () => { + const doc: XmlDocument = { + root: { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [], + }, + }; + + assertEquals(stringify(doc), ""); +}); + +// ============================================================================= +// Pretty Printing +// ============================================================================= + +Deno.test("stringify() pretty-prints with indent option", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [ + { + type: "element", + name: { local: "child" }, + attributes: {}, + children: [], + }, + ], + }; + + assertEquals( + stringify(element, { indent: " " }), + "\n \n", + ); +}); + +Deno.test("stringify() pretty-prints deeply nested elements", () => { + const element: XmlElement = { + type: "element", + name: { local: "a" }, + attributes: {}, + children: [ + { + type: "element", + name: { local: "b" }, + attributes: {}, + children: [ + { + type: "element", + name: { local: "c" }, + attributes: {}, + children: [], + }, + ], + }, + ], + }; + + assertEquals( + stringify(element, { indent: " " }), + "\n \n \n \n", + ); +}); + +Deno.test("stringify() keeps text content inline when pretty-printing", () => { + const element: XmlElement = { + type: "element", + name: { local: "p" }, + attributes: {}, + children: [{ type: "text", text: "Hello World" }], + }; + + assertEquals(stringify(element, { indent: " " }), "

Hello World

"); +}); + +Deno.test("stringify() pretty-prints declaration on separate line", () => { + const doc: XmlDocument = { + declaration: { + type: "declaration", + version: "1.0", + line: 1, + column: 1, + offset: 0, + }, + root: { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [], + }, + }; + + assertEquals( + stringify(doc, { indent: " " }), + '\n', + ); +}); + +Deno.test("stringify() pretty-prints comments", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [ + { type: "comment", text: " comment " }, + { + type: "element", + name: { local: "child" }, + attributes: {}, + children: [], + }, + ], + }; + + assertEquals( + stringify(element, { indent: " " }), + "\n \n \n", + ); +}); + +// ============================================================================= +// Complex Documents +// ============================================================================= + +Deno.test("stringify() handles complex document", () => { + const doc: XmlDocument = { + declaration: { + type: "declaration", + version: "1.0", + encoding: "UTF-8", + line: 1, + column: 1, + offset: 0, + }, + root: { + type: "element", + name: { local: "catalog" }, + attributes: {}, + children: [ + { + type: "element", + name: { local: "product" }, + attributes: { id: "1" }, + children: [ + { + type: "element", + name: { local: "name" }, + attributes: {}, + children: [{ type: "text", text: "Widget" }], + }, + ], + }, + ], + }, + }; + + const expected = '\n' + + "\n" + + ' \n' + + " Widget\n" + + " \n" + + ""; + + assertEquals(stringify(doc, { indent: " " }), expected); +}); + +// ============================================================================= +// Edge Cases +// ============================================================================= + +Deno.test("stringify() handles Unicode content", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "text", text: "日本語 🎉 émoji" }], + }; + + assertEquals(stringify(element), "日本語 🎉 émoji"); +}); + +Deno.test("stringify() handles empty string attribute", () => { + const element: XmlElement = { + type: "element", + name: { local: "item" }, + attributes: { value: "" }, + children: [], + }; + + assertEquals(stringify(element), ''); +}); + +// ============================================================================= +// CDATA Edge Cases (XML 1.0 §2.7) +// ============================================================================= + +Deno.test("stringify() escapes ]]> in CDATA by splitting", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "cdata", text: "contains ]]> sequence" }], + }; + + assertEquals( + stringify(element), + " sequence]]>", + ); +}); + +Deno.test("stringify() escapes multiple ]]> in CDATA", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "cdata", text: "a]]>b]]>c" }], + }; + + assertEquals( + stringify(element), + "b]]]]>c]]>", + ); +}); + +Deno.test("stringify() handles ]]> at start of CDATA", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "cdata", text: "]]>text" }], + }; + + assertEquals( + stringify(element), + "text]]>", + ); +}); + +Deno.test("stringify() handles ]]> at end of CDATA", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "cdata", text: "text]]>" }], + }; + + assertEquals( + stringify(element), + "]]>", + ); +}); + +// ============================================================================= +// Comment Edge Cases (XML 1.0 §2.5) +// ============================================================================= + +Deno.test("stringify() throws for -- in comment", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "comment", text: "contains -- sequence" }], + }; + + assertThrows( + () => stringify(element), + TypeError, + 'contains "--"', + ); +}); + +Deno.test("stringify() throws for comment ending with -", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "comment", text: "ends with hyphen-" }], + }; + + assertThrows( + () => stringify(element), + TypeError, + 'ends with "-"', + ); +}); + +Deno.test("stringify() allows single hyphen in comment", () => { + const element: XmlElement = { + type: "element", + name: { local: "root" }, + attributes: {}, + children: [{ type: "comment", text: " single-hyphen is fine " }], + }; + + assertEquals( + stringify(element), + "", + ); +}); diff --git a/xml/testdata/cdata-edge-cases.xml b/xml/testdata/cdata-edge-cases.xml new file mode 100644 index 000000000000..5614b77c9da7 --- /dev/null +++ b/xml/testdata/cdata-edge-cases.xml @@ -0,0 +1,53 @@ + + + + + + + + Content]]> + + + & " ' are all allowed in CDATA]]> + + + inside]]> + + + + + + after]]> + + + 0 && data[0] !== null) { + return data.filter(x => x > 0); + } + return []; + } + ]]> + + + + + + + + + + + + + + + Text before text after + diff --git a/xml/testdata/cdata.xml b/xml/testdata/cdata.xml new file mode 100644 index 000000000000..74e294f32584 --- /dev/null +++ b/xml/testdata/cdata.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + & "characters"]]> + diff --git a/xml/testdata/comments-edge-cases.xml b/xml/testdata/comments-edge-cases.xml new file mode 100644 index 000000000000..326380ba4a12 --- /dev/null +++ b/xml/testdata/comments-edge-cases.xml @@ -0,0 +1,48 @@ + + + + + Content + + + More content + + + Yet more content + + + Content + + + Content + + + Content + + + Content + + + Content + + + + + Content + + + + + + + After intentionally empty comment + diff --git a/xml/testdata/deep-nesting.xml b/xml/testdata/deep-nesting.xml new file mode 100644 index 000000000000..21c9b18fc7e1 --- /dev/null +++ b/xml/testdata/deep-nesting.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + You found the treasure! + + + + + + + + + + Sibling at level 3 + + + + + + + Another deep path + + + + + diff --git a/xml/testdata/doctype-internal.xml b/xml/testdata/doctype-internal.xml new file mode 100644 index 000000000000..3e236e981d68 --- /dev/null +++ b/xml/testdata/doctype-internal.xml @@ -0,0 +1,22 @@ + + + + + + + + +]> + + + + Widget Pro + 29.99 + A professional-grade widget. + + + Gadget Plus + 49.99 + + diff --git a/xml/testdata/doctype.xml b/xml/testdata/doctype.xml new file mode 100644 index 000000000000..d30f76992581 --- /dev/null +++ b/xml/testdata/doctype.xml @@ -0,0 +1,16 @@ + + + + + + DOCTYPE Test Document + + + +

Testing DOCTYPE Declarations

+

This document tests parsing of DOCTYPE with PUBLIC and SYSTEM identifiers.

+
+

The parser should correctly skip the DOCTYPE declaration while preserving the document structure.

+
+ + diff --git a/xml/testdata/edge-cases.xml b/xml/testdata/edge-cases.xml new file mode 100644 index 000000000000..80efa5348faf --- /dev/null +++ b/xml/testdata/edge-cases.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + multiple spaces here + +Line 1 +Line 2 +Line 3 + + tab separated values + + + + + + + + + + + + + + + + This uses the odd prefix + + + + + + + diff --git a/xml/testdata/entities.xml b/xml/testdata/entities.xml new file mode 100644 index 000000000000..844b6fc08a9f --- /dev/null +++ b/xml/testdata/entities.xml @@ -0,0 +1,22 @@ + + + + + < + > + & + ' + " + Tom & Jerry <3 + + + + + <>& + <>& + 😀🎉🚀 + + + + + diff --git a/xml/testdata/gpx.xml b/xml/testdata/gpx.xml new file mode 100644 index 000000000000..00b0f92ff1fc --- /dev/null +++ b/xml/testdata/gpx.xml @@ -0,0 +1,50 @@ + + + + Morning Run + A 5K morning run through the park + + John Runner + + + + + + 5K Run Track + Running + + + 25.5 + + + + 26.2 + + + + 27.8 + + + + 28.1 + + + + 29.5 + + + + + + Start Point + Flag, Green + + + Checkpoint 1 + Flag, Blue + + diff --git a/xml/testdata/large.xml b/xml/testdata/large.xml new file mode 100644 index 000000000000..2371f387e0f3 --- /dev/null +++ b/xml/testdata/large.xml @@ -0,0 +1,11003 @@ + + + + Product 1 + This is the description for product number 1 in the catalog. + 1.99 + Category 1 + + tag-1 + tag-1 + + + + Product 2 + This is the description for product number 2 in the catalog. + 3.98 + Category 2 + + tag-2 + tag-2 + + + + Product 3 + This is the description for product number 3 in the catalog. + 5.97 + Category 3 + + tag-3 + tag-3 + + + + Product 4 + This is the description for product number 4 in the catalog. + 7.96 + Category 4 + + tag-4 + tag-4 + + + + Product 5 + This is the description for product number 5 in the catalog. + 9.95 + Category 5 + + tag-0 + tag-5 + + + + Product 6 + This is the description for product number 6 in the catalog. + 11.94 + Category 6 + + tag-1 + tag-6 + + + + Product 7 + This is the description for product number 7 in the catalog. + 13.93 + Category 7 + + tag-2 + tag-0 + + + + Product 8 + This is the description for product number 8 in the catalog. + 15.92 + Category 8 + + tag-3 + tag-1 + + + + Product 9 + This is the description for product number 9 in the catalog. + 17.91 + Category 9 + + tag-4 + tag-2 + + + + Product 10 + This is the description for product number 10 in the catalog. + 19.90 + Category 0 + + tag-0 + tag-3 + + + + Product 11 + This is the description for product number 11 in the catalog. + 21.89 + Category 1 + + tag-1 + tag-4 + + + + Product 12 + This is the description for product number 12 in the catalog. + 23.88 + Category 2 + + tag-2 + tag-5 + + + + Product 13 + This is the description for product number 13 in the catalog. + 25.87 + Category 3 + + tag-3 + tag-6 + + + + Product 14 + This is the description for product number 14 in the catalog. + 27.86 + Category 4 + + tag-4 + tag-0 + + + + Product 15 + This is the description for product number 15 in the catalog. + 29.85 + Category 5 + + tag-0 + tag-1 + + + + Product 16 + This is the description for product number 16 in the catalog. + 31.84 + Category 6 + + tag-1 + tag-2 + + + + Product 17 + This is the description for product number 17 in the catalog. + 33.83 + Category 7 + + tag-2 + tag-3 + + + + Product 18 + This is the description for product number 18 in the catalog. + 35.82 + Category 8 + + tag-3 + tag-4 + + + + Product 19 + This is the description for product number 19 in the catalog. + 37.81 + Category 9 + + tag-4 + tag-5 + + + + Product 20 + This is the description for product number 20 in the catalog. + 39.80 + Category 0 + + tag-0 + tag-6 + + + + Product 21 + This is the description for product number 21 in the catalog. + 41.79 + Category 1 + + tag-1 + tag-0 + + + + Product 22 + This is the description for product number 22 in the catalog. + 43.78 + Category 2 + + tag-2 + tag-1 + + + + Product 23 + This is the description for product number 23 in the catalog. + 45.77 + Category 3 + + tag-3 + tag-2 + + + + Product 24 + This is the description for product number 24 in the catalog. + 47.76 + Category 4 + + tag-4 + tag-3 + + + + Product 25 + This is the description for product number 25 in the catalog. + 49.75 + Category 5 + + tag-0 + tag-4 + + + + Product 26 + This is the description for product number 26 in the catalog. + 51.74 + Category 6 + + tag-1 + tag-5 + + + + Product 27 + This is the description for product number 27 in the catalog. + 53.73 + Category 7 + + tag-2 + tag-6 + + + + Product 28 + This is the description for product number 28 in the catalog. + 55.72 + Category 8 + + tag-3 + tag-0 + + + + Product 29 + This is the description for product number 29 in the catalog. + 57.71 + Category 9 + + tag-4 + tag-1 + + + + Product 30 + This is the description for product number 30 in the catalog. + 59.70 + Category 0 + + tag-0 + tag-2 + + + + Product 31 + This is the description for product number 31 in the catalog. + 61.69 + Category 1 + + tag-1 + tag-3 + + + + Product 32 + This is the description for product number 32 in the catalog. + 63.68 + Category 2 + + tag-2 + tag-4 + + + + Product 33 + This is the description for product number 33 in the catalog. + 65.67 + Category 3 + + tag-3 + tag-5 + + + + Product 34 + This is the description for product number 34 in the catalog. + 67.66 + Category 4 + + tag-4 + tag-6 + + + + Product 35 + This is the description for product number 35 in the catalog. + 69.65 + Category 5 + + tag-0 + tag-0 + + + + Product 36 + This is the description for product number 36 in the catalog. + 71.64 + Category 6 + + tag-1 + tag-1 + + + + Product 37 + This is the description for product number 37 in the catalog. + 73.63 + Category 7 + + tag-2 + tag-2 + + + + Product 38 + This is the description for product number 38 in the catalog. + 75.62 + Category 8 + + tag-3 + tag-3 + + + + Product 39 + This is the description for product number 39 in the catalog. + 77.61 + Category 9 + + tag-4 + tag-4 + + + + Product 40 + This is the description for product number 40 in the catalog. + 79.60 + Category 0 + + tag-0 + tag-5 + + + + Product 41 + This is the description for product number 41 in the catalog. + 81.59 + Category 1 + + tag-1 + tag-6 + + + + Product 42 + This is the description for product number 42 in the catalog. + 83.58 + Category 2 + + tag-2 + tag-0 + + + + Product 43 + This is the description for product number 43 in the catalog. + 85.57 + Category 3 + + tag-3 + tag-1 + + + + Product 44 + This is the description for product number 44 in the catalog. + 87.56 + Category 4 + + tag-4 + tag-2 + + + + Product 45 + This is the description for product number 45 in the catalog. + 89.55 + Category 5 + + tag-0 + tag-3 + + + + Product 46 + This is the description for product number 46 in the catalog. + 91.54 + Category 6 + + tag-1 + tag-4 + + + + Product 47 + This is the description for product number 47 in the catalog. + 93.53 + Category 7 + + tag-2 + tag-5 + + + + Product 48 + This is the description for product number 48 in the catalog. + 95.52 + Category 8 + + tag-3 + tag-6 + + + + Product 49 + This is the description for product number 49 in the catalog. + 97.51 + Category 9 + + tag-4 + tag-0 + + + + Product 50 + This is the description for product number 50 in the catalog. + 99.50 + Category 0 + + tag-0 + tag-1 + + + + Product 51 + This is the description for product number 51 in the catalog. + 101.49 + Category 1 + + tag-1 + tag-2 + + + + Product 52 + This is the description for product number 52 in the catalog. + 103.48 + Category 2 + + tag-2 + tag-3 + + + + Product 53 + This is the description for product number 53 in the catalog. + 105.47 + Category 3 + + tag-3 + tag-4 + + + + Product 54 + This is the description for product number 54 in the catalog. + 107.46 + Category 4 + + tag-4 + tag-5 + + + + Product 55 + This is the description for product number 55 in the catalog. + 109.45 + Category 5 + + tag-0 + tag-6 + + + + Product 56 + This is the description for product number 56 in the catalog. + 111.44 + Category 6 + + tag-1 + tag-0 + + + + Product 57 + This is the description for product number 57 in the catalog. + 113.43 + Category 7 + + tag-2 + tag-1 + + + + Product 58 + This is the description for product number 58 in the catalog. + 115.42 + Category 8 + + tag-3 + tag-2 + + + + Product 59 + This is the description for product number 59 in the catalog. + 117.41 + Category 9 + + tag-4 + tag-3 + + + + Product 60 + This is the description for product number 60 in the catalog. + 119.40 + Category 0 + + tag-0 + tag-4 + + + + Product 61 + This is the description for product number 61 in the catalog. + 121.39 + Category 1 + + tag-1 + tag-5 + + + + Product 62 + This is the description for product number 62 in the catalog. + 123.38 + Category 2 + + tag-2 + tag-6 + + + + Product 63 + This is the description for product number 63 in the catalog. + 125.37 + Category 3 + + tag-3 + tag-0 + + + + Product 64 + This is the description for product number 64 in the catalog. + 127.36 + Category 4 + + tag-4 + tag-1 + + + + Product 65 + This is the description for product number 65 in the catalog. + 129.35 + Category 5 + + tag-0 + tag-2 + + + + Product 66 + This is the description for product number 66 in the catalog. + 131.34 + Category 6 + + tag-1 + tag-3 + + + + Product 67 + This is the description for product number 67 in the catalog. + 133.33 + Category 7 + + tag-2 + tag-4 + + + + Product 68 + This is the description for product number 68 in the catalog. + 135.32 + Category 8 + + tag-3 + tag-5 + + + + Product 69 + This is the description for product number 69 in the catalog. + 137.31 + Category 9 + + tag-4 + tag-6 + + + + Product 70 + This is the description for product number 70 in the catalog. + 139.30 + Category 0 + + tag-0 + tag-0 + + + + Product 71 + This is the description for product number 71 in the catalog. + 141.29 + Category 1 + + tag-1 + tag-1 + + + + Product 72 + This is the description for product number 72 in the catalog. + 143.28 + Category 2 + + tag-2 + tag-2 + + + + Product 73 + This is the description for product number 73 in the catalog. + 145.27 + Category 3 + + tag-3 + tag-3 + + + + Product 74 + This is the description for product number 74 in the catalog. + 147.26 + Category 4 + + tag-4 + tag-4 + + + + Product 75 + This is the description for product number 75 in the catalog. + 149.25 + Category 5 + + tag-0 + tag-5 + + + + Product 76 + This is the description for product number 76 in the catalog. + 151.24 + Category 6 + + tag-1 + tag-6 + + + + Product 77 + This is the description for product number 77 in the catalog. + 153.23 + Category 7 + + tag-2 + tag-0 + + + + Product 78 + This is the description for product number 78 in the catalog. + 155.22 + Category 8 + + tag-3 + tag-1 + + + + Product 79 + This is the description for product number 79 in the catalog. + 157.21 + Category 9 + + tag-4 + tag-2 + + + + Product 80 + This is the description for product number 80 in the catalog. + 159.20 + Category 0 + + tag-0 + tag-3 + + + + Product 81 + This is the description for product number 81 in the catalog. + 161.19 + Category 1 + + tag-1 + tag-4 + + + + Product 82 + This is the description for product number 82 in the catalog. + 163.18 + Category 2 + + tag-2 + tag-5 + + + + Product 83 + This is the description for product number 83 in the catalog. + 165.17 + Category 3 + + tag-3 + tag-6 + + + + Product 84 + This is the description for product number 84 in the catalog. + 167.16 + Category 4 + + tag-4 + tag-0 + + + + Product 85 + This is the description for product number 85 in the catalog. + 169.15 + Category 5 + + tag-0 + tag-1 + + + + Product 86 + This is the description for product number 86 in the catalog. + 171.14 + Category 6 + + tag-1 + tag-2 + + + + Product 87 + This is the description for product number 87 in the catalog. + 173.13 + Category 7 + + tag-2 + tag-3 + + + + Product 88 + This is the description for product number 88 in the catalog. + 175.12 + Category 8 + + tag-3 + tag-4 + + + + Product 89 + This is the description for product number 89 in the catalog. + 177.11 + Category 9 + + tag-4 + tag-5 + + + + Product 90 + This is the description for product number 90 in the catalog. + 179.10 + Category 0 + + tag-0 + tag-6 + + + + Product 91 + This is the description for product number 91 in the catalog. + 181.09 + Category 1 + + tag-1 + tag-0 + + + + Product 92 + This is the description for product number 92 in the catalog. + 183.08 + Category 2 + + tag-2 + tag-1 + + + + Product 93 + This is the description for product number 93 in the catalog. + 185.07 + Category 3 + + tag-3 + tag-2 + + + + Product 94 + This is the description for product number 94 in the catalog. + 187.06 + Category 4 + + tag-4 + tag-3 + + + + Product 95 + This is the description for product number 95 in the catalog. + 189.05 + Category 5 + + tag-0 + tag-4 + + + + Product 96 + This is the description for product number 96 in the catalog. + 191.04 + Category 6 + + tag-1 + tag-5 + + + + Product 97 + This is the description for product number 97 in the catalog. + 193.03 + Category 7 + + tag-2 + tag-6 + + + + Product 98 + This is the description for product number 98 in the catalog. + 195.02 + Category 8 + + tag-3 + tag-0 + + + + Product 99 + This is the description for product number 99 in the catalog. + 197.01 + Category 9 + + tag-4 + tag-1 + + + + Product 100 + This is the description for product number 100 in the catalog. + 199.00 + Category 0 + + tag-0 + tag-2 + + + + Product 101 + This is the description for product number 101 in the catalog. + 200.99 + Category 1 + + tag-1 + tag-3 + + + + Product 102 + This is the description for product number 102 in the catalog. + 202.98 + Category 2 + + tag-2 + tag-4 + + + + Product 103 + This is the description for product number 103 in the catalog. + 204.97 + Category 3 + + tag-3 + tag-5 + + + + Product 104 + This is the description for product number 104 in the catalog. + 206.96 + Category 4 + + tag-4 + tag-6 + + + + Product 105 + This is the description for product number 105 in the catalog. + 208.95 + Category 5 + + tag-0 + tag-0 + + + + Product 106 + This is the description for product number 106 in the catalog. + 210.94 + Category 6 + + tag-1 + tag-1 + + + + Product 107 + This is the description for product number 107 in the catalog. + 212.93 + Category 7 + + tag-2 + tag-2 + + + + Product 108 + This is the description for product number 108 in the catalog. + 214.92 + Category 8 + + tag-3 + tag-3 + + + + Product 109 + This is the description for product number 109 in the catalog. + 216.91 + Category 9 + + tag-4 + tag-4 + + + + Product 110 + This is the description for product number 110 in the catalog. + 218.90 + Category 0 + + tag-0 + tag-5 + + + + Product 111 + This is the description for product number 111 in the catalog. + 220.89 + Category 1 + + tag-1 + tag-6 + + + + Product 112 + This is the description for product number 112 in the catalog. + 222.88 + Category 2 + + tag-2 + tag-0 + + + + Product 113 + This is the description for product number 113 in the catalog. + 224.87 + Category 3 + + tag-3 + tag-1 + + + + Product 114 + This is the description for product number 114 in the catalog. + 226.86 + Category 4 + + tag-4 + tag-2 + + + + Product 115 + This is the description for product number 115 in the catalog. + 228.85 + Category 5 + + tag-0 + tag-3 + + + + Product 116 + This is the description for product number 116 in the catalog. + 230.84 + Category 6 + + tag-1 + tag-4 + + + + Product 117 + This is the description for product number 117 in the catalog. + 232.83 + Category 7 + + tag-2 + tag-5 + + + + Product 118 + This is the description for product number 118 in the catalog. + 234.82 + Category 8 + + tag-3 + tag-6 + + + + Product 119 + This is the description for product number 119 in the catalog. + 236.81 + Category 9 + + tag-4 + tag-0 + + + + Product 120 + This is the description for product number 120 in the catalog. + 238.80 + Category 0 + + tag-0 + tag-1 + + + + Product 121 + This is the description for product number 121 in the catalog. + 240.79 + Category 1 + + tag-1 + tag-2 + + + + Product 122 + This is the description for product number 122 in the catalog. + 242.78 + Category 2 + + tag-2 + tag-3 + + + + Product 123 + This is the description for product number 123 in the catalog. + 244.77 + Category 3 + + tag-3 + tag-4 + + + + Product 124 + This is the description for product number 124 in the catalog. + 246.76 + Category 4 + + tag-4 + tag-5 + + + + Product 125 + This is the description for product number 125 in the catalog. + 248.75 + Category 5 + + tag-0 + tag-6 + + + + Product 126 + This is the description for product number 126 in the catalog. + 250.74 + Category 6 + + tag-1 + tag-0 + + + + Product 127 + This is the description for product number 127 in the catalog. + 252.73 + Category 7 + + tag-2 + tag-1 + + + + Product 128 + This is the description for product number 128 in the catalog. + 254.72 + Category 8 + + tag-3 + tag-2 + + + + Product 129 + This is the description for product number 129 in the catalog. + 256.71 + Category 9 + + tag-4 + tag-3 + + + + Product 130 + This is the description for product number 130 in the catalog. + 258.70 + Category 0 + + tag-0 + tag-4 + + + + Product 131 + This is the description for product number 131 in the catalog. + 260.69 + Category 1 + + tag-1 + tag-5 + + + + Product 132 + This is the description for product number 132 in the catalog. + 262.68 + Category 2 + + tag-2 + tag-6 + + + + Product 133 + This is the description for product number 133 in the catalog. + 264.67 + Category 3 + + tag-3 + tag-0 + + + + Product 134 + This is the description for product number 134 in the catalog. + 266.66 + Category 4 + + tag-4 + tag-1 + + + + Product 135 + This is the description for product number 135 in the catalog. + 268.65 + Category 5 + + tag-0 + tag-2 + + + + Product 136 + This is the description for product number 136 in the catalog. + 270.64 + Category 6 + + tag-1 + tag-3 + + + + Product 137 + This is the description for product number 137 in the catalog. + 272.63 + Category 7 + + tag-2 + tag-4 + + + + Product 138 + This is the description for product number 138 in the catalog. + 274.62 + Category 8 + + tag-3 + tag-5 + + + + Product 139 + This is the description for product number 139 in the catalog. + 276.61 + Category 9 + + tag-4 + tag-6 + + + + Product 140 + This is the description for product number 140 in the catalog. + 278.60 + Category 0 + + tag-0 + tag-0 + + + + Product 141 + This is the description for product number 141 in the catalog. + 280.59 + Category 1 + + tag-1 + tag-1 + + + + Product 142 + This is the description for product number 142 in the catalog. + 282.58 + Category 2 + + tag-2 + tag-2 + + + + Product 143 + This is the description for product number 143 in the catalog. + 284.57 + Category 3 + + tag-3 + tag-3 + + + + Product 144 + This is the description for product number 144 in the catalog. + 286.56 + Category 4 + + tag-4 + tag-4 + + + + Product 145 + This is the description for product number 145 in the catalog. + 288.55 + Category 5 + + tag-0 + tag-5 + + + + Product 146 + This is the description for product number 146 in the catalog. + 290.54 + Category 6 + + tag-1 + tag-6 + + + + Product 147 + This is the description for product number 147 in the catalog. + 292.53 + Category 7 + + tag-2 + tag-0 + + + + Product 148 + This is the description for product number 148 in the catalog. + 294.52 + Category 8 + + tag-3 + tag-1 + + + + Product 149 + This is the description for product number 149 in the catalog. + 296.51 + Category 9 + + tag-4 + tag-2 + + + + Product 150 + This is the description for product number 150 in the catalog. + 298.50 + Category 0 + + tag-0 + tag-3 + + + + Product 151 + This is the description for product number 151 in the catalog. + 300.49 + Category 1 + + tag-1 + tag-4 + + + + Product 152 + This is the description for product number 152 in the catalog. + 302.48 + Category 2 + + tag-2 + tag-5 + + + + Product 153 + This is the description for product number 153 in the catalog. + 304.47 + Category 3 + + tag-3 + tag-6 + + + + Product 154 + This is the description for product number 154 in the catalog. + 306.46 + Category 4 + + tag-4 + tag-0 + + + + Product 155 + This is the description for product number 155 in the catalog. + 308.45 + Category 5 + + tag-0 + tag-1 + + + + Product 156 + This is the description for product number 156 in the catalog. + 310.44 + Category 6 + + tag-1 + tag-2 + + + + Product 157 + This is the description for product number 157 in the catalog. + 312.43 + Category 7 + + tag-2 + tag-3 + + + + Product 158 + This is the description for product number 158 in the catalog. + 314.42 + Category 8 + + tag-3 + tag-4 + + + + Product 159 + This is the description for product number 159 in the catalog. + 316.41 + Category 9 + + tag-4 + tag-5 + + + + Product 160 + This is the description for product number 160 in the catalog. + 318.40 + Category 0 + + tag-0 + tag-6 + + + + Product 161 + This is the description for product number 161 in the catalog. + 320.39 + Category 1 + + tag-1 + tag-0 + + + + Product 162 + This is the description for product number 162 in the catalog. + 322.38 + Category 2 + + tag-2 + tag-1 + + + + Product 163 + This is the description for product number 163 in the catalog. + 324.37 + Category 3 + + tag-3 + tag-2 + + + + Product 164 + This is the description for product number 164 in the catalog. + 326.36 + Category 4 + + tag-4 + tag-3 + + + + Product 165 + This is the description for product number 165 in the catalog. + 328.35 + Category 5 + + tag-0 + tag-4 + + + + Product 166 + This is the description for product number 166 in the catalog. + 330.34 + Category 6 + + tag-1 + tag-5 + + + + Product 167 + This is the description for product number 167 in the catalog. + 332.33 + Category 7 + + tag-2 + tag-6 + + + + Product 168 + This is the description for product number 168 in the catalog. + 334.32 + Category 8 + + tag-3 + tag-0 + + + + Product 169 + This is the description for product number 169 in the catalog. + 336.31 + Category 9 + + tag-4 + tag-1 + + + + Product 170 + This is the description for product number 170 in the catalog. + 338.30 + Category 0 + + tag-0 + tag-2 + + + + Product 171 + This is the description for product number 171 in the catalog. + 340.29 + Category 1 + + tag-1 + tag-3 + + + + Product 172 + This is the description for product number 172 in the catalog. + 342.28 + Category 2 + + tag-2 + tag-4 + + + + Product 173 + This is the description for product number 173 in the catalog. + 344.27 + Category 3 + + tag-3 + tag-5 + + + + Product 174 + This is the description for product number 174 in the catalog. + 346.26 + Category 4 + + tag-4 + tag-6 + + + + Product 175 + This is the description for product number 175 in the catalog. + 348.25 + Category 5 + + tag-0 + tag-0 + + + + Product 176 + This is the description for product number 176 in the catalog. + 350.24 + Category 6 + + tag-1 + tag-1 + + + + Product 177 + This is the description for product number 177 in the catalog. + 352.23 + Category 7 + + tag-2 + tag-2 + + + + Product 178 + This is the description for product number 178 in the catalog. + 354.22 + Category 8 + + tag-3 + tag-3 + + + + Product 179 + This is the description for product number 179 in the catalog. + 356.21 + Category 9 + + tag-4 + tag-4 + + + + Product 180 + This is the description for product number 180 in the catalog. + 358.20 + Category 0 + + tag-0 + tag-5 + + + + Product 181 + This is the description for product number 181 in the catalog. + 360.19 + Category 1 + + tag-1 + tag-6 + + + + Product 182 + This is the description for product number 182 in the catalog. + 362.18 + Category 2 + + tag-2 + tag-0 + + + + Product 183 + This is the description for product number 183 in the catalog. + 364.17 + Category 3 + + tag-3 + tag-1 + + + + Product 184 + This is the description for product number 184 in the catalog. + 366.16 + Category 4 + + tag-4 + tag-2 + + + + Product 185 + This is the description for product number 185 in the catalog. + 368.15 + Category 5 + + tag-0 + tag-3 + + + + Product 186 + This is the description for product number 186 in the catalog. + 370.14 + Category 6 + + tag-1 + tag-4 + + + + Product 187 + This is the description for product number 187 in the catalog. + 372.13 + Category 7 + + tag-2 + tag-5 + + + + Product 188 + This is the description for product number 188 in the catalog. + 374.12 + Category 8 + + tag-3 + tag-6 + + + + Product 189 + This is the description for product number 189 in the catalog. + 376.11 + Category 9 + + tag-4 + tag-0 + + + + Product 190 + This is the description for product number 190 in the catalog. + 378.10 + Category 0 + + tag-0 + tag-1 + + + + Product 191 + This is the description for product number 191 in the catalog. + 380.09 + Category 1 + + tag-1 + tag-2 + + + + Product 192 + This is the description for product number 192 in the catalog. + 382.08 + Category 2 + + tag-2 + tag-3 + + + + Product 193 + This is the description for product number 193 in the catalog. + 384.07 + Category 3 + + tag-3 + tag-4 + + + + Product 194 + This is the description for product number 194 in the catalog. + 386.06 + Category 4 + + tag-4 + tag-5 + + + + Product 195 + This is the description for product number 195 in the catalog. + 388.05 + Category 5 + + tag-0 + tag-6 + + + + Product 196 + This is the description for product number 196 in the catalog. + 390.04 + Category 6 + + tag-1 + tag-0 + + + + Product 197 + This is the description for product number 197 in the catalog. + 392.03 + Category 7 + + tag-2 + tag-1 + + + + Product 198 + This is the description for product number 198 in the catalog. + 394.02 + Category 8 + + tag-3 + tag-2 + + + + Product 199 + This is the description for product number 199 in the catalog. + 396.01 + Category 9 + + tag-4 + tag-3 + + + + Product 200 + This is the description for product number 200 in the catalog. + 398.00 + Category 0 + + tag-0 + tag-4 + + + + Product 201 + This is the description for product number 201 in the catalog. + 399.99 + Category 1 + + tag-1 + tag-5 + + + + Product 202 + This is the description for product number 202 in the catalog. + 401.98 + Category 2 + + tag-2 + tag-6 + + + + Product 203 + This is the description for product number 203 in the catalog. + 403.97 + Category 3 + + tag-3 + tag-0 + + + + Product 204 + This is the description for product number 204 in the catalog. + 405.96 + Category 4 + + tag-4 + tag-1 + + + + Product 205 + This is the description for product number 205 in the catalog. + 407.95 + Category 5 + + tag-0 + tag-2 + + + + Product 206 + This is the description for product number 206 in the catalog. + 409.94 + Category 6 + + tag-1 + tag-3 + + + + Product 207 + This is the description for product number 207 in the catalog. + 411.93 + Category 7 + + tag-2 + tag-4 + + + + Product 208 + This is the description for product number 208 in the catalog. + 413.92 + Category 8 + + tag-3 + tag-5 + + + + Product 209 + This is the description for product number 209 in the catalog. + 415.91 + Category 9 + + tag-4 + tag-6 + + + + Product 210 + This is the description for product number 210 in the catalog. + 417.90 + Category 0 + + tag-0 + tag-0 + + + + Product 211 + This is the description for product number 211 in the catalog. + 419.89 + Category 1 + + tag-1 + tag-1 + + + + Product 212 + This is the description for product number 212 in the catalog. + 421.88 + Category 2 + + tag-2 + tag-2 + + + + Product 213 + This is the description for product number 213 in the catalog. + 423.87 + Category 3 + + tag-3 + tag-3 + + + + Product 214 + This is the description for product number 214 in the catalog. + 425.86 + Category 4 + + tag-4 + tag-4 + + + + Product 215 + This is the description for product number 215 in the catalog. + 427.85 + Category 5 + + tag-0 + tag-5 + + + + Product 216 + This is the description for product number 216 in the catalog. + 429.84 + Category 6 + + tag-1 + tag-6 + + + + Product 217 + This is the description for product number 217 in the catalog. + 431.83 + Category 7 + + tag-2 + tag-0 + + + + Product 218 + This is the description for product number 218 in the catalog. + 433.82 + Category 8 + + tag-3 + tag-1 + + + + Product 219 + This is the description for product number 219 in the catalog. + 435.81 + Category 9 + + tag-4 + tag-2 + + + + Product 220 + This is the description for product number 220 in the catalog. + 437.80 + Category 0 + + tag-0 + tag-3 + + + + Product 221 + This is the description for product number 221 in the catalog. + 439.79 + Category 1 + + tag-1 + tag-4 + + + + Product 222 + This is the description for product number 222 in the catalog. + 441.78 + Category 2 + + tag-2 + tag-5 + + + + Product 223 + This is the description for product number 223 in the catalog. + 443.77 + Category 3 + + tag-3 + tag-6 + + + + Product 224 + This is the description for product number 224 in the catalog. + 445.76 + Category 4 + + tag-4 + tag-0 + + + + Product 225 + This is the description for product number 225 in the catalog. + 447.75 + Category 5 + + tag-0 + tag-1 + + + + Product 226 + This is the description for product number 226 in the catalog. + 449.74 + Category 6 + + tag-1 + tag-2 + + + + Product 227 + This is the description for product number 227 in the catalog. + 451.73 + Category 7 + + tag-2 + tag-3 + + + + Product 228 + This is the description for product number 228 in the catalog. + 453.72 + Category 8 + + tag-3 + tag-4 + + + + Product 229 + This is the description for product number 229 in the catalog. + 455.71 + Category 9 + + tag-4 + tag-5 + + + + Product 230 + This is the description for product number 230 in the catalog. + 457.70 + Category 0 + + tag-0 + tag-6 + + + + Product 231 + This is the description for product number 231 in the catalog. + 459.69 + Category 1 + + tag-1 + tag-0 + + + + Product 232 + This is the description for product number 232 in the catalog. + 461.68 + Category 2 + + tag-2 + tag-1 + + + + Product 233 + This is the description for product number 233 in the catalog. + 463.67 + Category 3 + + tag-3 + tag-2 + + + + Product 234 + This is the description for product number 234 in the catalog. + 465.66 + Category 4 + + tag-4 + tag-3 + + + + Product 235 + This is the description for product number 235 in the catalog. + 467.65 + Category 5 + + tag-0 + tag-4 + + + + Product 236 + This is the description for product number 236 in the catalog. + 469.64 + Category 6 + + tag-1 + tag-5 + + + + Product 237 + This is the description for product number 237 in the catalog. + 471.63 + Category 7 + + tag-2 + tag-6 + + + + Product 238 + This is the description for product number 238 in the catalog. + 473.62 + Category 8 + + tag-3 + tag-0 + + + + Product 239 + This is the description for product number 239 in the catalog. + 475.61 + Category 9 + + tag-4 + tag-1 + + + + Product 240 + This is the description for product number 240 in the catalog. + 477.60 + Category 0 + + tag-0 + tag-2 + + + + Product 241 + This is the description for product number 241 in the catalog. + 479.59 + Category 1 + + tag-1 + tag-3 + + + + Product 242 + This is the description for product number 242 in the catalog. + 481.58 + Category 2 + + tag-2 + tag-4 + + + + Product 243 + This is the description for product number 243 in the catalog. + 483.57 + Category 3 + + tag-3 + tag-5 + + + + Product 244 + This is the description for product number 244 in the catalog. + 485.56 + Category 4 + + tag-4 + tag-6 + + + + Product 245 + This is the description for product number 245 in the catalog. + 487.55 + Category 5 + + tag-0 + tag-0 + + + + Product 246 + This is the description for product number 246 in the catalog. + 489.54 + Category 6 + + tag-1 + tag-1 + + + + Product 247 + This is the description for product number 247 in the catalog. + 491.53 + Category 7 + + tag-2 + tag-2 + + + + Product 248 + This is the description for product number 248 in the catalog. + 493.52 + Category 8 + + tag-3 + tag-3 + + + + Product 249 + This is the description for product number 249 in the catalog. + 495.51 + Category 9 + + tag-4 + tag-4 + + + + Product 250 + This is the description for product number 250 in the catalog. + 497.50 + Category 0 + + tag-0 + tag-5 + + + + Product 251 + This is the description for product number 251 in the catalog. + 499.49 + Category 1 + + tag-1 + tag-6 + + + + Product 252 + This is the description for product number 252 in the catalog. + 501.48 + Category 2 + + tag-2 + tag-0 + + + + Product 253 + This is the description for product number 253 in the catalog. + 503.47 + Category 3 + + tag-3 + tag-1 + + + + Product 254 + This is the description for product number 254 in the catalog. + 505.46 + Category 4 + + tag-4 + tag-2 + + + + Product 255 + This is the description for product number 255 in the catalog. + 507.45 + Category 5 + + tag-0 + tag-3 + + + + Product 256 + This is the description for product number 256 in the catalog. + 509.44 + Category 6 + + tag-1 + tag-4 + + + + Product 257 + This is the description for product number 257 in the catalog. + 511.43 + Category 7 + + tag-2 + tag-5 + + + + Product 258 + This is the description for product number 258 in the catalog. + 513.42 + Category 8 + + tag-3 + tag-6 + + + + Product 259 + This is the description for product number 259 in the catalog. + 515.41 + Category 9 + + tag-4 + tag-0 + + + + Product 260 + This is the description for product number 260 in the catalog. + 517.40 + Category 0 + + tag-0 + tag-1 + + + + Product 261 + This is the description for product number 261 in the catalog. + 519.39 + Category 1 + + tag-1 + tag-2 + + + + Product 262 + This is the description for product number 262 in the catalog. + 521.38 + Category 2 + + tag-2 + tag-3 + + + + Product 263 + This is the description for product number 263 in the catalog. + 523.37 + Category 3 + + tag-3 + tag-4 + + + + Product 264 + This is the description for product number 264 in the catalog. + 525.36 + Category 4 + + tag-4 + tag-5 + + + + Product 265 + This is the description for product number 265 in the catalog. + 527.35 + Category 5 + + tag-0 + tag-6 + + + + Product 266 + This is the description for product number 266 in the catalog. + 529.34 + Category 6 + + tag-1 + tag-0 + + + + Product 267 + This is the description for product number 267 in the catalog. + 531.33 + Category 7 + + tag-2 + tag-1 + + + + Product 268 + This is the description for product number 268 in the catalog. + 533.32 + Category 8 + + tag-3 + tag-2 + + + + Product 269 + This is the description for product number 269 in the catalog. + 535.31 + Category 9 + + tag-4 + tag-3 + + + + Product 270 + This is the description for product number 270 in the catalog. + 537.30 + Category 0 + + tag-0 + tag-4 + + + + Product 271 + This is the description for product number 271 in the catalog. + 539.29 + Category 1 + + tag-1 + tag-5 + + + + Product 272 + This is the description for product number 272 in the catalog. + 541.28 + Category 2 + + tag-2 + tag-6 + + + + Product 273 + This is the description for product number 273 in the catalog. + 543.27 + Category 3 + + tag-3 + tag-0 + + + + Product 274 + This is the description for product number 274 in the catalog. + 545.26 + Category 4 + + tag-4 + tag-1 + + + + Product 275 + This is the description for product number 275 in the catalog. + 547.25 + Category 5 + + tag-0 + tag-2 + + + + Product 276 + This is the description for product number 276 in the catalog. + 549.24 + Category 6 + + tag-1 + tag-3 + + + + Product 277 + This is the description for product number 277 in the catalog. + 551.23 + Category 7 + + tag-2 + tag-4 + + + + Product 278 + This is the description for product number 278 in the catalog. + 553.22 + Category 8 + + tag-3 + tag-5 + + + + Product 279 + This is the description for product number 279 in the catalog. + 555.21 + Category 9 + + tag-4 + tag-6 + + + + Product 280 + This is the description for product number 280 in the catalog. + 557.20 + Category 0 + + tag-0 + tag-0 + + + + Product 281 + This is the description for product number 281 in the catalog. + 559.19 + Category 1 + + tag-1 + tag-1 + + + + Product 282 + This is the description for product number 282 in the catalog. + 561.18 + Category 2 + + tag-2 + tag-2 + + + + Product 283 + This is the description for product number 283 in the catalog. + 563.17 + Category 3 + + tag-3 + tag-3 + + + + Product 284 + This is the description for product number 284 in the catalog. + 565.16 + Category 4 + + tag-4 + tag-4 + + + + Product 285 + This is the description for product number 285 in the catalog. + 567.15 + Category 5 + + tag-0 + tag-5 + + + + Product 286 + This is the description for product number 286 in the catalog. + 569.14 + Category 6 + + tag-1 + tag-6 + + + + Product 287 + This is the description for product number 287 in the catalog. + 571.13 + Category 7 + + tag-2 + tag-0 + + + + Product 288 + This is the description for product number 288 in the catalog. + 573.12 + Category 8 + + tag-3 + tag-1 + + + + Product 289 + This is the description for product number 289 in the catalog. + 575.11 + Category 9 + + tag-4 + tag-2 + + + + Product 290 + This is the description for product number 290 in the catalog. + 577.10 + Category 0 + + tag-0 + tag-3 + + + + Product 291 + This is the description for product number 291 in the catalog. + 579.09 + Category 1 + + tag-1 + tag-4 + + + + Product 292 + This is the description for product number 292 in the catalog. + 581.08 + Category 2 + + tag-2 + tag-5 + + + + Product 293 + This is the description for product number 293 in the catalog. + 583.07 + Category 3 + + tag-3 + tag-6 + + + + Product 294 + This is the description for product number 294 in the catalog. + 585.06 + Category 4 + + tag-4 + tag-0 + + + + Product 295 + This is the description for product number 295 in the catalog. + 587.05 + Category 5 + + tag-0 + tag-1 + + + + Product 296 + This is the description for product number 296 in the catalog. + 589.04 + Category 6 + + tag-1 + tag-2 + + + + Product 297 + This is the description for product number 297 in the catalog. + 591.03 + Category 7 + + tag-2 + tag-3 + + + + Product 298 + This is the description for product number 298 in the catalog. + 593.02 + Category 8 + + tag-3 + tag-4 + + + + Product 299 + This is the description for product number 299 in the catalog. + 595.01 + Category 9 + + tag-4 + tag-5 + + + + Product 300 + This is the description for product number 300 in the catalog. + 597.00 + Category 0 + + tag-0 + tag-6 + + + + Product 301 + This is the description for product number 301 in the catalog. + 598.99 + Category 1 + + tag-1 + tag-0 + + + + Product 302 + This is the description for product number 302 in the catalog. + 600.98 + Category 2 + + tag-2 + tag-1 + + + + Product 303 + This is the description for product number 303 in the catalog. + 602.97 + Category 3 + + tag-3 + tag-2 + + + + Product 304 + This is the description for product number 304 in the catalog. + 604.96 + Category 4 + + tag-4 + tag-3 + + + + Product 305 + This is the description for product number 305 in the catalog. + 606.95 + Category 5 + + tag-0 + tag-4 + + + + Product 306 + This is the description for product number 306 in the catalog. + 608.94 + Category 6 + + tag-1 + tag-5 + + + + Product 307 + This is the description for product number 307 in the catalog. + 610.93 + Category 7 + + tag-2 + tag-6 + + + + Product 308 + This is the description for product number 308 in the catalog. + 612.92 + Category 8 + + tag-3 + tag-0 + + + + Product 309 + This is the description for product number 309 in the catalog. + 614.91 + Category 9 + + tag-4 + tag-1 + + + + Product 310 + This is the description for product number 310 in the catalog. + 616.90 + Category 0 + + tag-0 + tag-2 + + + + Product 311 + This is the description for product number 311 in the catalog. + 618.89 + Category 1 + + tag-1 + tag-3 + + + + Product 312 + This is the description for product number 312 in the catalog. + 620.88 + Category 2 + + tag-2 + tag-4 + + + + Product 313 + This is the description for product number 313 in the catalog. + 622.87 + Category 3 + + tag-3 + tag-5 + + + + Product 314 + This is the description for product number 314 in the catalog. + 624.86 + Category 4 + + tag-4 + tag-6 + + + + Product 315 + This is the description for product number 315 in the catalog. + 626.85 + Category 5 + + tag-0 + tag-0 + + + + Product 316 + This is the description for product number 316 in the catalog. + 628.84 + Category 6 + + tag-1 + tag-1 + + + + Product 317 + This is the description for product number 317 in the catalog. + 630.83 + Category 7 + + tag-2 + tag-2 + + + + Product 318 + This is the description for product number 318 in the catalog. + 632.82 + Category 8 + + tag-3 + tag-3 + + + + Product 319 + This is the description for product number 319 in the catalog. + 634.81 + Category 9 + + tag-4 + tag-4 + + + + Product 320 + This is the description for product number 320 in the catalog. + 636.80 + Category 0 + + tag-0 + tag-5 + + + + Product 321 + This is the description for product number 321 in the catalog. + 638.79 + Category 1 + + tag-1 + tag-6 + + + + Product 322 + This is the description for product number 322 in the catalog. + 640.78 + Category 2 + + tag-2 + tag-0 + + + + Product 323 + This is the description for product number 323 in the catalog. + 642.77 + Category 3 + + tag-3 + tag-1 + + + + Product 324 + This is the description for product number 324 in the catalog. + 644.76 + Category 4 + + tag-4 + tag-2 + + + + Product 325 + This is the description for product number 325 in the catalog. + 646.75 + Category 5 + + tag-0 + tag-3 + + + + Product 326 + This is the description for product number 326 in the catalog. + 648.74 + Category 6 + + tag-1 + tag-4 + + + + Product 327 + This is the description for product number 327 in the catalog. + 650.73 + Category 7 + + tag-2 + tag-5 + + + + Product 328 + This is the description for product number 328 in the catalog. + 652.72 + Category 8 + + tag-3 + tag-6 + + + + Product 329 + This is the description for product number 329 in the catalog. + 654.71 + Category 9 + + tag-4 + tag-0 + + + + Product 330 + This is the description for product number 330 in the catalog. + 656.70 + Category 0 + + tag-0 + tag-1 + + + + Product 331 + This is the description for product number 331 in the catalog. + 658.69 + Category 1 + + tag-1 + tag-2 + + + + Product 332 + This is the description for product number 332 in the catalog. + 660.68 + Category 2 + + tag-2 + tag-3 + + + + Product 333 + This is the description for product number 333 in the catalog. + 662.67 + Category 3 + + tag-3 + tag-4 + + + + Product 334 + This is the description for product number 334 in the catalog. + 664.66 + Category 4 + + tag-4 + tag-5 + + + + Product 335 + This is the description for product number 335 in the catalog. + 666.65 + Category 5 + + tag-0 + tag-6 + + + + Product 336 + This is the description for product number 336 in the catalog. + 668.64 + Category 6 + + tag-1 + tag-0 + + + + Product 337 + This is the description for product number 337 in the catalog. + 670.63 + Category 7 + + tag-2 + tag-1 + + + + Product 338 + This is the description for product number 338 in the catalog. + 672.62 + Category 8 + + tag-3 + tag-2 + + + + Product 339 + This is the description for product number 339 in the catalog. + 674.61 + Category 9 + + tag-4 + tag-3 + + + + Product 340 + This is the description for product number 340 in the catalog. + 676.60 + Category 0 + + tag-0 + tag-4 + + + + Product 341 + This is the description for product number 341 in the catalog. + 678.59 + Category 1 + + tag-1 + tag-5 + + + + Product 342 + This is the description for product number 342 in the catalog. + 680.58 + Category 2 + + tag-2 + tag-6 + + + + Product 343 + This is the description for product number 343 in the catalog. + 682.57 + Category 3 + + tag-3 + tag-0 + + + + Product 344 + This is the description for product number 344 in the catalog. + 684.56 + Category 4 + + tag-4 + tag-1 + + + + Product 345 + This is the description for product number 345 in the catalog. + 686.55 + Category 5 + + tag-0 + tag-2 + + + + Product 346 + This is the description for product number 346 in the catalog. + 688.54 + Category 6 + + tag-1 + tag-3 + + + + Product 347 + This is the description for product number 347 in the catalog. + 690.53 + Category 7 + + tag-2 + tag-4 + + + + Product 348 + This is the description for product number 348 in the catalog. + 692.52 + Category 8 + + tag-3 + tag-5 + + + + Product 349 + This is the description for product number 349 in the catalog. + 694.51 + Category 9 + + tag-4 + tag-6 + + + + Product 350 + This is the description for product number 350 in the catalog. + 696.50 + Category 0 + + tag-0 + tag-0 + + + + Product 351 + This is the description for product number 351 in the catalog. + 698.49 + Category 1 + + tag-1 + tag-1 + + + + Product 352 + This is the description for product number 352 in the catalog. + 700.48 + Category 2 + + tag-2 + tag-2 + + + + Product 353 + This is the description for product number 353 in the catalog. + 702.47 + Category 3 + + tag-3 + tag-3 + + + + Product 354 + This is the description for product number 354 in the catalog. + 704.46 + Category 4 + + tag-4 + tag-4 + + + + Product 355 + This is the description for product number 355 in the catalog. + 706.45 + Category 5 + + tag-0 + tag-5 + + + + Product 356 + This is the description for product number 356 in the catalog. + 708.44 + Category 6 + + tag-1 + tag-6 + + + + Product 357 + This is the description for product number 357 in the catalog. + 710.43 + Category 7 + + tag-2 + tag-0 + + + + Product 358 + This is the description for product number 358 in the catalog. + 712.42 + Category 8 + + tag-3 + tag-1 + + + + Product 359 + This is the description for product number 359 in the catalog. + 714.41 + Category 9 + + tag-4 + tag-2 + + + + Product 360 + This is the description for product number 360 in the catalog. + 716.40 + Category 0 + + tag-0 + tag-3 + + + + Product 361 + This is the description for product number 361 in the catalog. + 718.39 + Category 1 + + tag-1 + tag-4 + + + + Product 362 + This is the description for product number 362 in the catalog. + 720.38 + Category 2 + + tag-2 + tag-5 + + + + Product 363 + This is the description for product number 363 in the catalog. + 722.37 + Category 3 + + tag-3 + tag-6 + + + + Product 364 + This is the description for product number 364 in the catalog. + 724.36 + Category 4 + + tag-4 + tag-0 + + + + Product 365 + This is the description for product number 365 in the catalog. + 726.35 + Category 5 + + tag-0 + tag-1 + + + + Product 366 + This is the description for product number 366 in the catalog. + 728.34 + Category 6 + + tag-1 + tag-2 + + + + Product 367 + This is the description for product number 367 in the catalog. + 730.33 + Category 7 + + tag-2 + tag-3 + + + + Product 368 + This is the description for product number 368 in the catalog. + 732.32 + Category 8 + + tag-3 + tag-4 + + + + Product 369 + This is the description for product number 369 in the catalog. + 734.31 + Category 9 + + tag-4 + tag-5 + + + + Product 370 + This is the description for product number 370 in the catalog. + 736.30 + Category 0 + + tag-0 + tag-6 + + + + Product 371 + This is the description for product number 371 in the catalog. + 738.29 + Category 1 + + tag-1 + tag-0 + + + + Product 372 + This is the description for product number 372 in the catalog. + 740.28 + Category 2 + + tag-2 + tag-1 + + + + Product 373 + This is the description for product number 373 in the catalog. + 742.27 + Category 3 + + tag-3 + tag-2 + + + + Product 374 + This is the description for product number 374 in the catalog. + 744.26 + Category 4 + + tag-4 + tag-3 + + + + Product 375 + This is the description for product number 375 in the catalog. + 746.25 + Category 5 + + tag-0 + tag-4 + + + + Product 376 + This is the description for product number 376 in the catalog. + 748.24 + Category 6 + + tag-1 + tag-5 + + + + Product 377 + This is the description for product number 377 in the catalog. + 750.23 + Category 7 + + tag-2 + tag-6 + + + + Product 378 + This is the description for product number 378 in the catalog. + 752.22 + Category 8 + + tag-3 + tag-0 + + + + Product 379 + This is the description for product number 379 in the catalog. + 754.21 + Category 9 + + tag-4 + tag-1 + + + + Product 380 + This is the description for product number 380 in the catalog. + 756.20 + Category 0 + + tag-0 + tag-2 + + + + Product 381 + This is the description for product number 381 in the catalog. + 758.19 + Category 1 + + tag-1 + tag-3 + + + + Product 382 + This is the description for product number 382 in the catalog. + 760.18 + Category 2 + + tag-2 + tag-4 + + + + Product 383 + This is the description for product number 383 in the catalog. + 762.17 + Category 3 + + tag-3 + tag-5 + + + + Product 384 + This is the description for product number 384 in the catalog. + 764.16 + Category 4 + + tag-4 + tag-6 + + + + Product 385 + This is the description for product number 385 in the catalog. + 766.15 + Category 5 + + tag-0 + tag-0 + + + + Product 386 + This is the description for product number 386 in the catalog. + 768.14 + Category 6 + + tag-1 + tag-1 + + + + Product 387 + This is the description for product number 387 in the catalog. + 770.13 + Category 7 + + tag-2 + tag-2 + + + + Product 388 + This is the description for product number 388 in the catalog. + 772.12 + Category 8 + + tag-3 + tag-3 + + + + Product 389 + This is the description for product number 389 in the catalog. + 774.11 + Category 9 + + tag-4 + tag-4 + + + + Product 390 + This is the description for product number 390 in the catalog. + 776.10 + Category 0 + + tag-0 + tag-5 + + + + Product 391 + This is the description for product number 391 in the catalog. + 778.09 + Category 1 + + tag-1 + tag-6 + + + + Product 392 + This is the description for product number 392 in the catalog. + 780.08 + Category 2 + + tag-2 + tag-0 + + + + Product 393 + This is the description for product number 393 in the catalog. + 782.07 + Category 3 + + tag-3 + tag-1 + + + + Product 394 + This is the description for product number 394 in the catalog. + 784.06 + Category 4 + + tag-4 + tag-2 + + + + Product 395 + This is the description for product number 395 in the catalog. + 786.05 + Category 5 + + tag-0 + tag-3 + + + + Product 396 + This is the description for product number 396 in the catalog. + 788.04 + Category 6 + + tag-1 + tag-4 + + + + Product 397 + This is the description for product number 397 in the catalog. + 790.03 + Category 7 + + tag-2 + tag-5 + + + + Product 398 + This is the description for product number 398 in the catalog. + 792.02 + Category 8 + + tag-3 + tag-6 + + + + Product 399 + This is the description for product number 399 in the catalog. + 794.01 + Category 9 + + tag-4 + tag-0 + + + + Product 400 + This is the description for product number 400 in the catalog. + 796.00 + Category 0 + + tag-0 + tag-1 + + + + Product 401 + This is the description for product number 401 in the catalog. + 797.99 + Category 1 + + tag-1 + tag-2 + + + + Product 402 + This is the description for product number 402 in the catalog. + 799.98 + Category 2 + + tag-2 + tag-3 + + + + Product 403 + This is the description for product number 403 in the catalog. + 801.97 + Category 3 + + tag-3 + tag-4 + + + + Product 404 + This is the description for product number 404 in the catalog. + 803.96 + Category 4 + + tag-4 + tag-5 + + + + Product 405 + This is the description for product number 405 in the catalog. + 805.95 + Category 5 + + tag-0 + tag-6 + + + + Product 406 + This is the description for product number 406 in the catalog. + 807.94 + Category 6 + + tag-1 + tag-0 + + + + Product 407 + This is the description for product number 407 in the catalog. + 809.93 + Category 7 + + tag-2 + tag-1 + + + + Product 408 + This is the description for product number 408 in the catalog. + 811.92 + Category 8 + + tag-3 + tag-2 + + + + Product 409 + This is the description for product number 409 in the catalog. + 813.91 + Category 9 + + tag-4 + tag-3 + + + + Product 410 + This is the description for product number 410 in the catalog. + 815.90 + Category 0 + + tag-0 + tag-4 + + + + Product 411 + This is the description for product number 411 in the catalog. + 817.89 + Category 1 + + tag-1 + tag-5 + + + + Product 412 + This is the description for product number 412 in the catalog. + 819.88 + Category 2 + + tag-2 + tag-6 + + + + Product 413 + This is the description for product number 413 in the catalog. + 821.87 + Category 3 + + tag-3 + tag-0 + + + + Product 414 + This is the description for product number 414 in the catalog. + 823.86 + Category 4 + + tag-4 + tag-1 + + + + Product 415 + This is the description for product number 415 in the catalog. + 825.85 + Category 5 + + tag-0 + tag-2 + + + + Product 416 + This is the description for product number 416 in the catalog. + 827.84 + Category 6 + + tag-1 + tag-3 + + + + Product 417 + This is the description for product number 417 in the catalog. + 829.83 + Category 7 + + tag-2 + tag-4 + + + + Product 418 + This is the description for product number 418 in the catalog. + 831.82 + Category 8 + + tag-3 + tag-5 + + + + Product 419 + This is the description for product number 419 in the catalog. + 833.81 + Category 9 + + tag-4 + tag-6 + + + + Product 420 + This is the description for product number 420 in the catalog. + 835.80 + Category 0 + + tag-0 + tag-0 + + + + Product 421 + This is the description for product number 421 in the catalog. + 837.79 + Category 1 + + tag-1 + tag-1 + + + + Product 422 + This is the description for product number 422 in the catalog. + 839.78 + Category 2 + + tag-2 + tag-2 + + + + Product 423 + This is the description for product number 423 in the catalog. + 841.77 + Category 3 + + tag-3 + tag-3 + + + + Product 424 + This is the description for product number 424 in the catalog. + 843.76 + Category 4 + + tag-4 + tag-4 + + + + Product 425 + This is the description for product number 425 in the catalog. + 845.75 + Category 5 + + tag-0 + tag-5 + + + + Product 426 + This is the description for product number 426 in the catalog. + 847.74 + Category 6 + + tag-1 + tag-6 + + + + Product 427 + This is the description for product number 427 in the catalog. + 849.73 + Category 7 + + tag-2 + tag-0 + + + + Product 428 + This is the description for product number 428 in the catalog. + 851.72 + Category 8 + + tag-3 + tag-1 + + + + Product 429 + This is the description for product number 429 in the catalog. + 853.71 + Category 9 + + tag-4 + tag-2 + + + + Product 430 + This is the description for product number 430 in the catalog. + 855.70 + Category 0 + + tag-0 + tag-3 + + + + Product 431 + This is the description for product number 431 in the catalog. + 857.69 + Category 1 + + tag-1 + tag-4 + + + + Product 432 + This is the description for product number 432 in the catalog. + 859.68 + Category 2 + + tag-2 + tag-5 + + + + Product 433 + This is the description for product number 433 in the catalog. + 861.67 + Category 3 + + tag-3 + tag-6 + + + + Product 434 + This is the description for product number 434 in the catalog. + 863.66 + Category 4 + + tag-4 + tag-0 + + + + Product 435 + This is the description for product number 435 in the catalog. + 865.65 + Category 5 + + tag-0 + tag-1 + + + + Product 436 + This is the description for product number 436 in the catalog. + 867.64 + Category 6 + + tag-1 + tag-2 + + + + Product 437 + This is the description for product number 437 in the catalog. + 869.63 + Category 7 + + tag-2 + tag-3 + + + + Product 438 + This is the description for product number 438 in the catalog. + 871.62 + Category 8 + + tag-3 + tag-4 + + + + Product 439 + This is the description for product number 439 in the catalog. + 873.61 + Category 9 + + tag-4 + tag-5 + + + + Product 440 + This is the description for product number 440 in the catalog. + 875.60 + Category 0 + + tag-0 + tag-6 + + + + Product 441 + This is the description for product number 441 in the catalog. + 877.59 + Category 1 + + tag-1 + tag-0 + + + + Product 442 + This is the description for product number 442 in the catalog. + 879.58 + Category 2 + + tag-2 + tag-1 + + + + Product 443 + This is the description for product number 443 in the catalog. + 881.57 + Category 3 + + tag-3 + tag-2 + + + + Product 444 + This is the description for product number 444 in the catalog. + 883.56 + Category 4 + + tag-4 + tag-3 + + + + Product 445 + This is the description for product number 445 in the catalog. + 885.55 + Category 5 + + tag-0 + tag-4 + + + + Product 446 + This is the description for product number 446 in the catalog. + 887.54 + Category 6 + + tag-1 + tag-5 + + + + Product 447 + This is the description for product number 447 in the catalog. + 889.53 + Category 7 + + tag-2 + tag-6 + + + + Product 448 + This is the description for product number 448 in the catalog. + 891.52 + Category 8 + + tag-3 + tag-0 + + + + Product 449 + This is the description for product number 449 in the catalog. + 893.51 + Category 9 + + tag-4 + tag-1 + + + + Product 450 + This is the description for product number 450 in the catalog. + 895.50 + Category 0 + + tag-0 + tag-2 + + + + Product 451 + This is the description for product number 451 in the catalog. + 897.49 + Category 1 + + tag-1 + tag-3 + + + + Product 452 + This is the description for product number 452 in the catalog. + 899.48 + Category 2 + + tag-2 + tag-4 + + + + Product 453 + This is the description for product number 453 in the catalog. + 901.47 + Category 3 + + tag-3 + tag-5 + + + + Product 454 + This is the description for product number 454 in the catalog. + 903.46 + Category 4 + + tag-4 + tag-6 + + + + Product 455 + This is the description for product number 455 in the catalog. + 905.45 + Category 5 + + tag-0 + tag-0 + + + + Product 456 + This is the description for product number 456 in the catalog. + 907.44 + Category 6 + + tag-1 + tag-1 + + + + Product 457 + This is the description for product number 457 in the catalog. + 909.43 + Category 7 + + tag-2 + tag-2 + + + + Product 458 + This is the description for product number 458 in the catalog. + 911.42 + Category 8 + + tag-3 + tag-3 + + + + Product 459 + This is the description for product number 459 in the catalog. + 913.41 + Category 9 + + tag-4 + tag-4 + + + + Product 460 + This is the description for product number 460 in the catalog. + 915.40 + Category 0 + + tag-0 + tag-5 + + + + Product 461 + This is the description for product number 461 in the catalog. + 917.39 + Category 1 + + tag-1 + tag-6 + + + + Product 462 + This is the description for product number 462 in the catalog. + 919.38 + Category 2 + + tag-2 + tag-0 + + + + Product 463 + This is the description for product number 463 in the catalog. + 921.37 + Category 3 + + tag-3 + tag-1 + + + + Product 464 + This is the description for product number 464 in the catalog. + 923.36 + Category 4 + + tag-4 + tag-2 + + + + Product 465 + This is the description for product number 465 in the catalog. + 925.35 + Category 5 + + tag-0 + tag-3 + + + + Product 466 + This is the description for product number 466 in the catalog. + 927.34 + Category 6 + + tag-1 + tag-4 + + + + Product 467 + This is the description for product number 467 in the catalog. + 929.33 + Category 7 + + tag-2 + tag-5 + + + + Product 468 + This is the description for product number 468 in the catalog. + 931.32 + Category 8 + + tag-3 + tag-6 + + + + Product 469 + This is the description for product number 469 in the catalog. + 933.31 + Category 9 + + tag-4 + tag-0 + + + + Product 470 + This is the description for product number 470 in the catalog. + 935.30 + Category 0 + + tag-0 + tag-1 + + + + Product 471 + This is the description for product number 471 in the catalog. + 937.29 + Category 1 + + tag-1 + tag-2 + + + + Product 472 + This is the description for product number 472 in the catalog. + 939.28 + Category 2 + + tag-2 + tag-3 + + + + Product 473 + This is the description for product number 473 in the catalog. + 941.27 + Category 3 + + tag-3 + tag-4 + + + + Product 474 + This is the description for product number 474 in the catalog. + 943.26 + Category 4 + + tag-4 + tag-5 + + + + Product 475 + This is the description for product number 475 in the catalog. + 945.25 + Category 5 + + tag-0 + tag-6 + + + + Product 476 + This is the description for product number 476 in the catalog. + 947.24 + Category 6 + + tag-1 + tag-0 + + + + Product 477 + This is the description for product number 477 in the catalog. + 949.23 + Category 7 + + tag-2 + tag-1 + + + + Product 478 + This is the description for product number 478 in the catalog. + 951.22 + Category 8 + + tag-3 + tag-2 + + + + Product 479 + This is the description for product number 479 in the catalog. + 953.21 + Category 9 + + tag-4 + tag-3 + + + + Product 480 + This is the description for product number 480 in the catalog. + 955.20 + Category 0 + + tag-0 + tag-4 + + + + Product 481 + This is the description for product number 481 in the catalog. + 957.19 + Category 1 + + tag-1 + tag-5 + + + + Product 482 + This is the description for product number 482 in the catalog. + 959.18 + Category 2 + + tag-2 + tag-6 + + + + Product 483 + This is the description for product number 483 in the catalog. + 961.17 + Category 3 + + tag-3 + tag-0 + + + + Product 484 + This is the description for product number 484 in the catalog. + 963.16 + Category 4 + + tag-4 + tag-1 + + + + Product 485 + This is the description for product number 485 in the catalog. + 965.15 + Category 5 + + tag-0 + tag-2 + + + + Product 486 + This is the description for product number 486 in the catalog. + 967.14 + Category 6 + + tag-1 + tag-3 + + + + Product 487 + This is the description for product number 487 in the catalog. + 969.13 + Category 7 + + tag-2 + tag-4 + + + + Product 488 + This is the description for product number 488 in the catalog. + 971.12 + Category 8 + + tag-3 + tag-5 + + + + Product 489 + This is the description for product number 489 in the catalog. + 973.11 + Category 9 + + tag-4 + tag-6 + + + + Product 490 + This is the description for product number 490 in the catalog. + 975.10 + Category 0 + + tag-0 + tag-0 + + + + Product 491 + This is the description for product number 491 in the catalog. + 977.09 + Category 1 + + tag-1 + tag-1 + + + + Product 492 + This is the description for product number 492 in the catalog. + 979.08 + Category 2 + + tag-2 + tag-2 + + + + Product 493 + This is the description for product number 493 in the catalog. + 981.07 + Category 3 + + tag-3 + tag-3 + + + + Product 494 + This is the description for product number 494 in the catalog. + 983.06 + Category 4 + + tag-4 + tag-4 + + + + Product 495 + This is the description for product number 495 in the catalog. + 985.05 + Category 5 + + tag-0 + tag-5 + + + + Product 496 + This is the description for product number 496 in the catalog. + 987.04 + Category 6 + + tag-1 + tag-6 + + + + Product 497 + This is the description for product number 497 in the catalog. + 989.03 + Category 7 + + tag-2 + tag-0 + + + + Product 498 + This is the description for product number 498 in the catalog. + 991.02 + Category 8 + + tag-3 + tag-1 + + + + Product 499 + This is the description for product number 499 in the catalog. + 993.01 + Category 9 + + tag-4 + tag-2 + + + + Product 500 + This is the description for product number 500 in the catalog. + 995.00 + Category 0 + + tag-0 + tag-3 + + + + Product 501 + This is the description for product number 501 in the catalog. + 996.99 + Category 1 + + tag-1 + tag-4 + + + + Product 502 + This is the description for product number 502 in the catalog. + 998.98 + Category 2 + + tag-2 + tag-5 + + + + Product 503 + This is the description for product number 503 in the catalog. + 1000.97 + Category 3 + + tag-3 + tag-6 + + + + Product 504 + This is the description for product number 504 in the catalog. + 1002.96 + Category 4 + + tag-4 + tag-0 + + + + Product 505 + This is the description for product number 505 in the catalog. + 1004.95 + Category 5 + + tag-0 + tag-1 + + + + Product 506 + This is the description for product number 506 in the catalog. + 1006.94 + Category 6 + + tag-1 + tag-2 + + + + Product 507 + This is the description for product number 507 in the catalog. + 1008.93 + Category 7 + + tag-2 + tag-3 + + + + Product 508 + This is the description for product number 508 in the catalog. + 1010.92 + Category 8 + + tag-3 + tag-4 + + + + Product 509 + This is the description for product number 509 in the catalog. + 1012.91 + Category 9 + + tag-4 + tag-5 + + + + Product 510 + This is the description for product number 510 in the catalog. + 1014.90 + Category 0 + + tag-0 + tag-6 + + + + Product 511 + This is the description for product number 511 in the catalog. + 1016.89 + Category 1 + + tag-1 + tag-0 + + + + Product 512 + This is the description for product number 512 in the catalog. + 1018.88 + Category 2 + + tag-2 + tag-1 + + + + Product 513 + This is the description for product number 513 in the catalog. + 1020.87 + Category 3 + + tag-3 + tag-2 + + + + Product 514 + This is the description for product number 514 in the catalog. + 1022.86 + Category 4 + + tag-4 + tag-3 + + + + Product 515 + This is the description for product number 515 in the catalog. + 1024.85 + Category 5 + + tag-0 + tag-4 + + + + Product 516 + This is the description for product number 516 in the catalog. + 1026.84 + Category 6 + + tag-1 + tag-5 + + + + Product 517 + This is the description for product number 517 in the catalog. + 1028.83 + Category 7 + + tag-2 + tag-6 + + + + Product 518 + This is the description for product number 518 in the catalog. + 1030.82 + Category 8 + + tag-3 + tag-0 + + + + Product 519 + This is the description for product number 519 in the catalog. + 1032.81 + Category 9 + + tag-4 + tag-1 + + + + Product 520 + This is the description for product number 520 in the catalog. + 1034.80 + Category 0 + + tag-0 + tag-2 + + + + Product 521 + This is the description for product number 521 in the catalog. + 1036.79 + Category 1 + + tag-1 + tag-3 + + + + Product 522 + This is the description for product number 522 in the catalog. + 1038.78 + Category 2 + + tag-2 + tag-4 + + + + Product 523 + This is the description for product number 523 in the catalog. + 1040.77 + Category 3 + + tag-3 + tag-5 + + + + Product 524 + This is the description for product number 524 in the catalog. + 1042.76 + Category 4 + + tag-4 + tag-6 + + + + Product 525 + This is the description for product number 525 in the catalog. + 1044.75 + Category 5 + + tag-0 + tag-0 + + + + Product 526 + This is the description for product number 526 in the catalog. + 1046.74 + Category 6 + + tag-1 + tag-1 + + + + Product 527 + This is the description for product number 527 in the catalog. + 1048.73 + Category 7 + + tag-2 + tag-2 + + + + Product 528 + This is the description for product number 528 in the catalog. + 1050.72 + Category 8 + + tag-3 + tag-3 + + + + Product 529 + This is the description for product number 529 in the catalog. + 1052.71 + Category 9 + + tag-4 + tag-4 + + + + Product 530 + This is the description for product number 530 in the catalog. + 1054.70 + Category 0 + + tag-0 + tag-5 + + + + Product 531 + This is the description for product number 531 in the catalog. + 1056.69 + Category 1 + + tag-1 + tag-6 + + + + Product 532 + This is the description for product number 532 in the catalog. + 1058.68 + Category 2 + + tag-2 + tag-0 + + + + Product 533 + This is the description for product number 533 in the catalog. + 1060.67 + Category 3 + + tag-3 + tag-1 + + + + Product 534 + This is the description for product number 534 in the catalog. + 1062.66 + Category 4 + + tag-4 + tag-2 + + + + Product 535 + This is the description for product number 535 in the catalog. + 1064.65 + Category 5 + + tag-0 + tag-3 + + + + Product 536 + This is the description for product number 536 in the catalog. + 1066.64 + Category 6 + + tag-1 + tag-4 + + + + Product 537 + This is the description for product number 537 in the catalog. + 1068.63 + Category 7 + + tag-2 + tag-5 + + + + Product 538 + This is the description for product number 538 in the catalog. + 1070.62 + Category 8 + + tag-3 + tag-6 + + + + Product 539 + This is the description for product number 539 in the catalog. + 1072.61 + Category 9 + + tag-4 + tag-0 + + + + Product 540 + This is the description for product number 540 in the catalog. + 1074.60 + Category 0 + + tag-0 + tag-1 + + + + Product 541 + This is the description for product number 541 in the catalog. + 1076.59 + Category 1 + + tag-1 + tag-2 + + + + Product 542 + This is the description for product number 542 in the catalog. + 1078.58 + Category 2 + + tag-2 + tag-3 + + + + Product 543 + This is the description for product number 543 in the catalog. + 1080.57 + Category 3 + + tag-3 + tag-4 + + + + Product 544 + This is the description for product number 544 in the catalog. + 1082.56 + Category 4 + + tag-4 + tag-5 + + + + Product 545 + This is the description for product number 545 in the catalog. + 1084.55 + Category 5 + + tag-0 + tag-6 + + + + Product 546 + This is the description for product number 546 in the catalog. + 1086.54 + Category 6 + + tag-1 + tag-0 + + + + Product 547 + This is the description for product number 547 in the catalog. + 1088.53 + Category 7 + + tag-2 + tag-1 + + + + Product 548 + This is the description for product number 548 in the catalog. + 1090.52 + Category 8 + + tag-3 + tag-2 + + + + Product 549 + This is the description for product number 549 in the catalog. + 1092.51 + Category 9 + + tag-4 + tag-3 + + + + Product 550 + This is the description for product number 550 in the catalog. + 1094.50 + Category 0 + + tag-0 + tag-4 + + + + Product 551 + This is the description for product number 551 in the catalog. + 1096.49 + Category 1 + + tag-1 + tag-5 + + + + Product 552 + This is the description for product number 552 in the catalog. + 1098.48 + Category 2 + + tag-2 + tag-6 + + + + Product 553 + This is the description for product number 553 in the catalog. + 1100.47 + Category 3 + + tag-3 + tag-0 + + + + Product 554 + This is the description for product number 554 in the catalog. + 1102.46 + Category 4 + + tag-4 + tag-1 + + + + Product 555 + This is the description for product number 555 in the catalog. + 1104.45 + Category 5 + + tag-0 + tag-2 + + + + Product 556 + This is the description for product number 556 in the catalog. + 1106.44 + Category 6 + + tag-1 + tag-3 + + + + Product 557 + This is the description for product number 557 in the catalog. + 1108.43 + Category 7 + + tag-2 + tag-4 + + + + Product 558 + This is the description for product number 558 in the catalog. + 1110.42 + Category 8 + + tag-3 + tag-5 + + + + Product 559 + This is the description for product number 559 in the catalog. + 1112.41 + Category 9 + + tag-4 + tag-6 + + + + Product 560 + This is the description for product number 560 in the catalog. + 1114.40 + Category 0 + + tag-0 + tag-0 + + + + Product 561 + This is the description for product number 561 in the catalog. + 1116.39 + Category 1 + + tag-1 + tag-1 + + + + Product 562 + This is the description for product number 562 in the catalog. + 1118.38 + Category 2 + + tag-2 + tag-2 + + + + Product 563 + This is the description for product number 563 in the catalog. + 1120.37 + Category 3 + + tag-3 + tag-3 + + + + Product 564 + This is the description for product number 564 in the catalog. + 1122.36 + Category 4 + + tag-4 + tag-4 + + + + Product 565 + This is the description for product number 565 in the catalog. + 1124.35 + Category 5 + + tag-0 + tag-5 + + + + Product 566 + This is the description for product number 566 in the catalog. + 1126.34 + Category 6 + + tag-1 + tag-6 + + + + Product 567 + This is the description for product number 567 in the catalog. + 1128.33 + Category 7 + + tag-2 + tag-0 + + + + Product 568 + This is the description for product number 568 in the catalog. + 1130.32 + Category 8 + + tag-3 + tag-1 + + + + Product 569 + This is the description for product number 569 in the catalog. + 1132.31 + Category 9 + + tag-4 + tag-2 + + + + Product 570 + This is the description for product number 570 in the catalog. + 1134.30 + Category 0 + + tag-0 + tag-3 + + + + Product 571 + This is the description for product number 571 in the catalog. + 1136.29 + Category 1 + + tag-1 + tag-4 + + + + Product 572 + This is the description for product number 572 in the catalog. + 1138.28 + Category 2 + + tag-2 + tag-5 + + + + Product 573 + This is the description for product number 573 in the catalog. + 1140.27 + Category 3 + + tag-3 + tag-6 + + + + Product 574 + This is the description for product number 574 in the catalog. + 1142.26 + Category 4 + + tag-4 + tag-0 + + + + Product 575 + This is the description for product number 575 in the catalog. + 1144.25 + Category 5 + + tag-0 + tag-1 + + + + Product 576 + This is the description for product number 576 in the catalog. + 1146.24 + Category 6 + + tag-1 + tag-2 + + + + Product 577 + This is the description for product number 577 in the catalog. + 1148.23 + Category 7 + + tag-2 + tag-3 + + + + Product 578 + This is the description for product number 578 in the catalog. + 1150.22 + Category 8 + + tag-3 + tag-4 + + + + Product 579 + This is the description for product number 579 in the catalog. + 1152.21 + Category 9 + + tag-4 + tag-5 + + + + Product 580 + This is the description for product number 580 in the catalog. + 1154.20 + Category 0 + + tag-0 + tag-6 + + + + Product 581 + This is the description for product number 581 in the catalog. + 1156.19 + Category 1 + + tag-1 + tag-0 + + + + Product 582 + This is the description for product number 582 in the catalog. + 1158.18 + Category 2 + + tag-2 + tag-1 + + + + Product 583 + This is the description for product number 583 in the catalog. + 1160.17 + Category 3 + + tag-3 + tag-2 + + + + Product 584 + This is the description for product number 584 in the catalog. + 1162.16 + Category 4 + + tag-4 + tag-3 + + + + Product 585 + This is the description for product number 585 in the catalog. + 1164.15 + Category 5 + + tag-0 + tag-4 + + + + Product 586 + This is the description for product number 586 in the catalog. + 1166.14 + Category 6 + + tag-1 + tag-5 + + + + Product 587 + This is the description for product number 587 in the catalog. + 1168.13 + Category 7 + + tag-2 + tag-6 + + + + Product 588 + This is the description for product number 588 in the catalog. + 1170.12 + Category 8 + + tag-3 + tag-0 + + + + Product 589 + This is the description for product number 589 in the catalog. + 1172.11 + Category 9 + + tag-4 + tag-1 + + + + Product 590 + This is the description for product number 590 in the catalog. + 1174.10 + Category 0 + + tag-0 + tag-2 + + + + Product 591 + This is the description for product number 591 in the catalog. + 1176.09 + Category 1 + + tag-1 + tag-3 + + + + Product 592 + This is the description for product number 592 in the catalog. + 1178.08 + Category 2 + + tag-2 + tag-4 + + + + Product 593 + This is the description for product number 593 in the catalog. + 1180.07 + Category 3 + + tag-3 + tag-5 + + + + Product 594 + This is the description for product number 594 in the catalog. + 1182.06 + Category 4 + + tag-4 + tag-6 + + + + Product 595 + This is the description for product number 595 in the catalog. + 1184.05 + Category 5 + + tag-0 + tag-0 + + + + Product 596 + This is the description for product number 596 in the catalog. + 1186.04 + Category 6 + + tag-1 + tag-1 + + + + Product 597 + This is the description for product number 597 in the catalog. + 1188.03 + Category 7 + + tag-2 + tag-2 + + + + Product 598 + This is the description for product number 598 in the catalog. + 1190.02 + Category 8 + + tag-3 + tag-3 + + + + Product 599 + This is the description for product number 599 in the catalog. + 1192.01 + Category 9 + + tag-4 + tag-4 + + + + Product 600 + This is the description for product number 600 in the catalog. + 1194.00 + Category 0 + + tag-0 + tag-5 + + + + Product 601 + This is the description for product number 601 in the catalog. + 1195.99 + Category 1 + + tag-1 + tag-6 + + + + Product 602 + This is the description for product number 602 in the catalog. + 1197.98 + Category 2 + + tag-2 + tag-0 + + + + Product 603 + This is the description for product number 603 in the catalog. + 1199.97 + Category 3 + + tag-3 + tag-1 + + + + Product 604 + This is the description for product number 604 in the catalog. + 1201.96 + Category 4 + + tag-4 + tag-2 + + + + Product 605 + This is the description for product number 605 in the catalog. + 1203.95 + Category 5 + + tag-0 + tag-3 + + + + Product 606 + This is the description for product number 606 in the catalog. + 1205.94 + Category 6 + + tag-1 + tag-4 + + + + Product 607 + This is the description for product number 607 in the catalog. + 1207.93 + Category 7 + + tag-2 + tag-5 + + + + Product 608 + This is the description for product number 608 in the catalog. + 1209.92 + Category 8 + + tag-3 + tag-6 + + + + Product 609 + This is the description for product number 609 in the catalog. + 1211.91 + Category 9 + + tag-4 + tag-0 + + + + Product 610 + This is the description for product number 610 in the catalog. + 1213.90 + Category 0 + + tag-0 + tag-1 + + + + Product 611 + This is the description for product number 611 in the catalog. + 1215.89 + Category 1 + + tag-1 + tag-2 + + + + Product 612 + This is the description for product number 612 in the catalog. + 1217.88 + Category 2 + + tag-2 + tag-3 + + + + Product 613 + This is the description for product number 613 in the catalog. + 1219.87 + Category 3 + + tag-3 + tag-4 + + + + Product 614 + This is the description for product number 614 in the catalog. + 1221.86 + Category 4 + + tag-4 + tag-5 + + + + Product 615 + This is the description for product number 615 in the catalog. + 1223.85 + Category 5 + + tag-0 + tag-6 + + + + Product 616 + This is the description for product number 616 in the catalog. + 1225.84 + Category 6 + + tag-1 + tag-0 + + + + Product 617 + This is the description for product number 617 in the catalog. + 1227.83 + Category 7 + + tag-2 + tag-1 + + + + Product 618 + This is the description for product number 618 in the catalog. + 1229.82 + Category 8 + + tag-3 + tag-2 + + + + Product 619 + This is the description for product number 619 in the catalog. + 1231.81 + Category 9 + + tag-4 + tag-3 + + + + Product 620 + This is the description for product number 620 in the catalog. + 1233.80 + Category 0 + + tag-0 + tag-4 + + + + Product 621 + This is the description for product number 621 in the catalog. + 1235.79 + Category 1 + + tag-1 + tag-5 + + + + Product 622 + This is the description for product number 622 in the catalog. + 1237.78 + Category 2 + + tag-2 + tag-6 + + + + Product 623 + This is the description for product number 623 in the catalog. + 1239.77 + Category 3 + + tag-3 + tag-0 + + + + Product 624 + This is the description for product number 624 in the catalog. + 1241.76 + Category 4 + + tag-4 + tag-1 + + + + Product 625 + This is the description for product number 625 in the catalog. + 1243.75 + Category 5 + + tag-0 + tag-2 + + + + Product 626 + This is the description for product number 626 in the catalog. + 1245.74 + Category 6 + + tag-1 + tag-3 + + + + Product 627 + This is the description for product number 627 in the catalog. + 1247.73 + Category 7 + + tag-2 + tag-4 + + + + Product 628 + This is the description for product number 628 in the catalog. + 1249.72 + Category 8 + + tag-3 + tag-5 + + + + Product 629 + This is the description for product number 629 in the catalog. + 1251.71 + Category 9 + + tag-4 + tag-6 + + + + Product 630 + This is the description for product number 630 in the catalog. + 1253.70 + Category 0 + + tag-0 + tag-0 + + + + Product 631 + This is the description for product number 631 in the catalog. + 1255.69 + Category 1 + + tag-1 + tag-1 + + + + Product 632 + This is the description for product number 632 in the catalog. + 1257.68 + Category 2 + + tag-2 + tag-2 + + + + Product 633 + This is the description for product number 633 in the catalog. + 1259.67 + Category 3 + + tag-3 + tag-3 + + + + Product 634 + This is the description for product number 634 in the catalog. + 1261.66 + Category 4 + + tag-4 + tag-4 + + + + Product 635 + This is the description for product number 635 in the catalog. + 1263.65 + Category 5 + + tag-0 + tag-5 + + + + Product 636 + This is the description for product number 636 in the catalog. + 1265.64 + Category 6 + + tag-1 + tag-6 + + + + Product 637 + This is the description for product number 637 in the catalog. + 1267.63 + Category 7 + + tag-2 + tag-0 + + + + Product 638 + This is the description for product number 638 in the catalog. + 1269.62 + Category 8 + + tag-3 + tag-1 + + + + Product 639 + This is the description for product number 639 in the catalog. + 1271.61 + Category 9 + + tag-4 + tag-2 + + + + Product 640 + This is the description for product number 640 in the catalog. + 1273.60 + Category 0 + + tag-0 + tag-3 + + + + Product 641 + This is the description for product number 641 in the catalog. + 1275.59 + Category 1 + + tag-1 + tag-4 + + + + Product 642 + This is the description for product number 642 in the catalog. + 1277.58 + Category 2 + + tag-2 + tag-5 + + + + Product 643 + This is the description for product number 643 in the catalog. + 1279.57 + Category 3 + + tag-3 + tag-6 + + + + Product 644 + This is the description for product number 644 in the catalog. + 1281.56 + Category 4 + + tag-4 + tag-0 + + + + Product 645 + This is the description for product number 645 in the catalog. + 1283.55 + Category 5 + + tag-0 + tag-1 + + + + Product 646 + This is the description for product number 646 in the catalog. + 1285.54 + Category 6 + + tag-1 + tag-2 + + + + Product 647 + This is the description for product number 647 in the catalog. + 1287.53 + Category 7 + + tag-2 + tag-3 + + + + Product 648 + This is the description for product number 648 in the catalog. + 1289.52 + Category 8 + + tag-3 + tag-4 + + + + Product 649 + This is the description for product number 649 in the catalog. + 1291.51 + Category 9 + + tag-4 + tag-5 + + + + Product 650 + This is the description for product number 650 in the catalog. + 1293.50 + Category 0 + + tag-0 + tag-6 + + + + Product 651 + This is the description for product number 651 in the catalog. + 1295.49 + Category 1 + + tag-1 + tag-0 + + + + Product 652 + This is the description for product number 652 in the catalog. + 1297.48 + Category 2 + + tag-2 + tag-1 + + + + Product 653 + This is the description for product number 653 in the catalog. + 1299.47 + Category 3 + + tag-3 + tag-2 + + + + Product 654 + This is the description for product number 654 in the catalog. + 1301.46 + Category 4 + + tag-4 + tag-3 + + + + Product 655 + This is the description for product number 655 in the catalog. + 1303.45 + Category 5 + + tag-0 + tag-4 + + + + Product 656 + This is the description for product number 656 in the catalog. + 1305.44 + Category 6 + + tag-1 + tag-5 + + + + Product 657 + This is the description for product number 657 in the catalog. + 1307.43 + Category 7 + + tag-2 + tag-6 + + + + Product 658 + This is the description for product number 658 in the catalog. + 1309.42 + Category 8 + + tag-3 + tag-0 + + + + Product 659 + This is the description for product number 659 in the catalog. + 1311.41 + Category 9 + + tag-4 + tag-1 + + + + Product 660 + This is the description for product number 660 in the catalog. + 1313.40 + Category 0 + + tag-0 + tag-2 + + + + Product 661 + This is the description for product number 661 in the catalog. + 1315.39 + Category 1 + + tag-1 + tag-3 + + + + Product 662 + This is the description for product number 662 in the catalog. + 1317.38 + Category 2 + + tag-2 + tag-4 + + + + Product 663 + This is the description for product number 663 in the catalog. + 1319.37 + Category 3 + + tag-3 + tag-5 + + + + Product 664 + This is the description for product number 664 in the catalog. + 1321.36 + Category 4 + + tag-4 + tag-6 + + + + Product 665 + This is the description for product number 665 in the catalog. + 1323.35 + Category 5 + + tag-0 + tag-0 + + + + Product 666 + This is the description for product number 666 in the catalog. + 1325.34 + Category 6 + + tag-1 + tag-1 + + + + Product 667 + This is the description for product number 667 in the catalog. + 1327.33 + Category 7 + + tag-2 + tag-2 + + + + Product 668 + This is the description for product number 668 in the catalog. + 1329.32 + Category 8 + + tag-3 + tag-3 + + + + Product 669 + This is the description for product number 669 in the catalog. + 1331.31 + Category 9 + + tag-4 + tag-4 + + + + Product 670 + This is the description for product number 670 in the catalog. + 1333.30 + Category 0 + + tag-0 + tag-5 + + + + Product 671 + This is the description for product number 671 in the catalog. + 1335.29 + Category 1 + + tag-1 + tag-6 + + + + Product 672 + This is the description for product number 672 in the catalog. + 1337.28 + Category 2 + + tag-2 + tag-0 + + + + Product 673 + This is the description for product number 673 in the catalog. + 1339.27 + Category 3 + + tag-3 + tag-1 + + + + Product 674 + This is the description for product number 674 in the catalog. + 1341.26 + Category 4 + + tag-4 + tag-2 + + + + Product 675 + This is the description for product number 675 in the catalog. + 1343.25 + Category 5 + + tag-0 + tag-3 + + + + Product 676 + This is the description for product number 676 in the catalog. + 1345.24 + Category 6 + + tag-1 + tag-4 + + + + Product 677 + This is the description for product number 677 in the catalog. + 1347.23 + Category 7 + + tag-2 + tag-5 + + + + Product 678 + This is the description for product number 678 in the catalog. + 1349.22 + Category 8 + + tag-3 + tag-6 + + + + Product 679 + This is the description for product number 679 in the catalog. + 1351.21 + Category 9 + + tag-4 + tag-0 + + + + Product 680 + This is the description for product number 680 in the catalog. + 1353.20 + Category 0 + + tag-0 + tag-1 + + + + Product 681 + This is the description for product number 681 in the catalog. + 1355.19 + Category 1 + + tag-1 + tag-2 + + + + Product 682 + This is the description for product number 682 in the catalog. + 1357.18 + Category 2 + + tag-2 + tag-3 + + + + Product 683 + This is the description for product number 683 in the catalog. + 1359.17 + Category 3 + + tag-3 + tag-4 + + + + Product 684 + This is the description for product number 684 in the catalog. + 1361.16 + Category 4 + + tag-4 + tag-5 + + + + Product 685 + This is the description for product number 685 in the catalog. + 1363.15 + Category 5 + + tag-0 + tag-6 + + + + Product 686 + This is the description for product number 686 in the catalog. + 1365.14 + Category 6 + + tag-1 + tag-0 + + + + Product 687 + This is the description for product number 687 in the catalog. + 1367.13 + Category 7 + + tag-2 + tag-1 + + + + Product 688 + This is the description for product number 688 in the catalog. + 1369.12 + Category 8 + + tag-3 + tag-2 + + + + Product 689 + This is the description for product number 689 in the catalog. + 1371.11 + Category 9 + + tag-4 + tag-3 + + + + Product 690 + This is the description for product number 690 in the catalog. + 1373.10 + Category 0 + + tag-0 + tag-4 + + + + Product 691 + This is the description for product number 691 in the catalog. + 1375.09 + Category 1 + + tag-1 + tag-5 + + + + Product 692 + This is the description for product number 692 in the catalog. + 1377.08 + Category 2 + + tag-2 + tag-6 + + + + Product 693 + This is the description for product number 693 in the catalog. + 1379.07 + Category 3 + + tag-3 + tag-0 + + + + Product 694 + This is the description for product number 694 in the catalog. + 1381.06 + Category 4 + + tag-4 + tag-1 + + + + Product 695 + This is the description for product number 695 in the catalog. + 1383.05 + Category 5 + + tag-0 + tag-2 + + + + Product 696 + This is the description for product number 696 in the catalog. + 1385.04 + Category 6 + + tag-1 + tag-3 + + + + Product 697 + This is the description for product number 697 in the catalog. + 1387.03 + Category 7 + + tag-2 + tag-4 + + + + Product 698 + This is the description for product number 698 in the catalog. + 1389.02 + Category 8 + + tag-3 + tag-5 + + + + Product 699 + This is the description for product number 699 in the catalog. + 1391.01 + Category 9 + + tag-4 + tag-6 + + + + Product 700 + This is the description for product number 700 in the catalog. + 1393.00 + Category 0 + + tag-0 + tag-0 + + + + Product 701 + This is the description for product number 701 in the catalog. + 1394.99 + Category 1 + + tag-1 + tag-1 + + + + Product 702 + This is the description for product number 702 in the catalog. + 1396.98 + Category 2 + + tag-2 + tag-2 + + + + Product 703 + This is the description for product number 703 in the catalog. + 1398.97 + Category 3 + + tag-3 + tag-3 + + + + Product 704 + This is the description for product number 704 in the catalog. + 1400.96 + Category 4 + + tag-4 + tag-4 + + + + Product 705 + This is the description for product number 705 in the catalog. + 1402.95 + Category 5 + + tag-0 + tag-5 + + + + Product 706 + This is the description for product number 706 in the catalog. + 1404.94 + Category 6 + + tag-1 + tag-6 + + + + Product 707 + This is the description for product number 707 in the catalog. + 1406.93 + Category 7 + + tag-2 + tag-0 + + + + Product 708 + This is the description for product number 708 in the catalog. + 1408.92 + Category 8 + + tag-3 + tag-1 + + + + Product 709 + This is the description for product number 709 in the catalog. + 1410.91 + Category 9 + + tag-4 + tag-2 + + + + Product 710 + This is the description for product number 710 in the catalog. + 1412.90 + Category 0 + + tag-0 + tag-3 + + + + Product 711 + This is the description for product number 711 in the catalog. + 1414.89 + Category 1 + + tag-1 + tag-4 + + + + Product 712 + This is the description for product number 712 in the catalog. + 1416.88 + Category 2 + + tag-2 + tag-5 + + + + Product 713 + This is the description for product number 713 in the catalog. + 1418.87 + Category 3 + + tag-3 + tag-6 + + + + Product 714 + This is the description for product number 714 in the catalog. + 1420.86 + Category 4 + + tag-4 + tag-0 + + + + Product 715 + This is the description for product number 715 in the catalog. + 1422.85 + Category 5 + + tag-0 + tag-1 + + + + Product 716 + This is the description for product number 716 in the catalog. + 1424.84 + Category 6 + + tag-1 + tag-2 + + + + Product 717 + This is the description for product number 717 in the catalog. + 1426.83 + Category 7 + + tag-2 + tag-3 + + + + Product 718 + This is the description for product number 718 in the catalog. + 1428.82 + Category 8 + + tag-3 + tag-4 + + + + Product 719 + This is the description for product number 719 in the catalog. + 1430.81 + Category 9 + + tag-4 + tag-5 + + + + Product 720 + This is the description for product number 720 in the catalog. + 1432.80 + Category 0 + + tag-0 + tag-6 + + + + Product 721 + This is the description for product number 721 in the catalog. + 1434.79 + Category 1 + + tag-1 + tag-0 + + + + Product 722 + This is the description for product number 722 in the catalog. + 1436.78 + Category 2 + + tag-2 + tag-1 + + + + Product 723 + This is the description for product number 723 in the catalog. + 1438.77 + Category 3 + + tag-3 + tag-2 + + + + Product 724 + This is the description for product number 724 in the catalog. + 1440.76 + Category 4 + + tag-4 + tag-3 + + + + Product 725 + This is the description for product number 725 in the catalog. + 1442.75 + Category 5 + + tag-0 + tag-4 + + + + Product 726 + This is the description for product number 726 in the catalog. + 1444.74 + Category 6 + + tag-1 + tag-5 + + + + Product 727 + This is the description for product number 727 in the catalog. + 1446.73 + Category 7 + + tag-2 + tag-6 + + + + Product 728 + This is the description for product number 728 in the catalog. + 1448.72 + Category 8 + + tag-3 + tag-0 + + + + Product 729 + This is the description for product number 729 in the catalog. + 1450.71 + Category 9 + + tag-4 + tag-1 + + + + Product 730 + This is the description for product number 730 in the catalog. + 1452.70 + Category 0 + + tag-0 + tag-2 + + + + Product 731 + This is the description for product number 731 in the catalog. + 1454.69 + Category 1 + + tag-1 + tag-3 + + + + Product 732 + This is the description for product number 732 in the catalog. + 1456.68 + Category 2 + + tag-2 + tag-4 + + + + Product 733 + This is the description for product number 733 in the catalog. + 1458.67 + Category 3 + + tag-3 + tag-5 + + + + Product 734 + This is the description for product number 734 in the catalog. + 1460.66 + Category 4 + + tag-4 + tag-6 + + + + Product 735 + This is the description for product number 735 in the catalog. + 1462.65 + Category 5 + + tag-0 + tag-0 + + + + Product 736 + This is the description for product number 736 in the catalog. + 1464.64 + Category 6 + + tag-1 + tag-1 + + + + Product 737 + This is the description for product number 737 in the catalog. + 1466.63 + Category 7 + + tag-2 + tag-2 + + + + Product 738 + This is the description for product number 738 in the catalog. + 1468.62 + Category 8 + + tag-3 + tag-3 + + + + Product 739 + This is the description for product number 739 in the catalog. + 1470.61 + Category 9 + + tag-4 + tag-4 + + + + Product 740 + This is the description for product number 740 in the catalog. + 1472.60 + Category 0 + + tag-0 + tag-5 + + + + Product 741 + This is the description for product number 741 in the catalog. + 1474.59 + Category 1 + + tag-1 + tag-6 + + + + Product 742 + This is the description for product number 742 in the catalog. + 1476.58 + Category 2 + + tag-2 + tag-0 + + + + Product 743 + This is the description for product number 743 in the catalog. + 1478.57 + Category 3 + + tag-3 + tag-1 + + + + Product 744 + This is the description for product number 744 in the catalog. + 1480.56 + Category 4 + + tag-4 + tag-2 + + + + Product 745 + This is the description for product number 745 in the catalog. + 1482.55 + Category 5 + + tag-0 + tag-3 + + + + Product 746 + This is the description for product number 746 in the catalog. + 1484.54 + Category 6 + + tag-1 + tag-4 + + + + Product 747 + This is the description for product number 747 in the catalog. + 1486.53 + Category 7 + + tag-2 + tag-5 + + + + Product 748 + This is the description for product number 748 in the catalog. + 1488.52 + Category 8 + + tag-3 + tag-6 + + + + Product 749 + This is the description for product number 749 in the catalog. + 1490.51 + Category 9 + + tag-4 + tag-0 + + + + Product 750 + This is the description for product number 750 in the catalog. + 1492.50 + Category 0 + + tag-0 + tag-1 + + + + Product 751 + This is the description for product number 751 in the catalog. + 1494.49 + Category 1 + + tag-1 + tag-2 + + + + Product 752 + This is the description for product number 752 in the catalog. + 1496.48 + Category 2 + + tag-2 + tag-3 + + + + Product 753 + This is the description for product number 753 in the catalog. + 1498.47 + Category 3 + + tag-3 + tag-4 + + + + Product 754 + This is the description for product number 754 in the catalog. + 1500.46 + Category 4 + + tag-4 + tag-5 + + + + Product 755 + This is the description for product number 755 in the catalog. + 1502.45 + Category 5 + + tag-0 + tag-6 + + + + Product 756 + This is the description for product number 756 in the catalog. + 1504.44 + Category 6 + + tag-1 + tag-0 + + + + Product 757 + This is the description for product number 757 in the catalog. + 1506.43 + Category 7 + + tag-2 + tag-1 + + + + Product 758 + This is the description for product number 758 in the catalog. + 1508.42 + Category 8 + + tag-3 + tag-2 + + + + Product 759 + This is the description for product number 759 in the catalog. + 1510.41 + Category 9 + + tag-4 + tag-3 + + + + Product 760 + This is the description for product number 760 in the catalog. + 1512.40 + Category 0 + + tag-0 + tag-4 + + + + Product 761 + This is the description for product number 761 in the catalog. + 1514.39 + Category 1 + + tag-1 + tag-5 + + + + Product 762 + This is the description for product number 762 in the catalog. + 1516.38 + Category 2 + + tag-2 + tag-6 + + + + Product 763 + This is the description for product number 763 in the catalog. + 1518.37 + Category 3 + + tag-3 + tag-0 + + + + Product 764 + This is the description for product number 764 in the catalog. + 1520.36 + Category 4 + + tag-4 + tag-1 + + + + Product 765 + This is the description for product number 765 in the catalog. + 1522.35 + Category 5 + + tag-0 + tag-2 + + + + Product 766 + This is the description for product number 766 in the catalog. + 1524.34 + Category 6 + + tag-1 + tag-3 + + + + Product 767 + This is the description for product number 767 in the catalog. + 1526.33 + Category 7 + + tag-2 + tag-4 + + + + Product 768 + This is the description for product number 768 in the catalog. + 1528.32 + Category 8 + + tag-3 + tag-5 + + + + Product 769 + This is the description for product number 769 in the catalog. + 1530.31 + Category 9 + + tag-4 + tag-6 + + + + Product 770 + This is the description for product number 770 in the catalog. + 1532.30 + Category 0 + + tag-0 + tag-0 + + + + Product 771 + This is the description for product number 771 in the catalog. + 1534.29 + Category 1 + + tag-1 + tag-1 + + + + Product 772 + This is the description for product number 772 in the catalog. + 1536.28 + Category 2 + + tag-2 + tag-2 + + + + Product 773 + This is the description for product number 773 in the catalog. + 1538.27 + Category 3 + + tag-3 + tag-3 + + + + Product 774 + This is the description for product number 774 in the catalog. + 1540.26 + Category 4 + + tag-4 + tag-4 + + + + Product 775 + This is the description for product number 775 in the catalog. + 1542.25 + Category 5 + + tag-0 + tag-5 + + + + Product 776 + This is the description for product number 776 in the catalog. + 1544.24 + Category 6 + + tag-1 + tag-6 + + + + Product 777 + This is the description for product number 777 in the catalog. + 1546.23 + Category 7 + + tag-2 + tag-0 + + + + Product 778 + This is the description for product number 778 in the catalog. + 1548.22 + Category 8 + + tag-3 + tag-1 + + + + Product 779 + This is the description for product number 779 in the catalog. + 1550.21 + Category 9 + + tag-4 + tag-2 + + + + Product 780 + This is the description for product number 780 in the catalog. + 1552.20 + Category 0 + + tag-0 + tag-3 + + + + Product 781 + This is the description for product number 781 in the catalog. + 1554.19 + Category 1 + + tag-1 + tag-4 + + + + Product 782 + This is the description for product number 782 in the catalog. + 1556.18 + Category 2 + + tag-2 + tag-5 + + + + Product 783 + This is the description for product number 783 in the catalog. + 1558.17 + Category 3 + + tag-3 + tag-6 + + + + Product 784 + This is the description for product number 784 in the catalog. + 1560.16 + Category 4 + + tag-4 + tag-0 + + + + Product 785 + This is the description for product number 785 in the catalog. + 1562.15 + Category 5 + + tag-0 + tag-1 + + + + Product 786 + This is the description for product number 786 in the catalog. + 1564.14 + Category 6 + + tag-1 + tag-2 + + + + Product 787 + This is the description for product number 787 in the catalog. + 1566.13 + Category 7 + + tag-2 + tag-3 + + + + Product 788 + This is the description for product number 788 in the catalog. + 1568.12 + Category 8 + + tag-3 + tag-4 + + + + Product 789 + This is the description for product number 789 in the catalog. + 1570.11 + Category 9 + + tag-4 + tag-5 + + + + Product 790 + This is the description for product number 790 in the catalog. + 1572.10 + Category 0 + + tag-0 + tag-6 + + + + Product 791 + This is the description for product number 791 in the catalog. + 1574.09 + Category 1 + + tag-1 + tag-0 + + + + Product 792 + This is the description for product number 792 in the catalog. + 1576.08 + Category 2 + + tag-2 + tag-1 + + + + Product 793 + This is the description for product number 793 in the catalog. + 1578.07 + Category 3 + + tag-3 + tag-2 + + + + Product 794 + This is the description for product number 794 in the catalog. + 1580.06 + Category 4 + + tag-4 + tag-3 + + + + Product 795 + This is the description for product number 795 in the catalog. + 1582.05 + Category 5 + + tag-0 + tag-4 + + + + Product 796 + This is the description for product number 796 in the catalog. + 1584.04 + Category 6 + + tag-1 + tag-5 + + + + Product 797 + This is the description for product number 797 in the catalog. + 1586.03 + Category 7 + + tag-2 + tag-6 + + + + Product 798 + This is the description for product number 798 in the catalog. + 1588.02 + Category 8 + + tag-3 + tag-0 + + + + Product 799 + This is the description for product number 799 in the catalog. + 1590.01 + Category 9 + + tag-4 + tag-1 + + + + Product 800 + This is the description for product number 800 in the catalog. + 1592.00 + Category 0 + + tag-0 + tag-2 + + + + Product 801 + This is the description for product number 801 in the catalog. + 1593.99 + Category 1 + + tag-1 + tag-3 + + + + Product 802 + This is the description for product number 802 in the catalog. + 1595.98 + Category 2 + + tag-2 + tag-4 + + + + Product 803 + This is the description for product number 803 in the catalog. + 1597.97 + Category 3 + + tag-3 + tag-5 + + + + Product 804 + This is the description for product number 804 in the catalog. + 1599.96 + Category 4 + + tag-4 + tag-6 + + + + Product 805 + This is the description for product number 805 in the catalog. + 1601.95 + Category 5 + + tag-0 + tag-0 + + + + Product 806 + This is the description for product number 806 in the catalog. + 1603.94 + Category 6 + + tag-1 + tag-1 + + + + Product 807 + This is the description for product number 807 in the catalog. + 1605.93 + Category 7 + + tag-2 + tag-2 + + + + Product 808 + This is the description for product number 808 in the catalog. + 1607.92 + Category 8 + + tag-3 + tag-3 + + + + Product 809 + This is the description for product number 809 in the catalog. + 1609.91 + Category 9 + + tag-4 + tag-4 + + + + Product 810 + This is the description for product number 810 in the catalog. + 1611.90 + Category 0 + + tag-0 + tag-5 + + + + Product 811 + This is the description for product number 811 in the catalog. + 1613.89 + Category 1 + + tag-1 + tag-6 + + + + Product 812 + This is the description for product number 812 in the catalog. + 1615.88 + Category 2 + + tag-2 + tag-0 + + + + Product 813 + This is the description for product number 813 in the catalog. + 1617.87 + Category 3 + + tag-3 + tag-1 + + + + Product 814 + This is the description for product number 814 in the catalog. + 1619.86 + Category 4 + + tag-4 + tag-2 + + + + Product 815 + This is the description for product number 815 in the catalog. + 1621.85 + Category 5 + + tag-0 + tag-3 + + + + Product 816 + This is the description for product number 816 in the catalog. + 1623.84 + Category 6 + + tag-1 + tag-4 + + + + Product 817 + This is the description for product number 817 in the catalog. + 1625.83 + Category 7 + + tag-2 + tag-5 + + + + Product 818 + This is the description for product number 818 in the catalog. + 1627.82 + Category 8 + + tag-3 + tag-6 + + + + Product 819 + This is the description for product number 819 in the catalog. + 1629.81 + Category 9 + + tag-4 + tag-0 + + + + Product 820 + This is the description for product number 820 in the catalog. + 1631.80 + Category 0 + + tag-0 + tag-1 + + + + Product 821 + This is the description for product number 821 in the catalog. + 1633.79 + Category 1 + + tag-1 + tag-2 + + + + Product 822 + This is the description for product number 822 in the catalog. + 1635.78 + Category 2 + + tag-2 + tag-3 + + + + Product 823 + This is the description for product number 823 in the catalog. + 1637.77 + Category 3 + + tag-3 + tag-4 + + + + Product 824 + This is the description for product number 824 in the catalog. + 1639.76 + Category 4 + + tag-4 + tag-5 + + + + Product 825 + This is the description for product number 825 in the catalog. + 1641.75 + Category 5 + + tag-0 + tag-6 + + + + Product 826 + This is the description for product number 826 in the catalog. + 1643.74 + Category 6 + + tag-1 + tag-0 + + + + Product 827 + This is the description for product number 827 in the catalog. + 1645.73 + Category 7 + + tag-2 + tag-1 + + + + Product 828 + This is the description for product number 828 in the catalog. + 1647.72 + Category 8 + + tag-3 + tag-2 + + + + Product 829 + This is the description for product number 829 in the catalog. + 1649.71 + Category 9 + + tag-4 + tag-3 + + + + Product 830 + This is the description for product number 830 in the catalog. + 1651.70 + Category 0 + + tag-0 + tag-4 + + + + Product 831 + This is the description for product number 831 in the catalog. + 1653.69 + Category 1 + + tag-1 + tag-5 + + + + Product 832 + This is the description for product number 832 in the catalog. + 1655.68 + Category 2 + + tag-2 + tag-6 + + + + Product 833 + This is the description for product number 833 in the catalog. + 1657.67 + Category 3 + + tag-3 + tag-0 + + + + Product 834 + This is the description for product number 834 in the catalog. + 1659.66 + Category 4 + + tag-4 + tag-1 + + + + Product 835 + This is the description for product number 835 in the catalog. + 1661.65 + Category 5 + + tag-0 + tag-2 + + + + Product 836 + This is the description for product number 836 in the catalog. + 1663.64 + Category 6 + + tag-1 + tag-3 + + + + Product 837 + This is the description for product number 837 in the catalog. + 1665.63 + Category 7 + + tag-2 + tag-4 + + + + Product 838 + This is the description for product number 838 in the catalog. + 1667.62 + Category 8 + + tag-3 + tag-5 + + + + Product 839 + This is the description for product number 839 in the catalog. + 1669.61 + Category 9 + + tag-4 + tag-6 + + + + Product 840 + This is the description for product number 840 in the catalog. + 1671.60 + Category 0 + + tag-0 + tag-0 + + + + Product 841 + This is the description for product number 841 in the catalog. + 1673.59 + Category 1 + + tag-1 + tag-1 + + + + Product 842 + This is the description for product number 842 in the catalog. + 1675.58 + Category 2 + + tag-2 + tag-2 + + + + Product 843 + This is the description for product number 843 in the catalog. + 1677.57 + Category 3 + + tag-3 + tag-3 + + + + Product 844 + This is the description for product number 844 in the catalog. + 1679.56 + Category 4 + + tag-4 + tag-4 + + + + Product 845 + This is the description for product number 845 in the catalog. + 1681.55 + Category 5 + + tag-0 + tag-5 + + + + Product 846 + This is the description for product number 846 in the catalog. + 1683.54 + Category 6 + + tag-1 + tag-6 + + + + Product 847 + This is the description for product number 847 in the catalog. + 1685.53 + Category 7 + + tag-2 + tag-0 + + + + Product 848 + This is the description for product number 848 in the catalog. + 1687.52 + Category 8 + + tag-3 + tag-1 + + + + Product 849 + This is the description for product number 849 in the catalog. + 1689.51 + Category 9 + + tag-4 + tag-2 + + + + Product 850 + This is the description for product number 850 in the catalog. + 1691.50 + Category 0 + + tag-0 + tag-3 + + + + Product 851 + This is the description for product number 851 in the catalog. + 1693.49 + Category 1 + + tag-1 + tag-4 + + + + Product 852 + This is the description for product number 852 in the catalog. + 1695.48 + Category 2 + + tag-2 + tag-5 + + + + Product 853 + This is the description for product number 853 in the catalog. + 1697.47 + Category 3 + + tag-3 + tag-6 + + + + Product 854 + This is the description for product number 854 in the catalog. + 1699.46 + Category 4 + + tag-4 + tag-0 + + + + Product 855 + This is the description for product number 855 in the catalog. + 1701.45 + Category 5 + + tag-0 + tag-1 + + + + Product 856 + This is the description for product number 856 in the catalog. + 1703.44 + Category 6 + + tag-1 + tag-2 + + + + Product 857 + This is the description for product number 857 in the catalog. + 1705.43 + Category 7 + + tag-2 + tag-3 + + + + Product 858 + This is the description for product number 858 in the catalog. + 1707.42 + Category 8 + + tag-3 + tag-4 + + + + Product 859 + This is the description for product number 859 in the catalog. + 1709.41 + Category 9 + + tag-4 + tag-5 + + + + Product 860 + This is the description for product number 860 in the catalog. + 1711.40 + Category 0 + + tag-0 + tag-6 + + + + Product 861 + This is the description for product number 861 in the catalog. + 1713.39 + Category 1 + + tag-1 + tag-0 + + + + Product 862 + This is the description for product number 862 in the catalog. + 1715.38 + Category 2 + + tag-2 + tag-1 + + + + Product 863 + This is the description for product number 863 in the catalog. + 1717.37 + Category 3 + + tag-3 + tag-2 + + + + Product 864 + This is the description for product number 864 in the catalog. + 1719.36 + Category 4 + + tag-4 + tag-3 + + + + Product 865 + This is the description for product number 865 in the catalog. + 1721.35 + Category 5 + + tag-0 + tag-4 + + + + Product 866 + This is the description for product number 866 in the catalog. + 1723.34 + Category 6 + + tag-1 + tag-5 + + + + Product 867 + This is the description for product number 867 in the catalog. + 1725.33 + Category 7 + + tag-2 + tag-6 + + + + Product 868 + This is the description for product number 868 in the catalog. + 1727.32 + Category 8 + + tag-3 + tag-0 + + + + Product 869 + This is the description for product number 869 in the catalog. + 1729.31 + Category 9 + + tag-4 + tag-1 + + + + Product 870 + This is the description for product number 870 in the catalog. + 1731.30 + Category 0 + + tag-0 + tag-2 + + + + Product 871 + This is the description for product number 871 in the catalog. + 1733.29 + Category 1 + + tag-1 + tag-3 + + + + Product 872 + This is the description for product number 872 in the catalog. + 1735.28 + Category 2 + + tag-2 + tag-4 + + + + Product 873 + This is the description for product number 873 in the catalog. + 1737.27 + Category 3 + + tag-3 + tag-5 + + + + Product 874 + This is the description for product number 874 in the catalog. + 1739.26 + Category 4 + + tag-4 + tag-6 + + + + Product 875 + This is the description for product number 875 in the catalog. + 1741.25 + Category 5 + + tag-0 + tag-0 + + + + Product 876 + This is the description for product number 876 in the catalog. + 1743.24 + Category 6 + + tag-1 + tag-1 + + + + Product 877 + This is the description for product number 877 in the catalog. + 1745.23 + Category 7 + + tag-2 + tag-2 + + + + Product 878 + This is the description for product number 878 in the catalog. + 1747.22 + Category 8 + + tag-3 + tag-3 + + + + Product 879 + This is the description for product number 879 in the catalog. + 1749.21 + Category 9 + + tag-4 + tag-4 + + + + Product 880 + This is the description for product number 880 in the catalog. + 1751.20 + Category 0 + + tag-0 + tag-5 + + + + Product 881 + This is the description for product number 881 in the catalog. + 1753.19 + Category 1 + + tag-1 + tag-6 + + + + Product 882 + This is the description for product number 882 in the catalog. + 1755.18 + Category 2 + + tag-2 + tag-0 + + + + Product 883 + This is the description for product number 883 in the catalog. + 1757.17 + Category 3 + + tag-3 + tag-1 + + + + Product 884 + This is the description for product number 884 in the catalog. + 1759.16 + Category 4 + + tag-4 + tag-2 + + + + Product 885 + This is the description for product number 885 in the catalog. + 1761.15 + Category 5 + + tag-0 + tag-3 + + + + Product 886 + This is the description for product number 886 in the catalog. + 1763.14 + Category 6 + + tag-1 + tag-4 + + + + Product 887 + This is the description for product number 887 in the catalog. + 1765.13 + Category 7 + + tag-2 + tag-5 + + + + Product 888 + This is the description for product number 888 in the catalog. + 1767.12 + Category 8 + + tag-3 + tag-6 + + + + Product 889 + This is the description for product number 889 in the catalog. + 1769.11 + Category 9 + + tag-4 + tag-0 + + + + Product 890 + This is the description for product number 890 in the catalog. + 1771.10 + Category 0 + + tag-0 + tag-1 + + + + Product 891 + This is the description for product number 891 in the catalog. + 1773.09 + Category 1 + + tag-1 + tag-2 + + + + Product 892 + This is the description for product number 892 in the catalog. + 1775.08 + Category 2 + + tag-2 + tag-3 + + + + Product 893 + This is the description for product number 893 in the catalog. + 1777.07 + Category 3 + + tag-3 + tag-4 + + + + Product 894 + This is the description for product number 894 in the catalog. + 1779.06 + Category 4 + + tag-4 + tag-5 + + + + Product 895 + This is the description for product number 895 in the catalog. + 1781.05 + Category 5 + + tag-0 + tag-6 + + + + Product 896 + This is the description for product number 896 in the catalog. + 1783.04 + Category 6 + + tag-1 + tag-0 + + + + Product 897 + This is the description for product number 897 in the catalog. + 1785.03 + Category 7 + + tag-2 + tag-1 + + + + Product 898 + This is the description for product number 898 in the catalog. + 1787.02 + Category 8 + + tag-3 + tag-2 + + + + Product 899 + This is the description for product number 899 in the catalog. + 1789.01 + Category 9 + + tag-4 + tag-3 + + + + Product 900 + This is the description for product number 900 in the catalog. + 1791.00 + Category 0 + + tag-0 + tag-4 + + + + Product 901 + This is the description for product number 901 in the catalog. + 1792.99 + Category 1 + + tag-1 + tag-5 + + + + Product 902 + This is the description for product number 902 in the catalog. + 1794.98 + Category 2 + + tag-2 + tag-6 + + + + Product 903 + This is the description for product number 903 in the catalog. + 1796.97 + Category 3 + + tag-3 + tag-0 + + + + Product 904 + This is the description for product number 904 in the catalog. + 1798.96 + Category 4 + + tag-4 + tag-1 + + + + Product 905 + This is the description for product number 905 in the catalog. + 1800.95 + Category 5 + + tag-0 + tag-2 + + + + Product 906 + This is the description for product number 906 in the catalog. + 1802.94 + Category 6 + + tag-1 + tag-3 + + + + Product 907 + This is the description for product number 907 in the catalog. + 1804.93 + Category 7 + + tag-2 + tag-4 + + + + Product 908 + This is the description for product number 908 in the catalog. + 1806.92 + Category 8 + + tag-3 + tag-5 + + + + Product 909 + This is the description for product number 909 in the catalog. + 1808.91 + Category 9 + + tag-4 + tag-6 + + + + Product 910 + This is the description for product number 910 in the catalog. + 1810.90 + Category 0 + + tag-0 + tag-0 + + + + Product 911 + This is the description for product number 911 in the catalog. + 1812.89 + Category 1 + + tag-1 + tag-1 + + + + Product 912 + This is the description for product number 912 in the catalog. + 1814.88 + Category 2 + + tag-2 + tag-2 + + + + Product 913 + This is the description for product number 913 in the catalog. + 1816.87 + Category 3 + + tag-3 + tag-3 + + + + Product 914 + This is the description for product number 914 in the catalog. + 1818.86 + Category 4 + + tag-4 + tag-4 + + + + Product 915 + This is the description for product number 915 in the catalog. + 1820.85 + Category 5 + + tag-0 + tag-5 + + + + Product 916 + This is the description for product number 916 in the catalog. + 1822.84 + Category 6 + + tag-1 + tag-6 + + + + Product 917 + This is the description for product number 917 in the catalog. + 1824.83 + Category 7 + + tag-2 + tag-0 + + + + Product 918 + This is the description for product number 918 in the catalog. + 1826.82 + Category 8 + + tag-3 + tag-1 + + + + Product 919 + This is the description for product number 919 in the catalog. + 1828.81 + Category 9 + + tag-4 + tag-2 + + + + Product 920 + This is the description for product number 920 in the catalog. + 1830.80 + Category 0 + + tag-0 + tag-3 + + + + Product 921 + This is the description for product number 921 in the catalog. + 1832.79 + Category 1 + + tag-1 + tag-4 + + + + Product 922 + This is the description for product number 922 in the catalog. + 1834.78 + Category 2 + + tag-2 + tag-5 + + + + Product 923 + This is the description for product number 923 in the catalog. + 1836.77 + Category 3 + + tag-3 + tag-6 + + + + Product 924 + This is the description for product number 924 in the catalog. + 1838.76 + Category 4 + + tag-4 + tag-0 + + + + Product 925 + This is the description for product number 925 in the catalog. + 1840.75 + Category 5 + + tag-0 + tag-1 + + + + Product 926 + This is the description for product number 926 in the catalog. + 1842.74 + Category 6 + + tag-1 + tag-2 + + + + Product 927 + This is the description for product number 927 in the catalog. + 1844.73 + Category 7 + + tag-2 + tag-3 + + + + Product 928 + This is the description for product number 928 in the catalog. + 1846.72 + Category 8 + + tag-3 + tag-4 + + + + Product 929 + This is the description for product number 929 in the catalog. + 1848.71 + Category 9 + + tag-4 + tag-5 + + + + Product 930 + This is the description for product number 930 in the catalog. + 1850.70 + Category 0 + + tag-0 + tag-6 + + + + Product 931 + This is the description for product number 931 in the catalog. + 1852.69 + Category 1 + + tag-1 + tag-0 + + + + Product 932 + This is the description for product number 932 in the catalog. + 1854.68 + Category 2 + + tag-2 + tag-1 + + + + Product 933 + This is the description for product number 933 in the catalog. + 1856.67 + Category 3 + + tag-3 + tag-2 + + + + Product 934 + This is the description for product number 934 in the catalog. + 1858.66 + Category 4 + + tag-4 + tag-3 + + + + Product 935 + This is the description for product number 935 in the catalog. + 1860.65 + Category 5 + + tag-0 + tag-4 + + + + Product 936 + This is the description for product number 936 in the catalog. + 1862.64 + Category 6 + + tag-1 + tag-5 + + + + Product 937 + This is the description for product number 937 in the catalog. + 1864.63 + Category 7 + + tag-2 + tag-6 + + + + Product 938 + This is the description for product number 938 in the catalog. + 1866.62 + Category 8 + + tag-3 + tag-0 + + + + Product 939 + This is the description for product number 939 in the catalog. + 1868.61 + Category 9 + + tag-4 + tag-1 + + + + Product 940 + This is the description for product number 940 in the catalog. + 1870.60 + Category 0 + + tag-0 + tag-2 + + + + Product 941 + This is the description for product number 941 in the catalog. + 1872.59 + Category 1 + + tag-1 + tag-3 + + + + Product 942 + This is the description for product number 942 in the catalog. + 1874.58 + Category 2 + + tag-2 + tag-4 + + + + Product 943 + This is the description for product number 943 in the catalog. + 1876.57 + Category 3 + + tag-3 + tag-5 + + + + Product 944 + This is the description for product number 944 in the catalog. + 1878.56 + Category 4 + + tag-4 + tag-6 + + + + Product 945 + This is the description for product number 945 in the catalog. + 1880.55 + Category 5 + + tag-0 + tag-0 + + + + Product 946 + This is the description for product number 946 in the catalog. + 1882.54 + Category 6 + + tag-1 + tag-1 + + + + Product 947 + This is the description for product number 947 in the catalog. + 1884.53 + Category 7 + + tag-2 + tag-2 + + + + Product 948 + This is the description for product number 948 in the catalog. + 1886.52 + Category 8 + + tag-3 + tag-3 + + + + Product 949 + This is the description for product number 949 in the catalog. + 1888.51 + Category 9 + + tag-4 + tag-4 + + + + Product 950 + This is the description for product number 950 in the catalog. + 1890.50 + Category 0 + + tag-0 + tag-5 + + + + Product 951 + This is the description for product number 951 in the catalog. + 1892.49 + Category 1 + + tag-1 + tag-6 + + + + Product 952 + This is the description for product number 952 in the catalog. + 1894.48 + Category 2 + + tag-2 + tag-0 + + + + Product 953 + This is the description for product number 953 in the catalog. + 1896.47 + Category 3 + + tag-3 + tag-1 + + + + Product 954 + This is the description for product number 954 in the catalog. + 1898.46 + Category 4 + + tag-4 + tag-2 + + + + Product 955 + This is the description for product number 955 in the catalog. + 1900.45 + Category 5 + + tag-0 + tag-3 + + + + Product 956 + This is the description for product number 956 in the catalog. + 1902.44 + Category 6 + + tag-1 + tag-4 + + + + Product 957 + This is the description for product number 957 in the catalog. + 1904.43 + Category 7 + + tag-2 + tag-5 + + + + Product 958 + This is the description for product number 958 in the catalog. + 1906.42 + Category 8 + + tag-3 + tag-6 + + + + Product 959 + This is the description for product number 959 in the catalog. + 1908.41 + Category 9 + + tag-4 + tag-0 + + + + Product 960 + This is the description for product number 960 in the catalog. + 1910.40 + Category 0 + + tag-0 + tag-1 + + + + Product 961 + This is the description for product number 961 in the catalog. + 1912.39 + Category 1 + + tag-1 + tag-2 + + + + Product 962 + This is the description for product number 962 in the catalog. + 1914.38 + Category 2 + + tag-2 + tag-3 + + + + Product 963 + This is the description for product number 963 in the catalog. + 1916.37 + Category 3 + + tag-3 + tag-4 + + + + Product 964 + This is the description for product number 964 in the catalog. + 1918.36 + Category 4 + + tag-4 + tag-5 + + + + Product 965 + This is the description for product number 965 in the catalog. + 1920.35 + Category 5 + + tag-0 + tag-6 + + + + Product 966 + This is the description for product number 966 in the catalog. + 1922.34 + Category 6 + + tag-1 + tag-0 + + + + Product 967 + This is the description for product number 967 in the catalog. + 1924.33 + Category 7 + + tag-2 + tag-1 + + + + Product 968 + This is the description for product number 968 in the catalog. + 1926.32 + Category 8 + + tag-3 + tag-2 + + + + Product 969 + This is the description for product number 969 in the catalog. + 1928.31 + Category 9 + + tag-4 + tag-3 + + + + Product 970 + This is the description for product number 970 in the catalog. + 1930.30 + Category 0 + + tag-0 + tag-4 + + + + Product 971 + This is the description for product number 971 in the catalog. + 1932.29 + Category 1 + + tag-1 + tag-5 + + + + Product 972 + This is the description for product number 972 in the catalog. + 1934.28 + Category 2 + + tag-2 + tag-6 + + + + Product 973 + This is the description for product number 973 in the catalog. + 1936.27 + Category 3 + + tag-3 + tag-0 + + + + Product 974 + This is the description for product number 974 in the catalog. + 1938.26 + Category 4 + + tag-4 + tag-1 + + + + Product 975 + This is the description for product number 975 in the catalog. + 1940.25 + Category 5 + + tag-0 + tag-2 + + + + Product 976 + This is the description for product number 976 in the catalog. + 1942.24 + Category 6 + + tag-1 + tag-3 + + + + Product 977 + This is the description for product number 977 in the catalog. + 1944.23 + Category 7 + + tag-2 + tag-4 + + + + Product 978 + This is the description for product number 978 in the catalog. + 1946.22 + Category 8 + + tag-3 + tag-5 + + + + Product 979 + This is the description for product number 979 in the catalog. + 1948.21 + Category 9 + + tag-4 + tag-6 + + + + Product 980 + This is the description for product number 980 in the catalog. + 1950.20 + Category 0 + + tag-0 + tag-0 + + + + Product 981 + This is the description for product number 981 in the catalog. + 1952.19 + Category 1 + + tag-1 + tag-1 + + + + Product 982 + This is the description for product number 982 in the catalog. + 1954.18 + Category 2 + + tag-2 + tag-2 + + + + Product 983 + This is the description for product number 983 in the catalog. + 1956.17 + Category 3 + + tag-3 + tag-3 + + + + Product 984 + This is the description for product number 984 in the catalog. + 1958.16 + Category 4 + + tag-4 + tag-4 + + + + Product 985 + This is the description for product number 985 in the catalog. + 1960.15 + Category 5 + + tag-0 + tag-5 + + + + Product 986 + This is the description for product number 986 in the catalog. + 1962.14 + Category 6 + + tag-1 + tag-6 + + + + Product 987 + This is the description for product number 987 in the catalog. + 1964.13 + Category 7 + + tag-2 + tag-0 + + + + Product 988 + This is the description for product number 988 in the catalog. + 1966.12 + Category 8 + + tag-3 + tag-1 + + + + Product 989 + This is the description for product number 989 in the catalog. + 1968.11 + Category 9 + + tag-4 + tag-2 + + + + Product 990 + This is the description for product number 990 in the catalog. + 1970.10 + Category 0 + + tag-0 + tag-3 + + + + Product 991 + This is the description for product number 991 in the catalog. + 1972.09 + Category 1 + + tag-1 + tag-4 + + + + Product 992 + This is the description for product number 992 in the catalog. + 1974.08 + Category 2 + + tag-2 + tag-5 + + + + Product 993 + This is the description for product number 993 in the catalog. + 1976.07 + Category 3 + + tag-3 + tag-6 + + + + Product 994 + This is the description for product number 994 in the catalog. + 1978.06 + Category 4 + + tag-4 + tag-0 + + + + Product 995 + This is the description for product number 995 in the catalog. + 1980.05 + Category 5 + + tag-0 + tag-1 + + + + Product 996 + This is the description for product number 996 in the catalog. + 1982.04 + Category 6 + + tag-1 + tag-2 + + + + Product 997 + This is the description for product number 997 in the catalog. + 1984.03 + Category 7 + + tag-2 + tag-3 + + + + Product 998 + This is the description for product number 998 in the catalog. + 1986.02 + Category 8 + + tag-3 + tag-4 + + + + Product 999 + This is the description for product number 999 in the catalog. + 1988.01 + Category 9 + + tag-4 + tag-5 + + + + Product 1000 + This is the description for product number 1000 in the catalog. + 1990.00 + Category 0 + + tag-0 + tag-6 + + + diff --git a/xml/testdata/malformed/double-dash-comment.xml b/xml/testdata/malformed/double-dash-comment.xml new file mode 100644 index 000000000000..9a3c7178c600 --- /dev/null +++ b/xml/testdata/malformed/double-dash-comment.xml @@ -0,0 +1,5 @@ + + + + Content + diff --git a/xml/testdata/malformed/invalid-char.xml b/xml/testdata/malformed/invalid-char.xml new file mode 100644 index 000000000000..6234adfb2c09 --- /dev/null +++ b/xml/testdata/malformed/invalid-char.xml @@ -0,0 +1,4 @@ + + + Invalid unquoted attribute + diff --git a/xml/testdata/malformed/mismatched.xml b/xml/testdata/malformed/mismatched.xml new file mode 100644 index 000000000000..c72530a001e7 --- /dev/null +++ b/xml/testdata/malformed/mismatched.xml @@ -0,0 +1,5 @@ + + + Content + + diff --git a/xml/testdata/malformed/unclosed.xml b/xml/testdata/malformed/unclosed.xml new file mode 100644 index 000000000000..599dd66a8f8e --- /dev/null +++ b/xml/testdata/malformed/unclosed.xml @@ -0,0 +1,7 @@ + + + First item + Second item + Third item + + diff --git a/xml/testdata/mixed-content.xml b/xml/testdata/mixed-content.xml new file mode 100644 index 000000000000..99658c1f753f --- /dev/null +++ b/xml/testdata/mixed-content.xml @@ -0,0 +1,25 @@ + + +
+ Understanding XML Mixed Content + Jane Doe + + This article explains mixed content in XML. Mixed content allows + elements to contain both text and child elements. + + Here's an example of mixed content within an XML element. + + It's commonly used in document-centric XML applications. + + Benefits of mixed content: + Flexible content structuring + Natural representation of text with inline markup + Suitable for narrative content + + However, it can be more challenging to process than element-only content. + + + + The quick brown fox jumps over the lazy dog. + +
diff --git a/xml/testdata/namespaced.xml b/xml/testdata/namespaced.xml new file mode 100644 index 000000000000..be652714a7bd --- /dev/null +++ b/xml/testdata/namespaced.xml @@ -0,0 +1,19 @@ + + + Product Feed + + SKU-001 + Premium Widget + 49.99 USD + in stock + + + SKU-002 + Deluxe Gadget + 79.99 USD + preorder + + diff --git a/xml/testdata/processing-instructions.xml b/xml/testdata/processing-instructions.xml new file mode 100644 index 000000000000..4b073e9d4fd0 --- /dev/null +++ b/xml/testdata/processing-instructions.xml @@ -0,0 +1,24 @@ + + + + + + + + + 2024-01-15 + 2024-06-20 + + + +
+ Introduction + This document demonstrates XML processing instructions. +
+ +
+ Main Content + Processing instructions can appear anywhere in the document. +
+
+
diff --git a/xml/testdata/rss.xml b/xml/testdata/rss.xml new file mode 100644 index 000000000000..1ff1704c6a26 --- /dev/null +++ b/xml/testdata/rss.xml @@ -0,0 +1,27 @@ + + + + Example Feed + https://example.com + An example RSS feed for testing + en-us + + First Article + https://example.com/article/1 + This is the first article. + Mon, 01 Jan 2024 12:00:00 GMT + + + Second Article + https://example.com/article/2 + This is the second article. + Tue, 02 Jan 2024 12:00:00 GMT + + + Third Article + https://example.com/article/3 + This is the third article. + Wed, 03 Jan 2024 12:00:00 GMT + + + diff --git a/xml/testdata/simple.xml b/xml/testdata/simple.xml new file mode 100644 index 000000000000..777bff45a23f --- /dev/null +++ b/xml/testdata/simple.xml @@ -0,0 +1,11 @@ + + + + Widget + 19.99 + + + Gadget + 29.99 + + diff --git a/xml/testdata/sitemap.xml b/xml/testdata/sitemap.xml new file mode 100644 index 000000000000..8ff60a33934e --- /dev/null +++ b/xml/testdata/sitemap.xml @@ -0,0 +1,41 @@ + + + + https://www.example.com/ + 2024-01-15 + daily + 1.0 + + + https://www.example.com/about + 2024-01-10 + monthly + 0.8 + + + https://www.example.com/products + 2024-01-14 + weekly + 0.9 + + https://www.example.com/images/product1.jpg + Product One + + + https://www.example.com/images/product2.jpg + Product Two + + + + https://www.example.com/video/demo + + https://www.example.com/thumbs/demo.jpg + Product Demo Video + A demonstration of our product features. + https://www.example.com/videos/demo.mp4 + 300 + + + diff --git a/xml/testdata/soap-envelope.xml b/xml/testdata/soap-envelope.xml new file mode 100644 index 000000000000..80c53af3b9f9 --- /dev/null +++ b/xml/testdata/soap-envelope.xml @@ -0,0 +1,25 @@ + + + + + + testuser + secret123 + abc123== + 2024-01-15T10:30:00Z + + + + + + ORD-2024-0001 + true + JSON + + + diff --git a/xml/testdata/svg.xml b/xml/testdata/svg.xml new file mode 100644 index 000000000000..2bde24cec64a --- /dev/null +++ b/xml/testdata/svg.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + SVG Test + + + + + diff --git a/xml/testdata/unicode.xml b/xml/testdata/unicode.xml new file mode 100644 index 000000000000..c71671437765 --- /dev/null +++ b/xml/testdata/unicode.xml @@ -0,0 +1,36 @@ + + + + + Größe und Überblick über die Wärme + Ça c'est très bien, n'est-ce pas? + ¿Cómo estás? ¡Muy bien! + Blåbærsyltetøy og rømme + Räksmörgås med dill + + + 日本語テスト:こんにちは世界 + 中文测试:你好世界 + 한국어 테스트: 안녕하세요 + ภาษาไทย: สวัสดีครับ + + + اللغة العربية: مرحبا بالعالم + עברית: שלום עולם + + + Русский язык: Привет мир + Українська мова: Привіт світ + + + Ελληνικά: Γεια σου κόσμε + + + 🎉 🚀 ❤️ 🌟 🎸 🍕 🦄 👋 + © ® ™ € £ ¥ § † ‡ • ◆ ★ + ∑ ∏ √ ∞ ≠ ≈ ≤ ≥ ∫ ∂ Δ π + + + + + diff --git a/xml/types.ts b/xml/types.ts new file mode 100644 index 000000000000..db05d241d279 --- /dev/null +++ b/xml/types.ts @@ -0,0 +1,483 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +// This module is browser compatible. + +/** + * Position information for error reporting. + * + * @example Usage + * ```ts + * import type { XmlPosition } from "@std/xml/types"; + * + * const pos: XmlPosition = { line: 10, column: 5, offset: 150 }; + * ``` + */ +export interface XmlPosition { + /** Line number (1-indexed). */ + readonly line: number; + /** Column number (1-indexed). */ + readonly column: number; + /** Byte offset in the input. */ + readonly offset: number; +} + +/** + * Error thrown when XML parsing fails. + * + * @example Usage + * ```ts + * import { XmlSyntaxError } from "@std/xml/types"; + * import { assertInstanceOf } from "@std/assert"; + * + * const error = new XmlSyntaxError("Unexpected character", { line: 10, column: 5, offset: 150 }); + * assertInstanceOf(error, SyntaxError); + * ``` + */ +export class XmlSyntaxError extends SyntaxError { + /** + * The line number where the error occurred (1-indexed). + * + * @example Usage + * ```ts + * import { XmlSyntaxError } from "@std/xml/types"; + * import { assertEquals } from "@std/assert"; + * + * const error = new XmlSyntaxError("Test", { line: 5, column: 10, offset: 50 }); + * assertEquals(error.line, 5); + * ``` + */ + readonly line: number; + /** + * The column number where the error occurred (1-indexed). + * + * @example Usage + * ```ts + * import { XmlSyntaxError } from "@std/xml/types"; + * import { assertEquals } from "@std/assert"; + * + * const error = new XmlSyntaxError("Test", { line: 5, column: 10, offset: 50 }); + * assertEquals(error.column, 10); + * ``` + */ + readonly column: number; + /** + * The byte offset where the error occurred. + * + * @example Usage + * ```ts + * import { XmlSyntaxError } from "@std/xml/types"; + * import { assertEquals } from "@std/assert"; + * + * const error = new XmlSyntaxError("Test", { line: 5, column: 10, offset: 50 }); + * assertEquals(error.offset, 50); + * ``` + */ + readonly offset: number; + + /** + * Constructs a new XmlSyntaxError. + * + * @param message The error message describing what went wrong. + * @param position The position in the input where the error occurred. + */ + constructor(message: string, position: XmlPosition) { + super(`${message} at line ${position.line}, column ${position.column}`); + this.name = "XmlSyntaxError"; + this.line = position.line; + this.column = position.column; + this.offset = position.offset; + } +} + +/** + * A qualified XML name with optional namespace prefix. + * + * @example Usage + * ```ts + * import type { XmlName } from "@std/xml/types"; + * + * const name: XmlName = { local: "item", prefix: "ns" }; + * ``` + */ +export interface XmlName { + /** The local part of the name (after the colon, or the whole name). */ + readonly local: string; + /** The namespace prefix (before the colon), if present. */ + readonly prefix?: string; +} + +/** + * An XML attribute with its qualified name and value. + * + * @example Usage + * ```ts + * import type { XmlAttribute } from "@std/xml/types"; + * + * const attr: XmlAttribute = { + * name: { local: "id" }, + * value: "123", + * }; + * ``` + */ +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. + * + * @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. */ + readonly type: "declaration"; + /** The XML version (always "1.0" for XML 1.0 documents). */ + readonly version: string; + /** The declared character encoding, if specified. */ + readonly encoding?: string; + /** Whether the document is standalone (§2.9). */ + 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; + +/** + * Options for {@linkcode XmlParseStream}. + * + * @example Usage + * ```ts + * import type { ParseStreamOptions } from "@std/xml/types"; + * + * const options: ParseStreamOptions = { + * ignoreWhitespace: true, + * ignoreComments: true, + * }; + * ``` + */ +export interface ParseStreamOptions { + /** + * If true, text nodes containing only whitespace are not emitted. + * + * @default {false} + */ + readonly ignoreWhitespace?: boolean; + + /** + * If true, comment events are not emitted. + * + * @default {false} + */ + readonly ignoreComments?: boolean; + + /** + * If true, processing instruction events are not emitted. + * + * @default {false} + */ + readonly ignoreProcessingInstructions?: boolean; + + /** + * If true, CDATA sections are emitted as regular text events. + * + * @default {false} + */ + readonly coerceCDataToText?: boolean; +} + +/** + * Options for {@linkcode parse}. + * + * @example Usage + * ```ts + * import type { ParseOptions } from "@std/xml/types"; + * + * const options: ParseOptions = { + * ignoreWhitespace: true, + * ignoreComments: true, + * }; + * ``` + */ +export interface ParseOptions { + /** + * If true, text nodes containing only whitespace are removed. + * + * @default {false} + */ + readonly ignoreWhitespace?: boolean; + + /** + * If true, comments are not included in the tree. + * + * @default {false} + */ + readonly ignoreComments?: boolean; +} + +/** + * Options for {@linkcode stringify}. + * + * @example Usage + * ```ts + * import type { StringifyOptions } from "@std/xml/types"; + * + * const options: StringifyOptions = { + * indent: " ", + * declaration: true, + * }; + * ``` + */ +export interface StringifyOptions { + /** + * Indentation string for pretty-printing (e.g., " " or "\t"). + * When undefined, output is compact with no extra whitespace. + * + * @default {undefined} + */ + readonly indent?: string; + + /** + * If true, include the XML declaration when stringifying a document. + * Only applies when the input is an XmlDocument with a declaration. + * + * @default {true} + */ + readonly declaration?: boolean; +} + +// ============================================================================ +// Node Types (for DOM-style tree) +// ============================================================================ + +/** + * A text node in the XML tree. + */ +export interface XmlTextNode { + /** The node type discriminant. */ + readonly type: "text"; + /** The decoded text content. */ + readonly text: string; +} + +/** + * A CDATA section node in the XML tree. + */ +export interface XmlCDataNode { + /** The node type discriminant. */ + readonly type: "cdata"; + /** The raw CDATA content. */ + readonly text: string; +} + +/** + * A comment node in the XML tree. + */ +export interface XmlCommentNode { + /** The node type discriminant. */ + readonly type: "comment"; + /** The comment text. */ + readonly text: string; +} + +/** + * An element node in the XML tree. + */ +export interface XmlElement { + /** The node type discriminant. */ + readonly type: "element"; + /** The qualified name of the element. */ + readonly name: XmlName; + /** Attribute lookup by local name. */ + readonly attributes: Readonly>; + /** The child nodes of this element. */ + readonly children: ReadonlyArray; +} + +/** + * Discriminated union of all node types in an XML tree. + */ +export type XmlNode = + | XmlElement + | XmlTextNode + | XmlCDataNode + | XmlCommentNode; + +/** + * A parsed XML document. + */ +export interface XmlDocument { + /** The XML declaration, if present. */ + readonly declaration?: XmlDeclarationEvent; + /** The root element of the document. */ + readonly root: XmlElement; +} + +// ============================================================================ +// Type Guards +// ============================================================================ + +/** + * Type guard to check if a node is an element. + * + * @example Usage + * ```ts ignore + * import { parse, isElement } from "@std/xml"; + * + * const doc = parse(""); + * for (const child of doc.root.children) { + * if (isElement(child)) { + * console.log(child.name.local); + * } + * } + * ``` + * + * @param node The node to check. + * @returns `true` if the node is an element, `false` otherwise. + */ +export function isElement(node: XmlNode): node is XmlElement { + return node.type === "element"; +} + +/** + * Type guard to check if a node is a text node. + * + * @example Usage + * ```ts + * import { isText } from "@std/xml/types"; + * import { assertEquals } from "@std/assert"; + * + * const node = { type: "text" as const, text: "Hello" }; + * assertEquals(isText(node), true); + * ``` + * + * @param node The node to check. + * @returns `true` if the node is a text node, `false` otherwise. + */ +export function isText(node: XmlNode): node is XmlTextNode { + return node.type === "text"; +} + +/** + * Type guard to check if a node is a CDATA node. + * + * @example Usage + * ```ts + * import { isCData } from "@std/xml/types"; + * import { assertEquals } from "@std/assert"; + * + * const node = { type: "cdata" as const, text: "