From 64167a02893ac7056cb360e12ee23e35865701d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 14:31:26 +0000 Subject: [PATCH 1/3] Add Go implementation of directive plugin Port of the TypeScript @jsonic/directive plugin to Go, following the pattern established by github.com/jsonicjs/ini/go. Includes: - directive.go: types (DirectiveOptions, Action, RuleMod, etc.) and Apply() helper - plugin.go: Directive plugin function with open/close token registration, grammar rule modification, and replacement chain resolution for Go slices - directive_test.go: tests covering happy path, close tokens, adder/multiplier directives, edge cases, and custom rule modifications https://claude.ai/code/session_01RiMHE2zSU49VFuFzWygBBF --- go/directive.go | 113 ++++++++++++ go/directive_test.go | 398 +++++++++++++++++++++++++++++++++++++++++++ go/go.mod | 5 + go/go.sum | 2 + go/plugin.go | 225 ++++++++++++++++++++++++ 5 files changed, 743 insertions(+) create mode 100644 go/directive.go create mode 100644 go/directive_test.go create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 go/plugin.go diff --git a/go/directive.go b/go/directive.go new file mode 100644 index 0000000..870b358 --- /dev/null +++ b/go/directive.go @@ -0,0 +1,113 @@ +/* Copyright (c) 2021-2025 Richard Rodger and other contributors, MIT License */ + +package directive + +import ( + jsonic "github.com/jsonicjs/jsonic/go" +) + +// Action is called when a directive is processed. +// It receives the directive rule and parse context. The rule's Child.Node +// contains the parsed content between open (and optional close) tokens. +// Set rule.Node to the directive's result value. +type Action func(rule *jsonic.Rule, ctx *jsonic.Context) + +// RuleMod configures how a directive integrates with an existing grammar rule. +type RuleMod struct { + // C is an optional condition that must be true for the directive to match + // within this rule. + C jsonic.AltCond +} + +// RulesOption configures which grammar rules are modified by the directive. +// Open rules detect the directive open token and push to the directive rule. +// Close rules detect the close token (if any) to end sibling parsing. +type RulesOption struct { + Open map[string]*RuleMod + Close map[string]*RuleMod +} + +// CustomFunc allows additional customization of the jsonic instance +// after the directive rule is created. +type CustomFunc func(j *jsonic.Jsonic, config DirectiveConfig) + +// DirectiveConfig holds the resolved token Tins for a directive, +// passed to CustomFunc callbacks. +type DirectiveConfig struct { + OPEN jsonic.Tin + CLOSE jsonic.Tin // -1 if no close token + Name string +} + +// DirectiveOptions configures the Directive plugin. +type DirectiveOptions struct { + // Name is the directive name, used as the rule name and token prefix. + Name string + + // Open is the character sequence that starts the directive. + // Must be unique (not already a registered fixed token). + Open string + + // Close is the optional character sequence that ends the directive. + // If empty, the directive consumes a single value after the open token. + Close string + + // Action is called when the directive content has been parsed. + Action Action + + // Rules controls which existing grammar rules are modified. + // nil means use defaults: open="val", close="list,elem,map,pair". + // Set to &RulesOption{} to override defaults with no rules. + Rules *RulesOption + + // Custom allows additional jsonic customization after directive setup. + Custom CustomFunc +} + +// Apply registers a Directive plugin on the given jsonic instance. +// Returns the jsonic instance for chaining. +func Apply(j *jsonic.Jsonic, opts DirectiveOptions) *jsonic.Jsonic { + pluginMap := map[string]any{"_opts": &opts} + j.Use(Directive, pluginMap) + return j +} + +// defaultRules returns the default rules configuration. +func defaultRules() *RulesOption { + return &RulesOption{ + Open: map[string]*RuleMod{ + "val": {}, + }, + Close: map[string]*RuleMod{ + "list": {}, + "elem": {}, + "map": {}, + "pair": {}, + }, + } +} + +// resolveRules normalizes a rules map, ensuring no nil entries. +func resolveRules(rules map[string]*RuleMod) map[string]*RuleMod { + if rules == nil { + return map[string]*RuleMod{} + } + result := make(map[string]*RuleMod, len(rules)) + for k, v := range rules { + if v == nil { + v = &RuleMod{} + } + result[k] = v + } + return result +} + +// extractOptions retrieves DirectiveOptions from the plugin options map. +func extractOptions(m map[string]any) *DirectiveOptions { + if m != nil { + if opts, ok := m["_opts"].(*DirectiveOptions); ok { + return opts + } + } + return &DirectiveOptions{} +} diff --git a/go/directive_test.go b/go/directive_test.go new file mode 100644 index 0000000..9ab7982 --- /dev/null +++ b/go/directive_test.go @@ -0,0 +1,398 @@ +/* Copyright (c) 2021-2025 Richard Rodger and other contributors, MIT License */ + +package directive + +import ( + "fmt" + "reflect" + "strings" + "testing" + + jsonic "github.com/jsonicjs/jsonic/go" +) + +// assert is a test helper that checks deep equality. +func assert(t *testing.T, name string, got, want any) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Errorf("%s:\n got: %#v\n want: %#v", name, got, want) + } +} + +// mustParse parses and fatals on error. +func mustParse(t *testing.T, j *jsonic.Jsonic, src string) any { + t.Helper() + result, err := j.Parse(src) + if err != nil { + t.Fatalf("Parse(%q) error: %v", src, err) + } + return result +} + +// mustError parses and expects an error. +func mustError(t *testing.T, j *jsonic.Jsonic, src string) { + t.Helper() + _, err := j.Parse(src) + if err == nil { + t.Fatalf("Parse(%q) expected error, got nil", src) + } +} + +func TestHappy(t *testing.T) { + j := jsonic.Make() + Apply(j, DirectiveOptions{ + Name: "upper", + Open: "@", + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) { + s := fmt.Sprintf("%v", rule.Child.Node) + rule.Node = strings.ToUpper(s) + }, + }) + + // Single value. + assert(t, "upper-a", mustParse(t, j, "@a"), "A") + + // In lists (with brackets). + assert(t, "empty-list", mustParse(t, j, "[]"), []any{}) + assert(t, "list-1", mustParse(t, j, "[1]"), []any{float64(1)}) + assert(t, "list-1-2", mustParse(t, j, "[1, 2]"), []any{float64(1), float64(2)}) + assert(t, "list-1-2-3", mustParse(t, j, "[1, 2, 3]"), + []any{float64(1), float64(2), float64(3)}) + assert(t, "list-@a", mustParse(t, j, "[@a]"), []any{"A"}) + assert(t, "list-1-@a", mustParse(t, j, "[1, @a]"), + []any{float64(1), "A"}) + assert(t, "list-1-2-@a", mustParse(t, j, "[1, 2, @a]"), + []any{float64(1), float64(2), "A"}) + assert(t, "list-1-@a-2", mustParse(t, j, "[1, @a, 2]"), + []any{float64(1), "A", float64(2)}) + assert(t, "list-@a-2", mustParse(t, j, "[@a, 2]"), + []any{"A", float64(2)}) + assert(t, "list-@a-@b", mustParse(t, j, "[@a, @b]"), + []any{"A", "B"}) + assert(t, "list-@a-@b-@c", mustParse(t, j, "[@a, @b, @c]"), + []any{"A", "B", "C"}) + + // Space-separated lists. + assert(t, "list-sp-1-2", mustParse(t, j, "[1 2]"), + []any{float64(1), float64(2)}) + assert(t, "list-sp-@a", mustParse(t, j, "[@a]"), []any{"A"}) + assert(t, "list-sp-1-@a", mustParse(t, j, "[1 @a]"), + []any{float64(1), "A"}) + assert(t, "list-sp-@a-2", mustParse(t, j, "[@a 2]"), + []any{"A", float64(2)}) + assert(t, "list-sp-@a-@b", mustParse(t, j, "[@a @b]"), + []any{"A", "B"}) + + // In maps (with braces). + assert(t, "empty-map", mustParse(t, j, "{}"), map[string]any{}) + assert(t, "map-x1", mustParse(t, j, "{x:1}"), + map[string]any{"x": float64(1)}) + assert(t, "map-x1-y2", mustParse(t, j, "{x:1, y:2}"), + map[string]any{"x": float64(1), "y": float64(2)}) + assert(t, "map-x-@a", mustParse(t, j, "{x:@a}"), + map[string]any{"x": "A"}) + assert(t, "map-y1-x@a", mustParse(t, j, "{y:1, x:@a}"), + map[string]any{"y": float64(1), "x": "A"}) + assert(t, "map-y1-x@a-z2", mustParse(t, j, "{y:1, x:@a, z:2}"), + map[string]any{"y": float64(1), "x": "A", "z": float64(2)}) + assert(t, "map-x@a-y@b", mustParse(t, j, "{x:@a, y:@b}"), + map[string]any{"x": "A", "y": "B"}) + + // Space-separated maps. + assert(t, "map-sp-x@a", mustParse(t, j, "{x:@a}"), + map[string]any{"x": "A"}) + assert(t, "map-sp-y1-x@a", mustParse(t, j, "{y:1 x:@a}"), + map[string]any{"y": float64(1), "x": "A"}) + assert(t, "map-sp-x@a-y@b", mustParse(t, j, "{x:@a y:@b}"), + map[string]any{"x": "A", "y": "B"}) + + // Implicit lists (comma-separated, no brackets). + assert(t, "imp-1-@a", mustParse(t, j, "1, @a"), + []any{float64(1), "A"}) + assert(t, "imp-@a-1", mustParse(t, j, "@a, 1"), + []any{"A", float64(1)}) + assert(t, "imp-1-@a-2", mustParse(t, j, "1, @a, 2"), + []any{float64(1), "A", float64(2)}) + + // Implicit lists (space-separated, no brackets). + assert(t, "imp-sp-1-@a", mustParse(t, j, "1 @a"), + []any{float64(1), "A"}) + assert(t, "imp-sp-@a-1", mustParse(t, j, "@a 1"), + []any{"A", float64(1)}) + assert(t, "imp-sp-1-@a-2", mustParse(t, j, "1 @a 2"), + []any{float64(1), "A", float64(2)}) + + // Multiple directives in implicit lists. + assert(t, "imp-1-@a-@b", mustParse(t, j, "1, @a, @b"), + []any{float64(1), "A", "B"}) + assert(t, "imp-sp-1-@a-@b", mustParse(t, j, "1 @a @b"), + []any{float64(1), "A", "B"}) + assert(t, "imp-@a-@b-1", mustParse(t, j, "@a, @b, 1"), + []any{"A", "B", float64(1)}) + assert(t, "imp-sp-@a-@b-1", mustParse(t, j, "@a @b 1"), + []any{"A", "B", float64(1)}) +} + +func TestClose(t *testing.T) { + j := jsonic.Make() + Apply(j, DirectiveOptions{ + Name: "foo", + Open: "foo<", + Close: ">", + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) { + rule.Node = "FOO" + }, + }) + + assert(t, "foo-t", mustParse(t, j, "foo"), "FOO") + assert(t, "foo-empty", mustParse(t, j, "foo<>"), "FOO") + + assert(t, "map-a1", mustParse(t, j, `{"a":1}`), + map[string]any{"a": float64(1)}) + assert(t, "map-a-foo", mustParse(t, j, `{"a":foo< a >}`), + map[string]any{"a": "FOO"}) + assert(t, "map-a-foo-obj", mustParse(t, j, `{"a":foo<{x:1}>}`), + map[string]any{"a": "FOO"}) + assert(t, "map-a-foo-nested", mustParse(t, j, `{"a":foo>}`), + map[string]any{"a": "FOO"}) + + assert(t, "map-a1-b-foo", mustParse(t, j, `{"a":1,b:foo}`), + map[string]any{"a": float64(1), "b": "FOO"}) + assert(t, "map-a1-b-foo-list", mustParse(t, j, `{"a":1,b:foo<[2]>}`), + map[string]any{"a": float64(1), "b": "FOO"}) + + assert(t, "list-1-foo", mustParse(t, j, `{"a":[1,foo]}`), + map[string]any{"a": []any{float64(1), "FOO"}}) + + // Close without open should error. + mustError(t, j, ">") + mustError(t, j, "a:>") + + // Second directive sharing the same close token. + Apply(j, DirectiveOptions{ + Name: "bar", + Open: "bar<", + Close: ">", + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) { + rule.Node = "BAR" + }, + }) + + assert(t, "bar-a", mustParse(t, j, `{"a":bar< a >}`), + map[string]any{"a": "BAR"}) + assert(t, "bar-obj", mustParse(t, j, `{"a":bar<{x:1}>}`), + map[string]any{"a": "BAR"}) + + assert(t, "foo-after-bar", mustParse(t, j, `{"a":foo< a >}`), + map[string]any{"a": "FOO"}) + + assert(t, "foo-and-bar", mustParse(t, j, `{"a":foo< a >, b:bar<>}`), + map[string]any{"a": "FOO", "b": "BAR"}) + + // Duplicate open token should panic. + defer func() { + r := recover() + if r == nil { + t.Fatal("expected panic for duplicate open token") + } + }() + Apply(j, DirectiveOptions{ + Name: "baz", + Open: "bar<", + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) {}, + }) +} + +func TestAdder(t *testing.T) { + j := jsonic.Make() + Apply(j, DirectiveOptions{ + Name: "adder", + Open: "add<", + Close: ">", + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) { + out := float64(0) + if arr, ok := rule.Child.Node.([]any); ok { + for _, v := range arr { + if n, ok := v.(float64); ok { + out += n + } else if s, ok := v.(string); ok { + // String concatenation: treat result as string. + result := "" + for _, sv := range arr { + result += fmt.Sprintf("%v", sv) + } + _ = s + rule.Node = result + return + } + } + } + rule.Node = out + }, + }) + + assert(t, "add-1-2", mustParse(t, j, "add<1,2>"), float64(3)) + assert(t, "map-add", mustParse(t, j, "a:add<1,2>"), + map[string]any{"a": float64(3)}) + assert(t, "list-add-str", mustParse(t, j, "[add]"), []any{"ab"}) + + // Second directive: multiplier. + Apply(j, DirectiveOptions{ + Name: "multiplier", + Open: "mul<", + Close: ">", + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) { + out := float64(0) + if arr, ok := rule.Child.Node.([]any); ok && len(arr) > 0 { + out = 1 + for _, v := range arr { + if n, ok := v.(float64); ok { + out *= n + } + } + } + rule.Node = out + }, + }) + + assert(t, "mul-2-3", mustParse(t, j, "mul<2,3>"), float64(6)) + assert(t, "map-mul", mustParse(t, j, "a:mul<2,3>"), + map[string]any{"a": float64(6)}) + + // Original adder still works. + assert(t, "add-after-mul", mustParse(t, j, "add<1,2>"), float64(3)) + assert(t, "map-add-after-mul", mustParse(t, j, "a:add<1,2>"), + map[string]any{"a": float64(3)}) +} + +func TestEdges(t *testing.T) { + j := jsonic.Make() + Apply(j, DirectiveOptions{ + Name: "none", + Open: "@", + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) {}, + Rules: &RulesOption{}, // Empty rules: no existing rules modified. + }) + + // @ is registered as a fixed token but no rule detects it → error. + mustError(t, j, "a:@x") +} + +func TestCustom(t *testing.T) { + // Test the Custom callback: create a directive that uses custom + // rule modifications to handle @foo as a map key-value shorthand. + j := jsonic.Make() + Apply(j, DirectiveOptions{ + Name: "subobj", + Open: "@", + Rules: &RulesOption{ + Open: map[string]*RuleMod{ + "val": {}, + "pair": { + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.Lte("pk", 0) + }, + }, + }, + }, + Action: func(rule *jsonic.Rule, ctx *jsonic.Context) { + key := fmt.Sprintf("%v", rule.Child.Node) + val := strings.ToUpper(key) + res := map[string]any{key: val} + + // Merge into grandparent node. + if rule.Parent != nil && rule.Parent != jsonic.NoRule && + rule.Parent.Parent != nil && rule.Parent.Parent != jsonic.NoRule { + if m, ok := rule.Parent.Parent.Node.(map[string]any); ok { + for k, v := range res { + m[k] = v + } + return + } + } + rule.Node = res + }, + Custom: func(j *jsonic.Jsonic, cfg DirectiveConfig) { + OPEN := cfg.OPEN + name := cfg.Name + + // Handle @foo at top level: assume a map. + j.Rule("val", func(rs *jsonic.RuleSpec) { + rs.PrependOpen( + &jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["pk"] > 0 + }, + B: 1, + G: name + "_undive", + }, + &jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.D == 0 + }, + P: "map", + B: 1, + N: map[string]int{name + "_top": 1}, + G: name + "_top", + }, + ) + }) + + j.Rule("map", func(rs *jsonic.RuleSpec) { + rs.PrependOpen(&jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.D == 1 && r.N[name+"_top"] == 1 + }, + P: "pair", + B: 1, + G: name + "_top", + }) + rs.PrependClose(&jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["pk"] > 0 + }, + B: 1, + G: name + "_undive", + }) + }) + + j.Rule("pair", func(rs *jsonic.RuleSpec) { + rs.PrependClose(&jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["pk"] > 0 + }, + B: 1, + G: name + "_undive", + }) + }) + }, + }) + + assert(t, "sub-@a", mustParse(t, j, "@a"), + map[string]any{"a": "A"}) + assert(t, "sub-{@a}", mustParse(t, j, "{@a}"), + map[string]any{"a": "A"}) + assert(t, "sub-{@a @b}", mustParse(t, j, "{@a @b}"), + map[string]any{"a": "A", "b": "B"}) + assert(t, "sub-{x:1 @a @b}", mustParse(t, j, "{x:1 @a @b}"), + map[string]any{"x": float64(1), "a": "A", "b": "B"}) + assert(t, "sub-{@a q:1}", mustParse(t, j, "{@a q:1}"), + map[string]any{"a": "A", "q": float64(1)}) + + assert(t, "sub-@a-q:1", mustParse(t, j, "@a q:1"), + map[string]any{"a": "A", "q": float64(1)}) + assert(t, "sub-q:1-@a", mustParse(t, j, "q:1 @a"), + map[string]any{"q": float64(1), "a": "A"}) + assert(t, "sub-q:1-@a-w:2", mustParse(t, j, "q:1 @a w:2"), + map[string]any{"q": float64(1), "a": "A", "w": float64(2)}) + + assert(t, "sub-@a-@b", mustParse(t, j, "@a @b"), + map[string]any{"a": "A", "b": "B"}) + assert(t, "sub-q:1-@a-@b", mustParse(t, j, "q:1 @a @b"), + map[string]any{"q": float64(1), "a": "A", "b": "B"}) +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..44b2fe4 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,5 @@ +module github.com/jsonicjs/directive/go + +go 1.24.7 + +require github.com/jsonicjs/jsonic/go v0.1.4 diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..dc99d17 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,2 @@ +github.com/jsonicjs/jsonic/go v0.1.4 h1:V1KEzmg/jIwk25+JYj8ig1+B7190rHmH8WqZbT7XlgA= +github.com/jsonicjs/jsonic/go v0.1.4/go.mod h1:ObNKlCG7esWoi4AHCpdgkILvPINV8bpvkbCd4llGGUg= diff --git a/go/plugin.go b/go/plugin.go new file mode 100644 index 0000000..2fef854 --- /dev/null +++ b/go/plugin.go @@ -0,0 +1,225 @@ +/* Copyright (c) 2021-2025 Richard Rodger and other contributors, MIT License */ + +package directive + +import ( + "fmt" + + jsonic "github.com/jsonicjs/jsonic/go" +) + +// Directive is a jsonic plugin that adds directive syntax support. +// A directive defines a custom token sequence (open and optional close) +// that triggers an action callback to transform the parsed content. +func Directive(j *jsonic.Jsonic, pluginOpts map[string]any) { + opts := extractOptions(pluginOpts) + + // Resolve rules: nil means use defaults. + var openRules, closeRules map[string]*RuleMod + if opts.Rules == nil { + defaults := defaultRules() + openRules = resolveRules(defaults.Open) + closeRules = resolveRules(defaults.Close) + } else { + openRules = resolveRules(opts.Rules.Open) + closeRules = resolveRules(opts.Rules.Close) + } + + name := opts.Name + open := opts.Open + close_ := opts.Close + action := opts.Action + custom := opts.Custom + hasClose := close_ != "" + + // The open token must not already be registered. + cfg := j.Config() + if _, exists := cfg.FixedTokens[open]; exists { + panic(fmt.Sprintf("Directive open token already in use: %s", open)) + } + + // Register the open fixed token. + openTN := "#OD_" + name + OPEN := j.Token(openTN, open) + + // Register or look up the close fixed token. + var CLOSE jsonic.Tin = -1 + if hasClose { + closeTN := "#CD_" + name + if existing, exists := cfg.FixedTokens[close_]; exists { + CLOSE = existing + } else { + CLOSE = j.Token(closeTN, close_) + } + } + + // Look up the comma token for close-with-comma alternatives. + CA := j.Token("#CA") + + // ---- Modify existing rules for OPEN token detection ---- + + for rulename, rulemod := range openRules { + rm := rulemod + dn := name + j.Rule(rulename, func(rs *jsonic.RuleSpec) { + // Match OPEN token → push to directive rule. + openAlt := &jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}}, + P: dn, + N: map[string]int{"dr_" + dn: 1}, + G: "start", + } + if rm.C != nil { + openAlt.C = rm.C + } + + if hasClose { + // Also match OPEN+CLOSE (empty directive). + emptyAlt := &jsonic.AltSpec{ + S: [][]jsonic.Tin{{OPEN}, {CLOSE}}, + B: 1, + P: dn, + N: map[string]int{"dr_" + dn: 1}, + G: "start,end", + } + + // Prepend close detection to this rule. + closeAlt := &jsonic.AltSpec{ + S: [][]jsonic.Tin{{CLOSE}}, + B: 1, + G: "end", + } + + // OPEN+CLOSE must be checked before OPEN alone. + rs.PrependOpen(openAlt) + rs.PrependOpen(emptyAlt) + rs.PrependClose(closeAlt) + } else { + rs.PrependOpen(openAlt) + } + }) + } + + // ---- Modify existing rules for CLOSE token detection ---- + + if hasClose { + for rulename, rulemod := range closeRules { + rm := rulemod + dn := name + j.Rule(rulename, func(rs *jsonic.RuleSpec) { + // CLOSE token ends the directive content. + closeAlt := &jsonic.AltSpec{ + S: [][]jsonic.Tin{{CLOSE}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + if r.N["dr_"+dn] != 1 { + return false + } + if rm.C != nil { + return rm.C(r, ctx) + } + return true + }, + B: 1, + G: "end", + } + + // COMMA + CLOSE also ends the directive. + commaCloseAlt := &jsonic.AltSpec{ + S: [][]jsonic.Tin{{CA}, {CLOSE}}, + C: func(r *jsonic.Rule, ctx *jsonic.Context) bool { + return r.N["dr_"+dn] == 1 + }, + B: 1, + G: "end,comma", + } + + rs.PrependClose(closeAlt, commaCloseAlt) + }) + } + } + + // ---- Create the directive rule ---- + + j.Rule(name, func(rs *jsonic.RuleSpec) { + rs.Clear() + + // Before open: initialize node as empty map. + rs.AddBO(func(r *jsonic.Rule, ctx *jsonic.Context) { + r.Node = make(map[string]any) + }) + + // Open alternatives. + openAlts := make([]*jsonic.AltSpec, 0, 2) + + // If close token exists, check for immediate close (empty directive). + if hasClose { + openAlts = append(openAlts, &jsonic.AltSpec{ + S: [][]jsonic.Tin{{CLOSE}}, + B: 1, + }) + } + + // Push to val rule to parse directive content. + // Counter settings control implicit list/map creation: + // With close: reset counters (allow implicits within boundaries) + // Without close: increment counters (prevent implicits consuming siblings) + counters := map[string]int{} + if hasClose { + counters["dlist"] = 0 + counters["dmap"] = 0 + } else { + counters["dlist"] = 1 + counters["dmap"] = 1 + } + openAlts = append(openAlts, &jsonic.AltSpec{ + P: "val", + N: counters, + }) + + rs.Open = openAlts + + // Before close: resolve the child node and invoke the action. + rs.AddBC(func(r *jsonic.Rule, ctx *jsonic.Context) { + // Follow the replacement chain to get the final child node. + // When a val rule is replaced by a list rule (implicit list), + // the original child's Node may be stale in Go because slice + // append can reallocate. Walk the Prev-linked replacement + // chain to find the last replacement and adopt its Node. + if r.Child != nil && r.Child != jsonic.NoRule { + final := r.Child + for final.Next != nil && final.Next != jsonic.NoRule && + final.Next.Prev == final { + final = final.Next + } + if final != r.Child { + r.Child.Node = final.Node + } + } + if action != nil { + action(r, ctx) + } + }) + + // Close alternatives (only if close token exists). + if hasClose { + rs.Close = []*jsonic.AltSpec{ + {S: [][]jsonic.Tin{{CLOSE}}}, + {S: [][]jsonic.Tin{{CA}, {CLOSE}}}, + } + } + }) + + // ---- Custom callback ---- + + if custom != nil { + closeTin := jsonic.Tin(-1) + if hasClose { + closeTin = CLOSE + } + custom(j, DirectiveConfig{ + OPEN: OPEN, + CLOSE: closeTin, + Name: name, + }) + } +} From d93e84c6f885e685ddcdbf06b9770fd96bf471a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 16:14:56 +0000 Subject: [PATCH 2/3] Update CI: Node 24/latest, add Go build and test job - Replace Node 18/20/22 matrix with 24.x and latest - Add Go job that builds and tests the go/ directory - Update actions/checkout to v4, actions/setup-node to v4 https://claude.ai/code/session_01RiMHE2zSU49VFuFzWygBBF --- .github/workflows/build.yml | 38 ++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54879bb..9e4fb0d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,4 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - +# Build and test Node.js and Go name: build on: @@ -10,20 +8,20 @@ on: branches: [ main ] jobs: - build: + node: strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [18.x, 20.x, 22.x] + node-version: [24.x, latest] + + runs-on: ${{ matrix.os }} - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm i @@ -36,3 +34,25 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: ./coverage/lcov.info + go: + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: Build + working-directory: ./go + run: go build ./... + - name: Test + working-directory: ./go + run: go test ./... + From e8074ad43e559f63082c40be13d71c17f59653e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 19 Mar 2026 16:23:22 +0000 Subject: [PATCH 3/3] Remove Coveralls step from CI workflow https://claude.ai/code/session_01RiMHE2zSU49VFuFzWygBBF --- .github/workflows/build.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9e4fb0d..5d4204d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,12 +28,6 @@ jobs: - run: npm run build --if-present - run: npm test - - name: Coveralls - uses: coverallsapp/github-action@main - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - path-to-lcov: ./coverage/lcov.info - go: strategy: