Only about fifteen Unicode blocks were tabulated, so a pattern over any other
block (\p{IsArmenian}, \p{IsTamil}, \p{IsHangulJamo}, and many more) failed to
compile and rejected valid instances. The table now carries the Unicode 3.1
blocks the XSD regular-expression appendix fixes; surrogate blocks are omitted
since surrogates are not scalar values, and Specials uses the main FFF0..FFFD
range. Each range is verified bidirectionally by the XSTS corpus: valid
in-block instances pass and invalid out-of-block instances still fail, so no
range over-covers.
XSTS: valid instances rejected 502 to 395. Baseline updated; regression test
covers in-block matches, out-of-block rejection, and unknown block names.
Refs #147, #129.
151 lines
7.3 KiB
Swift
151 lines
7.3 KiB
Swift
extension PureXML.Regex {
|
|
/// A `\p{...}` / `\P{...}` Unicode category or block test. A category maps to
|
|
/// the scalar's Unicode general category through Swift's own `generalCategory`,
|
|
/// so no character tables are vendored; an `Is<Block>` name tests a contiguous
|
|
/// Unicode block. An unknown name is rejected at compile time.
|
|
struct CategoryPredicate: Equatable, Sendable {
|
|
let name: String
|
|
let negated: Bool
|
|
|
|
init?(name: String, negated: Bool) {
|
|
guard CategoryMatcher.isKnown(name) else { return nil }
|
|
self.name = name
|
|
self.negated = negated
|
|
}
|
|
|
|
func contains(_ scalar: Unicode.Scalar) -> Bool {
|
|
let hit = CategoryMatcher.matches(name, scalar)
|
|
return negated ? !hit : hit
|
|
}
|
|
}
|
|
|
|
/// Resolves `\p{...}` names: a one-letter general-category group (`L`, `N`, ...),
|
|
/// a two-letter category (`Lu`, `Nd`, ...), or an `Is<Block>` block name.
|
|
enum CategoryMatcher {
|
|
static func isKnown(_ name: String) -> Bool {
|
|
if name.hasPrefix("Is") { return blocks[String(name.dropFirst(2))] != nil }
|
|
return groups.contains(name) || codeValues.contains(name)
|
|
}
|
|
|
|
static func matches(_ name: String, _ scalar: Unicode.Scalar) -> Bool {
|
|
if name.hasPrefix("Is") {
|
|
guard let range = blocks[String(name.dropFirst(2))] else { return false }
|
|
return range.contains(scalar.value)
|
|
}
|
|
let code = codes[scalar.properties.generalCategory] ?? "Cn"
|
|
return name.count == 1 ? code.hasPrefix(name) : code == name
|
|
}
|
|
|
|
private static let groups: Set<String> = ["L", "M", "N", "P", "S", "Z", "C"]
|
|
private static let codeValues = Set(codes.values)
|
|
|
|
private static let codes: [Unicode.GeneralCategory: String] = [
|
|
.uppercaseLetter: "Lu", .lowercaseLetter: "Ll", .titlecaseLetter: "Lt",
|
|
.modifierLetter: "Lm", .otherLetter: "Lo",
|
|
.nonspacingMark: "Mn", .spacingMark: "Mc", .enclosingMark: "Me",
|
|
.decimalNumber: "Nd", .letterNumber: "Nl", .otherNumber: "No",
|
|
.connectorPunctuation: "Pc", .dashPunctuation: "Pd", .openPunctuation: "Ps",
|
|
.closePunctuation: "Pe", .initialPunctuation: "Pi", .finalPunctuation: "Pf",
|
|
.otherPunctuation: "Po",
|
|
.mathSymbol: "Sm", .currencySymbol: "Sc", .modifierSymbol: "Sk", .otherSymbol: "So",
|
|
.spaceSeparator: "Zs", .lineSeparator: "Zl", .paragraphSeparator: "Zp",
|
|
.control: "Cc", .format: "Cf", .surrogate: "Cs", .privateUse: "Co", .unassigned: "Cn",
|
|
]
|
|
|
|
/// The Unicode block names XSD 1.0 admits in `\p{Is<Block>}` (the
|
|
/// Unicode 3.1 blocks the regular-expression appendix fixes), each mapped
|
|
/// to its code-point range. An `Is<Block>` name outside this set is
|
|
/// rejected rather than guessed, so a pattern never matches against a
|
|
/// fabricated range. Surrogate blocks are omitted: surrogates are not
|
|
/// scalar values. `Specials` uses the main `FFF0..FFFD` range (the spec's
|
|
/// separate `FEFF` half is the byte-order mark, not matched here).
|
|
private static let blocks: [String: ClosedRange<UInt32>] = [
|
|
"BasicLatin": 0x0000 ... 0x007F,
|
|
"Latin-1Supplement": 0x0080 ... 0x00FF,
|
|
"LatinExtended-A": 0x0100 ... 0x017F,
|
|
"LatinExtended-B": 0x0180 ... 0x024F,
|
|
"IPAExtensions": 0x0250 ... 0x02AF,
|
|
"SpacingModifierLetters": 0x02B0 ... 0x02FF,
|
|
"CombiningDiacriticalMarks": 0x0300 ... 0x036F,
|
|
"Greek": 0x0370 ... 0x03FF,
|
|
"GreekandCoptic": 0x0370 ... 0x03FF,
|
|
"Cyrillic": 0x0400 ... 0x04FF,
|
|
"Armenian": 0x0530 ... 0x058F,
|
|
"Hebrew": 0x0590 ... 0x05FF,
|
|
"Arabic": 0x0600 ... 0x06FF,
|
|
"Syriac": 0x0700 ... 0x074F,
|
|
"Thaana": 0x0780 ... 0x07BF,
|
|
"Devanagari": 0x0900 ... 0x097F,
|
|
"Bengali": 0x0980 ... 0x09FF,
|
|
"Gurmukhi": 0x0A00 ... 0x0A7F,
|
|
"Gujarati": 0x0A80 ... 0x0AFF,
|
|
"Oriya": 0x0B00 ... 0x0B7F,
|
|
"Tamil": 0x0B80 ... 0x0BFF,
|
|
"Telugu": 0x0C00 ... 0x0C7F,
|
|
"Kannada": 0x0C80 ... 0x0CFF,
|
|
"Malayalam": 0x0D00 ... 0x0D7F,
|
|
"Sinhala": 0x0D80 ... 0x0DFF,
|
|
"Thai": 0x0E00 ... 0x0E7F,
|
|
"Lao": 0x0E80 ... 0x0EFF,
|
|
"Tibetan": 0x0F00 ... 0x0FFF,
|
|
"Myanmar": 0x1000 ... 0x109F,
|
|
"Georgian": 0x10A0 ... 0x10FF,
|
|
"HangulJamo": 0x1100 ... 0x11FF,
|
|
"Ethiopic": 0x1200 ... 0x137F,
|
|
"Cherokee": 0x13A0 ... 0x13FF,
|
|
"UnifiedCanadianAboriginalSyllabics": 0x1400 ... 0x167F,
|
|
"Ogham": 0x1680 ... 0x169F,
|
|
"Runic": 0x16A0 ... 0x16FF,
|
|
"Khmer": 0x1780 ... 0x17FF,
|
|
"Mongolian": 0x1800 ... 0x18AF,
|
|
"LatinExtendedAdditional": 0x1E00 ... 0x1EFF,
|
|
"GreekExtended": 0x1F00 ... 0x1FFF,
|
|
"GeneralPunctuation": 0x2000 ... 0x206F,
|
|
"SuperscriptsandSubscripts": 0x2070 ... 0x209F,
|
|
"CurrencySymbols": 0x20A0 ... 0x20CF,
|
|
"CombiningMarksforSymbols": 0x20D0 ... 0x20FF,
|
|
"LetterlikeSymbols": 0x2100 ... 0x214F,
|
|
"NumberForms": 0x2150 ... 0x218F,
|
|
"Arrows": 0x2190 ... 0x21FF,
|
|
"MathematicalOperators": 0x2200 ... 0x22FF,
|
|
"MiscellaneousTechnical": 0x2300 ... 0x23FF,
|
|
"ControlPictures": 0x2400 ... 0x243F,
|
|
"OpticalCharacterRecognition": 0x2440 ... 0x245F,
|
|
"EnclosedAlphanumerics": 0x2460 ... 0x24FF,
|
|
"BoxDrawing": 0x2500 ... 0x257F,
|
|
"BlockElements": 0x2580 ... 0x259F,
|
|
"GeometricShapes": 0x25A0 ... 0x25FF,
|
|
"MiscellaneousSymbols": 0x2600 ... 0x26FF,
|
|
"Dingbats": 0x2700 ... 0x27BF,
|
|
"BraillePatterns": 0x2800 ... 0x28FF,
|
|
"CJKRadicalsSupplement": 0x2E80 ... 0x2EFF,
|
|
"KangxiRadicals": 0x2F00 ... 0x2FDF,
|
|
"IdeographicDescriptionCharacters": 0x2FF0 ... 0x2FFF,
|
|
"CJKSymbolsandPunctuation": 0x3000 ... 0x303F,
|
|
"Hiragana": 0x3040 ... 0x309F,
|
|
"Katakana": 0x30A0 ... 0x30FF,
|
|
"Bopomofo": 0x3100 ... 0x312F,
|
|
"HangulCompatibilityJamo": 0x3130 ... 0x318F,
|
|
"Kanbun": 0x3190 ... 0x319F,
|
|
"BopomofoExtended": 0x31A0 ... 0x31BF,
|
|
"EnclosedCJKLettersandMonths": 0x3200 ... 0x32FF,
|
|
"CJKCompatibility": 0x3300 ... 0x33FF,
|
|
"CJKUnifiedIdeographsExtensionA": 0x3400 ... 0x4DB5,
|
|
"CJKUnifiedIdeographs": 0x4E00 ... 0x9FFF,
|
|
"YiSyllables": 0xA000 ... 0xA48F,
|
|
"YiRadicals": 0xA490 ... 0xA4CF,
|
|
"HangulSyllables": 0xAC00 ... 0xD7A3,
|
|
"PrivateUse": 0xE000 ... 0xF8FF,
|
|
"CJKCompatibilityIdeographs": 0xF900 ... 0xFAFF,
|
|
"AlphabeticPresentationForms": 0xFB00 ... 0xFB4F,
|
|
"ArabicPresentationForms-A": 0xFB50 ... 0xFDFF,
|
|
"CombiningHalfMarks": 0xFE20 ... 0xFE2F,
|
|
"CJKCompatibilityForms": 0xFE30 ... 0xFE4F,
|
|
"SmallFormVariants": 0xFE50 ... 0xFE6F,
|
|
"ArabicPresentationForms-B": 0xFE70 ... 0xFEFF,
|
|
"HalfwidthandFullwidthForms": 0xFF00 ... 0xFFEF,
|
|
"Specials": 0xFFF0 ... 0xFFFD,
|
|
]
|
|
}
|
|
}
|