mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 22:05:46 +01:00
- Update documentation with negative step range workaround - Document limitations and solutions in FINDINGS.md and SOLUTION.md - Clean up code style and remove experimental files - Optimize performance in core functions - Fix whitespace and end-of-file newlines 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
292 lines
6.6 KiB
Go
292 lines
6.6 KiB
Go
package twig
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// DebugLevel represents the verbosity level of debugging
|
|
type DebugLevel int
|
|
|
|
const (
|
|
// Debug levels
|
|
DebugOff DebugLevel = iota
|
|
DebugError
|
|
DebugWarning
|
|
DebugInfo
|
|
DebugVerbose
|
|
)
|
|
|
|
// Debugger provides logging and debugging tools for the Twig engine
|
|
type Debugger struct {
|
|
mu sync.Mutex
|
|
level DebugLevel
|
|
writer io.Writer
|
|
logger *log.Logger
|
|
traces []string
|
|
enabled bool
|
|
filename string
|
|
line int
|
|
}
|
|
|
|
// Global debugger instance
|
|
var debugger = &Debugger{
|
|
level: DebugOff,
|
|
writer: os.Stderr,
|
|
logger: log.New(os.Stderr, "[TWIG] ", log.LstdFlags),
|
|
enabled: false,
|
|
}
|
|
|
|
// SetDebugLevel sets the global debug level
|
|
func SetDebugLevel(level DebugLevel) {
|
|
debugger.mu.Lock()
|
|
defer debugger.mu.Unlock()
|
|
debugger.level = level
|
|
debugger.enabled = level > DebugOff
|
|
}
|
|
|
|
// SetDebugWriter sets the output writer for debug messages
|
|
func SetDebugWriter(w io.Writer) {
|
|
debugger.mu.Lock()
|
|
defer debugger.mu.Unlock()
|
|
debugger.writer = w
|
|
debugger.logger = log.New(w, "[TWIG] ", log.LstdFlags)
|
|
}
|
|
|
|
// IsDebugEnabled returns true if debugging is enabled
|
|
func IsDebugEnabled() bool {
|
|
return debugger.enabled
|
|
}
|
|
|
|
// LogError logs an error with source information
|
|
func LogError(err error, context ...string) {
|
|
if debugger.level >= DebugError {
|
|
_, file, line, _ := runtime.Caller(1)
|
|
contextStr := ""
|
|
if len(context) > 0 {
|
|
contextStr = " " + strings.Join(context, " ")
|
|
}
|
|
debugger.logger.Printf("ERROR:%s:%d:%s%s", filepath.Base(file), line, err, contextStr)
|
|
}
|
|
}
|
|
|
|
// LogWarning logs a warning with source information
|
|
func LogWarning(msg string, args ...interface{}) {
|
|
if debugger.level >= DebugWarning {
|
|
_, file, line, _ := runtime.Caller(1)
|
|
debugger.logger.Printf("WARNING:%s:%d:%s", filepath.Base(file), line, fmt.Sprintf(msg, args...))
|
|
}
|
|
}
|
|
|
|
// LogInfo logs an informational message
|
|
func LogInfo(msg string, args ...interface{}) {
|
|
if debugger.level >= DebugInfo {
|
|
debugger.logger.Printf("INFO:%s", fmt.Sprintf(msg, args...))
|
|
}
|
|
}
|
|
|
|
// LogVerbose logs detailed information for debugging
|
|
func LogVerbose(msg string, args ...interface{}) {
|
|
if debugger.level >= DebugVerbose {
|
|
_, file, line, _ := runtime.Caller(1)
|
|
debugger.logger.Printf("VERBOSE:%s:%d:%s", filepath.Base(file), line, fmt.Sprintf(msg, args...))
|
|
}
|
|
}
|
|
|
|
// LogDebug logs debugging information when debug mode is enabled
|
|
func LogDebug(msg string, args ...interface{}) {
|
|
if debugger.enabled {
|
|
debugger.logger.Printf("DEBUG:%s", fmt.Sprintf(msg, args...))
|
|
}
|
|
}
|
|
|
|
// StartTrace begins a trace of template rendering
|
|
func StartTrace(templateName string) func() {
|
|
if !debugger.enabled {
|
|
return func() {}
|
|
}
|
|
|
|
traceID := fmt.Sprintf("TRACE-%s-%d", templateName, time.Now().UnixNano())
|
|
start := time.Now()
|
|
|
|
debugger.mu.Lock()
|
|
debugger.traces = append(debugger.traces, traceID)
|
|
debugger.mu.Unlock()
|
|
|
|
LogInfo("Begin rendering template: %s", templateName)
|
|
|
|
return func() {
|
|
elapsed := time.Since(start)
|
|
LogInfo("Completed rendering template: %s (took %s)", templateName, elapsed)
|
|
|
|
debugger.mu.Lock()
|
|
defer debugger.mu.Unlock()
|
|
|
|
// Remove this trace from active traces
|
|
for i, t := range debugger.traces {
|
|
if t == traceID {
|
|
debugger.traces = append(debugger.traces[:i], debugger.traces[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TraceSection traces a section of template rendering
|
|
func TraceSection(name string) func() {
|
|
if !debugger.enabled {
|
|
return func() {}
|
|
}
|
|
|
|
start := time.Now()
|
|
LogVerbose("Begin section: %s", name)
|
|
|
|
return func() {
|
|
elapsed := time.Since(start)
|
|
LogVerbose("End section: %s (took %s)", name, elapsed)
|
|
}
|
|
}
|
|
|
|
// DebugRender enables detailed rendering information
|
|
func DebugRender(w io.Writer, tmpl *Template, ctx *RenderContext) error {
|
|
if !debugger.enabled {
|
|
return tmpl.RenderTo(w, ctx.context)
|
|
}
|
|
|
|
LogInfo("Rendering template %s with context containing %d variables",
|
|
tmpl.name, len(ctx.context))
|
|
|
|
// Log context variables at verbose level
|
|
if debugger.level >= DebugVerbose {
|
|
for k, v := range ctx.context {
|
|
typeName := "nil"
|
|
if v != nil {
|
|
typeName = fmt.Sprintf("%T", v)
|
|
}
|
|
LogVerbose("Context var: %s = %v (type: %s)", k, v, typeName)
|
|
}
|
|
}
|
|
|
|
// Trace full template rendering
|
|
defer StartTrace(tmpl.name)()
|
|
|
|
return tmpl.RenderTo(w, ctx.context)
|
|
}
|
|
|
|
// FormatErrorContext creates a formatted context for syntax errors
|
|
// including the source line and position indicator
|
|
func FormatErrorContext(source string, position int, line int) string {
|
|
if source == "" || position < 0 {
|
|
return ""
|
|
}
|
|
|
|
lines := strings.Split(source, "\n")
|
|
if line <= 0 || line > len(lines) {
|
|
return ""
|
|
}
|
|
|
|
// Get the problematic line
|
|
errorLine := lines[line-1]
|
|
|
|
// Calculate column position within the line
|
|
lineStartIdx := 0
|
|
for i := 0; i < line-1; i++ {
|
|
lineStartIdx += len(lines[i]) + 1 // +1 for the newline
|
|
}
|
|
colPosition := position - lineStartIdx
|
|
|
|
// Ensure column position is valid
|
|
if colPosition < 0 {
|
|
colPosition = 0
|
|
}
|
|
if colPosition > len(errorLine) {
|
|
colPosition = len(errorLine)
|
|
}
|
|
|
|
// Build the context output
|
|
context := fmt.Sprintf("Line %d: %s\n", line, errorLine)
|
|
if colPosition >= 0 {
|
|
context += strings.Repeat(" ", colPosition+8) + "^\n"
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
// EnhancedError provides more detailed error information for debugging
|
|
type EnhancedError struct {
|
|
Err error
|
|
Template string
|
|
Line int
|
|
Column int
|
|
Source string
|
|
SourceCtx string
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e *EnhancedError) Error() string {
|
|
if e.Err == nil {
|
|
return "unknown error"
|
|
}
|
|
|
|
location := ""
|
|
if e.Template != "" {
|
|
location = fmt.Sprintf("in template '%s' ", e.Template)
|
|
}
|
|
|
|
position := ""
|
|
if e.Line > 0 {
|
|
position = fmt.Sprintf("at line %d", e.Line)
|
|
if e.Column > 0 {
|
|
position += fmt.Sprintf(", column %d", e.Column)
|
|
}
|
|
}
|
|
|
|
context := ""
|
|
if e.SourceCtx != "" {
|
|
context = "\n" + e.SourceCtx
|
|
}
|
|
|
|
return fmt.Sprintf("Error %s%s: %s%s", location, position, e.Err.Error(), context)
|
|
}
|
|
|
|
// Unwrap returns the underlying error
|
|
func (e *EnhancedError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// NewError creates an enhanced error with context
|
|
func NewError(err error, tmpl string, line int, col int, source string) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
e := &EnhancedError{
|
|
Err: err,
|
|
Template: tmpl,
|
|
Line: line,
|
|
Column: col,
|
|
Source: source,
|
|
}
|
|
|
|
if source != "" && line > 0 {
|
|
position := 0
|
|
if col > 0 {
|
|
// Calculate position from line and column
|
|
lines := strings.Split(source, "\n")
|
|
for i := 0; i < line-1 && i < len(lines); i++ {
|
|
position += len(lines[i]) + 1 // +1 for newline
|
|
}
|
|
position += col - 1
|
|
}
|
|
e.SourceCtx = FormatErrorContext(source, position, line)
|
|
}
|
|
|
|
return e
|
|
}
|