fix(expr): keep lone surrogates intact and make the casing tables regenerable
- commit
- a1598a3e29d370b7753916debd81e05001ebe40a
- author
- brice · · 2026-10-12 15:12 UTC
- parent
- 8798092
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +243 -40
M expr/casing.go +186 -37
| @@ -9,6 +9,8 @@ import ( | |||
| 9 | 9 | "unicode/utf8" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | + | // The tables in casing_tables.go are generated; see go:generate in doc.go. | |
| 13 | + | ||
| 12 | 14 | // This file reproduces the JavaScript string functions the reference | |
| 13 | 15 | // implementation relies on: toUpperCase and toLowerCase (full Unicode case | |
| 14 | 16 | // mapping, including multi-character results such as "ß" to "SS" and the | |
| @@ -39,52 +41,116 @@ func isASCII(s string) bool { | |||
| 39 | 41 | return true | |
| 40 | 42 | } | |
| 41 | 43 | ||
| 42 | - | func jsUpperRune(r rune) string { | |
| 43 | - | if s, ok := jsUpperDelta[r]; ok { | |
| 44 | - | return s | |
| 44 | + | // Strings are Go strings, so text that JavaScript would hold as a lone | |
| 45 | + | // UTF-16 surrogate (from fromJSON('"\ud800"')) is kept in WTF-8: the | |
| 46 | + | // three-byte form 0xED 0xA0-0xBF 0x80-0xBF that UTF-8 forbids. The helpers | |
| 47 | + | // below make every case and comparison function treat such a sequence as one | |
| 48 | + | // code unit and copy it unchanged, as JavaScript does, instead of letting Go | |
| 49 | + | // turn each byte into U+FFFD. | |
| 50 | + | ||
| 51 | + | // wtf8Surrogate reports whether s starts with a WTF-8 lone surrogate and | |
| 52 | + | // returns its code unit. | |
| 53 | + | func wtf8Surrogate(s string) (rune, bool) { | |
| 54 | + | if len(s) >= 3 && s[0] == 0xED && s[1] >= 0xA0 && s[1] <= 0xBF && s[2]&0xC0 == 0x80 { | |
| 55 | + | return rune(s[0]&0x0F)<<12 | rune(s[1]&0x3F)<<6 | rune(s[2]&0x3F), true | |
| 45 | 56 | } | |
| 46 | - | return string(unicode.ToUpper(r)) | |
| 57 | + | return 0, false | |
| 47 | 58 | } | |
| 48 | 59 | ||
| 49 | - | // jsToUpper is String.prototype.toUpperCase. | |
| 50 | - | func jsToUpper(s string) string { | |
| 51 | - | if isASCII(s) { | |
| 52 | - | return strings.ToUpper(s) | |
| 60 | + | // decodeRune is utf8.DecodeRuneInString that also decodes a WTF-8 lone | |
| 61 | + | // surrogate (width 3) to its code unit value. | |
| 62 | + | func decodeRune(s string) (rune, int) { | |
| 63 | + | r, w := utf8.DecodeRuneInString(s) | |
| 64 | + | if r == utf8.RuneError && w == 1 { | |
| 65 | + | if sur, ok := wtf8Surrogate(s); ok { | |
| 66 | + | return sur, 3 | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | return r, w | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | // writeRune writes r, using the WTF-8 form for a surrogate code unit. | |
| 73 | + | func writeRune(b *strings.Builder, r rune) { | |
| 74 | + | if r >= 0xD800 && r < 0xE000 { | |
| 75 | + | b.WriteByte(0xE0 | byte(r>>12)) | |
| 76 | + | b.WriteByte(0x80 | byte(r>>6)&0x3F) | |
| 77 | + | b.WriteByte(0x80 | byte(r)&0x3F) | |
| 78 | + | return | |
| 79 | + | } | |
| 80 | + | b.WriteRune(r) | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // normalizeWTF8 joins a lone high surrogate immediately followed by a lone low | |
| 84 | + | // surrogate into the supplementary character they form, as concatenating two | |
| 85 | + | // JavaScript strings does. Strings built by concatenation (format, join) go | |
| 86 | + | // through it. | |
| 87 | + | func normalizeWTF8(s string) string { | |
| 88 | + | if !strings.Contains(s, "\xed") { | |
| 89 | + | return s | |
| 53 | 90 | } | |
| 54 | 91 | var b strings.Builder | |
| 55 | 92 | b.Grow(len(s)) | |
| 56 | - | for _, r := range s { | |
| 57 | - | b.WriteString(jsUpperRune(r)) | |
| 93 | + | for i := 0; i < len(s); { | |
| 94 | + | r, w := decodeRune(s[i:]) | |
| 95 | + | if r >= 0xD800 && r < 0xDC00 { | |
| 96 | + | if lo, lw := decodeRune(s[i+w:]); lo >= 0xDC00 && lo < 0xE000 && lw == 3 && i+w < len(s) { | |
| 97 | + | b.WriteRune(0x10000 + (r-0xD800)<<10 + (lo - 0xDC00)) | |
| 98 | + | i += w + lw | |
| 99 | + | continue | |
| 100 | + | } | |
| 101 | + | } | |
| 102 | + | b.WriteString(s[i : i+w]) | |
| 103 | + | i += w | |
| 58 | 104 | } | |
| 59 | 105 | return b.String() | |
| 60 | 106 | } | |
| 61 | 107 | ||
| 62 | - | // toUpperSpecial upper-cases s like jsToUpper but leaves the dotless i | |
| 63 | - | // (U+0131) alone, as the reference implementation does for its | |
| 64 | - | // case-insensitive comparisons and for startsWith and endsWith. | |
| 65 | - | func toUpperSpecial(s string) string { | |
| 108 | + | func writeUpper(b *strings.Builder, r rune, keepDotlessI bool) { | |
| 109 | + | if keepDotlessI && r == 'ı' { | |
| 110 | + | b.WriteRune(r) | |
| 111 | + | return | |
| 112 | + | } | |
| 113 | + | if s, ok := jsUpperDelta[r]; ok { | |
| 114 | + | b.WriteString(s) | |
| 115 | + | return | |
| 116 | + | } | |
| 117 | + | writeRune(b, unicode.ToUpper(r)) | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | func upperWith(s string, keepDotlessI bool) string { | |
| 66 | 121 | if isASCII(s) { | |
| 67 | 122 | return strings.ToUpper(s) | |
| 68 | 123 | } | |
| 69 | 124 | var b strings.Builder | |
| 70 | 125 | b.Grow(len(s)) | |
| 71 | - | for _, r := range s { | |
| 72 | - | if r == 'ı' { | |
| 73 | - | b.WriteRune(r) | |
| 74 | - | continue | |
| 75 | - | } | |
| 76 | - | b.WriteString(jsUpperRune(r)) | |
| 126 | + | for i := 0; i < len(s); { | |
| 127 | + | r, w := decodeRune(s[i:]) | |
| 128 | + | i += w | |
| 129 | + | writeUpper(&b, r, keepDotlessI) | |
| 77 | 130 | } | |
| 78 | 131 | return b.String() | |
| 79 | 132 | } | |
| 80 | 133 | ||
| 134 | + | // jsToUpper is String.prototype.toUpperCase. | |
| 135 | + | func jsToUpper(s string) string { return upperWith(s, false) } | |
| 136 | + | ||
| 137 | + | // toUpperSpecial upper-cases s like jsToUpper but leaves the dotless i | |
| 138 | + | // (U+0131) alone, as the reference implementation does for its | |
| 139 | + | // case-insensitive comparisons and for startsWith and endsWith. | |
| 140 | + | func toUpperSpecial(s string) string { return upperWith(s, true) } | |
| 141 | + | ||
| 81 | 142 | // jsToLower is String.prototype.toLowerCase, including the Final_Sigma | |
| 82 | 143 | // context rule (a capital sigma at the end of a word becomes ς). | |
| 83 | 144 | func jsToLower(s string) string { | |
| 84 | 145 | if isASCII(s) { | |
| 85 | 146 | return strings.ToLower(s) | |
| 86 | 147 | } | |
| 87 | - | runes := []rune(s) | |
| 148 | + | runes := make([]rune, 0, len(s)) | |
| 149 | + | for i := 0; i < len(s); { | |
| 150 | + | r, w := decodeRune(s[i:]) | |
| 151 | + | runes = append(runes, r) | |
| 152 | + | i += w | |
| 153 | + | } | |
| 88 | 154 | var b strings.Builder | |
| 89 | 155 | b.Grow(len(s)) | |
| 90 | 156 | for i, r := range runes { | |
| @@ -99,7 +165,7 @@ func jsToLower(s string) string { | |||
| 99 | 165 | if m, ok := jsLowerDelta[r]; ok { | |
| 100 | 166 | b.WriteString(m) | |
| 101 | 167 | } else { | |
| 102 | - | b.WriteRune(unicode.ToLower(r)) | |
| 168 | + | writeRune(&b, unicode.ToLower(r)) | |
| 103 | 169 | } | |
| 104 | 170 | } | |
| 105 | 171 | } | |
| @@ -143,36 +209,119 @@ func isCaseIgnorable(r rune) bool { | |||
| 143 | 209 | return unicode.In(r, unicode.Mn, unicode.Me, unicode.Cf, unicode.Lm, unicode.Sk) | |
| 144 | 210 | } | |
| 145 | 211 | ||
| 212 | + | // utf16Units encodes s as the UTF-16 code units a JavaScript string holds, | |
| 213 | + | // WTF-8 lone surrogates included. | |
| 214 | + | func utf16Units(s string) []uint16 { | |
| 215 | + | out := make([]uint16, 0, len(s)) | |
| 216 | + | for i := 0; i < len(s); { | |
| 217 | + | r, w := decodeRune(s[i:]) | |
| 218 | + | i += w | |
| 219 | + | if r >= 0xD800 && r < 0xE000 { | |
| 220 | + | out = append(out, uint16(r)) | |
| 221 | + | continue | |
| 222 | + | } | |
| 223 | + | if r >= 0x10000 { | |
| 224 | + | hi, lo := utf16.EncodeRune(r) | |
| 225 | + | out = append(out, uint16(hi), uint16(lo)) | |
| 226 | + | continue | |
| 227 | + | } | |
| 228 | + | out = append(out, uint16(r)) | |
| 229 | + | } | |
| 230 | + | return out | |
| 231 | + | } | |
| 232 | + | ||
| 146 | 233 | // compareUTF16 compares two strings by UTF-16 code unit, which is how | |
| 147 | 234 | // JavaScript orders strings with < and >. It differs from byte or rune order | |
| 148 | 235 | // only for supplementary-plane characters against U+E000..U+FFFF. | |
| 149 | 236 | func compareUTF16(a, b string) int { | |
| 150 | - | for len(a) > 0 && len(b) > 0 { | |
| 151 | - | ra, wa := utf8.DecodeRuneInString(a) | |
| 152 | - | rb, wb := utf8.DecodeRuneInString(b) | |
| 153 | - | if ra != rb { | |
| 154 | - | if utf16Key(ra) < utf16Key(rb) { | |
| 237 | + | if isASCII(a) && isASCII(b) { | |
| 238 | + | return strings.Compare(a, b) | |
| 239 | + | } | |
| 240 | + | ua, ub := utf16Units(a), utf16Units(b) | |
| 241 | + | for i := 0; i < len(ua) && i < len(ub); i++ { | |
| 242 | + | if ua[i] != ub[i] { | |
| 243 | + | if ua[i] < ub[i] { | |
| 155 | 244 | return -1 | |
| 156 | 245 | } | |
| 157 | 246 | return 1 | |
| 158 | 247 | } | |
| 159 | - | a, b = a[wa:], b[wb:] | |
| 160 | 248 | } | |
| 161 | 249 | switch { | |
| 162 | - | case len(a) > 0: | |
| 250 | + | case len(ua) > len(ub): | |
| 163 | 251 | return 1 | |
| 164 | - | case len(b) > 0: | |
| 252 | + | case len(ua) < len(ub): | |
| 165 | 253 | return -1 | |
| 166 | 254 | } | |
| 167 | 255 | return 0 | |
| 168 | 256 | } | |
| 169 | 257 | ||
| 170 | - | // utf16Key packs the UTF-16 encoding of r into an integer that sorts like | |
| 171 | - | // the code unit sequence. | |
| 172 | - | func utf16Key(r rune) uint32 { | |
| 173 | - | hi, lo := utf16.EncodeRune(r) | |
| 174 | - | if hi == unicode.ReplacementChar && lo == unicode.ReplacementChar { | |
| 175 | - | return uint32(r) << 16 // BMP (EncodeRune reports no pair) | |
| 258 | + | // exceedsUTF16Len reports whether s is longer than max UTF-16 code units. It | |
| 259 | + | // stops counting at max and allocates nothing. Invalid UTF-8 bytes count as | |
| 260 | + | // one unit each (U+FFFD), like the conversion Lex applies. | |
| 261 | + | func exceedsUTF16Len(s string, max int) bool { | |
| 262 | + | n := 0 | |
| 263 | + | for _, r := range s { | |
| 264 | + | n++ | |
| 265 | + | if r >= 0x10000 { | |
| 266 | + | n++ | |
| 267 | + | } | |
| 268 | + | if n > max { | |
| 269 | + | return true | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | return false | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | // hasWTF8 reports whether s holds a lone surrogate in WTF-8 form. | |
| 276 | + | func hasWTF8(s string) bool { | |
| 277 | + | for i := 0; i < len(s); i++ { | |
| 278 | + | if s[i] == 0xED { | |
| 279 | + | if _, ok := wtf8Surrogate(s[i:]); ok { | |
| 280 | + | return true | |
| 281 | + | } | |
| 282 | + | } | |
| 283 | + | } | |
| 284 | + | return false | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | // jsIncludes, jsHasPrefix and jsHasSuffix are String.prototype.includes, | |
| 288 | + | // startsWith and endsWith. They work on bytes, which is equivalent to UTF-16 | |
| 289 | + | // code units except around lone surrogates (a lone high surrogate is found | |
| 290 | + | // inside a supplementary character), where they switch to code units. | |
| 291 | + | func jsIncludes(s, sub string) bool { | |
| 292 | + | if !hasWTF8(s) && !hasWTF8(sub) { | |
| 293 | + | return strings.Contains(s, sub) | |
| 294 | + | } | |
| 295 | + | us, usub := utf16Units(s), utf16Units(sub) | |
| 296 | + | for i := 0; i+len(usub) <= len(us); i++ { | |
| 297 | + | if unitsEqual(us[i:i+len(usub)], usub) { | |
| 298 | + | return true | |
| 299 | + | } | |
| 300 | + | } | |
| 301 | + | return false | |
| 302 | + | } | |
| 303 | + | ||
| 304 | + | func jsHasPrefix(s, prefix string) bool { | |
| 305 | + | if !hasWTF8(s) && !hasWTF8(prefix) { | |
| 306 | + | return strings.HasPrefix(s, prefix) | |
| 307 | + | } | |
| 308 | + | us, up := utf16Units(s), utf16Units(prefix) | |
| 309 | + | return len(up) <= len(us) && unitsEqual(us[:len(up)], up) | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | func jsHasSuffix(s, suffix string) bool { | |
| 313 | + | if !hasWTF8(s) && !hasWTF8(suffix) { | |
| 314 | + | return strings.HasSuffix(s, suffix) | |
| 315 | + | } | |
| 316 | + | us, up := utf16Units(s), utf16Units(suffix) | |
| 317 | + | return len(up) <= len(us) && unitsEqual(us[len(us)-len(up):], up) | |
| 318 | + | } | |
| 319 | + | ||
| 320 | + | func unitsEqual(a, b []uint16) bool { | |
| 321 | + | for i := range a { | |
| 322 | + | if a[i] != b[i] { | |
| 323 | + | return false | |
| 324 | + | } | |
| 176 | 325 | } | |
| 177 | - | return uint32(hi)<<16 | uint32(lo) | |
| 326 | + | return len(a) == len(b) | |
| 178 | 327 | } | |
| @@ -9,6 +9,8 @@ import ( | |||||
| 9 | "unicode/utf8" | 9 | "unicode/utf8" | ||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | + | // The tables in casing_tables.go are generated; see go:generate in doc.go. | |||
| 13 | + | ||||
| 12 | // This file reproduces the JavaScript string functions the reference | 14 | // This file reproduces the JavaScript string functions the reference | ||
| 13 | // implementation relies on: toUpperCase and toLowerCase (full Unicode case | 15 | // implementation relies on: toUpperCase and toLowerCase (full Unicode case | ||
| 14 | // mapping, including multi-character results such as "ß" to "SS" and the | 16 | // mapping, including multi-character results such as "ß" to "SS" and the | ||
| @@ -39,52 +41,116 @@ func isASCII(s string) bool { | |||||
| 39 | return true | 41 | return true | ||
| 40 | } | 42 | } | ||
| 41 | 43 | ||||
| 42 | - | func jsUpperRune(r rune) string { | 44 | + | // Strings are Go strings, so text that JavaScript would hold as a lone |
| 43 | - | if s, ok := jsUpperDelta[r]; ok { | 45 | + | // UTF-16 surrogate (from fromJSON('"\ud800"')) is kept in WTF-8: the |
| 44 | - | return s | 46 | + | // three-byte form 0xED 0xA0-0xBF 0x80-0xBF that UTF-8 forbids. The helpers |
| 47 | + | // below make every case and comparison function treat such a sequence as one | |||
| 48 | + | // code unit and copy it unchanged, as JavaScript does, instead of letting Go | |||
| 49 | + | // turn each byte into U+FFFD. | |||
| 50 | + | ||||
| 51 | + | // wtf8Surrogate reports whether s starts with a WTF-8 lone surrogate and | |||
| 52 | + | // returns its code unit. | |||
| 53 | + | func wtf8Surrogate(s string) (rune, bool) { | |||
| 54 | + | if len(s) >= 3 && s[0] == 0xED && s[1] >= 0xA0 && s[1] <= 0xBF && s[2]&0xC0 == 0x80 { | |||
| 55 | + | return rune(s[0]&0x0F)<<12 | rune(s[1]&0x3F)<<6 | rune(s[2]&0x3F), true | |||
| 45 | } | 56 | } | ||
| 46 | - | return string(unicode.ToUpper(r)) | 57 | + | return 0, false |
| 47 | } | 58 | } | ||
| 48 | 59 | ||||
| 49 | - | // jsToUpper is String.prototype.toUpperCase. | 60 | + | // decodeRune is utf8.DecodeRuneInString that also decodes a WTF-8 lone |
| 50 | - | func jsToUpper(s string) string { | 61 | + | // surrogate (width 3) to its code unit value. |
| 51 | - | if isASCII(s) { | 62 | + | func decodeRune(s string) (rune, int) { |
| 52 | - | return strings.ToUpper(s) | 63 | + | r, w := utf8.DecodeRuneInString(s) |
| 64 | + | if r == utf8.RuneError && w == 1 { | |||
| 65 | + | if sur, ok := wtf8Surrogate(s); ok { | |||
| 66 | + | return sur, 3 | |||
| 67 | + | } | |||
| 68 | + | } | |||
| 69 | + | return r, w | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | // writeRune writes r, using the WTF-8 form for a surrogate code unit. | |||
| 73 | + | func writeRune(b *strings.Builder, r rune) { | |||
| 74 | + | if r >= 0xD800 && r < 0xE000 { | |||
| 75 | + | b.WriteByte(0xE0 | byte(r>>12)) | |||
| 76 | + | b.WriteByte(0x80 | byte(r>>6)&0x3F) | |||
| 77 | + | b.WriteByte(0x80 | byte(r)&0x3F) | |||
| 78 | + | return | |||
| 79 | + | } | |||
| 80 | + | b.WriteRune(r) | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // normalizeWTF8 joins a lone high surrogate immediately followed by a lone low | |||
| 84 | + | // surrogate into the supplementary character they form, as concatenating two | |||
| 85 | + | // JavaScript strings does. Strings built by concatenation (format, join) go | |||
| 86 | + | // through it. | |||
| 87 | + | func normalizeWTF8(s string) string { | |||
| 88 | + | if !strings.Contains(s, "\xed") { | |||
| 89 | + | return s | |||
| 53 | } | 90 | } | ||
| 54 | var b strings.Builder | 91 | var b strings.Builder | ||
| 55 | b.Grow(len(s)) | 92 | b.Grow(len(s)) | ||
| 56 | - | for _, r := range s { | 93 | + | for i := 0; i < len(s); { |
| 57 | - | b.WriteString(jsUpperRune(r)) | 94 | + | r, w := decodeRune(s[i:]) |
| 95 | + | if r >= 0xD800 && r < 0xDC00 { | |||
| 96 | + | if lo, lw := decodeRune(s[i+w:]); lo >= 0xDC00 && lo < 0xE000 && lw == 3 && i+w < len(s) { | |||
| 97 | + | b.WriteRune(0x10000 + (r-0xD800)<<10 + (lo - 0xDC00)) | |||
| 98 | + | i += w + lw | |||
| 99 | + | continue | |||
| 100 | + | } | |||
| 101 | + | } | |||
| 102 | + | b.WriteString(s[i : i+w]) | |||
| 103 | + | i += w | |||
| 58 | } | 104 | } | ||
| 59 | return b.String() | 105 | return b.String() | ||
| 60 | } | 106 | } | ||
| 61 | 107 | ||||
| 62 | - | // toUpperSpecial upper-cases s like jsToUpper but leaves the dotless i | 108 | + | func writeUpper(b *strings.Builder, r rune, keepDotlessI bool) { |
| 63 | - | // (U+0131) alone, as the reference implementation does for its | 109 | + | if keepDotlessI && r == 'ı' { |
| 64 | - | // case-insensitive comparisons and for startsWith and endsWith. | 110 | + | b.WriteRune(r) |
| 65 | - | func toUpperSpecial(s string) string { | 111 | + | return |
| 112 | + | } | |||
| 113 | + | if s, ok := jsUpperDelta[r]; ok { | |||
| 114 | + | b.WriteString(s) | |||
| 115 | + | return | |||
| 116 | + | } | |||
| 117 | + | writeRune(b, unicode.ToUpper(r)) | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | func upperWith(s string, keepDotlessI bool) string { | |||
| 66 | if isASCII(s) { | 121 | if isASCII(s) { | ||
| 67 | return strings.ToUpper(s) | 122 | return strings.ToUpper(s) | ||
| 68 | } | 123 | } | ||
| 69 | var b strings.Builder | 124 | var b strings.Builder | ||
| 70 | b.Grow(len(s)) | 125 | b.Grow(len(s)) | ||
| 71 | - | for _, r := range s { | 126 | + | for i := 0; i < len(s); { |
| 72 | - | if r == 'ı' { | 127 | + | r, w := decodeRune(s[i:]) |
| 73 | - | b.WriteRune(r) | 128 | + | i += w |
| 74 | - | continue | 129 | + | writeUpper(&b, r, keepDotlessI) |
| 75 | - | } | |||
| 76 | - | b.WriteString(jsUpperRune(r)) | |||
| 77 | } | 130 | } | ||
| 78 | return b.String() | 131 | return b.String() | ||
| 79 | } | 132 | } | ||
| 80 | 133 | ||||
| 134 | + | // jsToUpper is String.prototype.toUpperCase. | |||
| 135 | + | func jsToUpper(s string) string { return upperWith(s, false) } | |||
| 136 | + | ||||
| 137 | + | // toUpperSpecial upper-cases s like jsToUpper but leaves the dotless i | |||
| 138 | + | // (U+0131) alone, as the reference implementation does for its | |||
| 139 | + | // case-insensitive comparisons and for startsWith and endsWith. | |||
| 140 | + | func toUpperSpecial(s string) string { return upperWith(s, true) } | |||
| 141 | + | ||||
| 81 | // jsToLower is String.prototype.toLowerCase, including the Final_Sigma | 142 | // jsToLower is String.prototype.toLowerCase, including the Final_Sigma | ||
| 82 | // context rule (a capital sigma at the end of a word becomes ς). | 143 | // context rule (a capital sigma at the end of a word becomes ς). | ||
| 83 | func jsToLower(s string) string { | 144 | func jsToLower(s string) string { | ||
| 84 | if isASCII(s) { | 145 | if isASCII(s) { | ||
| 85 | return strings.ToLower(s) | 146 | return strings.ToLower(s) | ||
| 86 | } | 147 | } | ||
| 87 | - | runes := []rune(s) | 148 | + | runes := make([]rune, 0, len(s)) |
| 149 | + | for i := 0; i < len(s); { | |||
| 150 | + | r, w := decodeRune(s[i:]) | |||
| 151 | + | runes = append(runes, r) | |||
| 152 | + | i += w | |||
| 153 | + | } | |||
| 88 | var b strings.Builder | 154 | var b strings.Builder | ||
| 89 | b.Grow(len(s)) | 155 | b.Grow(len(s)) | ||
| 90 | for i, r := range runes { | 156 | for i, r := range runes { | ||
| @@ -99,7 +165,7 @@ func jsToLower(s string) string { | |||||
| 99 | if m, ok := jsLowerDelta[r]; ok { | 165 | if m, ok := jsLowerDelta[r]; ok { | ||
| 100 | b.WriteString(m) | 166 | b.WriteString(m) | ||
| 101 | } else { | 167 | } else { | ||
| 102 | - | b.WriteRune(unicode.ToLower(r)) | 168 | + | writeRune(&b, unicode.ToLower(r)) |
| 103 | } | 169 | } | ||
| 104 | } | 170 | } | ||
| 105 | } | 171 | } | ||
| @@ -143,36 +209,119 @@ func isCaseIgnorable(r rune) bool { | |||||
| 143 | return unicode.In(r, unicode.Mn, unicode.Me, unicode.Cf, unicode.Lm, unicode.Sk) | 209 | return unicode.In(r, unicode.Mn, unicode.Me, unicode.Cf, unicode.Lm, unicode.Sk) | ||
| 144 | } | 210 | } | ||
| 145 | 211 | ||||
| 212 | + | // utf16Units encodes s as the UTF-16 code units a JavaScript string holds, | |||
| 213 | + | // WTF-8 lone surrogates included. | |||
| 214 | + | func utf16Units(s string) []uint16 { | |||
| 215 | + | out := make([]uint16, 0, len(s)) | |||
| 216 | + | for i := 0; i < len(s); { | |||
| 217 | + | r, w := decodeRune(s[i:]) | |||
| 218 | + | i += w | |||
| 219 | + | if r >= 0xD800 && r < 0xE000 { | |||
| 220 | + | out = append(out, uint16(r)) | |||
| 221 | + | continue | |||
| 222 | + | } | |||
| 223 | + | if r >= 0x10000 { | |||
| 224 | + | hi, lo := utf16.EncodeRune(r) | |||
| 225 | + | out = append(out, uint16(hi), uint16(lo)) | |||
| 226 | + | continue | |||
| 227 | + | } | |||
| 228 | + | out = append(out, uint16(r)) | |||
| 229 | + | } | |||
| 230 | + | return out | |||
| 231 | + | } | |||
| 232 | + | ||||
| 146 | // compareUTF16 compares two strings by UTF-16 code unit, which is how | 233 | // compareUTF16 compares two strings by UTF-16 code unit, which is how | ||
| 147 | // JavaScript orders strings with < and >. It differs from byte or rune order | 234 | // JavaScript orders strings with < and >. It differs from byte or rune order | ||
| 148 | // only for supplementary-plane characters against U+E000..U+FFFF. | 235 | // only for supplementary-plane characters against U+E000..U+FFFF. | ||
| 149 | func compareUTF16(a, b string) int { | 236 | func compareUTF16(a, b string) int { | ||
| 150 | - | for len(a) > 0 && len(b) > 0 { | 237 | + | if isASCII(a) && isASCII(b) { |
| 151 | - | ra, wa := utf8.DecodeRuneInString(a) | 238 | + | return strings.Compare(a, b) |
| 152 | - | rb, wb := utf8.DecodeRuneInString(b) | 239 | + | } |
| 153 | - | if ra != rb { | 240 | + | ua, ub := utf16Units(a), utf16Units(b) |
| 154 | - | if utf16Key(ra) < utf16Key(rb) { | 241 | + | for i := 0; i < len(ua) && i < len(ub); i++ { |
| 242 | + | if ua[i] != ub[i] { | |||
| 243 | + | if ua[i] < ub[i] { | |||
| 155 | return -1 | 244 | return -1 | ||
| 156 | } | 245 | } | ||
| 157 | return 1 | 246 | return 1 | ||
| 158 | } | 247 | } | ||
| 159 | - | a, b = a[wa:], b[wb:] | |||
| 160 | } | 248 | } | ||
| 161 | switch { | 249 | switch { | ||
| 162 | - | case len(a) > 0: | 250 | + | case len(ua) > len(ub): |
| 163 | return 1 | 251 | return 1 | ||
| 164 | - | case len(b) > 0: | 252 | + | case len(ua) < len(ub): |
| 165 | return -1 | 253 | return -1 | ||
| 166 | } | 254 | } | ||
| 167 | return 0 | 255 | return 0 | ||
| 168 | } | 256 | } | ||
| 169 | 257 | ||||
| 170 | - | // utf16Key packs the UTF-16 encoding of r into an integer that sorts like | 258 | + | // exceedsUTF16Len reports whether s is longer than max UTF-16 code units. It |
| 171 | - | // the code unit sequence. | 259 | + | // stops counting at max and allocates nothing. Invalid UTF-8 bytes count as |
| 172 | - | func utf16Key(r rune) uint32 { | 260 | + | // one unit each (U+FFFD), like the conversion Lex applies. |
| 173 | - | hi, lo := utf16.EncodeRune(r) | 261 | + | func exceedsUTF16Len(s string, max int) bool { |
| 174 | - | if hi == unicode.ReplacementChar && lo == unicode.ReplacementChar { | 262 | + | n := 0 |
| 175 | - | return uint32(r) << 16 // BMP (EncodeRune reports no pair) | 263 | + | for _, r := range s { |
| 264 | + | n++ | |||
| 265 | + | if r >= 0x10000 { | |||
| 266 | + | n++ | |||
| 267 | + | } | |||
| 268 | + | if n > max { | |||
| 269 | + | return true | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | return false | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | // hasWTF8 reports whether s holds a lone surrogate in WTF-8 form. | |||
| 276 | + | func hasWTF8(s string) bool { | |||
| 277 | + | for i := 0; i < len(s); i++ { | |||
| 278 | + | if s[i] == 0xED { | |||
| 279 | + | if _, ok := wtf8Surrogate(s[i:]); ok { | |||
| 280 | + | return true | |||
| 281 | + | } | |||
| 282 | + | } | |||
| 283 | + | } | |||
| 284 | + | return false | |||
| 285 | + | } | |||
| 286 | + | ||||
| 287 | + | // jsIncludes, jsHasPrefix and jsHasSuffix are String.prototype.includes, | |||
| 288 | + | // startsWith and endsWith. They work on bytes, which is equivalent to UTF-16 | |||
| 289 | + | // code units except around lone surrogates (a lone high surrogate is found | |||
| 290 | + | // inside a supplementary character), where they switch to code units. | |||
| 291 | + | func jsIncludes(s, sub string) bool { | |||
| 292 | + | if !hasWTF8(s) && !hasWTF8(sub) { | |||
| 293 | + | return strings.Contains(s, sub) | |||
| 294 | + | } | |||
| 295 | + | us, usub := utf16Units(s), utf16Units(sub) | |||
| 296 | + | for i := 0; i+len(usub) <= len(us); i++ { | |||
| 297 | + | if unitsEqual(us[i:i+len(usub)], usub) { | |||
| 298 | + | return true | |||
| 299 | + | } | |||
| 300 | + | } | |||
| 301 | + | return false | |||
| 302 | + | } | |||
| 303 | + | ||||
| 304 | + | func jsHasPrefix(s, prefix string) bool { | |||
| 305 | + | if !hasWTF8(s) && !hasWTF8(prefix) { | |||
| 306 | + | return strings.HasPrefix(s, prefix) | |||
| 307 | + | } | |||
| 308 | + | us, up := utf16Units(s), utf16Units(prefix) | |||
| 309 | + | return len(up) <= len(us) && unitsEqual(us[:len(up)], up) | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | func jsHasSuffix(s, suffix string) bool { | |||
| 313 | + | if !hasWTF8(s) && !hasWTF8(suffix) { | |||
| 314 | + | return strings.HasSuffix(s, suffix) | |||
| 315 | + | } | |||
| 316 | + | us, up := utf16Units(s), utf16Units(suffix) | |||
| 317 | + | return len(up) <= len(us) && unitsEqual(us[len(us)-len(up):], up) | |||
| 318 | + | } | |||
| 319 | + | ||||
| 320 | + | func unitsEqual(a, b []uint16) bool { | |||
| 321 | + | for i := range a { | |||
| 322 | + | if a[i] != b[i] { | |||
| 323 | + | return false | |||
| 324 | + | } | |||
| 176 | } | 325 | } | ||
| 177 | - | return uint32(hi)<<16 | uint32(lo) | 326 | + | return len(a) == len(b) |
| 178 | } | 327 | } | ||
M expr/casing_tables.go +4 -0
| @@ -4,6 +4,10 @@ | |||
| 4 | 4 | ||
| 5 | 5 | package expr | |
| 6 | 6 | ||
| 7 | + | // casingGoUnicodeVersion is the Unicode version of the Go toolchain that generated | |
| 8 | + | // the tables below: they are relative to its unicode.ToUpper and unicode.ToLower. | |
| 9 | + | const casingGoUnicodeVersion = "17.0.0" | |
| 10 | + | ||
| 7 | 11 | // jsUpperDelta maps the code points whose JavaScript String.prototype.toUpperCase result differs from the Go simple mapping. | |
| 8 | 12 | var jsUpperDelta = map[rune]string{ | |
| 9 | 13 | 0x00DF: "SS", // "ß" | |
| @@ -4,6 +4,10 @@ | |||||
| 4 | 4 | ||||
| 5 | package expr | 5 | package expr | ||
| 6 | 6 | ||||
| 7 | + | // casingGoUnicodeVersion is the Unicode version of the Go toolchain that generated | |||
| 8 | + | // the tables below: they are relative to its unicode.ToUpper and unicode.ToLower. | |||
| 9 | + | const casingGoUnicodeVersion = "17.0.0" | |||
| 10 | + | ||||
| 7 | // jsUpperDelta maps the code points whose JavaScript String.prototype.toUpperCase result differs from the Go simple mapping. | 11 | // jsUpperDelta maps the code points whose JavaScript String.prototype.toUpperCase result differs from the Go simple mapping. | ||
| 8 | var jsUpperDelta = map[rune]string{ | 12 | var jsUpperDelta = map[rune]string{ | ||
| 9 | 0x00DF: "SS", // "ß" | 13 | 0x00DF: "SS", // "ß" | ||
M expr/doc.go +9 -1
| @@ -1,5 +1,8 @@ | |||
| 1 | 1 | // SPDX-License-Identifier: MIT | |
| 2 | 2 | ||
| 3 | + | // Regenerate the case-mapping tables (needs node; see gen/casing_dump.mjs). | |
| 4 | + | //go:generate sh -c "node gen/casing_dump.mjs | go run gen/casing.go -o casing_tables.go" | |
| 5 | + | ||
| 3 | 6 | // Package expr implements GitHub Actions expressions: the language found | |
| 4 | 7 | // between the braces of ${{ }} in workflow files. | |
| 5 | 8 | // | |
| @@ -16,7 +19,7 @@ | |||
| 16 | 19 | // | |
| 17 | 20 | // node, err := expr.Parse("github.ref == 'refs/heads/main'", []string{"github"}, nil) | |
| 18 | 21 | // if err != nil { /* *expr.Error */ } | |
| 19 | - | // v, err := expr.Evaluate(node, expr.EvalContext{Contexts: map[string]expr.Value{"github": github}}) | |
| 22 | + | // v, err := expr.Evaluate(ctx, node, expr.EvalContext{Contexts: map[string]expr.Value{"github": github}}) | |
| 20 | 23 | // if err != nil { /* *expr.EvalError */ } | |
| 21 | 24 | // if expr.Truthy(v) { /* ... */ } | |
| 22 | 25 | // | |
| @@ -24,4 +27,9 @@ | |||
| 24 | 27 | // and [*Dictionary]. Dictionaries keep insertion order and look keys up | |
| 25 | 28 | // case-insensitively. [FromJSON] and [ToJSON] convert to and from JSON text | |
| 26 | 29 | // exactly as the fromJSON and toJSON functions do. | |
| 30 | + | // | |
| 31 | + | // Evaluation is bounded: [Evaluate] honours its context.Context and stops | |
| 32 | + | // when the strings and filtered arrays it builds exceed | |
| 33 | + | // [EvalContext.MaxResultBytes] (default [MaxResultBytes]); the parser limits | |
| 34 | + | // length and nesting ([MaxExpressionLength], [MaxParserDepth]). | |
| 27 | 35 | package expr | |
| @@ -1,5 +1,8 @@ | |||||
| 1 | // SPDX-License-Identifier: MIT | 1 | // SPDX-License-Identifier: MIT | ||
| 2 | 2 | ||||
| 3 | + | // Regenerate the case-mapping tables (needs node; see gen/casing_dump.mjs). | |||
| 4 | + | //go:generate sh -c "node gen/casing_dump.mjs | go run gen/casing.go -o casing_tables.go" | |||
| 5 | + | ||||
| 3 | // Package expr implements GitHub Actions expressions: the language found | 6 | // Package expr implements GitHub Actions expressions: the language found | ||
| 4 | // between the braces of ${{ }} in workflow files. | 7 | // between the braces of ${{ }} in workflow files. | ||
| 5 | // | 8 | // | ||
| @@ -16,7 +19,7 @@ | |||||
| 16 | // | 19 | // | ||
| 17 | // node, err := expr.Parse("github.ref == 'refs/heads/main'", []string{"github"}, nil) | 20 | // node, err := expr.Parse("github.ref == 'refs/heads/main'", []string{"github"}, nil) | ||
| 18 | // if err != nil { /* *expr.Error */ } | 21 | // if err != nil { /* *expr.Error */ } | ||
| 19 | - | // v, err := expr.Evaluate(node, expr.EvalContext{Contexts: map[string]expr.Value{"github": github}}) | 22 | + | // v, err := expr.Evaluate(ctx, node, expr.EvalContext{Contexts: map[string]expr.Value{"github": github}}) |
| 20 | // if err != nil { /* *expr.EvalError */ } | 23 | // if err != nil { /* *expr.EvalError */ } | ||
| 21 | // if expr.Truthy(v) { /* ... */ } | 24 | // if expr.Truthy(v) { /* ... */ } | ||
| 22 | // | 25 | // | ||
| @@ -24,4 +27,9 @@ | |||||
| 24 | // and [*Dictionary]. Dictionaries keep insertion order and look keys up | 27 | // and [*Dictionary]. Dictionaries keep insertion order and look keys up | ||
| 25 | // case-insensitively. [FromJSON] and [ToJSON] convert to and from JSON text | 28 | // case-insensitively. [FromJSON] and [ToJSON] convert to and from JSON text | ||
| 26 | // exactly as the fromJSON and toJSON functions do. | 29 | // exactly as the fromJSON and toJSON functions do. | ||
| 30 | + | // | |||
| 31 | + | // Evaluation is bounded: [Evaluate] honours its context.Context and stops | |||
| 32 | + | // when the strings and filtered arrays it builds exceed | |||
| 33 | + | // [EvalContext.MaxResultBytes] (default [MaxResultBytes]); the parser limits | |||
| 34 | + | // length and nesting ([MaxExpressionLength], [MaxParserDepth]). | |||
| 27 | package expr | 35 | package expr | ||
M expr/gen/casing.go +43 -1
| @@ -3,7 +3,7 @@ | |||
| 3 | 3 | //go:build ignore | |
| 4 | 4 | ||
| 5 | 5 | // Command casing reads the JSON produced by casing_dump.mjs on stdin and | |
| 6 | - | // writes casing_tables.go on stdout. Only the code points whose JavaScript | |
| 6 | + | // writes casing_tables.go (to the file given with -o, else on stdout). Only the code points whose JavaScript | |
| 7 | 7 | // mapping differs from the Go simple mapping (unicode.ToUpper/ToLower) are | |
| 8 | 8 | // emitted: multi-character expansions (U+00DF to "SS") and Unicode version | |
| 9 | 9 | // differences between Go and the JavaScript engine. | |
| @@ -11,7 +11,10 @@ package main | |||
| 11 | 11 | ||
| 12 | 12 | import ( | |
| 13 | 13 | "encoding/json" | |
| 14 | + | "flag" | |
| 14 | 15 | "fmt" | |
| 16 | + | "go/format" | |
| 17 | + | "io" | |
| 15 | 18 | "os" | |
| 16 | 19 | "sort" | |
| 17 | 20 | "strconv" | |
| @@ -24,17 +27,51 @@ type dump struct { | |||
| 24 | 27 | } | |
| 25 | 28 | ||
| 26 | 29 | func main() { | |
| 30 | + | out := flag.String("o", "", "output file (default stdout)") | |
| 31 | + | flag.Parse() | |
| 27 | 32 | var d dump | |
| 28 | 33 | if err := json.NewDecoder(os.Stdin).Decode(&d); err != nil { | |
| 29 | 34 | fmt.Fprintln(os.Stderr, err) | |
| 30 | 35 | os.Exit(1) | |
| 31 | 36 | } | |
| 37 | + | // Everything is printed to a pipe, gofmt-ed and then written, so that the | |
| 38 | + | // generated file is already in canonical form. | |
| 39 | + | r, w, err := os.Pipe() | |
| 40 | + | if err != nil { | |
| 41 | + | fatal(err) | |
| 42 | + | } | |
| 43 | + | stdout := os.Stdout | |
| 44 | + | os.Stdout = w | |
| 45 | + | done := make(chan []byte) | |
| 46 | + | go func() { | |
| 47 | + | b, _ := io.ReadAll(r) | |
| 48 | + | done <- b | |
| 49 | + | }() | |
| 50 | + | defer func() { | |
| 51 | + | _ = w.Close() | |
| 52 | + | os.Stdout = stdout | |
| 53 | + | src, err := format.Source(<-done) | |
| 54 | + | if err != nil { | |
| 55 | + | fatal(err) | |
| 56 | + | } | |
| 57 | + | if *out == "" { | |
| 58 | + | _, _ = stdout.Write(src) | |
| 59 | + | return | |
| 60 | + | } | |
| 61 | + | if err := os.WriteFile(*out, src, 0o644); err != nil { | |
| 62 | + | fatal(err) | |
| 63 | + | } | |
| 64 | + | }() | |
| 32 | 65 | fmt.Println("// SPDX-License-Identifier: MIT") | |
| 33 | 66 | fmt.Println() | |
| 34 | 67 | fmt.Println("// Code generated by gen/casing.go from gen/casing_dump.mjs; DO NOT EDIT.") | |
| 35 | 68 | fmt.Println() | |
| 36 | 69 | fmt.Println("package expr") | |
| 37 | 70 | fmt.Println() | |
| 71 | + | fmt.Println("// casingGoUnicodeVersion is the Unicode version of the Go toolchain that generated") | |
| 72 | + | fmt.Println("// the tables below: they are relative to its unicode.ToUpper and unicode.ToLower.") | |
| 73 | + | fmt.Printf("const casingGoUnicodeVersion = %q\n", unicode.Version) | |
| 74 | + | fmt.Println() | |
| 38 | 75 | emit("jsUpperDelta", "JavaScript String.prototype.toUpperCase", d.Upper, unicode.ToUpper) | |
| 39 | 76 | emit("jsLowerDelta", "JavaScript String.prototype.toLowerCase (without the Final_Sigma context)", d.Lower, unicode.ToLower) | |
| 40 | 77 | } | |
| @@ -72,3 +109,8 @@ func emit(name, doc string, m map[string]string, goMap func(rune) rune) { | |||
| 72 | 109 | fmt.Println("}") | |
| 73 | 110 | fmt.Println() | |
| 74 | 111 | } | |
| 112 | + | ||
| 113 | + | func fatal(err error) { | |
| 114 | + | fmt.Fprintln(os.Stderr, err) | |
| 115 | + | os.Exit(1) | |
| 116 | + | } | |
| @@ -3,7 +3,7 @@ | |||||
| 3 | //go:build ignore | 3 | //go:build ignore | ||
| 4 | 4 | ||||
| 5 | // Command casing reads the JSON produced by casing_dump.mjs on stdin and | 5 | // Command casing reads the JSON produced by casing_dump.mjs on stdin and | ||
| 6 | - | // writes casing_tables.go on stdout. Only the code points whose JavaScript | 6 | + | // writes casing_tables.go (to the file given with -o, else on stdout). Only the code points whose JavaScript |
| 7 | // mapping differs from the Go simple mapping (unicode.ToUpper/ToLower) are | 7 | // mapping differs from the Go simple mapping (unicode.ToUpper/ToLower) are | ||
| 8 | // emitted: multi-character expansions (U+00DF to "SS") and Unicode version | 8 | // emitted: multi-character expansions (U+00DF to "SS") and Unicode version | ||
| 9 | // differences between Go and the JavaScript engine. | 9 | // differences between Go and the JavaScript engine. | ||
| @@ -11,7 +11,10 @@ package main | |||||
| 11 | 11 | ||||
| 12 | import ( | 12 | import ( | ||
| 13 | "encoding/json" | 13 | "encoding/json" | ||
| 14 | + | "flag" | |||
| 14 | "fmt" | 15 | "fmt" | ||
| 16 | + | "go/format" | |||
| 17 | + | "io" | |||
| 15 | "os" | 18 | "os" | ||
| 16 | "sort" | 19 | "sort" | ||
| 17 | "strconv" | 20 | "strconv" | ||
| @@ -24,17 +27,51 @@ type dump struct { | |||||
| 24 | } | 27 | } | ||
| 25 | 28 | ||||
| 26 | func main() { | 29 | func main() { | ||
| 30 | + | out := flag.String("o", "", "output file (default stdout)") | |||
| 31 | + | flag.Parse() | |||
| 27 | var d dump | 32 | var d dump | ||
| 28 | if err := json.NewDecoder(os.Stdin).Decode(&d); err != nil { | 33 | if err := json.NewDecoder(os.Stdin).Decode(&d); err != nil { | ||
| 29 | fmt.Fprintln(os.Stderr, err) | 34 | fmt.Fprintln(os.Stderr, err) | ||
| 30 | os.Exit(1) | 35 | os.Exit(1) | ||
| 31 | } | 36 | } | ||
| 37 | + | // Everything is printed to a pipe, gofmt-ed and then written, so that the | |||
| 38 | + | // generated file is already in canonical form. | |||
| 39 | + | r, w, err := os.Pipe() | |||
| 40 | + | if err != nil { | |||
| 41 | + | fatal(err) | |||
| 42 | + | } | |||
| 43 | + | stdout := os.Stdout | |||
| 44 | + | os.Stdout = w | |||
| 45 | + | done := make(chan []byte) | |||
| 46 | + | go func() { | |||
| 47 | + | b, _ := io.ReadAll(r) | |||
| 48 | + | done <- b | |||
| 49 | + | }() | |||
| 50 | + | defer func() { | |||
| 51 | + | _ = w.Close() | |||
| 52 | + | os.Stdout = stdout | |||
| 53 | + | src, err := format.Source(<-done) | |||
| 54 | + | if err != nil { | |||
| 55 | + | fatal(err) | |||
| 56 | + | } | |||
| 57 | + | if *out == "" { | |||
| 58 | + | _, _ = stdout.Write(src) | |||
| 59 | + | return | |||
| 60 | + | } | |||
| 61 | + | if err := os.WriteFile(*out, src, 0o644); err != nil { | |||
| 62 | + | fatal(err) | |||
| 63 | + | } | |||
| 64 | + | }() | |||
| 32 | fmt.Println("// SPDX-License-Identifier: MIT") | 65 | fmt.Println("// SPDX-License-Identifier: MIT") | ||
| 33 | fmt.Println() | 66 | fmt.Println() | ||
| 34 | fmt.Println("// Code generated by gen/casing.go from gen/casing_dump.mjs; DO NOT EDIT.") | 67 | fmt.Println("// Code generated by gen/casing.go from gen/casing_dump.mjs; DO NOT EDIT.") | ||
| 35 | fmt.Println() | 68 | fmt.Println() | ||
| 36 | fmt.Println("package expr") | 69 | fmt.Println("package expr") | ||
| 37 | fmt.Println() | 70 | fmt.Println() | ||
| 71 | + | fmt.Println("// casingGoUnicodeVersion is the Unicode version of the Go toolchain that generated") | |||
| 72 | + | fmt.Println("// the tables below: they are relative to its unicode.ToUpper and unicode.ToLower.") | |||
| 73 | + | fmt.Printf("const casingGoUnicodeVersion = %q\n", unicode.Version) | |||
| 74 | + | fmt.Println() | |||
| 38 | emit("jsUpperDelta", "JavaScript String.prototype.toUpperCase", d.Upper, unicode.ToUpper) | 75 | emit("jsUpperDelta", "JavaScript String.prototype.toUpperCase", d.Upper, unicode.ToUpper) | ||
| 39 | emit("jsLowerDelta", "JavaScript String.prototype.toLowerCase (without the Final_Sigma context)", d.Lower, unicode.ToLower) | 76 | emit("jsLowerDelta", "JavaScript String.prototype.toLowerCase (without the Final_Sigma context)", d.Lower, unicode.ToLower) | ||
| 40 | } | 77 | } | ||
| @@ -72,3 +109,8 @@ func emit(name, doc string, m map[string]string, goMap func(rune) rune) { | |||||
| 72 | fmt.Println("}") | 109 | fmt.Println("}") | ||
| 73 | fmt.Println() | 110 | fmt.Println() | ||
| 74 | } | 111 | } | ||
| 112 | + | ||||
| 113 | + | func fatal(err error) { | |||
| 114 | + | fmt.Fprintln(os.Stderr, err) | |||
| 115 | + | os.Exit(1) | |||
| 116 | + | } | |||
M expr/gen/casing_dump.mjs +1 -1
| @@ -4,7 +4,7 @@ | |||
| 4 | 4 | // whose mapping is not the identity, as JSON on stdout. It is the input of | |
| 5 | 5 | // casing.go, which turns it into ../casing_tables.go: | |
| 6 | 6 | // | |
| 7 | - | // node casing_dump.mjs | go run casing.go > ../casing_tables.go | |
| 7 | + | // go generate ./expr (runs: node gen/casing_dump.mjs | go run gen/casing.go -o casing_tables.go) | |
| 8 | 8 | // | |
| 9 | 9 | // The mapping is that of the JavaScript engine running the script (Node/V8), | |
| 10 | 10 | // which is what @actions/expressions uses for case-insensitive comparisons. | |
| @@ -4,7 +4,7 @@ | |||||
| 4 | // whose mapping is not the identity, as JSON on stdout. It is the input of | 4 | // whose mapping is not the identity, as JSON on stdout. It is the input of | ||
| 5 | // casing.go, which turns it into ../casing_tables.go: | 5 | // casing.go, which turns it into ../casing_tables.go: | ||
| 6 | // | 6 | // | ||
| 7 | - | // node casing_dump.mjs | go run casing.go > ../casing_tables.go | 7 | + | // go generate ./expr (runs: node gen/casing_dump.mjs | go run gen/casing.go -o casing_tables.go) |
| 8 | // | 8 | // | ||
| 9 | // The mapping is that of the JavaScript engine running the script (Node/V8), | 9 | // The mapping is that of the JavaScript engine running the script (Node/V8), | ||
| 10 | // which is what @actions/expressions uses for case-insensitive comparisons. | 10 | // which is what @actions/expressions uses for case-insensitive comparisons. | ||