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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | + | } | |||