detached 5904904

feat(expr): add the value model, JSON conversion and JavaScript string semantics

commit
5904904d577d1849e9dae1391edb4bce70f6d5c8
author
brice · · 2026-10-12 14:46 UTC
parent
2cc2a9e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +1417 -0
A expr/casing.go +178 -0
@@ -0,0 +1,178 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import (
6+ "strings"
7+ "unicode"
8+ "unicode/utf16"
9+ "unicode/utf8"
10+)
11+
12+// This file reproduces the JavaScript string functions the reference
13+// implementation relies on: toUpperCase and toLowerCase (full Unicode case
14+// mapping, including multi-character results such as "ß" to "SS" and the
15+// Final_Sigma rule), trim, and the \s character class.
16+
17+// isJSSpace reports whether r is in JavaScript's WhiteSpace or
18+// LineTerminator sets. It is both the \s regular expression class and the
19+// set removed by String.prototype.trim and by Number().
20+func isJSSpace(r rune) bool {
21+ switch r {
22+ case '\t', '\n', '\v', '\f', '\r', ' ', 0xA0, 0x1680, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000, 0xFEFF:
23+ return true
24+ }
25+ return r >= 0x2000 && r <= 0x200A
26+}
27+
28+// jsTrim is String.prototype.trim.
29+func jsTrim(s string) string {
30+ return strings.TrimFunc(s, isJSSpace)
31+}
32+
33+func isASCII(s string) bool {
34+ for i := 0; i < len(s); i++ {
35+ if s[i] >= utf8.RuneSelf {
36+ return false
37+ }
38+ }
39+ return true
40+}
41+
42+func jsUpperRune(r rune) string {
43+ if s, ok := jsUpperDelta[r]; ok {
44+ return s
45+ }
46+ return string(unicode.ToUpper(r))
47+}
48+
49+// jsToUpper is String.prototype.toUpperCase.
50+func jsToUpper(s string) string {
51+ if isASCII(s) {
52+ return strings.ToUpper(s)
53+ }
54+ var b strings.Builder
55+ b.Grow(len(s))
56+ for _, r := range s {
57+ b.WriteString(jsUpperRune(r))
58+ }
59+ return b.String()
60+}
61+
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 {
66+ if isASCII(s) {
67+ return strings.ToUpper(s)
68+ }
69+ var b strings.Builder
70+ b.Grow(len(s))
71+ for _, r := range s {
72+ if r == 'ı' {
73+ b.WriteRune(r)
74+ continue
75+ }
76+ b.WriteString(jsUpperRune(r))
77+ }
78+ return b.String()
79+}
80+
81+// jsToLower is String.prototype.toLowerCase, including the Final_Sigma
82+// context rule (a capital sigma at the end of a word becomes ς).
83+func jsToLower(s string) string {
84+ if isASCII(s) {
85+ return strings.ToLower(s)
86+ }
87+ runes := []rune(s)
88+ var b strings.Builder
89+ b.Grow(len(s))
90+ for i, r := range runes {
91+ switch r {
92+ case 0x3A3:
93+ if finalSigma(runes, i) {
94+ b.WriteRune('ς')
95+ } else {
96+ b.WriteRune('σ')
97+ }
98+ default:
99+ if m, ok := jsLowerDelta[r]; ok {
100+ b.WriteString(m)
101+ } else {
102+ b.WriteRune(unicode.ToLower(r))
103+ }
104+ }
105+ }
106+ return b.String()
107+}
108+
109+// finalSigma implements the Final_Sigma condition of Unicode SpecialCasing:
110+// the sigma is preceded by a cased letter, ignoring case-ignorable
111+// characters, and is not followed by one.
112+func finalSigma(runes []rune, i int) bool {
113+ before := false
114+ for j := i - 1; j >= 0; j-- {
115+ if isCaseIgnorable(runes[j]) {
116+ continue
117+ }
118+ before = isCased(runes[j])
119+ break
120+ }
121+ if !before {
122+ return false
123+ }
124+ for j := i + 1; j < len(runes); j++ {
125+ if isCaseIgnorable(runes[j]) {
126+ continue
127+ }
128+ return !isCased(runes[j])
129+ }
130+ return true
131+}
132+
133+func isCased(r rune) bool {
134+ return unicode.IsUpper(r) || unicode.IsLower(r) || unicode.IsTitle(r) ||
135+ unicode.Is(unicode.Other_Lowercase, r) || unicode.Is(unicode.Other_Uppercase, r)
136+}
137+
138+func isCaseIgnorable(r rune) bool {
139+ switch r {
140+ case '\'', '.', ':', '^', '`', 0xA8, 0xAD, 0xAF, 0xB4, 0xB7, 0xB8, 0x2018, 0x2019, 0x2024, 0x2027, 0xFE13, 0xFE52, 0xFE55, 0xFF07, 0xFF0E, 0xFF1A:
141+ return true
142+ }
143+ return unicode.In(r, unicode.Mn, unicode.Me, unicode.Cf, unicode.Lm, unicode.Sk)
144+}
145+
146+// 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
148+// only for supplementary-plane characters against U+E000..U+FFFF.
149+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) {
155+ return -1
156+ }
157+ return 1
158+ }
159+ a, b = a[wa:], b[wb:]
160+ }
161+ switch {
162+ case len(a) > 0:
163+ return 1
164+ case len(b) > 0:
165+ return -1
166+ }
167+ return 0
168+}
169+
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)
176+ }
177+ return uint32(hi)<<16 | uint32(lo)
178+}
@@ -0,0 +1,178 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import (
6+ "strings"
7+ "unicode"
8+ "unicode/utf16"
9+ "unicode/utf8"
10+)
11+
12+// This file reproduces the JavaScript string functions the reference
13+// implementation relies on: toUpperCase and toLowerCase (full Unicode case
14+// mapping, including multi-character results such as "ß" to "SS" and the
15+// Final_Sigma rule), trim, and the \s character class.
16+
17+// isJSSpace reports whether r is in JavaScript's WhiteSpace or
18+// LineTerminator sets. It is both the \s regular expression class and the
19+// set removed by String.prototype.trim and by Number().
20+func isJSSpace(r rune) bool {
21+ switch r {
22+ case '\t', '\n', '\v', '\f', '\r', ' ', 0xA0, 0x1680, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000, 0xFEFF:
23+ return true
24+ }
25+ return r >= 0x2000 && r <= 0x200A
26+}
27+
28+// jsTrim is String.prototype.trim.
29+func jsTrim(s string) string {
30+ return strings.TrimFunc(s, isJSSpace)
31+}
32+
33+func isASCII(s string) bool {
34+ for i := 0; i < len(s); i++ {
35+ if s[i] >= utf8.RuneSelf {
36+ return false
37+ }
38+ }
39+ return true
40+}
41+
42+func jsUpperRune(r rune) string {
43+ if s, ok := jsUpperDelta[r]; ok {
44+ return s
45+ }
46+ return string(unicode.ToUpper(r))
47+}
48+
49+// jsToUpper is String.prototype.toUpperCase.
50+func jsToUpper(s string) string {
51+ if isASCII(s) {
52+ return strings.ToUpper(s)
53+ }
54+ var b strings.Builder
55+ b.Grow(len(s))
56+ for _, r := range s {
57+ b.WriteString(jsUpperRune(r))
58+ }
59+ return b.String()
60+}
61+
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 {
66+ if isASCII(s) {
67+ return strings.ToUpper(s)
68+ }
69+ var b strings.Builder
70+ b.Grow(len(s))
71+ for _, r := range s {
72+ if r == 'ı' {
73+ b.WriteRune(r)
74+ continue
75+ }
76+ b.WriteString(jsUpperRune(r))
77+ }
78+ return b.String()
79+}
80+
81+// jsToLower is String.prototype.toLowerCase, including the Final_Sigma
82+// context rule (a capital sigma at the end of a word becomes ς).
83+func jsToLower(s string) string {
84+ if isASCII(s) {
85+ return strings.ToLower(s)
86+ }
87+ runes := []rune(s)
88+ var b strings.Builder
89+ b.Grow(len(s))
90+ for i, r := range runes {
91+ switch r {
92+ case 0x3A3:
93+ if finalSigma(runes, i) {
94+ b.WriteRune('ς')
95+ } else {
96+ b.WriteRune('σ')
97+ }
98+ default:
99+ if m, ok := jsLowerDelta[r]; ok {
100+ b.WriteString(m)
101+ } else {
102+ b.WriteRune(unicode.ToLower(r))
103+ }
104+ }
105+ }
106+ return b.String()
107+}
108+
109+// finalSigma implements the Final_Sigma condition of Unicode SpecialCasing:
110+// the sigma is preceded by a cased letter, ignoring case-ignorable
111+// characters, and is not followed by one.
112+func finalSigma(runes []rune, i int) bool {
113+ before := false
114+ for j := i - 1; j >= 0; j-- {
115+ if isCaseIgnorable(runes[j]) {
116+ continue
117+ }
118+ before = isCased(runes[j])
119+ break
120+ }
121+ if !before {
122+ return false
123+ }
124+ for j := i + 1; j < len(runes); j++ {
125+ if isCaseIgnorable(runes[j]) {
126+ continue
127+ }
128+ return !isCased(runes[j])
129+ }
130+ return true
131+}
132+
133+func isCased(r rune) bool {
134+ return unicode.IsUpper(r) || unicode.IsLower(r) || unicode.IsTitle(r) ||
135+ unicode.Is(unicode.Other_Lowercase, r) || unicode.Is(unicode.Other_Uppercase, r)
136+}
137+
138+func isCaseIgnorable(r rune) bool {
139+ switch r {
140+ case '\'', '.', ':', '^', '`', 0xA8, 0xAD, 0xAF, 0xB4, 0xB7, 0xB8, 0x2018, 0x2019, 0x2024, 0x2027, 0xFE13, 0xFE52, 0xFE55, 0xFF07, 0xFF0E, 0xFF1A:
141+ return true
142+ }
143+ return unicode.In(r, unicode.Mn, unicode.Me, unicode.Cf, unicode.Lm, unicode.Sk)
144+}
145+
146+// 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
148+// only for supplementary-plane characters against U+E000..U+FFFF.
149+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) {
155+ return -1
156+ }
157+ return 1
158+ }
159+ a, b = a[wa:], b[wb:]
160+ }
161+ switch {
162+ case len(a) > 0:
163+ return 1
164+ case len(b) > 0:
165+ return -1
166+ }
167+ return 0
168+}
169+
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)
176+ }
177+ return uint32(hi)<<16 | uint32(lo)
178+}
A expr/casing_tables.go +172 -0
@@ -0,0 +1,172 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Code generated by gen/casing.go from gen/casing_dump.mjs; DO NOT EDIT.
4+
5+package expr
6+
7+// jsUpperDelta maps the code points whose JavaScript String.prototype.toUpperCase result differs from the Go simple mapping.
8+var jsUpperDelta = map[rune]string{
9+ 0x00DF: "SS", // "ß"
10+ 0x0149: "ʼN", // "ʼn"
11+ 0x01F0: "J̌", // "ǰ"
12+ 0x0390: "Ϊ́", // "ΐ"
13+ 0x03B0: "Ϋ́", // "ΰ"
14+ 0x0587: "ԵՒ", // "և"
15+ 0x1E96: "H̱", // "ẖ"
16+ 0x1E97: "T̈", // "ẗ"
17+ 0x1E98: "W̊", // "ẘ"
18+ 0x1E99: "Y̊", // "ẙ"
19+ 0x1E9A: "Aʾ", // "ẚ"
20+ 0x1F50: "Υ̓", // "ὐ"
21+ 0x1F52: "Υ̓̀", // "ὒ"
22+ 0x1F54: "Υ̓́", // "ὔ"
23+ 0x1F56: "Υ̓͂", // "ὖ"
24+ 0x1F80: "ἈΙ", // "ᾀ"
25+ 0x1F81: "ἉΙ", // "ᾁ"
26+ 0x1F82: "ἊΙ", // "ᾂ"
27+ 0x1F83: "ἋΙ", // "ᾃ"
28+ 0x1F84: "ἌΙ", // "ᾄ"
29+ 0x1F85: "ἍΙ", // "ᾅ"
30+ 0x1F86: "ἎΙ", // "ᾆ"
31+ 0x1F87: "ἏΙ", // "ᾇ"
32+ 0x1F88: "ἈΙ", // "ᾈ"
33+ 0x1F89: "ἉΙ", // "ᾉ"
34+ 0x1F8A: "ἊΙ", // "ᾊ"
35+ 0x1F8B: "ἋΙ", // "ᾋ"
36+ 0x1F8C: "ἌΙ", // "ᾌ"
37+ 0x1F8D: "ἍΙ", // "ᾍ"
38+ 0x1F8E: "ἎΙ", // "ᾎ"
39+ 0x1F8F: "ἏΙ", // "ᾏ"
40+ 0x1F90: "ἨΙ", // "ᾐ"
41+ 0x1F91: "ἩΙ", // "ᾑ"
42+ 0x1F92: "ἪΙ", // "ᾒ"
43+ 0x1F93: "ἫΙ", // "ᾓ"
44+ 0x1F94: "ἬΙ", // "ᾔ"
45+ 0x1F95: "ἭΙ", // "ᾕ"
46+ 0x1F96: "ἮΙ", // "ᾖ"
47+ 0x1F97: "ἯΙ", // "ᾗ"
48+ 0x1F98: "ἨΙ", // "ᾘ"
49+ 0x1F99: "ἩΙ", // "ᾙ"
50+ 0x1F9A: "ἪΙ", // "ᾚ"
51+ 0x1F9B: "ἫΙ", // "ᾛ"
52+ 0x1F9C: "ἬΙ", // "ᾜ"
53+ 0x1F9D: "ἭΙ", // "ᾝ"
54+ 0x1F9E: "ἮΙ", // "ᾞ"
55+ 0x1F9F: "ἯΙ", // "ᾟ"
56+ 0x1FA0: "ὨΙ", // "ᾠ"
57+ 0x1FA1: "ὩΙ", // "ᾡ"
58+ 0x1FA2: "ὪΙ", // "ᾢ"
59+ 0x1FA3: "ὫΙ", // "ᾣ"
60+ 0x1FA4: "ὬΙ", // "ᾤ"
61+ 0x1FA5: "ὭΙ", // "ᾥ"
62+ 0x1FA6: "ὮΙ", // "ᾦ"
63+ 0x1FA7: "ὯΙ", // "ᾧ"
64+ 0x1FA8: "ὨΙ", // "ᾨ"
65+ 0x1FA9: "ὩΙ", // "ᾩ"
66+ 0x1FAA: "ὪΙ", // "ᾪ"
67+ 0x1FAB: "ὫΙ", // "ᾫ"
68+ 0x1FAC: "ὬΙ", // "ᾬ"
69+ 0x1FAD: "ὭΙ", // "ᾭ"
70+ 0x1FAE: "ὮΙ", // "ᾮ"
71+ 0x1FAF: "ὯΙ", // "ᾯ"
72+ 0x1FB2: "ᾺΙ", // "ᾲ"
73+ 0x1FB3: "ΑΙ", // "ᾳ"
74+ 0x1FB4: "ΆΙ", // "ᾴ"
75+ 0x1FB6: "Α͂", // "ᾶ"
76+ 0x1FB7: "Α͂Ι", // "ᾷ"
77+ 0x1FBC: "ΑΙ", // "ᾼ"
78+ 0x1FC2: "ῊΙ", // "ῂ"
79+ 0x1FC3: "ΗΙ", // "ῃ"
80+ 0x1FC4: "ΉΙ", // "ῄ"
81+ 0x1FC6: "Η͂", // "ῆ"
82+ 0x1FC7: "Η͂Ι", // "ῇ"
83+ 0x1FCC: "ΗΙ", // "ῌ"
84+ 0x1FD2: "Ϊ̀", // "ῒ"
85+ 0x1FD3: "Ϊ́", // "ΐ"
86+ 0x1FD6: "Ι͂", // "ῖ"
87+ 0x1FD7: "Ϊ͂", // "ῗ"
88+ 0x1FE2: "Ϋ̀", // "ῢ"
89+ 0x1FE3: "Ϋ́", // "ΰ"
90+ 0x1FE4: "Ρ̓", // "ῤ"
91+ 0x1FE6: "Υ͂", // "ῦ"
92+ 0x1FE7: "Ϋ͂", // "ῧ"
93+ 0x1FF2: "ῺΙ", // "ῲ"
94+ 0x1FF3: "ΩΙ", // "ῳ"
95+ 0x1FF4: "ΏΙ", // "ῴ"
96+ 0x1FF6: "Ω͂", // "ῶ"
97+ 0x1FF7: "Ω͂Ι", // "ῷ"
98+ 0x1FFC: "ΩΙ", // "ῼ"
99+ 0xA7CF: "꟏", // "꟏"
100+ 0xA7D3: "ꟓ", // "ꟓ"
101+ 0xA7D5: "ꟕ", // "ꟕ"
102+ 0xFB00: "FF", // "ff"
103+ 0xFB01: "FI", // "fi"
104+ 0xFB02: "FL", // "fl"
105+ 0xFB03: "FFI", // "ffi"
106+ 0xFB04: "FFL", // "ffl"
107+ 0xFB05: "ST", // "ſt"
108+ 0xFB06: "ST", // "st"
109+ 0xFB13: "ՄՆ", // "ﬓ"
110+ 0xFB14: "ՄԵ", // "ﬔ"
111+ 0xFB15: "ՄԻ", // "ﬕ"
112+ 0xFB16: "ՎՆ", // "ﬖ"
113+ 0xFB17: "ՄԽ", // "ﬗ"
114+ 0x16EBB: "𖺻", // "𖺻"
115+ 0x16EBC: "𖺼", // "𖺼"
116+ 0x16EBD: "𖺽", // "𖺽"
117+ 0x16EBE: "𖺾", // "𖺾"
118+ 0x16EBF: "𖺿", // "𖺿"
119+ 0x16EC0: "𖻀", // "𖻀"
120+ 0x16EC1: "𖻁", // "𖻁"
121+ 0x16EC2: "𖻂", // "𖻂"
122+ 0x16EC3: "𖻃", // "𖻃"
123+ 0x16EC4: "𖻄", // "𖻄"
124+ 0x16EC5: "𖻅", // "𖻅"
125+ 0x16EC6: "𖻆", // "𖻆"
126+ 0x16EC7: "𖻇", // "𖻇"
127+ 0x16EC8: "𖻈", // "𖻈"
128+ 0x16EC9: "𖻉", // "𖻉"
129+ 0x16ECA: "𖻊", // "𖻊"
130+ 0x16ECB: "𖻋", // "𖻋"
131+ 0x16ECC: "𖻌", // "𖻌"
132+ 0x16ECD: "𖻍", // "𖻍"
133+ 0x16ECE: "𖻎", // "𖻎"
134+ 0x16ECF: "𖻏", // "𖻏"
135+ 0x16ED0: "𖻐", // "𖻐"
136+ 0x16ED1: "𖻑", // "𖻑"
137+ 0x16ED2: "𖻒", // "𖻒"
138+ 0x16ED3: "𖻓", // "𖻓"
139+}
140+
141+// jsLowerDelta maps the code points whose JavaScript String.prototype.toLowerCase (without the Final_Sigma context) result differs from the Go simple mapping.
142+var jsLowerDelta = map[rune]string{
143+ 0x0130: "i̇", // "İ"
144+ 0xA7CE: "꟎", // "꟎"
145+ 0xA7D2: "꟒", // "꟒"
146+ 0xA7D4: "꟔", // "꟔"
147+ 0x16EA0: "𖺠", // "𖺠"
148+ 0x16EA1: "𖺡", // "𖺡"
149+ 0x16EA2: "𖺢", // "𖺢"
150+ 0x16EA3: "𖺣", // "𖺣"
151+ 0x16EA4: "𖺤", // "𖺤"
152+ 0x16EA5: "𖺥", // "𖺥"
153+ 0x16EA6: "𖺦", // "𖺦"
154+ 0x16EA7: "𖺧", // "𖺧"
155+ 0x16EA8: "𖺨", // "𖺨"
156+ 0x16EA9: "𖺩", // "𖺩"
157+ 0x16EAA: "𖺪", // "𖺪"
158+ 0x16EAB: "𖺫", // "𖺫"
159+ 0x16EAC: "𖺬", // "𖺬"
160+ 0x16EAD: "𖺭", // "𖺭"
161+ 0x16EAE: "𖺮", // "𖺮"
162+ 0x16EAF: "𖺯", // "𖺯"
163+ 0x16EB0: "𖺰", // "𖺰"
164+ 0x16EB1: "𖺱", // "𖺱"
165+ 0x16EB2: "𖺲", // "𖺲"
166+ 0x16EB3: "𖺳", // "𖺳"
167+ 0x16EB4: "𖺴", // "𖺴"
168+ 0x16EB5: "𖺵", // "𖺵"
169+ 0x16EB6: "𖺶", // "𖺶"
170+ 0x16EB7: "𖺷", // "𖺷"
171+ 0x16EB8: "𖺸", // "𖺸"
172+}
@@ -0,0 +1,172 @@
1+// SPDX-License-Identifier: MIT
2+
3+// Code generated by gen/casing.go from gen/casing_dump.mjs; DO NOT EDIT.
4+
5+package expr
6+
7+// jsUpperDelta maps the code points whose JavaScript String.prototype.toUpperCase result differs from the Go simple mapping.
8+var jsUpperDelta = map[rune]string{
9+ 0x00DF: "SS", // "ß"
10+ 0x0149: "ʼN", // "ʼn"
11+ 0x01F0: "J̌", // "ǰ"
12+ 0x0390: "Ϊ́", // "ΐ"
13+ 0x03B0: "Ϋ́", // "ΰ"
14+ 0x0587: "ԵՒ", // "և"
15+ 0x1E96: "H̱", // "ẖ"
16+ 0x1E97: "T̈", // "ẗ"
17+ 0x1E98: "W̊", // "ẘ"
18+ 0x1E99: "Y̊", // "ẙ"
19+ 0x1E9A: "Aʾ", // "ẚ"
20+ 0x1F50: "Υ̓", // "ὐ"
21+ 0x1F52: "Υ̓̀", // "ὒ"
22+ 0x1F54: "Υ̓́", // "ὔ"
23+ 0x1F56: "Υ̓͂", // "ὖ"
24+ 0x1F80: "ἈΙ", // "ᾀ"
25+ 0x1F81: "ἉΙ", // "ᾁ"
26+ 0x1F82: "ἊΙ", // "ᾂ"
27+ 0x1F83: "ἋΙ", // "ᾃ"
28+ 0x1F84: "ἌΙ", // "ᾄ"
29+ 0x1F85: "ἍΙ", // "ᾅ"
30+ 0x1F86: "ἎΙ", // "ᾆ"
31+ 0x1F87: "ἏΙ", // "ᾇ"
32+ 0x1F88: "ἈΙ", // "ᾈ"
33+ 0x1F89: "ἉΙ", // "ᾉ"
34+ 0x1F8A: "ἊΙ", // "ᾊ"
35+ 0x1F8B: "ἋΙ", // "ᾋ"
36+ 0x1F8C: "ἌΙ", // "ᾌ"
37+ 0x1F8D: "ἍΙ", // "ᾍ"
38+ 0x1F8E: "ἎΙ", // "ᾎ"
39+ 0x1F8F: "ἏΙ", // "ᾏ"
40+ 0x1F90: "ἨΙ", // "ᾐ"
41+ 0x1F91: "ἩΙ", // "ᾑ"
42+ 0x1F92: "ἪΙ", // "ᾒ"
43+ 0x1F93: "ἫΙ", // "ᾓ"
44+ 0x1F94: "ἬΙ", // "ᾔ"
45+ 0x1F95: "ἭΙ", // "ᾕ"
46+ 0x1F96: "ἮΙ", // "ᾖ"
47+ 0x1F97: "ἯΙ", // "ᾗ"
48+ 0x1F98: "ἨΙ", // "ᾘ"
49+ 0x1F99: "ἩΙ", // "ᾙ"
50+ 0x1F9A: "ἪΙ", // "ᾚ"
51+ 0x1F9B: "ἫΙ", // "ᾛ"
52+ 0x1F9C: "ἬΙ", // "ᾜ"
53+ 0x1F9D: "ἭΙ", // "ᾝ"
54+ 0x1F9E: "ἮΙ", // "ᾞ"
55+ 0x1F9F: "ἯΙ", // "ᾟ"
56+ 0x1FA0: "ὨΙ", // "ᾠ"
57+ 0x1FA1: "ὩΙ", // "ᾡ"
58+ 0x1FA2: "ὪΙ", // "ᾢ"
59+ 0x1FA3: "ὫΙ", // "ᾣ"
60+ 0x1FA4: "ὬΙ", // "ᾤ"
61+ 0x1FA5: "ὭΙ", // "ᾥ"
62+ 0x1FA6: "ὮΙ", // "ᾦ"
63+ 0x1FA7: "ὯΙ", // "ᾧ"
64+ 0x1FA8: "ὨΙ", // "ᾨ"
65+ 0x1FA9: "ὩΙ", // "ᾩ"
66+ 0x1FAA: "ὪΙ", // "ᾪ"
67+ 0x1FAB: "ὫΙ", // "ᾫ"
68+ 0x1FAC: "ὬΙ", // "ᾬ"
69+ 0x1FAD: "ὭΙ", // "ᾭ"
70+ 0x1FAE: "ὮΙ", // "ᾮ"
71+ 0x1FAF: "ὯΙ", // "ᾯ"
72+ 0x1FB2: "ᾺΙ", // "ᾲ"
73+ 0x1FB3: "ΑΙ", // "ᾳ"
74+ 0x1FB4: "ΆΙ", // "ᾴ"
75+ 0x1FB6: "Α͂", // "ᾶ"
76+ 0x1FB7: "Α͂Ι", // "ᾷ"
77+ 0x1FBC: "ΑΙ", // "ᾼ"
78+ 0x1FC2: "ῊΙ", // "ῂ"
79+ 0x1FC3: "ΗΙ", // "ῃ"
80+ 0x1FC4: "ΉΙ", // "ῄ"
81+ 0x1FC6: "Η͂", // "ῆ"
82+ 0x1FC7: "Η͂Ι", // "ῇ"
83+ 0x1FCC: "ΗΙ", // "ῌ"
84+ 0x1FD2: "Ϊ̀", // "ῒ"
85+ 0x1FD3: "Ϊ́", // "ΐ"
86+ 0x1FD6: "Ι͂", // "ῖ"
87+ 0x1FD7: "Ϊ͂", // "ῗ"
88+ 0x1FE2: "Ϋ̀", // "ῢ"
89+ 0x1FE3: "Ϋ́", // "ΰ"
90+ 0x1FE4: "Ρ̓", // "ῤ"
91+ 0x1FE6: "Υ͂", // "ῦ"
92+ 0x1FE7: "Ϋ͂", // "ῧ"
93+ 0x1FF2: "ῺΙ", // "ῲ"
94+ 0x1FF3: "ΩΙ", // "ῳ"
95+ 0x1FF4: "ΏΙ", // "ῴ"
96+ 0x1FF6: "Ω͂", // "ῶ"
97+ 0x1FF7: "Ω͂Ι", // "ῷ"
98+ 0x1FFC: "ΩΙ", // "ῼ"
99+ 0xA7CF: "꟏", // "꟏"
100+ 0xA7D3: "ꟓ", // "ꟓ"
101+ 0xA7D5: "ꟕ", // "ꟕ"
102+ 0xFB00: "FF", // "ff"
103+ 0xFB01: "FI", // "fi"
104+ 0xFB02: "FL", // "fl"
105+ 0xFB03: "FFI", // "ffi"
106+ 0xFB04: "FFL", // "ffl"
107+ 0xFB05: "ST", // "ſt"
108+ 0xFB06: "ST", // "st"
109+ 0xFB13: "ՄՆ", // "ﬓ"
110+ 0xFB14: "ՄԵ", // "ﬔ"
111+ 0xFB15: "ՄԻ", // "ﬕ"
112+ 0xFB16: "ՎՆ", // "ﬖ"
113+ 0xFB17: "ՄԽ", // "ﬗ"
114+ 0x16EBB: "𖺻", // "𖺻"
115+ 0x16EBC: "𖺼", // "𖺼"
116+ 0x16EBD: "𖺽", // "𖺽"
117+ 0x16EBE: "𖺾", // "𖺾"
118+ 0x16EBF: "𖺿", // "𖺿"
119+ 0x16EC0: "𖻀", // "𖻀"
120+ 0x16EC1: "𖻁", // "𖻁"
121+ 0x16EC2: "𖻂", // "𖻂"
122+ 0x16EC3: "𖻃", // "𖻃"
123+ 0x16EC4: "𖻄", // "𖻄"
124+ 0x16EC5: "𖻅", // "𖻅"
125+ 0x16EC6: "𖻆", // "𖻆"
126+ 0x16EC7: "𖻇", // "𖻇"
127+ 0x16EC8: "𖻈", // "𖻈"
128+ 0x16EC9: "𖻉", // "𖻉"
129+ 0x16ECA: "𖻊", // "𖻊"
130+ 0x16ECB: "𖻋", // "𖻋"
131+ 0x16ECC: "𖻌", // "𖻌"
132+ 0x16ECD: "𖻍", // "𖻍"
133+ 0x16ECE: "𖻎", // "𖻎"
134+ 0x16ECF: "𖻏", // "𖻏"
135+ 0x16ED0: "𖻐", // "𖻐"
136+ 0x16ED1: "𖻑", // "𖻑"
137+ 0x16ED2: "𖻒", // "𖻒"
138+ 0x16ED3: "𖻓", // "𖻓"
139+}
140+
141+// jsLowerDelta maps the code points whose JavaScript String.prototype.toLowerCase (without the Final_Sigma context) result differs from the Go simple mapping.
142+var jsLowerDelta = map[rune]string{
143+ 0x0130: "i̇", // "İ"
144+ 0xA7CE: "꟎", // "꟎"
145+ 0xA7D2: "꟒", // "꟒"
146+ 0xA7D4: "꟔", // "꟔"
147+ 0x16EA0: "𖺠", // "𖺠"
148+ 0x16EA1: "𖺡", // "𖺡"
149+ 0x16EA2: "𖺢", // "𖺢"
150+ 0x16EA3: "𖺣", // "𖺣"
151+ 0x16EA4: "𖺤", // "𖺤"
152+ 0x16EA5: "𖺥", // "𖺥"
153+ 0x16EA6: "𖺦", // "𖺦"
154+ 0x16EA7: "𖺧", // "𖺧"
155+ 0x16EA8: "𖺨", // "𖺨"
156+ 0x16EA9: "𖺩", // "𖺩"
157+ 0x16EAA: "𖺪", // "𖺪"
158+ 0x16EAB: "𖺫", // "𖺫"
159+ 0x16EAC: "𖺬", // "𖺬"
160+ 0x16EAD: "𖺭", // "𖺭"
161+ 0x16EAE: "𖺮", // "𖺮"
162+ 0x16EAF: "𖺯", // "𖺯"
163+ 0x16EB0: "𖺰", // "𖺰"
164+ 0x16EB1: "𖺱", // "𖺱"
165+ 0x16EB2: "𖺲", // "𖺲"
166+ 0x16EB3: "𖺳", // "𖺳"
167+ 0x16EB4: "𖺴", // "𖺴"
168+ 0x16EB5: "𖺵", // "𖺵"
169+ 0x16EB6: "𖺶", // "𖺶"
170+ 0x16EB7: "𖺷", // "𖺷"
171+ 0x16EB8: "𖺸", // "𖺸"
172+}
A expr/data.go +301 -0
@@ -0,0 +1,301 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import "math"
6+
7+// Kind is the type of a [Value].
8+type Kind int
9+
10+// The kinds of value an expression can produce.
11+const (
12+ KindNull Kind = iota
13+ KindBoolean
14+ KindNumber
15+ KindString
16+ KindArray
17+ KindDictionary
18+)
19+
20+// String returns the name GitHub uses for the kind when a non-primitive
21+// value is converted to a string: "Array" and "Object" for the two compound
22+// kinds.
23+func (k Kind) String() string {
24+ switch k {
25+ case KindNull:
26+ return "Null"
27+ case KindBoolean:
28+ return "Boolean"
29+ case KindNumber:
30+ return "Number"
31+ case KindString:
32+ return "String"
33+ case KindArray:
34+ return "Array"
35+ case KindDictionary:
36+ return "Object"
37+ }
38+ return "unknown"
39+}
40+
41+// Value is the result of an expression or an element of a context: one of
42+// [Null], [Boolean], [Number], [String], [*Array] and [*Dictionary]. The set
43+// is closed. A nil Value, or a nil *Array or *Dictionary, behaves as [Null].
44+type Value interface {
45+ // Kind reports the type of the value.
46+ Kind() Kind
47+ isValue()
48+}
49+
50+// Null is the null value.
51+type Null struct{}
52+
53+// Boolean is a true or false value.
54+type Boolean bool
55+
56+// Number is a double-precision floating-point number. NaN and the infinities
57+// are legal values.
58+type Number float64
59+
60+// String is a text value.
61+type String string
62+
63+func (Null) Kind() Kind { return KindNull }
64+func (Boolean) Kind() Kind { return KindBoolean }
65+func (Number) Kind() Kind { return KindNumber }
66+func (String) Kind() Kind { return KindString }
67+func (*Array) Kind() Kind { return KindArray }
68+func (*Dictionary) Kind() Kind { return KindDictionary }
69+
70+func (Null) isValue() {}
71+func (Boolean) isValue() {}
72+func (Number) isValue() {}
73+func (String) isValue() {}
74+func (*Array) isValue() {}
75+func (*Dictionary) isValue() {}
76+
77+// Array is an ordered list of values. Two arrays are equal in an expression
78+// only if they are the same *Array.
79+type Array struct {
80+ items []Value
81+ // filtered is set on arrays produced by the * filter ("a.*", "a[*]"),
82+ // which flatten one level when indexed again ("a.*.b").
83+ filtered bool
84+}
85+
86+// NewArray returns an array holding items.
87+func NewArray(items ...Value) *Array {
88+ return &Array{items: append([]Value(nil), items...)}
89+}
90+
91+// Len returns the number of elements.
92+func (a *Array) Len() int {
93+ if a == nil {
94+ return 0
95+ }
96+ return len(a.items)
97+}
98+
99+// At returns the element at index i, or [Null] when i is out of range.
100+func (a *Array) At(i int) Value {
101+ if a == nil || i < 0 || i >= len(a.items) {
102+ return Null{}
103+ }
104+ return normalize(a.items[i])
105+}
106+
107+// Items returns a copy of the elements.
108+func (a *Array) Items() []Value {
109+ if a == nil {
110+ return nil
111+ }
112+ out := make([]Value, len(a.items))
113+ for i, v := range a.items {
114+ out[i] = normalize(v)
115+ }
116+ return out
117+}
118+
119+// Append adds v at the end of the array.
120+func (a *Array) Append(v Value) { a.items = append(a.items, v) }
121+
122+// Filtered reports whether the array was produced by a * filter.
123+func (a *Array) Filtered() bool { return a != nil && a.filtered }
124+
125+// Pair is a key and its value in a [Dictionary].
126+type Pair struct {
127+ Key string
128+ Value Value
129+}
130+
131+// Dictionary is an ordered map from string keys to values. Keys are looked up
132+// case-insensitively and keep the spelling and position of their first
133+// insertion. Two dictionaries are equal in an expression only if they are
134+// the same *Dictionary.
135+type Dictionary struct {
136+ keys []string
137+ vals []Value
138+ index map[string]int
139+}
140+
141+// NewDictionary returns an empty dictionary.
142+func NewDictionary() *Dictionary {
143+ return &Dictionary{index: map[string]int{}}
144+}
145+
146+// Add inserts key with value v and reports whether it did. When a key that
147+// differs only by case is already present it does nothing and returns false:
148+// the first insertion wins.
149+func (d *Dictionary) Add(key string, v Value) bool {
150+ if d.index == nil {
151+ d.index = map[string]int{}
152+ }
153+ folded := foldKey(key)
154+ if _, ok := d.index[folded]; ok {
155+ return false
156+ }
157+ d.index[folded] = len(d.vals)
158+ d.keys = append(d.keys, key)
159+ d.vals = append(d.vals, v)
160+ return true
161+}
162+
163+// Get returns the value stored under key, compared case-insensitively.
164+func (d *Dictionary) Get(key string) (Value, bool) {
165+ if d == nil {
166+ return nil, false
167+ }
168+ i, ok := d.index[foldKey(key)]
169+ if !ok {
170+ return nil, false
171+ }
172+ return normalize(d.vals[i]), true
173+}
174+
175+// Len returns the number of entries.
176+func (d *Dictionary) Len() int {
177+ if d == nil {
178+ return 0
179+ }
180+ return len(d.vals)
181+}
182+
183+// Keys returns the keys in insertion order.
184+func (d *Dictionary) Keys() []string {
185+ if d == nil {
186+ return nil
187+ }
188+ return append([]string(nil), d.keys...)
189+}
190+
191+// Values returns the values in insertion order.
192+func (d *Dictionary) Values() []Value {
193+ if d == nil {
194+ return nil
195+ }
196+ out := make([]Value, len(d.vals))
197+ for i, v := range d.vals {
198+ out[i] = normalize(v)
199+ }
200+ return out
201+}
202+
203+// Pairs returns the entries in insertion order.
204+func (d *Dictionary) Pairs() []Pair {
205+ if d == nil {
206+ return nil
207+ }
208+ out := make([]Pair, len(d.keys))
209+ for i, k := range d.keys {
210+ out[i] = Pair{Key: k, Value: normalize(d.vals[i])}
211+ }
212+ return out
213+}
214+
215+// foldKey is the case-insensitive form of a dictionary key. GitHub lower-cases
216+// keys with JavaScript's toLowerCase.
217+func foldKey(key string) string { return jsToLower(key) }
218+
219+// normalize maps nil values to Null so that callers can switch on Kind.
220+func normalize(v Value) Value {
221+ switch x := v.(type) {
222+ case nil:
223+ return Null{}
224+ case *Array:
225+ if x == nil {
226+ return Null{}
227+ }
228+ case *Dictionary:
229+ if x == nil {
230+ return Null{}
231+ }
232+ }
233+ return v
234+}
235+
236+// isPrimitive reports whether v is a Null, Boolean, Number or String.
237+func isPrimitive(v Value) bool {
238+ switch normalize(v).Kind() {
239+ case KindNull, KindBoolean, KindNumber, KindString:
240+ return true
241+ }
242+ return false
243+}
244+
245+// toNumber converts v to a number the way GitHub does: null is 0, booleans
246+// are 1 and 0, strings are parsed like JavaScript's Number(), and arrays and
247+// dictionaries are NaN.
248+func toNumber(v Value) float64 {
249+ switch x := normalize(v).(type) {
250+ case Null:
251+ return 0
252+ case Boolean:
253+ if x {
254+ return 1
255+ }
256+ return 0
257+ case Number:
258+ return float64(x)
259+ case String:
260+ return jsToNumber(string(x))
261+ }
262+ return nan
263+}
264+
265+// CoerceString converts v to the string GitHub shows for it: null is "", booleans
266+// are "true" and "false", numbers are formatted with at most 15 decimals
267+// ("0.1" for 0.1+0.2) and arrays and dictionaries are "Array" and "Object".
268+func CoerceString(v Value) string {
269+ switch x := normalize(v).(type) {
270+ case Null:
271+ return ""
272+ case Boolean:
273+ if x {
274+ return "true"
275+ }
276+ return "false"
277+ case Number:
278+ return numberToCoercedString(float64(x))
279+ case String:
280+ return string(x)
281+ default:
282+ return x.Kind().String()
283+ }
284+}
285+
286+// Truthy reports whether v is true when used as a condition. Null, false, 0,
287+// NaN and the empty string are falsy; everything else, including every array
288+// and dictionary, is truthy.
289+func Truthy(v Value) bool {
290+ switch x := normalize(v).(type) {
291+ case Null:
292+ return false
293+ case Boolean:
294+ return bool(x)
295+ case Number:
296+ return x != 0 && !math.IsNaN(float64(x))
297+ case String:
298+ return len(x) != 0
299+ }
300+ return true
301+}
@@ -0,0 +1,301 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import "math"
6+
7+// Kind is the type of a [Value].
8+type Kind int
9+
10+// The kinds of value an expression can produce.
11+const (
12+ KindNull Kind = iota
13+ KindBoolean
14+ KindNumber
15+ KindString
16+ KindArray
17+ KindDictionary
18+)
19+
20+// String returns the name GitHub uses for the kind when a non-primitive
21+// value is converted to a string: "Array" and "Object" for the two compound
22+// kinds.
23+func (k Kind) String() string {
24+ switch k {
25+ case KindNull:
26+ return "Null"
27+ case KindBoolean:
28+ return "Boolean"
29+ case KindNumber:
30+ return "Number"
31+ case KindString:
32+ return "String"
33+ case KindArray:
34+ return "Array"
35+ case KindDictionary:
36+ return "Object"
37+ }
38+ return "unknown"
39+}
40+
41+// Value is the result of an expression or an element of a context: one of
42+// [Null], [Boolean], [Number], [String], [*Array] and [*Dictionary]. The set
43+// is closed. A nil Value, or a nil *Array or *Dictionary, behaves as [Null].
44+type Value interface {
45+ // Kind reports the type of the value.
46+ Kind() Kind
47+ isValue()
48+}
49+
50+// Null is the null value.
51+type Null struct{}
52+
53+// Boolean is a true or false value.
54+type Boolean bool
55+
56+// Number is a double-precision floating-point number. NaN and the infinities
57+// are legal values.
58+type Number float64
59+
60+// String is a text value.
61+type String string
62+
63+func (Null) Kind() Kind { return KindNull }
64+func (Boolean) Kind() Kind { return KindBoolean }
65+func (Number) Kind() Kind { return KindNumber }
66+func (String) Kind() Kind { return KindString }
67+func (*Array) Kind() Kind { return KindArray }
68+func (*Dictionary) Kind() Kind { return KindDictionary }
69+
70+func (Null) isValue() {}
71+func (Boolean) isValue() {}
72+func (Number) isValue() {}
73+func (String) isValue() {}
74+func (*Array) isValue() {}
75+func (*Dictionary) isValue() {}
76+
77+// Array is an ordered list of values. Two arrays are equal in an expression
78+// only if they are the same *Array.
79+type Array struct {
80+ items []Value
81+ // filtered is set on arrays produced by the * filter ("a.*", "a[*]"),
82+ // which flatten one level when indexed again ("a.*.b").
83+ filtered bool
84+}
85+
86+// NewArray returns an array holding items.
87+func NewArray(items ...Value) *Array {
88+ return &Array{items: append([]Value(nil), items...)}
89+}
90+
91+// Len returns the number of elements.
92+func (a *Array) Len() int {
93+ if a == nil {
94+ return 0
95+ }
96+ return len(a.items)
97+}
98+
99+// At returns the element at index i, or [Null] when i is out of range.
100+func (a *Array) At(i int) Value {
101+ if a == nil || i < 0 || i >= len(a.items) {
102+ return Null{}
103+ }
104+ return normalize(a.items[i])
105+}
106+
107+// Items returns a copy of the elements.
108+func (a *Array) Items() []Value {
109+ if a == nil {
110+ return nil
111+ }
112+ out := make([]Value, len(a.items))
113+ for i, v := range a.items {
114+ out[i] = normalize(v)
115+ }
116+ return out
117+}
118+
119+// Append adds v at the end of the array.
120+func (a *Array) Append(v Value) { a.items = append(a.items, v) }
121+
122+// Filtered reports whether the array was produced by a * filter.
123+func (a *Array) Filtered() bool { return a != nil && a.filtered }
124+
125+// Pair is a key and its value in a [Dictionary].
126+type Pair struct {
127+ Key string
128+ Value Value
129+}
130+
131+// Dictionary is an ordered map from string keys to values. Keys are looked up
132+// case-insensitively and keep the spelling and position of their first
133+// insertion. Two dictionaries are equal in an expression only if they are
134+// the same *Dictionary.
135+type Dictionary struct {
136+ keys []string
137+ vals []Value
138+ index map[string]int
139+}
140+
141+// NewDictionary returns an empty dictionary.
142+func NewDictionary() *Dictionary {
143+ return &Dictionary{index: map[string]int{}}
144+}
145+
146+// Add inserts key with value v and reports whether it did. When a key that
147+// differs only by case is already present it does nothing and returns false:
148+// the first insertion wins.
149+func (d *Dictionary) Add(key string, v Value) bool {
150+ if d.index == nil {
151+ d.index = map[string]int{}
152+ }
153+ folded := foldKey(key)
154+ if _, ok := d.index[folded]; ok {
155+ return false
156+ }
157+ d.index[folded] = len(d.vals)
158+ d.keys = append(d.keys, key)
159+ d.vals = append(d.vals, v)
160+ return true
161+}
162+
163+// Get returns the value stored under key, compared case-insensitively.
164+func (d *Dictionary) Get(key string) (Value, bool) {
165+ if d == nil {
166+ return nil, false
167+ }
168+ i, ok := d.index[foldKey(key)]
169+ if !ok {
170+ return nil, false
171+ }
172+ return normalize(d.vals[i]), true
173+}
174+
175+// Len returns the number of entries.
176+func (d *Dictionary) Len() int {
177+ if d == nil {
178+ return 0
179+ }
180+ return len(d.vals)
181+}
182+
183+// Keys returns the keys in insertion order.
184+func (d *Dictionary) Keys() []string {
185+ if d == nil {
186+ return nil
187+ }
188+ return append([]string(nil), d.keys...)
189+}
190+
191+// Values returns the values in insertion order.
192+func (d *Dictionary) Values() []Value {
193+ if d == nil {
194+ return nil
195+ }
196+ out := make([]Value, len(d.vals))
197+ for i, v := range d.vals {
198+ out[i] = normalize(v)
199+ }
200+ return out
201+}
202+
203+// Pairs returns the entries in insertion order.
204+func (d *Dictionary) Pairs() []Pair {
205+ if d == nil {
206+ return nil
207+ }
208+ out := make([]Pair, len(d.keys))
209+ for i, k := range d.keys {
210+ out[i] = Pair{Key: k, Value: normalize(d.vals[i])}
211+ }
212+ return out
213+}
214+
215+// foldKey is the case-insensitive form of a dictionary key. GitHub lower-cases
216+// keys with JavaScript's toLowerCase.
217+func foldKey(key string) string { return jsToLower(key) }
218+
219+// normalize maps nil values to Null so that callers can switch on Kind.
220+func normalize(v Value) Value {
221+ switch x := v.(type) {
222+ case nil:
223+ return Null{}
224+ case *Array:
225+ if x == nil {
226+ return Null{}
227+ }
228+ case *Dictionary:
229+ if x == nil {
230+ return Null{}
231+ }
232+ }
233+ return v
234+}
235+
236+// isPrimitive reports whether v is a Null, Boolean, Number or String.
237+func isPrimitive(v Value) bool {
238+ switch normalize(v).Kind() {
239+ case KindNull, KindBoolean, KindNumber, KindString:
240+ return true
241+ }
242+ return false
243+}
244+
245+// toNumber converts v to a number the way GitHub does: null is 0, booleans
246+// are 1 and 0, strings are parsed like JavaScript's Number(), and arrays and
247+// dictionaries are NaN.
248+func toNumber(v Value) float64 {
249+ switch x := normalize(v).(type) {
250+ case Null:
251+ return 0
252+ case Boolean:
253+ if x {
254+ return 1
255+ }
256+ return 0
257+ case Number:
258+ return float64(x)
259+ case String:
260+ return jsToNumber(string(x))
261+ }
262+ return nan
263+}
264+
265+// CoerceString converts v to the string GitHub shows for it: null is "", booleans
266+// are "true" and "false", numbers are formatted with at most 15 decimals
267+// ("0.1" for 0.1+0.2) and arrays and dictionaries are "Array" and "Object".
268+func CoerceString(v Value) string {
269+ switch x := normalize(v).(type) {
270+ case Null:
271+ return ""
272+ case Boolean:
273+ if x {
274+ return "true"
275+ }
276+ return "false"
277+ case Number:
278+ return numberToCoercedString(float64(x))
279+ case String:
280+ return string(x)
281+ default:
282+ return x.Kind().String()
283+ }
284+}
285+
286+// Truthy reports whether v is true when used as a condition. Null, false, 0,
287+// NaN and the empty string are falsy; everything else, including every array
288+// and dictionary, is truthy.
289+func Truthy(v Value) bool {
290+ switch x := normalize(v).(type) {
291+ case Null:
292+ return false
293+ case Boolean:
294+ return bool(x)
295+ case Number:
296+ return x != 0 && !math.IsNaN(float64(x))
297+ case String:
298+ return len(x) != 0
299+ }
300+ return true
301+}
A expr/gen/casing.go +74 -0
@@ -0,0 +1,74 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build ignore
4+
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
7+// mapping differs from the Go simple mapping (unicode.ToUpper/ToLower) are
8+// emitted: multi-character expansions (U+00DF to "SS") and Unicode version
9+// differences between Go and the JavaScript engine.
10+package main
11+
12+import (
13+ "encoding/json"
14+ "fmt"
15+ "os"
16+ "sort"
17+ "strconv"
18+ "unicode"
19+)
20+
21+type dump struct {
22+ Upper map[string]string `json:"upper"`
23+ Lower map[string]string `json:"lower"`
24+}
25+
26+func main() {
27+ var d dump
28+ if err := json.NewDecoder(os.Stdin).Decode(&d); err != nil {
29+ fmt.Fprintln(os.Stderr, err)
30+ os.Exit(1)
31+ }
32+ fmt.Println("// SPDX-License-Identifier: MIT")
33+ fmt.Println()
34+ fmt.Println("// Code generated by gen/casing.go from gen/casing_dump.mjs; DO NOT EDIT.")
35+ fmt.Println()
36+ fmt.Println("package expr")
37+ fmt.Println()
38+ 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)
40+}
41+
42+func emit(name, doc string, m map[string]string, goMap func(rune) rune) {
43+ // Include code points that JavaScript maps to themselves but Go does not.
44+ type entry struct {
45+ cp rune
46+ s string
47+ }
48+ var out []entry
49+ seen := map[rune]bool{}
50+ for k, v := range m {
51+ n, _ := strconv.Atoi(k)
52+ r := rune(n)
53+ seen[r] = true
54+ if string(goMap(r)) != v {
55+ out = append(out, entry{r, v})
56+ }
57+ }
58+ for r := rune(0); r <= unicode.MaxRune; r++ {
59+ if r >= 0xd800 && r <= 0xdfff || seen[r] {
60+ continue
61+ }
62+ if goMap(r) != r {
63+ out = append(out, entry{r, string(r)})
64+ }
65+ }
66+ sort.Slice(out, func(i, j int) bool { return out[i].cp < out[j].cp })
67+ fmt.Printf("// %s maps the code points whose %s result differs from the Go simple mapping.\n", name, doc)
68+ fmt.Printf("var %s = map[rune]string{\n", name)
69+ for _, e := range out {
70+ fmt.Printf("\t0x%04X: %s, // %s\n", e.cp, strconv.Quote(e.s), strconv.QuoteToGraphic(string(e.cp)))
71+ }
72+ fmt.Println("}")
73+ fmt.Println()
74+}
@@ -0,0 +1,74 @@
1+// SPDX-License-Identifier: MIT
2+
3+//go:build ignore
4+
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
7+// mapping differs from the Go simple mapping (unicode.ToUpper/ToLower) are
8+// emitted: multi-character expansions (U+00DF to "SS") and Unicode version
9+// differences between Go and the JavaScript engine.
10+package main
11+
12+import (
13+ "encoding/json"
14+ "fmt"
15+ "os"
16+ "sort"
17+ "strconv"
18+ "unicode"
19+)
20+
21+type dump struct {
22+ Upper map[string]string `json:"upper"`
23+ Lower map[string]string `json:"lower"`
24+}
25+
26+func main() {
27+ var d dump
28+ if err := json.NewDecoder(os.Stdin).Decode(&d); err != nil {
29+ fmt.Fprintln(os.Stderr, err)
30+ os.Exit(1)
31+ }
32+ fmt.Println("// SPDX-License-Identifier: MIT")
33+ fmt.Println()
34+ fmt.Println("// Code generated by gen/casing.go from gen/casing_dump.mjs; DO NOT EDIT.")
35+ fmt.Println()
36+ fmt.Println("package expr")
37+ fmt.Println()
38+ 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)
40+}
41+
42+func emit(name, doc string, m map[string]string, goMap func(rune) rune) {
43+ // Include code points that JavaScript maps to themselves but Go does not.
44+ type entry struct {
45+ cp rune
46+ s string
47+ }
48+ var out []entry
49+ seen := map[rune]bool{}
50+ for k, v := range m {
51+ n, _ := strconv.Atoi(k)
52+ r := rune(n)
53+ seen[r] = true
54+ if string(goMap(r)) != v {
55+ out = append(out, entry{r, v})
56+ }
57+ }
58+ for r := rune(0); r <= unicode.MaxRune; r++ {
59+ if r >= 0xd800 && r <= 0xdfff || seen[r] {
60+ continue
61+ }
62+ if goMap(r) != r {
63+ out = append(out, entry{r, string(r)})
64+ }
65+ }
66+ sort.Slice(out, func(i, j int) bool { return out[i].cp < out[j].cp })
67+ fmt.Printf("// %s maps the code points whose %s result differs from the Go simple mapping.\n", name, doc)
68+ fmt.Printf("var %s = map[rune]string{\n", name)
69+ for _, e := range out {
70+ fmt.Printf("\t0x%04X: %s, // %s\n", e.cp, strconv.Quote(e.s), strconv.QuoteToGraphic(string(e.cp)))
71+ }
72+ fmt.Println("}")
73+ fmt.Println()
74+}
A expr/gen/casing_dump.mjs +21 -0
@@ -0,0 +1,21 @@
1+// SPDX-License-Identifier: MIT
2+//
3+// Dumps the JavaScript toUpperCase/toLowerCase mapping of every code point
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:
6+//
7+// node casing_dump.mjs | go run casing.go > ../casing_tables.go
8+//
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.
11+const upper = {};
12+const lower = {};
13+for (let cp = 0; cp <= 0x10ffff; cp++) {
14+ if (cp >= 0xd800 && cp <= 0xdfff) continue;
15+ const s = String.fromCodePoint(cp);
16+ const u = s.toUpperCase();
17+ const l = s.toLowerCase();
18+ if (u !== s) upper[cp] = u;
19+ if (l !== s) lower[cp] = l;
20+}
21+process.stdout.write(JSON.stringify({ upper, lower }));
@@ -0,0 +1,21 @@
1+// SPDX-License-Identifier: MIT
2+//
3+// Dumps the JavaScript toUpperCase/toLowerCase mapping of every code point
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:
6+//
7+// node casing_dump.mjs | go run casing.go > ../casing_tables.go
8+//
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.
11+const upper = {};
12+const lower = {};
13+for (let cp = 0; cp <= 0x10ffff; cp++) {
14+ if (cp >= 0xd800 && cp <= 0xdfff) continue;
15+ const s = String.fromCodePoint(cp);
16+ const u = s.toUpperCase();
17+ const l = s.toLowerCase();
18+ if (u !== s) upper[cp] = u;
19+ if (l !== s) lower[cp] = l;
20+}
21+process.stdout.write(JSON.stringify({ upper, lower }));
A expr/json.go +491 -0
@@ -0,0 +1,491 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "math"
9+ "sort"
10+ "strconv"
11+ "strings"
12+ "unicode/utf8"
13+)
14+
15+// maxJSONDepth bounds the nesting FromJSON accepts. JavaScript engines fail
16+// with a stack overflow somewhere in that region, which fromJSON reports like
17+// any other parse error.
18+const maxJSONDepth = 10000
19+
20+// FromJSON parses JSON text into a [Value], like the fromJSON function.
21+//
22+// The grammar is strict RFC 8259 JSON as JSON.parse accepts it. Numbers are
23+// float64 (1e999 is +Inf). Objects become [*Dictionary] values: keys that
24+// are array indices ("0", "42") come first in ascending order, then the rest
25+// in document order, as JavaScript enumerates object keys; a repeated key
26+// keeps its first position and its last value; keys that differ only by case
27+// collapse to the first.
28+func FromJSON(data []byte) (Value, error) {
29+ s := string(data)
30+ if !utf8.ValidString(s) {
31+ s = string([]rune(s)) // invalid bytes become U+FFFD, as in a JS string
32+ }
33+ p := &jsonParser{s: s}
34+ p.skipSpace()
35+ v, err := p.value(0)
36+ if err != nil {
37+ return nil, err
38+ }
39+ p.skipSpace()
40+ if p.i < len(p.s) {
41+ return nil, p.unexpected()
42+ }
43+ return v, nil
44+}
45+
46+type jsonParser struct {
47+ s string
48+ i int
49+}
50+
51+func (p *jsonParser) unexpected() error {
52+ if p.i >= len(p.s) {
53+ return errors.New("unexpected end of JSON input")
54+ }
55+ r, _ := utf8.DecodeRuneInString(p.s[p.i:])
56+ return fmt.Errorf("unexpected token %q in JSON at position %d", r, p.i)
57+}
58+
59+func (p *jsonParser) skipSpace() {
60+ for p.i < len(p.s) {
61+ switch p.s[p.i] {
62+ case ' ', '\t', '\n', '\r':
63+ p.i++
64+ default:
65+ return
66+ }
67+ }
68+}
69+
70+func (p *jsonParser) literal(word string, v Value) (Value, error) {
71+ if !strings.HasPrefix(p.s[p.i:], word) {
72+ // Advance to the first mismatching byte for the error position.
73+ for j := 0; j < len(word) && p.i < len(p.s) && p.s[p.i] == word[j]; j++ {
74+ p.i++
75+ }
76+ return nil, p.unexpected()
77+ }
78+ p.i += len(word)
79+ return v, nil
80+}
81+
82+func (p *jsonParser) value(depth int) (Value, error) {
83+ if depth > maxJSONDepth {
84+ return nil, errors.New("maximum JSON nesting depth exceeded")
85+ }
86+ if p.i >= len(p.s) {
87+ return nil, p.unexpected()
88+ }
89+ switch c := p.s[p.i]; {
90+ case c == '{':
91+ return p.object(depth)
92+ case c == '[':
93+ return p.array(depth)
94+ case c == '"':
95+ s, err := p.str()
96+ if err != nil {
97+ return nil, err
98+ }
99+ return String(s), nil
100+ case c == 't':
101+ return p.literal("true", Boolean(true))
102+ case c == 'f':
103+ return p.literal("false", Boolean(false))
104+ case c == 'n':
105+ return p.literal("null", Null{})
106+ case c == '-' || c >= '0' && c <= '9':
107+ return p.number()
108+ }
109+ return nil, p.unexpected()
110+}
111+
112+func (p *jsonParser) number() (Value, error) {
113+ start := p.i
114+ if p.s[p.i] == '-' {
115+ p.i++
116+ }
117+ digits := func() int {
118+ n := 0
119+ for p.i < len(p.s) && p.s[p.i] >= '0' && p.s[p.i] <= '9' {
120+ p.i++
121+ n++
122+ }
123+ return n
124+ }
125+ switch {
126+ case p.i < len(p.s) && p.s[p.i] == '0':
127+ p.i++
128+ case digits() == 0:
129+ return nil, p.unexpected()
130+ }
131+ if p.i < len(p.s) && p.s[p.i] == '.' {
132+ p.i++
133+ if digits() == 0 {
134+ return nil, p.unexpected()
135+ }
136+ }
137+ if p.i < len(p.s) && (p.s[p.i] == 'e' || p.s[p.i] == 'E') {
138+ p.i++
139+ if p.i < len(p.s) && (p.s[p.i] == '+' || p.s[p.i] == '-') {
140+ p.i++
141+ }
142+ if digits() == 0 {
143+ return nil, p.unexpected()
144+ }
145+ }
146+ f, _ := strconv.ParseFloat(p.s[start:p.i], 64) // range errors give ±Inf
147+ return Number(f), nil
148+}
149+
150+// str reads a string literal; p.i is on the opening quote.
151+func (p *jsonParser) str() (string, error) {
152+ p.i++
153+ var b strings.Builder
154+ for {
155+ if p.i >= len(p.s) {
156+ return "", errors.New("unterminated string in JSON")
157+ }
158+ c := p.s[p.i]
159+ switch {
160+ case c == '"':
161+ p.i++
162+ return b.String(), nil
163+ case c < 0x20:
164+ return "", p.unexpected()
165+ case c != '\\':
166+ b.WriteByte(c)
167+ p.i++
168+ continue
169+ }
170+ p.i++
171+ if p.i >= len(p.s) {
172+ return "", errors.New("unterminated string in JSON")
173+ }
174+ e := p.s[p.i]
175+ p.i++
176+ switch e {
177+ case '"', '\\', '/':
178+ b.WriteByte(e)
179+ case 'b':
180+ b.WriteByte('\b')
181+ case 'f':
182+ b.WriteByte('\f')
183+ case 'n':
184+ b.WriteByte('\n')
185+ case 'r':
186+ b.WriteByte('\r')
187+ case 't':
188+ b.WriteByte('\t')
189+ case 'u':
190+ r, err := p.hex4()
191+ if err != nil {
192+ return "", err
193+ }
194+ if r >= 0xD800 && r < 0xDC00 && strings.HasPrefix(p.s[p.i:], `\u`) {
195+ save := p.i
196+ p.i += 2
197+ lo, err := p.hex4()
198+ if err != nil {
199+ return "", err
200+ }
201+ if lo >= 0xDC00 && lo < 0xE000 {
202+ r = 0x10000 + (r-0xD800)<<10 + (lo - 0xDC00)
203+ } else {
204+ p.i = save // not a pair; the second escape is read on its own
205+ }
206+ }
207+ if r >= 0xD800 && r < 0xE000 {
208+ // A lone surrogate cannot be a Go rune. Keep it as its
209+ // three-byte WTF-8 form so that toJSON writes it back as
210+ // the same \uXXXX escape, like JSON.stringify.
211+ b.WriteByte(0xE0 | byte(r>>12))
212+ b.WriteByte(0x80 | byte(r>>6)&0x3F)
213+ b.WriteByte(0x80 | byte(r)&0x3F)
214+ continue
215+ }
216+ b.WriteRune(r)
217+ default:
218+ p.i--
219+ return "", p.unexpected()
220+ }
221+ }
222+}
223+
224+func (p *jsonParser) hex4() (rune, error) {
225+ if p.i+4 > len(p.s) {
226+ p.i = len(p.s)
227+ return 0, p.unexpected()
228+ }
229+ var r rune
230+ for k := 0; k < 4; k++ {
231+ c := p.s[p.i]
232+ var d byte
233+ switch {
234+ case c >= '0' && c <= '9':
235+ d = c - '0'
236+ case c >= 'a' && c <= 'f':
237+ d = c - 'a' + 10
238+ case c >= 'A' && c <= 'F':
239+ d = c - 'A' + 10
240+ default:
241+ return 0, p.unexpected()
242+ }
243+ r = r<<4 | rune(d)
244+ p.i++
245+ }
246+ return r, nil
247+}
248+
249+func (p *jsonParser) array(depth int) (Value, error) {
250+ p.i++
251+ arr := &Array{}
252+ p.skipSpace()
253+ if p.i < len(p.s) && p.s[p.i] == ']' {
254+ p.i++
255+ return arr, nil
256+ }
257+ for {
258+ p.skipSpace()
259+ v, err := p.value(depth + 1)
260+ if err != nil {
261+ return nil, err
262+ }
263+ arr.items = append(arr.items, v)
264+ p.skipSpace()
265+ if p.i >= len(p.s) {
266+ return nil, p.unexpected()
267+ }
268+ switch p.s[p.i] {
269+ case ',':
270+ p.i++
271+ case ']':
272+ p.i++
273+ return arr, nil
274+ default:
275+ return nil, p.unexpected()
276+ }
277+ }
278+}
279+
280+func (p *jsonParser) object(depth int) (Value, error) {
281+ p.i++
282+ var keys []string
283+ vals := map[string]Value{}
284+ p.skipSpace()
285+ if p.i < len(p.s) && p.s[p.i] == '}' {
286+ p.i++
287+ return NewDictionary(), nil
288+ }
289+ for {
290+ p.skipSpace()
291+ if p.i >= len(p.s) || p.s[p.i] != '"' {
292+ return nil, p.unexpected()
293+ }
294+ k, err := p.str()
295+ if err != nil {
296+ return nil, err
297+ }
298+ p.skipSpace()
299+ if p.i >= len(p.s) || p.s[p.i] != ':' {
300+ return nil, p.unexpected()
301+ }
302+ p.i++
303+ p.skipSpace()
304+ v, err := p.value(depth + 1)
305+ if err != nil {
306+ return nil, err
307+ }
308+ if _, dup := vals[k]; !dup {
309+ keys = append(keys, k)
310+ }
311+ vals[k] = v // last value wins, at the first position
312+ p.skipSpace()
313+ if p.i >= len(p.s) {
314+ return nil, p.unexpected()
315+ }
316+ switch p.s[p.i] {
317+ case ',':
318+ p.i++
319+ continue
320+ case '}':
321+ p.i++
322+ default:
323+ return nil, p.unexpected()
324+ }
325+ break
326+ }
327+ d := NewDictionary()
328+ for _, k := range jsKeyOrder(keys) {
329+ d.Add(k, vals[k])
330+ }
331+ return d, nil
332+}
333+
334+// isArrayIndex reports whether k is the canonical decimal form of an integer
335+// in [0, 2^32-2], the keys JavaScript enumerates before all others.
336+func isArrayIndex(k string) bool {
337+ if k == "" || len(k) > 10 || (len(k) > 1 && k[0] == '0') {
338+ return false
339+ }
340+ for i := 0; i < len(k); i++ {
341+ if k[i] < '0' || k[i] > '9' {
342+ return false
343+ }
344+ }
345+ n, _ := strconv.ParseUint(k, 10, 64)
346+ return n < math.MaxUint32
347+}
348+
349+// jsKeyOrder reorders keys the way Object.keys enumerates a JavaScript
350+// object: array indices ascending, then the others in their given order.
351+func jsKeyOrder(keys []string) []string {
352+ var idx, rest []string
353+ for _, k := range keys {
354+ if isArrayIndex(k) {
355+ idx = append(idx, k)
356+ } else {
357+ rest = append(rest, k)
358+ }
359+ }
360+ if len(idx) == 0 {
361+ return keys
362+ }
363+ sort.Slice(idx, func(i, j int) bool {
364+ a, _ := strconv.ParseUint(idx[i], 10, 64)
365+ b, _ := strconv.ParseUint(idx[j], 10, 64)
366+ return a < b
367+ })
368+ return append(idx, rest...)
369+}
370+
371+// ToJSON renders v as the toJSON function does: JSON.stringify(v, replacer,
372+// " "), that is two-space indentation, "key": value pairs, "[]" and "{}"
373+// for empty containers, and numbers as JavaScript prints them (NaN and the
374+// infinities as null, -0 as 0, 1e21 as 1e+21). Dictionary keys that are array
375+// indices come first in ascending order, as when GitHub's replacer builds a
376+// JavaScript object. Control characters, quotes and backslashes in strings
377+// are escaped; everything else, including non-ASCII text, is written as is.
378+// Lone surrogates, which FromJSON keeps in WTF-8 form, are written as \uXXXX.
379+func ToJSON(v Value) string {
380+ var b strings.Builder
381+ writeJSON(&b, v, 0)
382+ return b.String()
383+}
384+
385+func writeJSON(b *strings.Builder, v Value, depth int) {
386+ switch x := normalize(v).(type) {
387+ case Null:
388+ b.WriteString("null")
389+ case Boolean:
390+ if x {
391+ b.WriteString("true")
392+ } else {
393+ b.WriteString("false")
394+ }
395+ case Number:
396+ f := float64(x)
397+ if math.IsNaN(f) || math.IsInf(f, 0) {
398+ b.WriteString("null")
399+ } else {
400+ b.WriteString(jsNumberToString(f))
401+ }
402+ case String:
403+ writeJSONString(b, string(x))
404+ case *Array:
405+ if len(x.items) == 0 {
406+ b.WriteString("[]")
407+ return
408+ }
409+ b.WriteString("[\n")
410+ for i, it := range x.items {
411+ indent(b, depth+1)
412+ writeJSON(b, it, depth+1)
413+ if i < len(x.items)-1 {
414+ b.WriteByte(',')
415+ }
416+ b.WriteByte('\n')
417+ }
418+ indent(b, depth)
419+ b.WriteByte(']')
420+ case *Dictionary:
421+ if len(x.keys) == 0 {
422+ b.WriteString("{}")
423+ return
424+ }
425+ keys := jsKeyOrder(x.keys)
426+ b.WriteString("{\n")
427+ for i, k := range keys {
428+ val, _ := x.Get(k)
429+ indent(b, depth+1)
430+ writeJSONString(b, k)
431+ b.WriteString(": ")
432+ writeJSON(b, val, depth+1)
433+ if i < len(keys)-1 {
434+ b.WriteByte(',')
435+ }
436+ b.WriteByte('\n')
437+ }
438+ indent(b, depth)
439+ b.WriteByte('}')
440+ }
441+}
442+
443+func indent(b *strings.Builder, depth int) {
444+ for i := 0; i < depth; i++ {
445+ b.WriteString(" ")
446+ }
447+}
448+
449+const hexDigits = "0123456789abcdef"
450+
451+func writeJSONString(b *strings.Builder, s string) {
452+ b.WriteByte('"')
453+ for i := 0; i < len(s); {
454+ r, w := utf8.DecodeRuneInString(s[i:])
455+ if r == utf8.RuneError && w == 1 && i+2 < len(s) && s[i] == 0xED && s[i+1] >= 0xA0 && s[i+1] <= 0xBF && s[i+2]&0xC0 == 0x80 {
456+ // WTF-8 lone surrogate (see FromJSON).
457+ sur := rune(s[i]&0x0F)<<12 | rune(s[i+1]&0x3F)<<6 | rune(s[i+2]&0x3F)
458+ b.WriteString(`\u`)
459+ b.WriteByte(hexDigits[sur>>12])
460+ b.WriteByte(hexDigits[sur>>8&0xF])
461+ b.WriteByte(hexDigits[sur>>4&0xF])
462+ b.WriteByte(hexDigits[sur&0xF])
463+ i += 3
464+ continue
465+ }
466+ i += w
467+ switch {
468+ case r == '"':
469+ b.WriteString(`\"`)
470+ case r == '\\':
471+ b.WriteString(`\\`)
472+ case r == '\b':
473+ b.WriteString(`\b`)
474+ case r == '\f':
475+ b.WriteString(`\f`)
476+ case r == '\n':
477+ b.WriteString(`\n`)
478+ case r == '\r':
479+ b.WriteString(`\r`)
480+ case r == '\t':
481+ b.WriteString(`\t`)
482+ case r < 0x20:
483+ b.WriteString(`\u00`)
484+ b.WriteByte(hexDigits[r>>4])
485+ b.WriteByte(hexDigits[r&0xF])
486+ default:
487+ b.WriteRune(r) // other invalid UTF-8 comes out as U+FFFD
488+ }
489+ }
490+ b.WriteByte('"')
491+}
@@ -0,0 +1,491 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "math"
9+ "sort"
10+ "strconv"
11+ "strings"
12+ "unicode/utf8"
13+)
14+
15+// maxJSONDepth bounds the nesting FromJSON accepts. JavaScript engines fail
16+// with a stack overflow somewhere in that region, which fromJSON reports like
17+// any other parse error.
18+const maxJSONDepth = 10000
19+
20+// FromJSON parses JSON text into a [Value], like the fromJSON function.
21+//
22+// The grammar is strict RFC 8259 JSON as JSON.parse accepts it. Numbers are
23+// float64 (1e999 is +Inf). Objects become [*Dictionary] values: keys that
24+// are array indices ("0", "42") come first in ascending order, then the rest
25+// in document order, as JavaScript enumerates object keys; a repeated key
26+// keeps its first position and its last value; keys that differ only by case
27+// collapse to the first.
28+func FromJSON(data []byte) (Value, error) {
29+ s := string(data)
30+ if !utf8.ValidString(s) {
31+ s = string([]rune(s)) // invalid bytes become U+FFFD, as in a JS string
32+ }
33+ p := &jsonParser{s: s}
34+ p.skipSpace()
35+ v, err := p.value(0)
36+ if err != nil {
37+ return nil, err
38+ }
39+ p.skipSpace()
40+ if p.i < len(p.s) {
41+ return nil, p.unexpected()
42+ }
43+ return v, nil
44+}
45+
46+type jsonParser struct {
47+ s string
48+ i int
49+}
50+
51+func (p *jsonParser) unexpected() error {
52+ if p.i >= len(p.s) {
53+ return errors.New("unexpected end of JSON input")
54+ }
55+ r, _ := utf8.DecodeRuneInString(p.s[p.i:])
56+ return fmt.Errorf("unexpected token %q in JSON at position %d", r, p.i)
57+}
58+
59+func (p *jsonParser) skipSpace() {
60+ for p.i < len(p.s) {
61+ switch p.s[p.i] {
62+ case ' ', '\t', '\n', '\r':
63+ p.i++
64+ default:
65+ return
66+ }
67+ }
68+}
69+
70+func (p *jsonParser) literal(word string, v Value) (Value, error) {
71+ if !strings.HasPrefix(p.s[p.i:], word) {
72+ // Advance to the first mismatching byte for the error position.
73+ for j := 0; j < len(word) && p.i < len(p.s) && p.s[p.i] == word[j]; j++ {
74+ p.i++
75+ }
76+ return nil, p.unexpected()
77+ }
78+ p.i += len(word)
79+ return v, nil
80+}
81+
82+func (p *jsonParser) value(depth int) (Value, error) {
83+ if depth > maxJSONDepth {
84+ return nil, errors.New("maximum JSON nesting depth exceeded")
85+ }
86+ if p.i >= len(p.s) {
87+ return nil, p.unexpected()
88+ }
89+ switch c := p.s[p.i]; {
90+ case c == '{':
91+ return p.object(depth)
92+ case c == '[':
93+ return p.array(depth)
94+ case c == '"':
95+ s, err := p.str()
96+ if err != nil {
97+ return nil, err
98+ }
99+ return String(s), nil
100+ case c == 't':
101+ return p.literal("true", Boolean(true))
102+ case c == 'f':
103+ return p.literal("false", Boolean(false))
104+ case c == 'n':
105+ return p.literal("null", Null{})
106+ case c == '-' || c >= '0' && c <= '9':
107+ return p.number()
108+ }
109+ return nil, p.unexpected()
110+}
111+
112+func (p *jsonParser) number() (Value, error) {
113+ start := p.i
114+ if p.s[p.i] == '-' {
115+ p.i++
116+ }
117+ digits := func() int {
118+ n := 0
119+ for p.i < len(p.s) && p.s[p.i] >= '0' && p.s[p.i] <= '9' {
120+ p.i++
121+ n++
122+ }
123+ return n
124+ }
125+ switch {
126+ case p.i < len(p.s) && p.s[p.i] == '0':
127+ p.i++
128+ case digits() == 0:
129+ return nil, p.unexpected()
130+ }
131+ if p.i < len(p.s) && p.s[p.i] == '.' {
132+ p.i++
133+ if digits() == 0 {
134+ return nil, p.unexpected()
135+ }
136+ }
137+ if p.i < len(p.s) && (p.s[p.i] == 'e' || p.s[p.i] == 'E') {
138+ p.i++
139+ if p.i < len(p.s) && (p.s[p.i] == '+' || p.s[p.i] == '-') {
140+ p.i++
141+ }
142+ if digits() == 0 {
143+ return nil, p.unexpected()
144+ }
145+ }
146+ f, _ := strconv.ParseFloat(p.s[start:p.i], 64) // range errors give ±Inf
147+ return Number(f), nil
148+}
149+
150+// str reads a string literal; p.i is on the opening quote.
151+func (p *jsonParser) str() (string, error) {
152+ p.i++
153+ var b strings.Builder
154+ for {
155+ if p.i >= len(p.s) {
156+ return "", errors.New("unterminated string in JSON")
157+ }
158+ c := p.s[p.i]
159+ switch {
160+ case c == '"':
161+ p.i++
162+ return b.String(), nil
163+ case c < 0x20:
164+ return "", p.unexpected()
165+ case c != '\\':
166+ b.WriteByte(c)
167+ p.i++
168+ continue
169+ }
170+ p.i++
171+ if p.i >= len(p.s) {
172+ return "", errors.New("unterminated string in JSON")
173+ }
174+ e := p.s[p.i]
175+ p.i++
176+ switch e {
177+ case '"', '\\', '/':
178+ b.WriteByte(e)
179+ case 'b':
180+ b.WriteByte('\b')
181+ case 'f':
182+ b.WriteByte('\f')
183+ case 'n':
184+ b.WriteByte('\n')
185+ case 'r':
186+ b.WriteByte('\r')
187+ case 't':
188+ b.WriteByte('\t')
189+ case 'u':
190+ r, err := p.hex4()
191+ if err != nil {
192+ return "", err
193+ }
194+ if r >= 0xD800 && r < 0xDC00 && strings.HasPrefix(p.s[p.i:], `\u`) {
195+ save := p.i
196+ p.i += 2
197+ lo, err := p.hex4()
198+ if err != nil {
199+ return "", err
200+ }
201+ if lo >= 0xDC00 && lo < 0xE000 {
202+ r = 0x10000 + (r-0xD800)<<10 + (lo - 0xDC00)
203+ } else {
204+ p.i = save // not a pair; the second escape is read on its own
205+ }
206+ }
207+ if r >= 0xD800 && r < 0xE000 {
208+ // A lone surrogate cannot be a Go rune. Keep it as its
209+ // three-byte WTF-8 form so that toJSON writes it back as
210+ // the same \uXXXX escape, like JSON.stringify.
211+ b.WriteByte(0xE0 | byte(r>>12))
212+ b.WriteByte(0x80 | byte(r>>6)&0x3F)
213+ b.WriteByte(0x80 | byte(r)&0x3F)
214+ continue
215+ }
216+ b.WriteRune(r)
217+ default:
218+ p.i--
219+ return "", p.unexpected()
220+ }
221+ }
222+}
223+
224+func (p *jsonParser) hex4() (rune, error) {
225+ if p.i+4 > len(p.s) {
226+ p.i = len(p.s)
227+ return 0, p.unexpected()
228+ }
229+ var r rune
230+ for k := 0; k < 4; k++ {
231+ c := p.s[p.i]
232+ var d byte
233+ switch {
234+ case c >= '0' && c <= '9':
235+ d = c - '0'
236+ case c >= 'a' && c <= 'f':
237+ d = c - 'a' + 10
238+ case c >= 'A' && c <= 'F':
239+ d = c - 'A' + 10
240+ default:
241+ return 0, p.unexpected()
242+ }
243+ r = r<<4 | rune(d)
244+ p.i++
245+ }
246+ return r, nil
247+}
248+
249+func (p *jsonParser) array(depth int) (Value, error) {
250+ p.i++
251+ arr := &Array{}
252+ p.skipSpace()
253+ if p.i < len(p.s) && p.s[p.i] == ']' {
254+ p.i++
255+ return arr, nil
256+ }
257+ for {
258+ p.skipSpace()
259+ v, err := p.value(depth + 1)
260+ if err != nil {
261+ return nil, err
262+ }
263+ arr.items = append(arr.items, v)
264+ p.skipSpace()
265+ if p.i >= len(p.s) {
266+ return nil, p.unexpected()
267+ }
268+ switch p.s[p.i] {
269+ case ',':
270+ p.i++
271+ case ']':
272+ p.i++
273+ return arr, nil
274+ default:
275+ return nil, p.unexpected()
276+ }
277+ }
278+}
279+
280+func (p *jsonParser) object(depth int) (Value, error) {
281+ p.i++
282+ var keys []string
283+ vals := map[string]Value{}
284+ p.skipSpace()
285+ if p.i < len(p.s) && p.s[p.i] == '}' {
286+ p.i++
287+ return NewDictionary(), nil
288+ }
289+ for {
290+ p.skipSpace()
291+ if p.i >= len(p.s) || p.s[p.i] != '"' {
292+ return nil, p.unexpected()
293+ }
294+ k, err := p.str()
295+ if err != nil {
296+ return nil, err
297+ }
298+ p.skipSpace()
299+ if p.i >= len(p.s) || p.s[p.i] != ':' {
300+ return nil, p.unexpected()
301+ }
302+ p.i++
303+ p.skipSpace()
304+ v, err := p.value(depth + 1)
305+ if err != nil {
306+ return nil, err
307+ }
308+ if _, dup := vals[k]; !dup {
309+ keys = append(keys, k)
310+ }
311+ vals[k] = v // last value wins, at the first position
312+ p.skipSpace()
313+ if p.i >= len(p.s) {
314+ return nil, p.unexpected()
315+ }
316+ switch p.s[p.i] {
317+ case ',':
318+ p.i++
319+ continue
320+ case '}':
321+ p.i++
322+ default:
323+ return nil, p.unexpected()
324+ }
325+ break
326+ }
327+ d := NewDictionary()
328+ for _, k := range jsKeyOrder(keys) {
329+ d.Add(k, vals[k])
330+ }
331+ return d, nil
332+}
333+
334+// isArrayIndex reports whether k is the canonical decimal form of an integer
335+// in [0, 2^32-2], the keys JavaScript enumerates before all others.
336+func isArrayIndex(k string) bool {
337+ if k == "" || len(k) > 10 || (len(k) > 1 && k[0] == '0') {
338+ return false
339+ }
340+ for i := 0; i < len(k); i++ {
341+ if k[i] < '0' || k[i] > '9' {
342+ return false
343+ }
344+ }
345+ n, _ := strconv.ParseUint(k, 10, 64)
346+ return n < math.MaxUint32
347+}
348+
349+// jsKeyOrder reorders keys the way Object.keys enumerates a JavaScript
350+// object: array indices ascending, then the others in their given order.
351+func jsKeyOrder(keys []string) []string {
352+ var idx, rest []string
353+ for _, k := range keys {
354+ if isArrayIndex(k) {
355+ idx = append(idx, k)
356+ } else {
357+ rest = append(rest, k)
358+ }
359+ }
360+ if len(idx) == 0 {
361+ return keys
362+ }
363+ sort.Slice(idx, func(i, j int) bool {
364+ a, _ := strconv.ParseUint(idx[i], 10, 64)
365+ b, _ := strconv.ParseUint(idx[j], 10, 64)
366+ return a < b
367+ })
368+ return append(idx, rest...)
369+}
370+
371+// ToJSON renders v as the toJSON function does: JSON.stringify(v, replacer,
372+// " "), that is two-space indentation, "key": value pairs, "[]" and "{}"
373+// for empty containers, and numbers as JavaScript prints them (NaN and the
374+// infinities as null, -0 as 0, 1e21 as 1e+21). Dictionary keys that are array
375+// indices come first in ascending order, as when GitHub's replacer builds a
376+// JavaScript object. Control characters, quotes and backslashes in strings
377+// are escaped; everything else, including non-ASCII text, is written as is.
378+// Lone surrogates, which FromJSON keeps in WTF-8 form, are written as \uXXXX.
379+func ToJSON(v Value) string {
380+ var b strings.Builder
381+ writeJSON(&b, v, 0)
382+ return b.String()
383+}
384+
385+func writeJSON(b *strings.Builder, v Value, depth int) {
386+ switch x := normalize(v).(type) {
387+ case Null:
388+ b.WriteString("null")
389+ case Boolean:
390+ if x {
391+ b.WriteString("true")
392+ } else {
393+ b.WriteString("false")
394+ }
395+ case Number:
396+ f := float64(x)
397+ if math.IsNaN(f) || math.IsInf(f, 0) {
398+ b.WriteString("null")
399+ } else {
400+ b.WriteString(jsNumberToString(f))
401+ }
402+ case String:
403+ writeJSONString(b, string(x))
404+ case *Array:
405+ if len(x.items) == 0 {
406+ b.WriteString("[]")
407+ return
408+ }
409+ b.WriteString("[\n")
410+ for i, it := range x.items {
411+ indent(b, depth+1)
412+ writeJSON(b, it, depth+1)
413+ if i < len(x.items)-1 {
414+ b.WriteByte(',')
415+ }
416+ b.WriteByte('\n')
417+ }
418+ indent(b, depth)
419+ b.WriteByte(']')
420+ case *Dictionary:
421+ if len(x.keys) == 0 {
422+ b.WriteString("{}")
423+ return
424+ }
425+ keys := jsKeyOrder(x.keys)
426+ b.WriteString("{\n")
427+ for i, k := range keys {
428+ val, _ := x.Get(k)
429+ indent(b, depth+1)
430+ writeJSONString(b, k)
431+ b.WriteString(": ")
432+ writeJSON(b, val, depth+1)
433+ if i < len(keys)-1 {
434+ b.WriteByte(',')
435+ }
436+ b.WriteByte('\n')
437+ }
438+ indent(b, depth)
439+ b.WriteByte('}')
440+ }
441+}
442+
443+func indent(b *strings.Builder, depth int) {
444+ for i := 0; i < depth; i++ {
445+ b.WriteString(" ")
446+ }
447+}
448+
449+const hexDigits = "0123456789abcdef"
450+
451+func writeJSONString(b *strings.Builder, s string) {
452+ b.WriteByte('"')
453+ for i := 0; i < len(s); {
454+ r, w := utf8.DecodeRuneInString(s[i:])
455+ if r == utf8.RuneError && w == 1 && i+2 < len(s) && s[i] == 0xED && s[i+1] >= 0xA0 && s[i+1] <= 0xBF && s[i+2]&0xC0 == 0x80 {
456+ // WTF-8 lone surrogate (see FromJSON).
457+ sur := rune(s[i]&0x0F)<<12 | rune(s[i+1]&0x3F)<<6 | rune(s[i+2]&0x3F)
458+ b.WriteString(`\u`)
459+ b.WriteByte(hexDigits[sur>>12])
460+ b.WriteByte(hexDigits[sur>>8&0xF])
461+ b.WriteByte(hexDigits[sur>>4&0xF])
462+ b.WriteByte(hexDigits[sur&0xF])
463+ i += 3
464+ continue
465+ }
466+ i += w
467+ switch {
468+ case r == '"':
469+ b.WriteString(`\"`)
470+ case r == '\\':
471+ b.WriteString(`\\`)
472+ case r == '\b':
473+ b.WriteString(`\b`)
474+ case r == '\f':
475+ b.WriteString(`\f`)
476+ case r == '\n':
477+ b.WriteString(`\n`)
478+ case r == '\r':
479+ b.WriteString(`\r`)
480+ case r == '\t':
481+ b.WriteString(`\t`)
482+ case r < 0x20:
483+ b.WriteString(`\u00`)
484+ b.WriteByte(hexDigits[r>>4])
485+ b.WriteByte(hexDigits[r&0xF])
486+ default:
487+ b.WriteRune(r) // other invalid UTF-8 comes out as U+FFFD
488+ }
489+ }
490+ b.WriteByte('"')
491+}
A expr/number.go +180 -0
@@ -0,0 +1,180 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import (
6+ "math"
7+ "math/big"
8+ "strconv"
9+ "strings"
10+)
11+
12+var (
13+ nan = math.NaN()
14+ posInf = math.Inf(1)
15+)
16+
17+// jsNumberToString is Number.prototype.toString: the shortest decimal that
18+// round-trips, in positional notation for exponents from -7 to 20 and in
19+// exponent notation ("1e+21", "1.5e-7") beyond.
20+func jsNumberToString(x float64) string {
21+ switch {
22+ case math.IsNaN(x):
23+ return "NaN"
24+ case x == 0:
25+ return "0"
26+ case math.IsInf(x, 1):
27+ return "Infinity"
28+ case math.IsInf(x, -1):
29+ return "-Infinity"
30+ }
31+ sign := ""
32+ if x < 0 {
33+ sign = "-"
34+ x = -x
35+ }
36+ s := strconv.FormatFloat(x, 'e', -1, 64) // d.ddde±xx
37+ mant, expStr, _ := strings.Cut(s, "e")
38+ exp, _ := strconv.Atoi(expStr)
39+ digits := strings.Replace(mant, ".", "", 1)
40+ k := len(digits)
41+ n := exp + 1
42+ switch {
43+ case k <= n && n <= 21:
44+ return sign + digits + strings.Repeat("0", n-k)
45+ case 0 < n && n <= 21:
46+ return sign + digits[:n] + "." + digits[n:]
47+ case -6 < n && n <= 0:
48+ return sign + "0." + strings.Repeat("0", -n) + digits
49+ }
50+ e := n - 1
51+ esign := "+"
52+ if e < 0 {
53+ esign = "-"
54+ e = -e
55+ }
56+ if k == 1 {
57+ return sign + digits + "e" + esign + strconv.Itoa(e)
58+ }
59+ return sign + digits[:1] + "." + digits[1:] + "e" + esign + strconv.Itoa(e)
60+}
61+
62+var pow10to15 = new(big.Int).Exp(big.NewInt(10), big.NewInt(15), nil)
63+
64+// numberToCoercedString is NumberData.coerceString of the reference
65+// implementation: (+x.toFixed(15)).toString(). Rounding to 15 decimals
66+// removes binary noise (0.1+0.2 is "0.3") and ties round half up like
67+// toFixed.
68+func numberToCoercedString(x float64) string {
69+ if x == 0 {
70+ return "0"
71+ }
72+ if math.IsNaN(x) || math.IsInf(x, 0) {
73+ return jsNumberToString(x)
74+ }
75+ a := math.Abs(x)
76+ // toFixed returns Number::toString from 1e21 up, and for integers the
77+ // fixed form parses back to the same number.
78+ if a >= 1e21 || a == math.Trunc(a) {
79+ return jsNumberToString(x)
80+ }
81+ r := new(big.Rat).SetFloat64(a)
82+ r.Mul(r, new(big.Rat).SetInt(pow10to15))
83+ q, rem := new(big.Int).QuoRem(r.Num(), r.Denom(), new(big.Int))
84+ if rem.Lsh(rem, 1).Cmp(r.Denom()) >= 0 {
85+ q.Add(q, big.NewInt(1))
86+ }
87+ digits := q.String()
88+ if len(digits) < 16 {
89+ digits = strings.Repeat("0", 16-len(digits)) + digits
90+ }
91+ fixed := digits[:len(digits)-15] + "." + digits[len(digits)-15:]
92+ v, _ := strconv.ParseFloat(fixed, 64)
93+ if x < 0 {
94+ v = -v
95+ }
96+ return jsNumberToString(v)
97+}
98+
99+// jsToNumber is Number(string): the StringToNumber algorithm of ECMAScript.
100+// Surrounding whitespace is ignored, the empty string is 0, and the accepted
101+// forms are decimal literals with optional sign, fraction and exponent,
102+// [+-]Infinity, and unsigned 0x, 0o and 0b integers. Everything else, including
103+// digit separators and "inf" or "nan", is NaN.
104+func jsToNumber(s string) float64 {
105+ s = jsTrim(s)
106+ if s == "" {
107+ return 0
108+ }
109+ if len(s) > 2 && s[0] == '0' {
110+ base := 0
111+ switch s[1] {
112+ case 'x', 'X':
113+ base = 16
114+ case 'o', 'O':
115+ base = 8
116+ case 'b', 'B':
117+ base = 2
118+ }
119+ if base != 0 {
120+ n, ok := new(big.Int).SetString(s[2:], base)
121+ if !ok || strings.ContainsAny(s[2:], "_+-") {
122+ return nan
123+ }
124+ f, _ := new(big.Float).SetInt(n).Float64()
125+ return f
126+ }
127+ }
128+ body := s
129+ neg := false
130+ if body[0] == '+' || body[0] == '-' {
131+ neg = body[0] == '-'
132+ body = body[1:]
133+ }
134+ if body == "Infinity" {
135+ if neg {
136+ return math.Inf(-1)
137+ }
138+ return math.Inf(1)
139+ }
140+ if !isDecimalLiteral(body) {
141+ return nan
142+ }
143+ v, _ := strconv.ParseFloat(body, 64) // out of range yields ±Inf, which is what JS does too
144+ if neg {
145+ v = -v
146+ }
147+ return v
148+}
149+
150+// isDecimalLiteral reports whether s matches
151+// (digits [. digits?] | . digits) ([eE] [+-]? digits)?.
152+func isDecimalLiteral(s string) bool {
153+ i := 0
154+ digits := func() int {
155+ start := i
156+ for i < len(s) && s[i] >= '0' && s[i] <= '9' {
157+ i++
158+ }
159+ return i - start
160+ }
161+ intDigits := digits()
162+ fracDigits := 0
163+ if i < len(s) && s[i] == '.' {
164+ i++
165+ fracDigits = digits()
166+ }
167+ if intDigits == 0 && fracDigits == 0 {
168+ return false
169+ }
170+ if i < len(s) && (s[i] == 'e' || s[i] == 'E') {
171+ i++
172+ if i < len(s) && (s[i] == '+' || s[i] == '-') {
173+ i++
174+ }
175+ if digits() == 0 {
176+ return false
177+ }
178+ }
179+ return i == len(s)
180+}
@@ -0,0 +1,180 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import (
6+ "math"
7+ "math/big"
8+ "strconv"
9+ "strings"
10+)
11+
12+var (
13+ nan = math.NaN()
14+ posInf = math.Inf(1)
15+)
16+
17+// jsNumberToString is Number.prototype.toString: the shortest decimal that
18+// round-trips, in positional notation for exponents from -7 to 20 and in
19+// exponent notation ("1e+21", "1.5e-7") beyond.
20+func jsNumberToString(x float64) string {
21+ switch {
22+ case math.IsNaN(x):
23+ return "NaN"
24+ case x == 0:
25+ return "0"
26+ case math.IsInf(x, 1):
27+ return "Infinity"
28+ case math.IsInf(x, -1):
29+ return "-Infinity"
30+ }
31+ sign := ""
32+ if x < 0 {
33+ sign = "-"
34+ x = -x
35+ }
36+ s := strconv.FormatFloat(x, 'e', -1, 64) // d.ddde±xx
37+ mant, expStr, _ := strings.Cut(s, "e")
38+ exp, _ := strconv.Atoi(expStr)
39+ digits := strings.Replace(mant, ".", "", 1)
40+ k := len(digits)
41+ n := exp + 1
42+ switch {
43+ case k <= n && n <= 21:
44+ return sign + digits + strings.Repeat("0", n-k)
45+ case 0 < n && n <= 21:
46+ return sign + digits[:n] + "." + digits[n:]
47+ case -6 < n && n <= 0:
48+ return sign + "0." + strings.Repeat("0", -n) + digits
49+ }
50+ e := n - 1
51+ esign := "+"
52+ if e < 0 {
53+ esign = "-"
54+ e = -e
55+ }
56+ if k == 1 {
57+ return sign + digits + "e" + esign + strconv.Itoa(e)
58+ }
59+ return sign + digits[:1] + "." + digits[1:] + "e" + esign + strconv.Itoa(e)
60+}
61+
62+var pow10to15 = new(big.Int).Exp(big.NewInt(10), big.NewInt(15), nil)
63+
64+// numberToCoercedString is NumberData.coerceString of the reference
65+// implementation: (+x.toFixed(15)).toString(). Rounding to 15 decimals
66+// removes binary noise (0.1+0.2 is "0.3") and ties round half up like
67+// toFixed.
68+func numberToCoercedString(x float64) string {
69+ if x == 0 {
70+ return "0"
71+ }
72+ if math.IsNaN(x) || math.IsInf(x, 0) {
73+ return jsNumberToString(x)
74+ }
75+ a := math.Abs(x)
76+ // toFixed returns Number::toString from 1e21 up, and for integers the
77+ // fixed form parses back to the same number.
78+ if a >= 1e21 || a == math.Trunc(a) {
79+ return jsNumberToString(x)
80+ }
81+ r := new(big.Rat).SetFloat64(a)
82+ r.Mul(r, new(big.Rat).SetInt(pow10to15))
83+ q, rem := new(big.Int).QuoRem(r.Num(), r.Denom(), new(big.Int))
84+ if rem.Lsh(rem, 1).Cmp(r.Denom()) >= 0 {
85+ q.Add(q, big.NewInt(1))
86+ }
87+ digits := q.String()
88+ if len(digits) < 16 {
89+ digits = strings.Repeat("0", 16-len(digits)) + digits
90+ }
91+ fixed := digits[:len(digits)-15] + "." + digits[len(digits)-15:]
92+ v, _ := strconv.ParseFloat(fixed, 64)
93+ if x < 0 {
94+ v = -v
95+ }
96+ return jsNumberToString(v)
97+}
98+
99+// jsToNumber is Number(string): the StringToNumber algorithm of ECMAScript.
100+// Surrounding whitespace is ignored, the empty string is 0, and the accepted
101+// forms are decimal literals with optional sign, fraction and exponent,
102+// [+-]Infinity, and unsigned 0x, 0o and 0b integers. Everything else, including
103+// digit separators and "inf" or "nan", is NaN.
104+func jsToNumber(s string) float64 {
105+ s = jsTrim(s)
106+ if s == "" {
107+ return 0
108+ }
109+ if len(s) > 2 && s[0] == '0' {
110+ base := 0
111+ switch s[1] {
112+ case 'x', 'X':
113+ base = 16
114+ case 'o', 'O':
115+ base = 8
116+ case 'b', 'B':
117+ base = 2
118+ }
119+ if base != 0 {
120+ n, ok := new(big.Int).SetString(s[2:], base)
121+ if !ok || strings.ContainsAny(s[2:], "_+-") {
122+ return nan
123+ }
124+ f, _ := new(big.Float).SetInt(n).Float64()
125+ return f
126+ }
127+ }
128+ body := s
129+ neg := false
130+ if body[0] == '+' || body[0] == '-' {
131+ neg = body[0] == '-'
132+ body = body[1:]
133+ }
134+ if body == "Infinity" {
135+ if neg {
136+ return math.Inf(-1)
137+ }
138+ return math.Inf(1)
139+ }
140+ if !isDecimalLiteral(body) {
141+ return nan
142+ }
143+ v, _ := strconv.ParseFloat(body, 64) // out of range yields ±Inf, which is what JS does too
144+ if neg {
145+ v = -v
146+ }
147+ return v
148+}
149+
150+// isDecimalLiteral reports whether s matches
151+// (digits [. digits?] | . digits) ([eE] [+-]? digits)?.
152+func isDecimalLiteral(s string) bool {
153+ i := 0
154+ digits := func() int {
155+ start := i
156+ for i < len(s) && s[i] >= '0' && s[i] <= '9' {
157+ i++
158+ }
159+ return i - start
160+ }
161+ intDigits := digits()
162+ fracDigits := 0
163+ if i < len(s) && s[i] == '.' {
164+ i++
165+ fracDigits = digits()
166+ }
167+ if intDigits == 0 && fracDigits == 0 {
168+ return false
169+ }
170+ if i < len(s) && (s[i] == 'e' || s[i] == 'E') {
171+ i++
172+ if i < len(s) && (s[i] == '+' || s[i] == '-') {
173+ i++
174+ }
175+ if digits() == 0 {
176+ return false
177+ }
178+ }
179+ return i == len(s)
180+}

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