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 }