(target: string, content: string)
| 667 | } |
| 668 | |
| 669 | #emitDeclaration(target: string, content: string): void { |
| 670 | // XML 1.0 §2.6: Only exact lowercase "xml" is valid for XML declaration |
| 671 | // Case variants like "XML", "xMl" are reserved and invalid |
| 672 | if (target !== "xml") { |
| 673 | this.#error( |
| 674 | `Processing instruction target '${target}' is reserved; 'xml' must be lowercase (XML 1.0 §2.6)`, |
| 675 | ); |
| 676 | } |
| 677 | |
| 678 | // XML 1.0 §2.8: XML declaration must be at the very beginning |
| 679 | // (only a UTF BOM may precede it) |
| 680 | // Check both: no content has been emitted, AND if position tracking is on, |
| 681 | // verify the offset is correct (handles BOM case) |
| 682 | if (!this.#xmlDeclAllowed) { |
| 683 | this.#error( |
| 684 | "XML declaration must appear at the start of the document (XML 1.0 §2.8)", |
| 685 | ); |
| 686 | } |
| 687 | // If position tracking is enabled, also verify exact offset |
| 688 | if (this.#trackPosition) { |
| 689 | const allowedOffset = this.#firstCharWasBOM ? 1 : 0; |
| 690 | if (this.#tokenOffset !== allowedOffset) { |
| 691 | this.#error( |
| 692 | "XML declaration must appear at the start of the document (XML 1.0 §2.8)", |
| 693 | ); |
| 694 | } |
| 695 | } |
| 696 | // After emitting declaration, no more XMLDecl is allowed |
| 697 | this.#xmlDeclAllowed = false; |
| 698 | |
| 699 | // Validate XML declaration syntax strictly |
| 700 | const result = validateXmlDeclaration(content); |
| 701 | if (!result.valid) { |
| 702 | this.#error(result.error); |
| 703 | } |
| 704 | |
| 705 | this.#callbacks.onDeclaration?.( |
| 706 | result.version, |
| 707 | result.encoding, |
| 708 | result.standalone, |
| 709 | this.#tokenLine, |
| 710 | this.#tokenColumn, |
| 711 | this.#tokenOffset, |
| 712 | ); |
| 713 | } |
| 714 | |
| 715 | /** Read a quoted string in DOCTYPE. Does not handle cross-chunk boundaries. */ |
| 716 | #readDoctypeQuotedString(): string { |
no test coverage detected