(target: string, content: string)
| 716 | } |
| 717 | |
| 718 | #emitDeclaration(target: string, content: string): void { |
| 719 | // XML 1.0 §2.6: Only exact lowercase "xml" is valid for XML declaration |
| 720 | // Case variants like "XML", "xMl" are reserved and invalid |
| 721 | if (target !== "xml") { |
| 722 | this.#error( |
| 723 | `Processing instruction target '${target}' is reserved; 'xml' must be lowercase (XML 1.0 §2.6)`, |
| 724 | ); |
| 725 | } |
| 726 | |
| 727 | // XML 1.0 §2.8: XML declaration must be at the very beginning |
| 728 | // (only a UTF BOM may precede it) |
| 729 | // Check both: no content has been emitted, AND if position tracking is on, |
| 730 | // verify the offset is correct (handles BOM case) |
| 731 | if (!this.#xmlDeclAllowed) { |
| 732 | this.#error( |
| 733 | "XML declaration must appear at the start of the document (XML 1.0 §2.8)", |
| 734 | ); |
| 735 | } |
| 736 | // If position tracking is enabled, also verify exact offset |
| 737 | if (this.#trackPosition) { |
| 738 | const allowedOffset = this.#firstCharWasBOM ? 1 : 0; |
| 739 | if (this.#tokenOffset !== allowedOffset) { |
| 740 | this.#error( |
| 741 | "XML declaration must appear at the start of the document (XML 1.0 §2.8)", |
| 742 | ); |
| 743 | } |
| 744 | } |
| 745 | // After emitting declaration, no more XMLDecl is allowed |
| 746 | this.#xmlDeclAllowed = false; |
| 747 | |
| 748 | // Validate XML declaration syntax strictly |
| 749 | const result = validateXmlDeclaration(content); |
| 750 | if (!result.valid) { |
| 751 | this.#error(result.error); |
| 752 | } |
| 753 | |
| 754 | this.#callbacks.onDeclaration?.( |
| 755 | result.version, |
| 756 | result.encoding, |
| 757 | result.standalone, |
| 758 | this.#tokenLine, |
| 759 | this.#tokenColumn, |
| 760 | this.#tokenOffset, |
| 761 | ); |
| 762 | } |
| 763 | |
| 764 | /** Read a quoted string in DOCTYPE. Does not handle cross-chunk boundaries. */ |
| 765 | #readDoctypeQuotedString(): string { |
no test coverage detected