@@ -3,36 +3,26 @@ package jinja
33import (
44 "encoding/json"
55 "fmt"
6- "maps"
76 "math"
87 "sort"
8+ "strconv"
99 "strings"
1010 "time"
1111 "unicode"
1212)
1313
1414// cachedBuiltins is a read-only scope populated once with all built-in
15- // globals and filters. Each render clones it cheaply via cloneScope.
15+ // globals and filters. It is shared across every render — no code path
16+ // writes to the builtins scope (only chained reads through parent
17+ // links and lookupFilter), so a single shared instance is safe and
18+ // avoids ~20 allocations per render.
1619var cachedBuiltins = func () * scope {
1720 s := newScope (nil )
1821 registerGlobals (s )
1922 registerFilters (s )
2023 return s
2124}()
2225
23- // cloneBuiltins returns a shallow copy of the cached builtins scope so
24- // each render gets its own scope without re-registering every function.
25- func cloneBuiltins () * scope {
26- return cloneScope (cachedBuiltins )
27- }
28-
29- // cloneScope creates a shallow copy of a scope (same parent, copied vars map).
30- func cloneScope (s * scope ) * scope {
31- vars := make (map [string ]Value , len (s .vars ))
32- maps .Copy (vars , s .vars )
33- return & scope {vars : vars , parent : s .parent }
34- }
35-
3626// =============================================================================
3727// Global functions
3828// =============================================================================
@@ -353,25 +343,105 @@ func filterTojson(args []Value, kwargs map[string]Value) (Value, error) {
353343 return NewString ("" ), nil
354344 }
355345
356- goVal := valueToGo (args [0 ])
357-
358- var data []byte
359- var err error
360-
346+ // The indented form is used rarely in chat templates; fall back to
347+ // the reflection-based encoder for correctness instead of duplicating
348+ // indent state in the direct encoder.
361349 if indent , ok := kwargs ["indent" ]; ok && ! indent .IsNone () && ! indent .IsUndefined () {
362350 n := int (toInt64 (indent ))
363- prefix := ""
364- indentStr := strings . Repeat ( " " , n )
365- data , err = json . MarshalIndent ( goVal , prefix , indentStr )
366- } else {
367- data , err = json . Marshal ( goVal )
351+ data , err := json . MarshalIndent ( valueToGo ( args [ 0 ]), "" , strings . Repeat ( " " , n ))
352+ if err != nil {
353+ return NewString ( "" ), nil
354+ }
355+ return NewString ( string ( data )), nil
368356 }
369357
370- if err != nil {
358+ // Direct Value-tree encoder. Bypasses valueToGo's intermediate
359+ // map[string]any/[]any allocations and json.Marshal's reflection
360+ // path. Per-render allocations drop from ~80 (Dict.Set + valueToGo
361+ // + reflect) to a handful (one strings.Builder grow + one Marshal
362+ // per leaf string for escaping).
363+ var sb strings.Builder
364+ if err := encodeValueJSON (& sb , args [0 ]); err != nil {
371365 return NewString ("" ), nil
372366 }
367+ return NewString (sb .String ()), nil
368+ }
369+
370+ // encodeValueJSON writes the JSON encoding of v directly into sb without
371+ // going through an intermediate Go-typed tree. Strings are delegated to
372+ // encoding/json so escaping rules match the standard library exactly.
373+ func encodeValueJSON (sb * strings.Builder , v Value ) error {
374+ switch v .kind {
375+ case KindUndefined , KindNone , KindCallable :
376+ sb .WriteString ("null" )
377+ return nil
378+
379+ case KindBool :
380+ if v .AsBool () {
381+ sb .WriteString ("true" )
382+ } else {
383+ sb .WriteString ("false" )
384+ }
385+ return nil
386+
387+ case KindInt :
388+ var buf [20 ]byte
389+ sb .Write (strconv .AppendInt (buf [:0 ], v .AsInt (), 10 ))
390+ return nil
391+
392+ case KindFloat :
393+ f := v .AsFloat ()
394+ if math .IsInf (f , 0 ) || math .IsNaN (f ) {
395+ return fmt .Errorf ("tojson: cannot encode %v" , f )
396+ }
397+ var buf [32 ]byte
398+ sb .Write (strconv .AppendFloat (buf [:0 ], f , 'g' , - 1 , 64 ))
399+ return nil
400+
401+ case KindString :
402+ data , err := json .Marshal (v .AsString ())
403+ if err != nil {
404+ return err
405+ }
406+ sb .Write (data )
407+ return nil
408+
409+ case KindList :
410+ sb .WriteByte ('[' )
411+ for i , item := range v .AsList ().Items {
412+ if i > 0 {
413+ sb .WriteByte (',' )
414+ }
415+ if err := encodeValueJSON (sb , item ); err != nil {
416+ return err
417+ }
418+ }
419+ sb .WriteByte (']' )
420+ return nil
421+
422+ case KindDict :
423+ sb .WriteByte ('{' )
424+ d := v .AsDict ()
425+ for i , key := range d .Keys {
426+ if i > 0 {
427+ sb .WriteByte (',' )
428+ }
429+ data , err := json .Marshal (key )
430+ if err != nil {
431+ return err
432+ }
433+ sb .Write (data )
434+ sb .WriteByte (':' )
435+ if err := encodeValueJSON (sb , d .Data [key ]); err != nil {
436+ return err
437+ }
438+ }
439+ sb .WriteByte ('}' )
440+ return nil
441+ }
373442
374- return NewString (string (data )), nil
443+ sb .WriteString ("null" )
444+ return nil
375445}
376446
377447func filterFromjson (args []Value , kwargs map [string ]Value ) (Value , error ) {
0 commit comments