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..0bce5c4db3ca --- /dev/null +++ b/xml/_parse_sync.ts @@ -0,0 +1,491 @@ +// 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 (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..273aa6a85101 --- /dev/null +++ b/xml/_parse_sync_test.ts @@ -0,0 +1,363 @@ +// 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 + 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 + 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 + 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("`); + + assertEquals(doc.root.children.length, 2); + // First child is comment, second is element + if (doc.root.children[0]!.type === "comment") { + assertEquals(doc.root.children[0]!.text, " line1\nline2\nline3 "); + } +}); + +// ============================================================================= +// Multi-line CDATA position tracking +// ============================================================================= + +Deno.test("parseSync() tracks line position in multi-line CDATA", () => { + // Tests newline tracking inside CDATA + const doc = parseSync(``); + + assertEquals(doc.root.children.length, 1); + if (doc.root.children[0]!.type === "cdata") { + assertEquals(doc.root.children[0]!.text, "line1\nline2\nline3"); + } +}); + +Deno.test("parseSync() tracks line position in multi-line processing instruction", () => { + // Tests newline tracking inside PI content + // PI content with newlines - should not throw + const doc = parseSync(` + +`); + + assertEquals(doc.root.name.local, "root"); + assertEquals(doc.declaration?.version, "1.0"); +}); + +// ============================================================================= +// Empty text fast path test +// ============================================================================= + +Deno.test("parseSync() handles adjacent elements with no text between", () => { + // Tests line 178: empty text fast path when text.length === 0 + const doc = parseSync(""); + assertEquals(doc.root.children.length, 2); + // Verify no empty text nodes were created + assertEquals( + doc.root.children.every((c) => c.type === "element"), + true, + ); +}); diff --git a/xml/_parser.ts b/xml/_parser.ts new file mode 100644 index 000000000000..1dd16eaa7163 --- /dev/null +++ b/xml/_parser.ts @@ -0,0 +1,316 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +// This module is browser compatible. + +/** + * Internal XML parser module. + * + * Transforms raw tokens from the tokenizer into high-level XmlEvent objects, + * handling namespace prefixes, entity decoding, and well-formedness validation. + * + * @module + */ + +import type { + ParseStreamOptions, + XmlAttribute, + XmlCDataEvent, + XmlCommentEvent, + XmlDeclarationEvent, + XmlEndElementEvent, + XmlEvent, + XmlProcessingInstructionEvent, + XmlStartElementEvent, + XmlTextEvent, +} from "./types.ts"; +import { XmlSyntaxError } from "./types.ts"; +import type { XmlToken } from "./_tokenizer.ts"; +import { decodeEntities } from "./_entities.ts"; +import { parseName, WHITESPACE_ONLY_RE } from "./_common.ts"; + +/** + * Normalizes attribute value per XML 1.0 §3.3.3. + * + * Per the specification: + * - Literal whitespace (#x9, #xA) is replaced with space (#x20) + * - Character references to whitespace ( , , etc.) are preserved + * + * Note: #xD (carriage return) has been converted to #xA by line-ending + * normalization in the tokenizer (§2.11), so we only need to handle #xA and #x9. + * + * @see {@link https://www.w3.org/TR/xml/#AVNormalize | XML 1.0 §3.3.3 Attribute-Value Normalization} + * + * @param raw The raw attribute value from the tokenizer. + * @returns The normalized and entity-decoded attribute value. + */ +function normalizeAttributeValue(raw: string): string { + // Step 1: Replace literal whitespace with space per §3.3.3 + // This is done BEFORE entity decoding to preserve char refs like + const normalized = raw.replace(/[\t\n]/g, " "); + + // Step 2: Decode entities ( becomes actual \n, preserving char refs) + return decodeEntities(normalized); +} + +/** + * Stateful XML Event Parser. + * + * Transforms raw XML tokens into high-level events. Handles element stacking, + * well-formedness validation, and optional filtering of whitespace/comments. + * + * @example Basic usage + * ```ts ignore + * const parser = new XmlEventParser({ ignoreWhitespace: true }); + * const events1 = parser.process(tokens1); + * const events2 = parser.process(tokens2); + * parser.finalize(); // throws if unclosed elements + * ``` + */ +export class XmlEventParser { + #elementStack: Array< + { name: string; line: number; column: number; offset: number } + > = []; + #pendingStartElement: { + name: string; + attributes: XmlAttribute[]; + line: number; + column: number; + offset: number; + } | null = null; + #options: ParseStreamOptions; + + /** + * Constructs a new XmlEventParser. + * + * @param options Options for filtering and behavior. + */ + constructor(options: ParseStreamOptions = {}) { + this.#options = options; + } + + /** + * Process tokens synchronously and return events. + * + * @param tokens Array of tokens from the tokenizer. + * @returns Array of events extracted from the tokens. + */ + process(tokens: XmlToken[]): XmlEvent[] { + const { + ignoreWhitespace = false, + ignoreComments = false, + ignoreProcessingInstructions = false, + coerceCDataToText = false, + } = this.#options; + + const events: XmlEvent[] = []; + + for (const token of tokens) { + switch (token.type) { + case "declaration": { + events.push( + { + type: "declaration", + version: token.version, + line: token.position.line, + column: token.position.column, + offset: token.position.offset, + ...(token.encoding !== undefined + ? { encoding: token.encoding } + : {}), + ...(token.standalone !== undefined + ? { standalone: token.standalone } + : {}), + } satisfies XmlDeclarationEvent, + ); + break; + } + + case "doctype": { + // DOCTYPE is parsed by tokenizer but not emitted as an event + break; + } + + case "start_tag_open": { + this.#pendingStartElement = { + name: token.name, + attributes: [], + line: token.position.line, + column: token.position.column, + offset: token.position.offset, + }; + break; + } + + case "attribute": { + if (this.#pendingStartElement) { + this.#pendingStartElement.attributes.push({ + name: parseName(token.name), + value: normalizeAttributeValue(token.value), + }); + } + break; + } + + case "start_tag_close": { + if (this.#pendingStartElement) { + const name = parseName(this.#pendingStartElement.name); + const { line, column, offset } = this.#pendingStartElement; + + events.push( + { + type: "start_element", + name, + attributes: this.#pendingStartElement.attributes, + selfClosing: token.selfClosing, + line, + column, + offset, + } satisfies XmlStartElementEvent, + ); + + if (token.selfClosing) { + events.push( + { + type: "end_element", + name, + line, + column, + offset, + } satisfies XmlEndElementEvent, + ); + } else { + this.#elementStack.push({ + name: this.#pendingStartElement.name, + line, + column, + offset, + }); + } + + this.#pendingStartElement = null; + } + break; + } + + case "end_tag": { + const expected = this.#elementStack.pop(); + if (expected === undefined) { + throw new XmlSyntaxError( + `Unexpected closing tag with no matching opening tag`, + token.position, + ); + } + if (expected.name !== token.name) { + throw new XmlSyntaxError( + `Mismatched closing tag: expected but found `, + token.position, + ); + } + + events.push( + { + type: "end_element", + name: parseName(token.name), + line: token.position.line, + column: token.position.column, + offset: token.position.offset, + } satisfies XmlEndElementEvent, + ); + break; + } + + case "text": { + const text = decodeEntities(token.content); + + if (ignoreWhitespace && WHITESPACE_ONLY_RE.test(text)) { + break; + } + + events.push( + { + type: "text", + text, + line: token.position.line, + column: token.position.column, + offset: token.position.offset, + } satisfies XmlTextEvent, + ); + break; + } + + case "cdata": { + if (coerceCDataToText) { + events.push( + { + type: "text", + text: token.content, + line: token.position.line, + column: token.position.column, + offset: token.position.offset, + } satisfies XmlTextEvent, + ); + } else { + events.push( + { + type: "cdata", + text: token.content, + line: token.position.line, + column: token.position.column, + offset: token.position.offset, + } satisfies XmlCDataEvent, + ); + } + break; + } + + case "comment": { + if (ignoreComments) { + break; + } + + events.push( + { + type: "comment", + text: token.content, + line: token.position.line, + column: token.position.column, + offset: token.position.offset, + } satisfies XmlCommentEvent, + ); + break; + } + + case "processing_instruction": { + if (ignoreProcessingInstructions) { + break; + } + + events.push( + { + type: "processing_instruction", + target: token.target, + content: token.content, + line: token.position.line, + column: token.position.column, + offset: token.position.offset, + } satisfies XmlProcessingInstructionEvent, + ); + break; + } + } + } + + return events; + } + + /** + * Finalize parsing and validate that all elements are closed. + * + * @throws {XmlSyntaxError} If there are unclosed elements. + */ + finalize(): void { + if (this.#elementStack.length > 0) { + const unclosed = this.#elementStack[this.#elementStack.length - 1]!; + throw new XmlSyntaxError(`Unclosed element <${unclosed.name}>`, unclosed); + } + } +} diff --git a/xml/_parser_test.ts b/xml/_parser_test.ts new file mode 100644 index 000000000000..12cef6408f0f --- /dev/null +++ b/xml/_parser_test.ts @@ -0,0 +1,431 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +import { assertEquals, assertThrows } from "@std/assert"; +import { XmlEventParser } from "./_parser.ts"; +import { XmlTokenizer } from "./_tokenizer.ts"; +import type { ParseStreamOptions, XmlEvent } from "./types.ts"; +import { XmlSyntaxError } from "./types.ts"; + +/** Helper to collect all events from parsing an XML string synchronously. */ +function collectEvents(xml: string, options?: ParseStreamOptions): XmlEvent[] { + const tokenizer = new XmlTokenizer(); + const parser = new XmlEventParser(options); + const tokens = tokenizer.process(xml); + const remaining = tokenizer.finalize(); + const events = parser.process([...tokens, ...remaining]); + parser.finalize(); + return events; +} + +// ============================================================================= +// Namespace Prefix Parsing (Parser-specific) +// ============================================================================= + +Deno.test("XmlEventParser.process() extracts namespace prefix from element name", () => { + const events = collectEvents(""); + + assertEquals(events[0]!.type, "start_element"); + if (events[0]!.type === "start_element") { + assertEquals(events[0]!.name.prefix, "ns"); + assertEquals(events[0]!.name.local, "element"); + } +}); + +Deno.test("XmlEventParser.process() extracts namespace prefix from attribute name", () => { + const events = collectEvents(''); + + assertEquals(events[0]!.type, "start_element"); + if (events[0]!.type === "start_element") { + assertEquals(events[0]!.attributes.length, 1); + assertEquals(events[0]!.attributes[0]!.name.prefix, "xml"); + assertEquals(events[0]!.attributes[0]!.name.local, "lang"); + assertEquals(events[0]!.attributes[0]!.value, "en"); + } +}); + +Deno.test("XmlEventParser.process() handles element without prefix", () => { + const events = collectEvents(""); + + assertEquals(events[0]!.type, "start_element"); + if (events[0]!.type === "start_element") { + assertEquals(events[0]!.name.prefix, undefined); + assertEquals(events[0]!.name.local, "element"); + } +}); + +// ============================================================================= +// Entity Decoding (Parser-specific) +// ============================================================================= + +Deno.test("XmlEventParser.process() decodes entities in attribute values", () => { + const events = collectEvents(''); + + assertEquals(events[0]!.type, "start_element"); + if (events[0]!.type === "start_element") { + assertEquals(events[0]!.attributes[0]!.value, "Tom & Jerry"); + } +}); + +Deno.test("XmlEventParser.process() decodes character references in attribute values", () => { + const events = collectEvents(''); + + assertEquals(events[0]!.type, "start_element"); + if (events[0]!.type === "start_element") { + assertEquals(events[0]!.attributes[0]!.value, "<>"); + } +}); + +Deno.test("XmlEventParser.process() decodes entities in text content", () => { + const events = collectEvents("<script>"); + + assertEquals(events[1]!.type, "text"); + if (events[1]!.type === "text") { + assertEquals(events[1]!.text, "" }], + }; + + 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: "