first commit
This commit is contained in:
210
go-api/internal/definition/jsvalue.go
Normal file
210
go-api/internal/definition/jsvalue.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package definition
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JavaScript value semantics, reproduced exactly.
|
||||
//
|
||||
// The frontend parser is JavaScript, and the compatibility contract is with
|
||||
// THAT parser, not with an idealised YAML. Three of its behaviours are load
|
||||
// bearing and none of them are Go's defaults:
|
||||
//
|
||||
// - `\s` and `String.prototype.trim` cover a different set of code points
|
||||
// than `unicode.IsSpace`. JS treats U+FEFF as whitespace and U+0085 as
|
||||
// not; Go is the other way round. A definition is trimmed on the way
|
||||
// through the parser at least four times, so the difference is reachable.
|
||||
// - Truthiness decides whether `id:` is used or derived, whether a name
|
||||
// falls back to `Untitled skill`, and what `prompt:` becomes. `0`, `false`
|
||||
// and `""` are falsy; `"0"` and `[]` are not.
|
||||
// - `String(x)` and `Number(x)` have defined results for every type, and the
|
||||
// validator interpolates them into messages an author reads. `[1,2]`
|
||||
// stringifies to `1,2`, an object to `[object Object]`.
|
||||
//
|
||||
// Reimplementing these is not gold-plating: each one is exercised by a case in
|
||||
// the conformance suite because each one is reachable from a definition an
|
||||
// author could write.
|
||||
|
||||
// jsIsSpace reports whether r is whitespace to JavaScript — the union of
|
||||
// WhiteSpace and LineTerminator in the specification.
|
||||
//
|
||||
// Deliberately NOT unicode.IsSpace: that set includes U+0085 (NEL), which JS
|
||||
// does not, and excludes U+FEFF, which JS does.
|
||||
func jsIsSpace(r rune) bool {
|
||||
switch r {
|
||||
case '\t', '\n', '\v', '\f', '\r', ' ',
|
||||
0x00A0, 0x1680, 0x2028, 0x2029, 0x202F, 0x205F, 0x3000, 0xFEFF:
|
||||
return true
|
||||
}
|
||||
return r >= 0x2000 && r <= 0x200A
|
||||
}
|
||||
|
||||
// jsTrim is String.prototype.trim.
|
||||
func jsTrim(s string) string { return strings.TrimFunc(s, jsIsSpace) }
|
||||
|
||||
// jsTrimStart is String.prototype.trimStart.
|
||||
func jsTrimStart(s string) string { return strings.TrimLeftFunc(s, jsIsSpace) }
|
||||
|
||||
// jsTruthy is the `!!x` of a parsed YAML value.
|
||||
//
|
||||
// The parser produces only nil, bool, float64, string, []any and
|
||||
// map[string]any, so those are the only cases that can arise. An empty array
|
||||
// and an empty object are both truthy in JavaScript, which is why they are not
|
||||
// listed alongside the empty string.
|
||||
func jsTruthy(v any) bool {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
return false
|
||||
case bool:
|
||||
return x
|
||||
case float64:
|
||||
return x != 0 && !math.IsNaN(x)
|
||||
case string:
|
||||
return x != ""
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// jsNumberToString is JavaScript's Number → String conversion for the values
|
||||
// this parser can produce.
|
||||
//
|
||||
// `-0` prints as `0`, integers print without a decimal point, and everything
|
||||
// else takes the shortest representation that round-trips — which is what
|
||||
// strconv's 'g' with precision -1 gives, in the range a definition can reach.
|
||||
func jsNumberToString(f float64) string {
|
||||
switch {
|
||||
case math.IsNaN(f):
|
||||
return "NaN"
|
||||
case math.IsInf(f, 1):
|
||||
return "Infinity"
|
||||
case math.IsInf(f, -1):
|
||||
return "-Infinity"
|
||||
case f == 0:
|
||||
return "0" // collapses -0
|
||||
}
|
||||
if f == math.Trunc(f) && math.Abs(f) < 1e21 {
|
||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||||
}
|
||||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||||
}
|
||||
|
||||
// jsString is the `String(x)` of a parsed YAML value.
|
||||
//
|
||||
// Arrays join on `,` with nil rendering as the empty string, which is
|
||||
// Array.prototype.toString; a mapping renders as `[object Object]`. Both are
|
||||
// reachable: `trigger:` may be written as a list, and the message an author
|
||||
// reads interpolates the result.
|
||||
func jsString(v any) string {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
case bool:
|
||||
if x {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case float64:
|
||||
return jsNumberToString(x)
|
||||
case string:
|
||||
return x
|
||||
case []any:
|
||||
parts := make([]string, len(x))
|
||||
for i, item := range x {
|
||||
if item == nil {
|
||||
parts[i] = ""
|
||||
continue
|
||||
}
|
||||
parts[i] = jsString(item)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
case map[string]any:
|
||||
return "[object Object]"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// jsTrimmed is the frontend's `trimmed()` helper: String(value ?? empty).trim().
|
||||
//
|
||||
// The nullish coalescing matters: a nil renders as the empty string here,
|
||||
// where a bare String(null) would render as the four characters `null`.
|
||||
func jsTrimmed(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return jsTrim(jsString(v))
|
||||
}
|
||||
|
||||
// jsNumber is the `Number(x)` of a parsed YAML value, NaN where JavaScript
|
||||
// gives NaN.
|
||||
//
|
||||
// Only reached from `version:`, where the result is checked with
|
||||
// Number.isInteger. The string cases below are the ones a YAML scalar can
|
||||
// still be carrying at that point: a quoted `"3"` stays a string, and so does
|
||||
// anything the numeric patterns in toScalar declined.
|
||||
func jsNumber(v any) float64 {
|
||||
switch x := v.(type) {
|
||||
case nil:
|
||||
return 0
|
||||
case bool:
|
||||
if x {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
case float64:
|
||||
return x
|
||||
case string:
|
||||
return jsNumberFromString(x)
|
||||
case []any:
|
||||
// Number([]) is 0 and Number([3]) is 3, via the same String()
|
||||
// conversion; anything longer stringifies with a comma and fails.
|
||||
if len(x) == 0 {
|
||||
return 0
|
||||
}
|
||||
if len(x) == 1 {
|
||||
return jsNumberFromString(jsString(x[0]))
|
||||
}
|
||||
}
|
||||
return math.NaN()
|
||||
}
|
||||
|
||||
func jsNumberFromString(s string) float64 {
|
||||
s = jsTrim(s)
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
switch s {
|
||||
case "Infinity", "+Infinity":
|
||||
return math.Inf(1)
|
||||
case "-Infinity":
|
||||
return math.Inf(-1)
|
||||
}
|
||||
// The radix prefixes JavaScript accepts in a numeric string literal. Signs
|
||||
// are not permitted with them, which ParseUint enforces by rejecting the
|
||||
// leading character.
|
||||
if len(s) > 2 && s[0] == '0' {
|
||||
var base int
|
||||
switch s[1] {
|
||||
case 'x', 'X':
|
||||
base = 16
|
||||
case 'o', 'O':
|
||||
base = 8
|
||||
case 'b', 'B':
|
||||
base = 2
|
||||
}
|
||||
if base != 0 {
|
||||
n, err := strconv.ParseUint(s[2:], base, 64)
|
||||
if err != nil {
|
||||
return math.NaN()
|
||||
}
|
||||
return float64(n)
|
||||
}
|
||||
}
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return math.NaN()
|
||||
}
|
||||
return f
|
||||
}
|
||||
Reference in New Issue
Block a user