detached a1598a3

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +243 -40
M expr/casing.go +186 -37
@@ -9,6 +9,8 @@ import (
99 "unicode/utf8"
1010 )
1111
12+// The tables in casing_tables.go are generated; see go:generate in doc.go.
13+
1214 // This file reproduces the JavaScript string functions the reference
1315 // implementation relies on: toUpperCase and toLowerCase (full Unicode case
1416 // mapping, including multi-character results such as "ß" to "SS" and the
@@ -39,52 +41,116 @@ func isASCII(s string) bool {
3941 return true
4042 }
4143
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
4556 }
46- return string(unicode.ToUpper(r))
57+ return 0, false
4758 }
4859
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
5390 }
5491 var b strings.Builder
5592 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
58104 }
59105 return b.String()
60106 }
61107
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 {
66121 if isASCII(s) {
67122 return strings.ToUpper(s)
68123 }
69124 var b strings.Builder
70125 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)
77130 }
78131 return b.String()
79132 }
80133
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+
81142 // jsToLower is String.prototype.toLowerCase, including the Final_Sigma
82143 // context rule (a capital sigma at the end of a word becomes ς).
83144 func jsToLower(s string) string {
84145 if isASCII(s) {
85146 return strings.ToLower(s)
86147 }
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+ }
88154 var b strings.Builder
89155 b.Grow(len(s))
90156 for i, r := range runes {
@@ -99,7 +165,7 @@ func jsToLower(s string) string {
99165 if m, ok := jsLowerDelta[r]; ok {
100166 b.WriteString(m)
101167 } else {
102- b.WriteRune(unicode.ToLower(r))
168+ writeRune(&b, unicode.ToLower(r))
103169 }
104170 }
105171 }
@@ -143,36 +209,119 @@ func isCaseIgnorable(r rune) bool {
143209 return unicode.In(r, unicode.Mn, unicode.Me, unicode.Cf, unicode.Lm, unicode.Sk)
144210 }
145211
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+
146233 // compareUTF16 compares two strings by UTF-16 code unit, which is how
147234 // JavaScript orders strings with < and >. It differs from byte or rune order
148235 // only for supplementary-plane characters against U+E000..U+FFFF.
149236 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] {
155244 return -1
156245 }
157246 return 1
158247 }
159- a, b = a[wa:], b[wb:]
160248 }
161249 switch {
162- case len(a) > 0:
250+ case len(ua) > len(ub):
163251 return 1
164- case len(b) > 0:
252+ case len(ua) < len(ub):
165253 return -1
166254 }
167255 return 0
168256 }
169257
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+ }
176325 }
177- return uint32(hi)<<16 | uint32(lo)
326+ return len(a) == len(b)
178327 }
@@ -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 reference14 // This file reproduces the JavaScript string functions the reference
13 // implementation relies on: toUpperCase and toLowerCase (full Unicode case15 // implementation relies on: toUpperCase and toLowerCase (full Unicode case
14 // mapping, including multi-character results such as "ß" to "SS" and the16 // mapping, including multi-character results such as "ß" to "SS" and the
@@ -39,52 +41,116 @@ func isASCII(s string) bool {
39 return true41 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 s46+// 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.Builder91 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 i108+func writeUpper(b *strings.Builder, r rune, keepDotlessI bool) {
63-// (U+0131) alone, as the reference implementation does for its109+ 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.Builder124 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- continue129+ 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_Sigma142 // 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.Builder154 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 how233 // compareUTF16 compares two strings by UTF-16 code unit, which is how
147 // JavaScript orders strings with < and >. It differs from byte or rune order234 // 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 -1244 return -1
156 }245 }
157 return 1246 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 1251 return 1
164- case len(b) > 0:252+ case len(ua) < len(ub):
165 return -1253 return -1
166 }254 }
167 return 0255 return 0
168 }256 }
169 257
170-// utf16Key packs the UTF-16 encoding of r into an integer that sorts like258+// 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 @@
44
55 package expr
66
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+
711 // jsUpperDelta maps the code points whose JavaScript String.prototype.toUpperCase result differs from the Go simple mapping.
812 var jsUpperDelta = map[rune]string{
913 0x00DF: "SS", // "ß"
@@ -4,6 +4,10 @@
4 4
5 package expr5 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 @@
11 // SPDX-License-Identifier: MIT
22
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+
36 // Package expr implements GitHub Actions expressions: the language found
47 // between the braces of ${{ }} in workflow files.
58 //
@@ -16,7 +19,7 @@
1619 //
1720 // node, err := expr.Parse("github.ref == 'refs/heads/main'", []string{"github"}, nil)
1821 // 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}})
2023 // if err != nil { /* *expr.EvalError */ }
2124 // if expr.Truthy(v) { /* ... */ }
2225 //
@@ -24,4 +27,9 @@
2427 // and [*Dictionary]. Dictionaries keep insertion order and look keys up
2528 // case-insensitively. [FromJSON] and [ToJSON] convert to and from JSON text
2629 // 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]).
2735 package expr
@@ -1,5 +1,8 @@
1 // SPDX-License-Identifier: MIT1 // 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 found6 // 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 up27 // and [*Dictionary]. Dictionaries keep insertion order and look keys up
25 // case-insensitively. [FromJSON] and [ToJSON] convert to and from JSON text28 // 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 expr35 package expr
M expr/gen/casing.go +43 -1
@@ -3,7 +3,7 @@
33 //go:build ignore
44
55 // 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
77 // mapping differs from the Go simple mapping (unicode.ToUpper/ToLower) are
88 // emitted: multi-character expansions (U+00DF to "SS") and Unicode version
99 // differences between Go and the JavaScript engine.
@@ -11,7 +11,10 @@ package main
1111
1212 import (
1313 "encoding/json"
14+ "flag"
1415 "fmt"
16+ "go/format"
17+ "io"
1518 "os"
1619 "sort"
1720 "strconv"
@@ -24,17 +27,51 @@ type dump struct {
2427 }
2528
2629 func main() {
30+ out := flag.String("o", "", "output file (default stdout)")
31+ flag.Parse()
2732 var d dump
2833 if err := json.NewDecoder(os.Stdin).Decode(&d); err != nil {
2934 fmt.Fprintln(os.Stderr, err)
3035 os.Exit(1)
3136 }
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+ }()
3265 fmt.Println("// SPDX-License-Identifier: MIT")
3366 fmt.Println()
3467 fmt.Println("// Code generated by gen/casing.go from gen/casing_dump.mjs; DO NOT EDIT.")
3568 fmt.Println()
3669 fmt.Println("package expr")
3770 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()
3875 emit("jsUpperDelta", "JavaScript String.prototype.toUpperCase", d.Upper, unicode.ToUpper)
3976 emit("jsLowerDelta", "JavaScript String.prototype.toLowerCase (without the Final_Sigma context)", d.Lower, unicode.ToLower)
4077 }
@@ -72,3 +109,8 @@ func emit(name, doc string, m map[string]string, goMap func(rune) rune) {
72109 fmt.Println("}")
73110 fmt.Println()
74111 }
112+
113+func fatal(err error) {
114+ fmt.Fprintln(os.Stderr, err)
115+ os.Exit(1)
116+}
@@ -3,7 +3,7 @@
3 //go:build ignore3 //go:build ignore
4 4
5 // Command casing reads the JSON produced by casing_dump.mjs on stdin and5 // 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 JavaScript6+// 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) are7 // mapping differs from the Go simple mapping (unicode.ToUpper/ToLower) are
8 // emitted: multi-character expansions (U+00DF to "SS") and Unicode version8 // 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 dump32 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 @@
44 // whose mapping is not the identity, as JSON on stdout. It is the input of
55 // casing.go, which turns it into ../casing_tables.go:
66 //
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)
88 //
99 // The mapping is that of the JavaScript engine running the script (Node/V8),
1010 // 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 of4 // 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.go7+// 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.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close