Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions go/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package jsonic

import (
"fmt"
"sort"
"strings"
)

// Debug is a plugin that provides introspection and tracing capabilities.
// It matches the TypeScript Debug plugin functionality.
//
// Usage:
//
// j := jsonic.Make()
// j.Use(jsonic.Debug, map[string]any{"trace": true})
// fmt.Println(jsonic.Describe(j))
var Debug Plugin = func(j *Jsonic, opts map[string]any) {
if opts != nil {
if trace, ok := opts["trace"]; ok {
if traceBool, ok := trace.(bool); ok && traceBool {
addTrace(j)
}
}
}
}

// addTrace installs lex and rule subscribers that log each step.
func addTrace(j *Jsonic) {
j.Sub(
func(tkn *Token, rule *Rule, ctx *Context) {
fmt.Printf("[lex] %s tin=%d src=%q val=%v at %d:%d\n",
tkn.Name, tkn.Tin, tkn.Src, tkn.Val, tkn.RI, tkn.CI)
},
func(rule *Rule, ctx *Context) {
fmt.Printf("[rule] %s state=%s node=%v ki=%d\n",
rule.Name, rule.State, rule.Node, ctx.KI)
},
)
}

// Describe returns a human-readable description of a Jsonic instance's configuration.
// It lists tokens, fixed tokens, rules, matchers, plugins, and key config settings.
func Describe(j *Jsonic) string {
var b strings.Builder

b.WriteString("=== Jsonic Instance ===\n")
if j.options != nil && j.options.Tag != "" {
b.WriteString(fmt.Sprintf("Tag: %s\n", j.options.Tag))
}

// Tokens
b.WriteString("\n--- Tokens ---\n")
names := make([]string, 0, len(j.tinByName))
for name := range j.tinByName {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
tin := j.tinByName[name]
b.WriteString(fmt.Sprintf(" %s = %d\n", name, tin))
}

// Fixed tokens
b.WriteString("\n--- Fixed Tokens ---\n")
cfg := j.Config()
if cfg.FixedTokens != nil {
ftKeys := make([]string, 0, len(cfg.FixedTokens))
for k := range cfg.FixedTokens {
ftKeys = append(ftKeys, k)
}
sort.Strings(ftKeys)
for _, k := range ftKeys {
tin := cfg.FixedTokens[k]
name := j.TinName(tin)
b.WriteString(fmt.Sprintf(" %q -> %s (%d)\n", k, name, tin))
}
}

// Rules
b.WriteString("\n--- Rules ---\n")
ruleNames := make([]string, 0, len(j.parser.RSM))
for name := range j.parser.RSM {
ruleNames = append(ruleNames, name)
}
sort.Strings(ruleNames)
for _, name := range ruleNames {
rs := j.parser.RSM[name]
b.WriteString(fmt.Sprintf(" %s: open=%d close=%d bo=%d ao=%d bc=%d ac=%d\n",
name, len(rs.Open), len(rs.Close), len(rs.BO), len(rs.AO), len(rs.BC), len(rs.AC)))
}

// Custom matchers
if len(cfg.CustomMatchers) > 0 {
b.WriteString("\n--- Custom Matchers ---\n")
for _, m := range cfg.CustomMatchers {
b.WriteString(fmt.Sprintf(" %s (priority=%d)\n", m.Name, m.Priority))
}
}

// Plugins
b.WriteString(fmt.Sprintf("\n--- Plugins: %d ---\n", len(j.plugins)))

// Subscriptions
b.WriteString(fmt.Sprintf("\n--- Subscriptions ---\n"))
b.WriteString(fmt.Sprintf(" Lex subscribers: %d\n", len(j.lexSubs)))
b.WriteString(fmt.Sprintf(" Rule subscribers: %d\n", len(j.ruleSubs)))

// Config summary
b.WriteString("\n--- Config ---\n")
b.WriteString(fmt.Sprintf(" FixedLex: %v\n", cfg.FixedLex))
b.WriteString(fmt.Sprintf(" SpaceLex: %v\n", cfg.SpaceLex))
b.WriteString(fmt.Sprintf(" LineLex: %v\n", cfg.LineLex))
b.WriteString(fmt.Sprintf(" TextLex: %v\n", cfg.TextLex))
b.WriteString(fmt.Sprintf(" NumberLex: %v\n", cfg.NumberLex))
b.WriteString(fmt.Sprintf(" CommentLex: %v\n", cfg.CommentLex))
b.WriteString(fmt.Sprintf(" StringLex: %v\n", cfg.StringLex))
b.WriteString(fmt.Sprintf(" ValueLex: %v\n", cfg.ValueLex))
b.WriteString(fmt.Sprintf(" MapExtend: %v\n", cfg.MapExtend))
b.WriteString(fmt.Sprintf(" ListProperty: %v\n", cfg.ListProperty))
b.WriteString(fmt.Sprintf(" SafeKey: %v\n", cfg.SafeKey))
b.WriteString(fmt.Sprintf(" FinishRule: %v\n", cfg.FinishRule))
b.WriteString(fmt.Sprintf(" RuleStart: %s\n", cfg.RuleStart))

return b.String()
}
160 changes: 156 additions & 4 deletions go/jsonic.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,159 @@
// the same matcher-based lexer and rule-based parser architecture.
package jsonic

import (
"strconv"
"strings"
)

// Error message templates matching TypeScript defaults.
var errorMessages = map[string]string{
"unexpected": "unexpected character(s): ",
"unterminated_string": "unterminated string: ",
"unterminated_comment": "unterminated comment: ",
"unknown": "unknown error: ",
}

// JsonicError is the error type returned by Parse when parsing fails.
// It includes structured details about the error location and cause.
type JsonicError struct {
Code string // Error code: "unterminated_string", "unterminated_comment", "unexpected"
Detail string // Human-readable detail message (e.g. "unterminated string: \"abc")
Pos int // 0-based character position in source
Row int // 1-based line number
Col int // 1-based column number
Src string // Source fragment at the error (the token text)
Hint string // Additional explanatory text for this error code

fullSource string // Complete input source (for generating site extract)
}

// Error returns a formatted error message matching the TypeScript JsonicError format:
//
// [jsonic/<code>]: <message>
// --> <row>:<col>
// <line-2> | <source>
// <line-1> | <source>
// <line> | <source with error>
// ^^^^ <message>
// <line+1> | <source>
// <line+2> | <source>
func (e *JsonicError) Error() string {
msg := e.Detail

var b strings.Builder

// Line 1: [jsonic/<code>]: <message>
b.WriteString("[jsonic/")
b.WriteString(e.Code)
b.WriteString("]: ")
b.WriteString(msg)

// Line 2: --> <row>:<col>
b.WriteString("\n --> ")
b.WriteString(strconv.Itoa(e.Row))
b.WriteString(":")
b.WriteString(strconv.Itoa(e.Col))

// Source site extract
if e.fullSource != "" {
site := errsite(e.fullSource, e.Src, msg, e.Row, e.Col)
if site != "" {
b.WriteString("\n")
b.WriteString(site)
}
}

// Hint
if e.Hint != "" {
b.WriteString("\n Hint: ")
b.WriteString(e.Hint)
}

return b.String()
}

// errsite generates a source code extract showing the error location,
// matching the TypeScript errsite() function output format.
func errsite(src, sub, msg string, row, col int) string {
if row < 1 {
row = 1
}
if col < 1 {
col = 1
}

lines := strings.Split(src, "\n")

// row is 1-based, convert to 0-based index
lineIdx := row - 1
if lineIdx >= len(lines) {
lineIdx = len(lines) - 1
}

// Determine padding width based on largest line number shown
maxLineNum := row + 2
pad := len(strconv.Itoa(maxLineNum)) + 2

// Build context lines: 2 before, error line, caret line, 2 after
var result []string

ln := func(num int, text string) string {
numStr := strconv.Itoa(num)
return strings.Repeat(" ", pad-len(numStr)) + numStr + " | " + text
}

// 2 lines before
if lineIdx-2 >= 0 {
result = append(result, ln(row-2, lines[lineIdx-2]))
}
if lineIdx-1 >= 0 {
result = append(result, ln(row-1, lines[lineIdx-1]))
}

// Error line
if lineIdx >= 0 && lineIdx < len(lines) {
result = append(result, ln(row, lines[lineIdx]))
}

// Caret line
caretCount := len(sub)
if caretCount < 1 {
caretCount = 1
}
indent := strings.Repeat(" ", pad) + " " + strings.Repeat(" ", col-1)
result = append(result, indent+strings.Repeat("^", caretCount)+" "+msg)

// 2 lines after
if lineIdx+1 < len(lines) {
result = append(result, ln(row+1, lines[lineIdx+1]))
}
if lineIdx+2 < len(lines) {
result = append(result, ln(row+2, lines[lineIdx+2]))
}

return strings.Join(result, "\n")
}

// makeJsonicError creates a JsonicError with the proper Detail message.
func makeJsonicError(code, src, fullSource string, pos, row, col int) *JsonicError {
tmpl, ok := errorMessages[code]
if !ok {
tmpl = errorMessages["unknown"]
}
detail := tmpl + src

return &JsonicError{
Code: code,
Detail: detail,
Pos: pos,
Row: row,
Col: col,
Src: src,
fullSource: fullSource,
}
}

// Parse parses a jsonic string and returns the resulting Go value.
// The returned value can be:
// - map[string]any for objects
Expand All @@ -14,10 +167,9 @@ package jsonic
// - string for strings
// - bool for booleans
// - nil for null or empty input
func Parse(src string) any {
// Preprocess: handle literal \n, \r\n, \t in test input
src = preprocessEscapes(src)

//
// Returns a *JsonicError if the input contains a syntax error.
func Parse(src string) (any, error) {
p := NewParser()
return p.Start(src)
}
Expand Down
Loading
Loading