From 18101b8ddcbf555530302aefa4fd3053d834cc9a Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 24 Jun 2026 18:11:58 +0200 Subject: [PATCH] feat(parsing): accept full YAML 1.2 anchor and alias names An anchor or alias name is ns-anchor-char+ (any printable character that is not whitespace, a line break, the BOM, or a flow indicator), so Unicode and punctuation names are valid. libyaml restricts the name to alphanumerics and rejects them. Canonical event conformance: 271 -> 275 of 308 (2SXE, 8XYN, W5VH, Y2GN). 379 tests pass. --- Sources/Parsing/LibYAML.Scanner.Anchor.swift | 26 +++++++++++++------- Tests/CanonicalEventConformanceTests.swift | 8 +++--- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Sources/Parsing/LibYAML.Scanner.Anchor.swift b/Sources/Parsing/LibYAML.Scanner.Anchor.swift index d8d2efa..1df8ba0 100644 --- a/Sources/Parsing/LibYAML.Scanner.Anchor.swift +++ b/Sources/Parsing/LibYAML.Scanner.Anchor.swift @@ -1,20 +1,21 @@ extension PureYAML.Parsing.LibYAML.Scanner { private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass - /// Port of `yaml_parser_scan_anchor`. Scans an anchor (`&name`) or alias - /// (`*name`): the name is a run of `IS_ALPHA` characters and must be non-empty - /// and followed by a blank, the end of input, or one of the indicators that may - /// terminate it. + /// Port of `yaml_parser_scan_anchor`, widened to YAML 1.2: an anchor (`&name`) + /// or alias (`*name`) name is `ns-anchor-char+`, i.e. any printable character + /// that is not whitespace, a line break, or a flow indicator (`,[]{}`). libyaml + /// restricts the name to alphanumerics, which rejects Unicode and punctuation + /// anchor names that YAML 1.2 allows. mutating func scanAnchor(isAnchor: Bool) throws -> PureYAML.Parsing.LibYAML.Token { let startMark = reader.mark reader.skip() var name = "" - while let character = reader.peek(), Classify.isAlpha(character) { + while let character = reader.peek(), isAnchorCharacter(character) { name.unicodeScalars.append(character) reader.skip() } let endMark = reader.mark - guard !name.isEmpty, Classify.isBlankZ(reader.peek()) || isAnchorTerminator(reader.peek()) else { + guard !name.isEmpty else { throw PureYAML.Parsing.LibYAML.ScannerError( context: isAnchor ? "while scanning an anchor" : "while scanning an alias", problem: "did not find expected alphabetic or numeric character", @@ -28,10 +29,17 @@ extension PureYAML.Parsing.LibYAML.Scanner { ) } - private func isAnchorTerminator(_ scalar: Unicode.Scalar?) -> Bool { + /// `ns-anchor-char`: a printable, non-blank, non-break character that is not a + /// flow indicator or the byte-order mark. + private func isAnchorCharacter(_ scalar: Unicode.Scalar) -> Bool { + guard Classify.isPrintable(scalar), !Classify.isBlankZ(scalar), scalar != "\u{FEFF}" else { + return false + } switch scalar { - case "?", ":", ",", "]", "}", "%", "@", "`": true - default: false + case ",", "[", "]", "{", "}": + return false + default: + return true } } } diff --git a/Tests/CanonicalEventConformanceTests.swift b/Tests/CanonicalEventConformanceTests.swift index f919823..af1b12c 100644 --- a/Tests/CanonicalEventConformanceTests.swift +++ b/Tests/CanonicalEventConformanceTests.swift @@ -21,14 +21,14 @@ struct CanonicalEventConformanceTests { // itself handles. // // false-rejects (parseEvents throws on valid input, as libyaml does) - "2JQS", "2SXE", "4ABK", "58MP", "5T43", "6BCT", "6CA3", "6M2F", - "8XYN", "96NN/00", "96NN/01", "A2M4", "CFD4", "DBG4", "DK3J", "DK95/00", + "2JQS", "4ABK", "58MP", "5T43", "6BCT", "6CA3", "6M2F", + "96NN/00", "96NN/01", "A2M4", "CFD4", "DBG4", "DK3J", "DK95/00", "DK95/04", "FP8R", "FRK4", "HM87/00", "M2N8/00", "M7A3", "NHX8", "NKF9", "Q5MG", "R4YG", "S3PD", "SM9W/01", "UKK6/00", "W4TN", - "W5VH", "Y79Y/001", "Y79Y/010", + "Y79Y/001", "Y79Y/010", // wrong event stream: the port matches libyaml but both deviate from the // canonical events (libyaml quirks to be surpassed). - "652Z", "HM87/01", "JEF9/02", "Y2GN", + "652Z", "HM87/01", "JEF9/02", ] @Test("Every valid case emits canonical events, except the recorded gaps")