211 lines
5.7 KiB
Go
211 lines
5.7 KiB
Go
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
|
|
}
|