feat(parsing): port libyaml character-class predicates to Swift
Stage 3 foundation of the faithful libyaml scanner port. Translates the IS_* character-class macros from yaml_private.h (IS_Z, IS_SPACE, IS_TAB, IS_BLANK, IS_BREAK, IS_BREAKZ, IS_SPACEZ, IS_BLANKZ, IS_DIGIT, IS_ALPHA, IS_HEX, IS_ASCII, IS_PRINTABLE) into PureYAML.Parsing.LibYAML.CharacterClass. libyaml inspects UTF-8 bytes; the Swift port operates on Unicode.Scalar, where the multi-byte line breaks NEL (U+0085), LS (U+2028), and PS (U+2029) are single scalars, and end of input is nil, libyaml's \0 sentinel (so isZ(nil) is true and the others are false). These predicates are what the token fetchers dispatch on. Unit-tested for end-of-input, blanks, the full break set, the alpha/digit/hex sets, and the ascii/printable boundaries.
This commit is contained in:
parent
9ce57e86e5
commit
348af8b77b
94
Sources/Parsing/LibYAML.CharacterClass.swift
Normal file
94
Sources/Parsing/LibYAML.CharacterClass.swift
Normal file
@ -0,0 +1,94 @@
|
||||
extension PureYAML.Parsing.LibYAML {
|
||||
/// Port of libyaml's character-class macros (`IS_*` in `yaml_private.h`).
|
||||
/// libyaml inspects UTF-8 bytes; this operates on `Unicode.Scalar`, where the
|
||||
/// multi-byte line breaks NEL (`U+0085`), LS (`U+2028`), and PS (`U+2029`) are
|
||||
/// single scalars. End of input is `nil`, which is libyaml's `\0` sentinel, so
|
||||
/// `isZ(nil)` is true and every other predicate is false for `nil`.
|
||||
enum CharacterClass {
|
||||
/// `IS_Z`: the end of the input (libyaml's `\0`).
|
||||
static func isZ(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
scalar == nil
|
||||
}
|
||||
|
||||
/// `IS_SPACE`.
|
||||
static func isSpace(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
scalar == " "
|
||||
}
|
||||
|
||||
/// `IS_TAB`.
|
||||
static func isTab(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
scalar == "\t"
|
||||
}
|
||||
|
||||
/// `IS_BLANK`: space or tab.
|
||||
static func isBlank(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
isSpace(scalar) || isTab(scalar)
|
||||
}
|
||||
|
||||
/// `IS_BREAK`: CR, LF, NEL, LS, or PS.
|
||||
static func isBreak(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
switch scalar {
|
||||
case "\r", "\n", "\u{85}", "\u{2028}", "\u{2029}": true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
/// `IS_BREAKZ`: a line break or the end of input.
|
||||
static func isBreakZ(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
isBreak(scalar) || isZ(scalar)
|
||||
}
|
||||
|
||||
/// `IS_SPACEZ`: a space, a line break, or the end of input.
|
||||
static func isSpaceZ(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
isSpace(scalar) || isBreakZ(scalar)
|
||||
}
|
||||
|
||||
/// `IS_BLANKZ`: blank (space or tab), a line break, or the end of input.
|
||||
static func isBlankZ(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
isBlank(scalar) || isBreakZ(scalar)
|
||||
}
|
||||
|
||||
/// `IS_DIGIT`.
|
||||
static func isDigit(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
guard let scalar else { return false }
|
||||
return scalar >= "0" && scalar <= "9"
|
||||
}
|
||||
|
||||
/// `IS_ALPHA`: a digit, ASCII letter, `_`, or `-` (the directive, anchor,
|
||||
/// and tag-handle name characters).
|
||||
static func isAlpha(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
guard let scalar else { return false }
|
||||
return (scalar >= "0" && scalar <= "9")
|
||||
|| (scalar >= "A" && scalar <= "Z")
|
||||
|| (scalar >= "a" && scalar <= "z")
|
||||
|| scalar == "_"
|
||||
|| scalar == "-"
|
||||
}
|
||||
|
||||
/// `IS_HEX`: a hexadecimal digit.
|
||||
static func isHex(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
guard let scalar else { return false }
|
||||
return (scalar >= "0" && scalar <= "9")
|
||||
|| (scalar >= "A" && scalar <= "F")
|
||||
|| (scalar >= "a" && scalar <= "f")
|
||||
}
|
||||
|
||||
/// `IS_ASCII`.
|
||||
static func isASCII(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
guard let scalar else { return false }
|
||||
return scalar.value <= 0x7F
|
||||
}
|
||||
|
||||
/// `IS_PRINTABLE`: the YAML `c-printable` set.
|
||||
static func isPrintable(_ scalar: Unicode.Scalar?) -> Bool {
|
||||
guard let scalar else { return false }
|
||||
let value = scalar.value
|
||||
return value == 0x09 || value == 0x0A || value == 0x0D
|
||||
|| (value >= 0x20 && value <= 0x7E)
|
||||
|| value == 0x85
|
||||
|| (value >= 0xA0 && value <= 0xD7FF)
|
||||
|| (value >= 0xE000 && value <= 0xFFFD)
|
||||
|| (value >= 0x10000 && value <= 0x10FFFF)
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Tests/LibYAMLCharacterClassTests.swift
Normal file
65
Tests/LibYAMLCharacterClassTests.swift
Normal file
@ -0,0 +1,65 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
/// Verifies the ported character-class predicates match libyaml's `IS_*` macros,
|
||||
/// including end-of-input as `nil` and the multi-byte line breaks.
|
||||
@Suite("LibYAML Character Class")
|
||||
struct LibYAMLCharacterClassTests {
|
||||
private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass
|
||||
|
||||
@Test("end of input is Z and a break, nothing else")
|
||||
func test_endOfInput() {
|
||||
#expect(Classify.isZ(nil))
|
||||
#expect(Classify.isBreakZ(nil))
|
||||
#expect(Classify.isBlankZ(nil))
|
||||
#expect(!Classify.isBreak(nil))
|
||||
#expect(!Classify.isBlank(nil))
|
||||
#expect(!Classify.isAlpha(nil))
|
||||
}
|
||||
|
||||
@Test("blanks are space and tab only")
|
||||
func test_blank() {
|
||||
#expect(Classify.isSpace(" "))
|
||||
#expect(Classify.isTab("\t"))
|
||||
#expect(Classify.isBlank(" "))
|
||||
#expect(Classify.isBlank("\t"))
|
||||
#expect(!Classify.isBlank("\n"))
|
||||
#expect(!Classify.isBlank("x"))
|
||||
}
|
||||
|
||||
@Test("line breaks include CR, LF, NEL, LS, PS")
|
||||
func test_break() {
|
||||
for scalar in ["\r", "\n", "\u{85}", "\u{2028}", "\u{2029}"] as [Unicode.Scalar] {
|
||||
#expect(Classify.isBreak(scalar))
|
||||
#expect(Classify.isBreakZ(scalar))
|
||||
#expect(Classify.isBlankZ(scalar))
|
||||
}
|
||||
#expect(!Classify.isBreak(" "))
|
||||
#expect(!Classify.isBreak("x"))
|
||||
}
|
||||
|
||||
@Test("alpha is alnum plus underscore and hyphen")
|
||||
func test_alpha() {
|
||||
for scalar in ["0", "9", "A", "Z", "a", "z", "_", "-"] as [Unicode.Scalar] {
|
||||
#expect(Classify.isAlpha(scalar))
|
||||
}
|
||||
#expect(!Classify.isAlpha("."))
|
||||
#expect(!Classify.isAlpha(" "))
|
||||
#expect(Classify.isDigit("5"))
|
||||
#expect(!Classify.isDigit("a"))
|
||||
#expect(Classify.isHex("f"))
|
||||
#expect(Classify.isHex("9"))
|
||||
#expect(!Classify.isHex("g"))
|
||||
}
|
||||
|
||||
@Test("ascii and printable boundaries")
|
||||
func test_asciiPrintable() {
|
||||
#expect(Classify.isASCII("\u{7F}"))
|
||||
#expect(!Classify.isASCII("\u{80}"))
|
||||
#expect(Classify.isPrintable("\t"))
|
||||
#expect(Classify.isPrintable("A"))
|
||||
#expect(Classify.isPrintable("\u{85}"))
|
||||
#expect(!Classify.isPrintable("\u{0}"))
|
||||
#expect(!Classify.isPrintable("\u{1F}"))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user