mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
Indent format fixes
This commit is contained in:
parent
529420e7f2
commit
0548a9d547
16 changed files with 465 additions and 465 deletions
108
buffer_pool.go
108
buffer_pool.go
|
|
@ -160,13 +160,13 @@ func (b *Buffer) WriteInt(i int) (n int, err error) {
|
|||
} else if i > -100 && i < 0 {
|
||||
return b.WriteString(smallNegIntStrings[-i])
|
||||
}
|
||||
|
||||
|
||||
// Optimization: manual integer formatting for common sizes
|
||||
// Avoid the allocations in strconv.Itoa for numbers we can handle directly
|
||||
if i >= -999999 && i <= 999999 {
|
||||
return b.formatInt(int64(i))
|
||||
}
|
||||
|
||||
|
||||
// For larger integers, fallback to standard formatting
|
||||
// This still allocates, but is rare enough to be acceptable
|
||||
s := strconv.FormatInt(int64(i), 10)
|
||||
|
|
@ -181,7 +181,7 @@ func (b *Buffer) formatInt(i int64) (int, error) {
|
|||
b.WriteByte('-')
|
||||
i = -i
|
||||
}
|
||||
|
||||
|
||||
// Count digits to determine buffer size
|
||||
var digits int
|
||||
if i < 10 {
|
||||
|
|
@ -197,7 +197,7 @@ func (b *Buffer) formatInt(i int64) (int, error) {
|
|||
} else {
|
||||
digits = 6
|
||||
}
|
||||
|
||||
|
||||
// Reserve space for the digits
|
||||
// Compute in reverse order, then reverse the result
|
||||
start := len(b.buf)
|
||||
|
|
@ -206,13 +206,13 @@ func (b *Buffer) formatInt(i int64) (int, error) {
|
|||
b.buf = append(b.buf, digit)
|
||||
i /= 10
|
||||
}
|
||||
|
||||
|
||||
// Reverse the digits
|
||||
end := len(b.buf) - 1
|
||||
for j := 0; j < digits/2; j++ {
|
||||
b.buf[start+j], b.buf[end-j] = b.buf[end-j], b.buf[start+j]
|
||||
}
|
||||
|
||||
|
||||
return digits, nil
|
||||
}
|
||||
|
||||
|
|
@ -226,12 +226,12 @@ func (b *Buffer) WriteFloat(f float64, fmt byte, prec int) (n int, err error) {
|
|||
return b.formatInt(int64(f))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Special case for small, common floating-point values with 1-2 decimal places
|
||||
if fmt == 'f' && f >= 0 && f < 1000 && (prec == 1 || prec == 2 || prec == -1) {
|
||||
// Try to format common floats manually without allocation
|
||||
intPart := int64(f)
|
||||
|
||||
|
||||
// Get the fractional part based on precision
|
||||
var fracFactor int64
|
||||
var fracPrec int
|
||||
|
|
@ -243,7 +243,7 @@ func (b *Buffer) WriteFloat(f float64, fmt byte, prec int) (n int, err error) {
|
|||
// It's a whole number
|
||||
return b.formatInt(intPart)
|
||||
}
|
||||
|
||||
|
||||
// Test if 1-2 decimal places is enough
|
||||
if fracPart*100 == float64(int64(fracPart*100)) {
|
||||
// Two decimal places is sufficient
|
||||
|
|
@ -264,20 +264,20 @@ func (b *Buffer) WriteFloat(f float64, fmt byte, prec int) (n int, err error) {
|
|||
fracFactor = 100
|
||||
fracPrec = 2
|
||||
}
|
||||
|
||||
|
||||
// Format integer part first
|
||||
intLen, err := b.formatInt(intPart)
|
||||
if err != nil {
|
||||
return intLen, err
|
||||
}
|
||||
|
||||
|
||||
// Add decimal point
|
||||
if err := b.WriteByte('.'); err != nil {
|
||||
return intLen, err
|
||||
}
|
||||
|
||||
|
||||
// Format fractional part, ensuring proper padding with zeros
|
||||
fracPart := int64((f - float64(intPart)) * float64(fracFactor) + 0.5) // Round
|
||||
fracPart := int64((f-float64(intPart))*float64(fracFactor) + 0.5) // Round
|
||||
if fracPart >= fracFactor {
|
||||
// Rounding caused carry
|
||||
fracPart = 0
|
||||
|
|
@ -291,22 +291,22 @@ func (b *Buffer) WriteFloat(f float64, fmt byte, prec int) (n int, err error) {
|
|||
return intLen, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Write fractional part with leading zeros if needed
|
||||
if fracPrec == 2 && fracPart < 10 {
|
||||
if err := b.WriteByte('0'); err != nil {
|
||||
return intLen + 1, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fracLen, err := b.formatInt(fracPart)
|
||||
if err != nil {
|
||||
return intLen + 1, err
|
||||
}
|
||||
|
||||
|
||||
return intLen + 1 + fracLen, nil
|
||||
}
|
||||
|
||||
|
||||
useStrconv:
|
||||
// Fallback to standard formatting for complex or unusual cases
|
||||
s := strconv.FormatFloat(f, fmt, prec, 64)
|
||||
|
|
@ -321,30 +321,30 @@ func (b *Buffer) WriteFormat(format string, args ...interface{}) (n int, err err
|
|||
if len(args) == 0 {
|
||||
return b.WriteString(format)
|
||||
}
|
||||
|
||||
|
||||
startIdx := 0
|
||||
argIdx := 0
|
||||
totalWritten := 0
|
||||
|
||||
|
||||
// Scan the format string for format specifiers
|
||||
for i := 0; i < len(format); i++ {
|
||||
if format[i] != '%' {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Found a potential format specifier
|
||||
if i+1 >= len(format) {
|
||||
// % at the end of the string is invalid
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// Check next character
|
||||
next := format[i+1]
|
||||
if next == '%' {
|
||||
// It's an escaped %
|
||||
// Write everything up to and including the first %
|
||||
if i > startIdx {
|
||||
written, err := b.WriteString(format[startIdx:i+1])
|
||||
written, err := b.WriteString(format[startIdx : i+1])
|
||||
totalWritten += written
|
||||
if err != nil {
|
||||
return totalWritten, err
|
||||
|
|
@ -352,10 +352,10 @@ func (b *Buffer) WriteFormat(format string, args ...interface{}) (n int, err err
|
|||
}
|
||||
// Skip the second %
|
||||
i++
|
||||
startIdx = i+1
|
||||
startIdx = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Write the part before the format specifier
|
||||
if i > startIdx {
|
||||
written, err := b.WriteString(format[startIdx:i])
|
||||
|
|
@ -364,17 +364,17 @@ func (b *Buffer) WriteFormat(format string, args ...interface{}) (n int, err err
|
|||
return totalWritten, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Make sure we have an argument for this specifier
|
||||
if argIdx >= len(args) {
|
||||
// More specifiers than arguments, skip
|
||||
startIdx = i
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
arg := args[argIdx]
|
||||
argIdx++
|
||||
|
||||
|
||||
// Process the format specifier
|
||||
switch next {
|
||||
case 's':
|
||||
|
|
@ -420,12 +420,12 @@ func (b *Buffer) WriteFormat(format string, args ...interface{}) (n int, err err
|
|||
}
|
||||
totalWritten++
|
||||
}
|
||||
|
||||
|
||||
// Move past the format specifier
|
||||
i++
|
||||
startIdx = i+1
|
||||
startIdx = i + 1
|
||||
}
|
||||
|
||||
|
||||
// Write any remaining part of the format string
|
||||
if startIdx < len(format) {
|
||||
written, err := b.WriteString(format[startIdx:])
|
||||
|
|
@ -434,7 +434,7 @@ func (b *Buffer) WriteFormat(format string, args ...interface{}) (n int, err err
|
|||
return totalWritten, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return totalWritten, nil
|
||||
}
|
||||
|
||||
|
|
@ -446,19 +446,19 @@ func (b *Buffer) Grow(n int) {
|
|||
if cap(b.buf) >= needed {
|
||||
return // Already have enough capacity
|
||||
}
|
||||
|
||||
|
||||
// Grow capacity with a smart algorithm that avoids frequent resizing
|
||||
// Double the capacity until we have enough, but with some optimizations:
|
||||
// - For small buffers (<1KB), grow more aggressively (2x)
|
||||
// - For medium buffers (1KB-64KB), grow at 1.5x
|
||||
// - For large buffers (>64KB), grow at 1.25x to avoid excessive memory usage
|
||||
|
||||
|
||||
newCap := cap(b.buf)
|
||||
const (
|
||||
smallBuffer = 1024 // 1KB
|
||||
mediumBuffer = 64 * 1024 // 64KB
|
||||
)
|
||||
|
||||
|
||||
for newCap < needed {
|
||||
if newCap < smallBuffer {
|
||||
newCap *= 2 // Double small buffers
|
||||
|
|
@ -468,7 +468,7 @@ func (b *Buffer) Grow(n int) {
|
|||
newCap = newCap + newCap/4 // Grow large buffers by 1.25x
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create new buffer with the calculated capacity
|
||||
newBuf := make([]byte, len(b.buf), newCap)
|
||||
copy(newBuf, b.buf)
|
||||
|
|
@ -517,16 +517,16 @@ func WriteValue(w io.Writer, val interface{}) (n int, err error) {
|
|||
if bw, ok := w.(*Buffer); ok {
|
||||
return writeValueToBuffer(bw, val)
|
||||
}
|
||||
|
||||
|
||||
// If writer is a StringWriter, we can optimize some cases
|
||||
if sw, ok := w.(io.StringWriter); ok {
|
||||
return writeValueToStringWriter(sw, val)
|
||||
}
|
||||
|
||||
|
||||
// Fallback path - use temp buffer for conversion to avoid allocating strings
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
_, _ = writeValueToBuffer(buf, val)
|
||||
return w.Write(buf.Bytes())
|
||||
}
|
||||
|
|
@ -536,7 +536,7 @@ func writeValueToBuffer(b *Buffer, val interface{}) (n int, err error) {
|
|||
if val == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
return b.WriteString(v)
|
||||
|
|
@ -561,7 +561,7 @@ func writeValueToStringWriter(w io.StringWriter, val interface{}) (n int, err er
|
|||
if val == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
return w.WriteString(v)
|
||||
|
|
@ -600,7 +600,7 @@ func stringify(val interface{}) string {
|
|||
if val == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
// Use type switch for efficient handling of common types
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
|
|
@ -622,7 +622,7 @@ func stringify(val interface{}) string {
|
|||
case []byte:
|
||||
return string(v)
|
||||
}
|
||||
|
||||
|
||||
// Fall back to fmt.Sprintf for complex types
|
||||
return fmt.Sprintf("%v", val)
|
||||
}
|
||||
|
|
@ -634,9 +634,9 @@ func GetTokenBuffer(templateSize int) *[]Token {
|
|||
// For medium templates, use the medium pool
|
||||
// For large templates, use the large pool
|
||||
// For extremely large templates, allocate directly
|
||||
|
||||
|
||||
var buffer *[]Token
|
||||
|
||||
|
||||
if templateSize < 4*1024 {
|
||||
// Small template
|
||||
buffer = SmallTokenBufferPool.Get().(*[]Token)
|
||||
|
|
@ -664,10 +664,10 @@ func GetTokenBuffer(templateSize int) *[]Token {
|
|||
newBuffer := make([]Token, 0, estimateTokenCount(templateSize))
|
||||
buffer = &newBuffer
|
||||
}
|
||||
|
||||
|
||||
// Clear the buffer in case it contains old tokens
|
||||
*buffer = (*buffer)[:0]
|
||||
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
|
|
@ -676,10 +676,10 @@ func ReleaseTokenBuffer(buffer *[]Token) {
|
|||
if buffer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Clear the buffer to prevent memory leaks
|
||||
*buffer = (*buffer)[:0]
|
||||
|
||||
|
||||
// Put back in the appropriate pool based on capacity
|
||||
cap := cap(*buffer)
|
||||
if cap <= 64 {
|
||||
|
|
@ -699,7 +699,7 @@ func GetTokenBufferWithCapacity(capacity int) *[]Token {
|
|||
// For large capacity requests, use the large pool
|
||||
// For very large capacity requests, allocate directly
|
||||
var buffer *[]Token
|
||||
|
||||
|
||||
if capacity <= 64 {
|
||||
buffer = SmallTokenBufferPool.Get().(*[]Token)
|
||||
if cap(*buffer) < capacity {
|
||||
|
|
@ -720,10 +720,10 @@ func GetTokenBufferWithCapacity(capacity int) *[]Token {
|
|||
newBuffer := make([]Token, 0, capacity)
|
||||
buffer = &newBuffer
|
||||
}
|
||||
|
||||
|
||||
// Clear the buffer
|
||||
*buffer = (*buffer)[:0]
|
||||
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
|
|
@ -734,7 +734,7 @@ func RecycleTokens(tokens []Token) []Token {
|
|||
if len(tokens) == 0 {
|
||||
return []Token{}
|
||||
}
|
||||
|
||||
|
||||
// Create a new slice with the same backing array
|
||||
recycled := tokens[:0]
|
||||
return recycled
|
||||
|
|
@ -747,7 +747,7 @@ func estimateTokenCount(templateSize int) int {
|
|||
// - Small templates: ~1 token per 12 bytes
|
||||
// - Medium templates: ~1 token per 15 bytes
|
||||
// - Large templates: ~1 token per 20 bytes
|
||||
|
||||
|
||||
if templateSize < 4*1024 {
|
||||
// Small template
|
||||
return max(64, templateSize/12+16)
|
||||
|
|
@ -758,4 +758,4 @@ func estimateTokenCount(templateSize int) int {
|
|||
// Large template
|
||||
return max(1024, templateSize/20+64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ func BenchmarkStandardBufferWrite(b *testing.B) {
|
|||
func BenchmarkBufferIntegerFormatting(b *testing.B) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
vals := []int{0, 5, -5, 123, -123, 9999, -9999, 123456789, -123456789}
|
||||
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
|
|
@ -48,9 +48,9 @@ func BenchmarkBufferIntegerFormatting(b *testing.B) {
|
|||
|
||||
func BenchmarkStandardIntegerFormatting(b *testing.B) {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
|
||||
vals := []int{0, 5, -5, 123, -123, 9999, -9999, 123456789, -123456789}
|
||||
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
|
|
@ -65,7 +65,7 @@ func BenchmarkSmallIntegerFormatting(b *testing.B) {
|
|||
// pre-computed string table
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
b.Run("Optimized_Small_Ints", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
|
|
@ -74,7 +74,7 @@ func BenchmarkSmallIntegerFormatting(b *testing.B) {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
b.Run("Standard_Small_Ints", func(b *testing.B) {
|
||||
sbuf := &bytes.Buffer{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
|
@ -89,15 +89,15 @@ func BenchmarkSmallIntegerFormatting(b *testing.B) {
|
|||
func BenchmarkFloatFormatting(b *testing.B) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
vals := []float64{
|
||||
0.0, 1.0, -1.0, // Whole numbers
|
||||
3.14, -2.718, // Common constants
|
||||
123.456, -789.012, // Medium floats
|
||||
0.123, 0.001, 9.999, // Small decimals
|
||||
1234567.89, -9876543.21, // Large numbers
|
||||
0.0, 1.0, -1.0, // Whole numbers
|
||||
3.14, -2.718, // Common constants
|
||||
123.456, -789.012, // Medium floats
|
||||
0.123, 0.001, 9.999, // Small decimals
|
||||
1234567.89, -9876543.21, // Large numbers
|
||||
}
|
||||
|
||||
|
||||
b.Run("OptimizedFloat", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
|
|
@ -106,7 +106,7 @@ func BenchmarkFloatFormatting(b *testing.B) {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
b.Run("StandardFloat", func(b *testing.B) {
|
||||
sbuf := &bytes.Buffer{}
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
|
@ -121,19 +121,19 @@ func BenchmarkFloatFormatting(b *testing.B) {
|
|||
func BenchmarkFormatString(b *testing.B) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
format := "Hello, %s! Count: %d, Value: %v"
|
||||
name := "World"
|
||||
count := 42
|
||||
value := true
|
||||
|
||||
|
||||
b.Run("BufferFormat", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
buf.WriteFormat(format, name, count, value)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
b.Run("FmtSprintf", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Each fmt.Sprintf creates a new string
|
||||
|
|
@ -148,19 +148,19 @@ func BenchmarkFormatInt(b *testing.B) {
|
|||
_ = FormatInt(42)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
b.Run("SmallInt_Standard", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strconv.Itoa(42)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
b.Run("LargeInt_Optimized", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = FormatInt(12345678)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
b.Run("LargeInt_Standard", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = strconv.Itoa(12345678)
|
||||
|
|
@ -171,7 +171,7 @@ func BenchmarkFormatInt(b *testing.B) {
|
|||
func BenchmarkWriteValue(b *testing.B) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
values := []interface{}{
|
||||
"string value",
|
||||
123,
|
||||
|
|
@ -180,7 +180,7 @@ func BenchmarkWriteValue(b *testing.B) {
|
|||
true,
|
||||
[]byte("byte slice"),
|
||||
}
|
||||
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
|
|
@ -199,7 +199,7 @@ func BenchmarkStringifyValues(b *testing.B) {
|
|||
true,
|
||||
[]byte("byte slice"),
|
||||
}
|
||||
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range values {
|
||||
|
|
@ -217,7 +217,7 @@ func BenchmarkBufferGrowth(b *testing.B) {
|
|||
buf.Release()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
b.Run("Medium", func(b *testing.B) {
|
||||
mediumStr := "medium string that is longer than the small one but still reasonable"
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
|
@ -226,7 +226,7 @@ func BenchmarkBufferGrowth(b *testing.B) {
|
|||
buf.Release()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
b.Run("Large", func(b *testing.B) {
|
||||
largeStr := string(make([]byte, 2048)) // 2KB string
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
|
@ -240,13 +240,13 @@ func BenchmarkBufferGrowth(b *testing.B) {
|
|||
func BenchmarkBufferToWriter(b *testing.B) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
str := "This is a test string that will be written to a discard writer"
|
||||
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf.Reset()
|
||||
buf.WriteString(str)
|
||||
buf.WriteTo(io.Discard)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func TestBufferPool(t *testing.T) {
|
|||
if buf == nil {
|
||||
t.Fatal("GetBuffer() returned nil")
|
||||
}
|
||||
|
||||
|
||||
// Test writing to the buffer
|
||||
str := "Hello, world!"
|
||||
n, err := buf.WriteString(str)
|
||||
|
|
@ -21,28 +21,28 @@ func TestBufferPool(t *testing.T) {
|
|||
if n != len(str) {
|
||||
t.Fatalf("WriteString returned wrong length: got %d, want %d", n, len(str))
|
||||
}
|
||||
|
||||
|
||||
// Test getting the string back
|
||||
if buf.String() != str {
|
||||
t.Fatalf("String() returned wrong value: got %q, want %q", buf.String(), str)
|
||||
}
|
||||
|
||||
|
||||
// Test resetting the buffer
|
||||
buf.Reset()
|
||||
if buf.Len() != 0 {
|
||||
t.Fatalf("Reset() didn't clear the buffer: length = %d", buf.Len())
|
||||
}
|
||||
|
||||
|
||||
// Test writing a different string after reset
|
||||
str2 := "Another string"
|
||||
buf.WriteString(str2)
|
||||
if buf.String() != str2 {
|
||||
t.Fatalf("String() after reset returned wrong value: got %q, want %q", buf.String(), str2)
|
||||
}
|
||||
|
||||
|
||||
// Test releasing the buffer
|
||||
buf.Release()
|
||||
|
||||
|
||||
// Getting a new buffer should not have any content
|
||||
buf2 := GetBuffer()
|
||||
if buf2.Len() != 0 {
|
||||
|
|
@ -54,7 +54,7 @@ func TestBufferPool(t *testing.T) {
|
|||
func TestWriteValue(t *testing.T) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
tests := []struct {
|
||||
value interface{}
|
||||
expected string
|
||||
|
|
@ -68,7 +68,7 @@ func TestWriteValue(t *testing.T) {
|
|||
{false, "false"},
|
||||
{[]byte("bytes"), "bytes"},
|
||||
}
|
||||
|
||||
|
||||
for _, test := range tests {
|
||||
buf.Reset()
|
||||
_, err := WriteValue(buf, test.value)
|
||||
|
|
@ -76,7 +76,7 @@ func TestWriteValue(t *testing.T) {
|
|||
t.Errorf("WriteValue(%v) error: %v", test.value, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("WriteValue(%v) = %q, want %q", test.value, buf.String(), test.expected)
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ func TestWriteValue(t *testing.T) {
|
|||
func TestWriteInt(t *testing.T) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
tests := []struct {
|
||||
value int
|
||||
expected string
|
||||
|
|
@ -101,7 +101,7 @@ func TestWriteInt(t *testing.T) {
|
|||
{123456789, "123456789"},
|
||||
{-123456789, "-123456789"},
|
||||
}
|
||||
|
||||
|
||||
for _, test := range tests {
|
||||
buf.Reset()
|
||||
_, err := buf.WriteInt(test.value)
|
||||
|
|
@ -109,7 +109,7 @@ func TestWriteInt(t *testing.T) {
|
|||
t.Errorf("WriteInt(%d) error: %v", test.value, err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
if buf.String() != test.expected {
|
||||
t.Errorf("WriteInt(%d) = %q, want %q", test.value, buf.String(), test.expected)
|
||||
}
|
||||
|
|
@ -119,13 +119,13 @@ func TestWriteInt(t *testing.T) {
|
|||
func TestBufferWriteTo(t *testing.T) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
testStr := "This is a test string"
|
||||
buf.WriteString(testStr)
|
||||
|
||||
|
||||
// Create a destination buffer to write to
|
||||
var dest strings.Builder
|
||||
|
||||
|
||||
n, err := buf.WriteTo(&dest)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteTo failed: %v", err)
|
||||
|
|
@ -133,7 +133,7 @@ func TestBufferWriteTo(t *testing.T) {
|
|||
if n != int64(len(testStr)) {
|
||||
t.Fatalf("WriteTo returned wrong length: got %d, want %d", n, len(testStr))
|
||||
}
|
||||
|
||||
|
||||
if dest.String() != testStr {
|
||||
t.Fatalf("WriteTo output mismatch: got %q, want %q", dest.String(), testStr)
|
||||
}
|
||||
|
|
@ -142,25 +142,25 @@ func TestBufferWriteTo(t *testing.T) {
|
|||
func TestBufferGrowCapacity(t *testing.T) {
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
// Start with small string
|
||||
initialStr := "small"
|
||||
buf.WriteString(initialStr)
|
||||
initialCapacity := cap(buf.buf)
|
||||
|
||||
|
||||
// Write a larger string that should cause a grow
|
||||
largeStr := strings.Repeat("abcdefghijklmnopqrstuvwxyz", 100) // 2600 bytes
|
||||
buf.WriteString(largeStr)
|
||||
|
||||
|
||||
// Verify capacity increased
|
||||
if cap(buf.buf) <= initialCapacity {
|
||||
t.Fatalf("Buffer didn't grow capacity: initial=%d, after=%d",
|
||||
t.Fatalf("Buffer didn't grow capacity: initial=%d, after=%d",
|
||||
initialCapacity, cap(buf.buf))
|
||||
}
|
||||
|
||||
|
||||
// Verify content is correct
|
||||
expected := initialStr + largeStr
|
||||
if buf.String() != expected {
|
||||
t.Fatalf("Buffer content incorrect after growth")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
84
expr.go
84
expr.go
|
|
@ -466,22 +466,22 @@ func NewFunctionNode(name string, args []Node, line int) *FunctionNode {
|
|||
func ParseExpressionOptimized(expr string) (Node, bool) {
|
||||
// Trim whitespace
|
||||
expr = strings.TrimSpace(expr)
|
||||
|
||||
|
||||
// Quick empty check
|
||||
if len(expr) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
||||
// Try parsing as literal
|
||||
if val, ok := ParseLiteralOptimized(expr); ok {
|
||||
return NewLiteralNode(val, 0), true
|
||||
}
|
||||
|
||||
|
||||
// Check for simple variable references
|
||||
if IsValidVariableName(expr) {
|
||||
return NewVariableNode(expr, 0), true
|
||||
}
|
||||
|
||||
|
||||
// More complex expression - will need the full parser
|
||||
return nil, false
|
||||
}
|
||||
|
|
@ -492,27 +492,27 @@ func ParseLiteralOptimized(expr string) (interface{}, bool) {
|
|||
if len(expr) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
||||
// Check for string literals
|
||||
if len(expr) >= 2 && ((expr[0] == '"' && expr[len(expr)-1] == '"') ||
|
||||
(expr[0] == '\'' && expr[len(expr)-1] == '\'')) {
|
||||
if len(expr) >= 2 && ((expr[0] == '"' && expr[len(expr)-1] == '"') ||
|
||||
(expr[0] == '\'' && expr[len(expr)-1] == '\'')) {
|
||||
// String literal
|
||||
return expr[1 : len(expr)-1], true
|
||||
}
|
||||
|
||||
|
||||
// Check for number literals
|
||||
if isDigit(expr[0]) || (expr[0] == '-' && len(expr) > 1 && isDigit(expr[1])) {
|
||||
// Try parsing as integer first (most common case)
|
||||
if i, err := strconv.Atoi(expr); err == nil {
|
||||
return i, true
|
||||
}
|
||||
|
||||
|
||||
// Try parsing as float
|
||||
if f, err := strconv.ParseFloat(expr, 64); err == nil {
|
||||
return f, true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for boolean literals
|
||||
if expr == "true" {
|
||||
return true, true
|
||||
|
|
@ -523,7 +523,7 @@ func ParseLiteralOptimized(expr string) (interface{}, bool) {
|
|||
if expr == "null" || expr == "nil" {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
|
||||
// Not a simple literal
|
||||
return nil, false
|
||||
}
|
||||
|
|
@ -533,25 +533,25 @@ func IsValidVariableName(name string) bool {
|
|||
if len(name) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// First character must be a letter or underscore
|
||||
if !isAlpha(name[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Rest can be letters, digits, or underscores
|
||||
for i := 1; i < len(name); i++ {
|
||||
if !isNameChar(name[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for reserved keywords
|
||||
switch name {
|
||||
case "true", "false", "null", "nil", "not", "and", "or", "in", "is":
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -566,22 +566,22 @@ func ProcessStringEscapes(text string) (string, bool) {
|
|||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no escapes, return the original string
|
||||
if !hasEscape {
|
||||
return text, false
|
||||
}
|
||||
|
||||
|
||||
// Process escapes
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(text))
|
||||
|
||||
|
||||
i := 0
|
||||
for i < len(text) {
|
||||
if text[i] == '\\' && i+1 < len(text) {
|
||||
// Escape sequence found
|
||||
i++
|
||||
|
||||
|
||||
// Process the escape sequence
|
||||
switch text[i] {
|
||||
case 'n':
|
||||
|
|
@ -611,7 +611,7 @@ func ProcessStringEscapes(text string) (string, bool) {
|
|||
}
|
||||
i++
|
||||
}
|
||||
|
||||
|
||||
// Return the processed string
|
||||
return sb.String(), true
|
||||
}
|
||||
|
|
@ -623,81 +623,81 @@ func ParseNumberOptimized(text string) (interface{}, bool) {
|
|||
if len(text) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
||||
// Check for negative sign
|
||||
isNegative := false
|
||||
pos := 0
|
||||
|
||||
|
||||
if text[pos] == '-' {
|
||||
isNegative = true
|
||||
pos++
|
||||
|
||||
|
||||
// Just a minus sign is not a number
|
||||
if pos >= len(text) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Parse the integer part
|
||||
var intValue int64
|
||||
hasDigits := false
|
||||
|
||||
|
||||
for pos < len(text) && isDigit(text[pos]) {
|
||||
digit := int64(text[pos] - '0')
|
||||
intValue = intValue*10 + digit
|
||||
hasDigits = true
|
||||
pos++
|
||||
}
|
||||
|
||||
|
||||
if !hasDigits {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
||||
// Check for decimal point
|
||||
hasDecimal := false
|
||||
var floatValue float64
|
||||
|
||||
|
||||
if pos < len(text) && text[pos] == '.' {
|
||||
hasDecimal = true
|
||||
pos++
|
||||
|
||||
|
||||
// Need at least one digit after decimal point
|
||||
if pos >= len(text) || !isDigit(text[pos]) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
||||
// Parse the fractional part
|
||||
floatValue = float64(intValue)
|
||||
fraction := 0.0
|
||||
multiplier := 0.1
|
||||
|
||||
|
||||
for pos < len(text) && isDigit(text[pos]) {
|
||||
digit := float64(text[pos] - '0')
|
||||
fraction += digit * multiplier
|
||||
multiplier *= 0.1
|
||||
pos++
|
||||
}
|
||||
|
||||
|
||||
floatValue += fraction
|
||||
}
|
||||
|
||||
|
||||
// Check for exponent
|
||||
if pos < len(text) && (text[pos] == 'e' || text[pos] == 'E') {
|
||||
hasDecimal = true
|
||||
pos++
|
||||
|
||||
|
||||
// Parse exponent sign
|
||||
expNegative := false
|
||||
if pos < len(text) && (text[pos] == '+' || text[pos] == '-') {
|
||||
expNegative = text[pos] == '-'
|
||||
pos++
|
||||
}
|
||||
|
||||
|
||||
// Need at least one digit in exponent
|
||||
if pos >= len(text) || !isDigit(text[pos]) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
||||
// Parse exponent value
|
||||
exp := 0
|
||||
for pos < len(text) && isDigit(text[pos]) {
|
||||
|
|
@ -705,14 +705,14 @@ func ParseNumberOptimized(text string) (interface{}, bool) {
|
|||
exp = exp*10 + digit
|
||||
pos++
|
||||
}
|
||||
|
||||
|
||||
// Apply exponent
|
||||
// Convert to float regardless of hasDecimal
|
||||
hasDecimal = true
|
||||
if floatValue == 0 { // Not set yet
|
||||
floatValue = float64(intValue)
|
||||
}
|
||||
|
||||
|
||||
// Apply exponent using a more efficient approach
|
||||
if expNegative {
|
||||
// For negative exponents, divide by 10^exp
|
||||
|
|
@ -730,12 +730,12 @@ func ParseNumberOptimized(text string) (interface{}, bool) {
|
|||
floatValue *= multiplier
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check that we consumed the whole string
|
||||
if pos < len(text) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
|
||||
// Return the appropriate value
|
||||
if hasDecimal {
|
||||
if isNegative {
|
||||
|
|
@ -768,10 +768,10 @@ func isNameChar(c byte) bool {
|
|||
// This is useful for consistent hashing of variable names or strings
|
||||
func calcStringHash(s string) uint32 {
|
||||
var h uint32
|
||||
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
h = 31*h + uint32(s[i])
|
||||
}
|
||||
|
||||
|
||||
return h
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ func BenchmarkExpressionRender(b *testing.B) {
|
|||
|
||||
// Create a buffer for testing
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
|
||||
// Run each benchmark
|
||||
for _, tc := range tests {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
|
|
@ -157,7 +157,7 @@ func BenchmarkExpressionRender(b *testing.B) {
|
|||
func BenchmarkFilterChain(b *testing.B) {
|
||||
engine := New()
|
||||
ctx := NewRenderContext(engine.environment, map[string]interface{}{
|
||||
"a": 10,
|
||||
"a": 10,
|
||||
"text": "Hello, World!",
|
||||
"html": "<p>This is a paragraph</p>",
|
||||
}, engine)
|
||||
|
|
@ -177,12 +177,12 @@ func BenchmarkFilterChain(b *testing.B) {
|
|||
node: NewFilterNode(
|
||||
NewFilterNode(
|
||||
NewVariableNode("text", 1),
|
||||
"upper",
|
||||
nil,
|
||||
"upper",
|
||||
nil,
|
||||
1,
|
||||
),
|
||||
"trim",
|
||||
nil,
|
||||
"trim",
|
||||
nil,
|
||||
1,
|
||||
),
|
||||
},
|
||||
|
|
@ -190,11 +190,11 @@ func BenchmarkFilterChain(b *testing.B) {
|
|||
name: "FilterWithArgs",
|
||||
node: NewFilterNode(
|
||||
NewVariableNode("text", 1),
|
||||
"replace",
|
||||
"replace",
|
||||
[]Node{
|
||||
NewLiteralNode("World", 1),
|
||||
NewLiteralNode("Universe", 1),
|
||||
},
|
||||
},
|
||||
1,
|
||||
),
|
||||
},
|
||||
|
|
@ -217,8 +217,8 @@ func BenchmarkFunctionCall(b *testing.B) {
|
|||
engine := New()
|
||||
ctx := NewRenderContext(engine.environment, map[string]interface{}{
|
||||
"numbers": []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
||||
"start": 1,
|
||||
"end": 10,
|
||||
"start": 1,
|
||||
"end": 10,
|
||||
}, engine)
|
||||
defer ctx.Release()
|
||||
|
||||
|
|
@ -265,12 +265,12 @@ func BenchmarkArgSlicePooling(b *testing.B) {
|
|||
engine := New()
|
||||
ctx := NewRenderContext(engine.environment, nil, engine)
|
||||
defer ctx.Release()
|
||||
|
||||
|
||||
smallArgs := []Node{
|
||||
NewLiteralNode(1, 1),
|
||||
NewLiteralNode(2, 1),
|
||||
}
|
||||
|
||||
|
||||
mediumArgs := []Node{
|
||||
NewLiteralNode(1, 1),
|
||||
NewLiteralNode(2, 1),
|
||||
|
|
@ -278,12 +278,12 @@ func BenchmarkArgSlicePooling(b *testing.B) {
|
|||
NewLiteralNode(4, 1),
|
||||
NewLiteralNode(5, 1),
|
||||
}
|
||||
|
||||
|
||||
largeArgs := make([]Node, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
largeArgs[i] = NewLiteralNode(i, 1)
|
||||
}
|
||||
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
node Node
|
||||
|
|
@ -305,7 +305,7 @@ func BenchmarkArgSlicePooling(b *testing.B) {
|
|||
node: NewFunctionNode("range", largeArgs, 1),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
for _, tc := range tests {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
|
@ -314,4 +314,4 @@ func BenchmarkArgSlicePooling(b *testing.B) {
|
|||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
40
expr_pool.go
40
expr_pool.go
|
|
@ -363,9 +363,9 @@ func GetArgSlice(size int) []interface{} {
|
|||
if size <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
var slice []interface{}
|
||||
|
||||
|
||||
switch {
|
||||
case size <= 2:
|
||||
slice = smallArgSlicePool.Get().([]interface{})
|
||||
|
|
@ -377,7 +377,7 @@ func GetArgSlice(size int) []interface{} {
|
|||
// For very large slices, just allocate directly
|
||||
return make([]interface{}, 0, size)
|
||||
}
|
||||
|
||||
|
||||
// Clear the slice but maintain capacity
|
||||
return slice[:0]
|
||||
}
|
||||
|
|
@ -387,15 +387,15 @@ func ReleaseArgSlice(slice []interface{}) {
|
|||
if slice == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Clear all references
|
||||
for i := range slice {
|
||||
slice[i] = nil
|
||||
}
|
||||
|
||||
|
||||
// Reset length to 0
|
||||
slice = slice[:0]
|
||||
|
||||
|
||||
// Return to appropriate pool based on capacity
|
||||
switch cap(slice) {
|
||||
case 2:
|
||||
|
|
@ -440,7 +440,7 @@ func GetHashMap(size int) map[string]interface{} {
|
|||
}
|
||||
return hashMap
|
||||
}
|
||||
|
||||
|
||||
// For larger maps, just allocate directly
|
||||
return make(map[string]interface{}, size)
|
||||
}
|
||||
|
|
@ -450,26 +450,26 @@ func ReleaseHashMap(hashMap map[string]interface{}) {
|
|||
if hashMap == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Clear all entries (not used directly in our defer block)
|
||||
// for k := range hashMap {
|
||||
// delete(hashMap, k)
|
||||
// }
|
||||
|
||||
|
||||
// Return to appropriate pool based on capacity
|
||||
// We don't actually clear the map when releasing through the defer,
|
||||
// We don't actually clear the map when releasing through the defer,
|
||||
// because we return the map as the result and deleting entries would
|
||||
// clear the returned result
|
||||
|
||||
|
||||
// Map doesn't have a built-in cap function
|
||||
// Not using pool return for maps directly returned as results
|
||||
|
||||
/*
|
||||
switch {
|
||||
case len(hashMap) <= 5:
|
||||
smallHashMapPool.Put(hashMap)
|
||||
case len(hashMap) <= 15:
|
||||
mediumHashMapPool.Put(hashMap)
|
||||
}
|
||||
|
||||
/*
|
||||
switch {
|
||||
case len(hashMap) <= 5:
|
||||
smallHashMapPool.Put(hashMap)
|
||||
case len(hashMap) <= 15:
|
||||
mediumHashMapPool.Put(hashMap)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,4 +297,4 @@ func ReleaseApplyNode(node *ApplyNode) {
|
|||
node.filter = ""
|
||||
node.args = nil
|
||||
ApplyNodePool.Put(node)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
parse_do.go
22
parse_do.go
|
|
@ -21,11 +21,11 @@ func (p *Parser) parseDo(parser *Parser) (Node, error) {
|
|||
// Look ahead to find possible assignment patterns
|
||||
// We need to check for NUMBER = EXPR which is invalid
|
||||
// as well as NAME = EXPR which is valid
|
||||
|
||||
|
||||
// Check if we have an equals sign in the next few tokens
|
||||
hasAssignment := false
|
||||
equalsPosition := -1
|
||||
|
||||
|
||||
// Scan ahead a bit to find possible equals sign
|
||||
for i := 0; i < 3 && parser.tokenIndex+i < len(parser.tokens); i++ {
|
||||
token := parser.tokens[parser.tokenIndex+i]
|
||||
|
|
@ -34,44 +34,44 @@ func (p *Parser) parseDo(parser *Parser) (Node, error) {
|
|||
equalsPosition = i
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// Stop scanning if we hit the end of the block
|
||||
if token.Type == TOKEN_BLOCK_END {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we found an equals sign, analyze the left-hand side
|
||||
if hasAssignment && equalsPosition > 0 {
|
||||
firstToken := parser.tokens[parser.tokenIndex]
|
||||
|
||||
|
||||
// Check if the left-hand side is a valid variable name
|
||||
isValidVariableName := firstToken.Type == TOKEN_NAME
|
||||
|
||||
|
||||
// If the left-hand side is a number or literal, that's an error
|
||||
if !isValidVariableName {
|
||||
return nil, fmt.Errorf("invalid variable name %q in do tag assignment at line %d", firstToken.Value, doLine)
|
||||
}
|
||||
|
||||
|
||||
// Handle assignment case
|
||||
if isValidVariableName && hasAssignment {
|
||||
varName := parser.tokens[parser.tokenIndex].Value
|
||||
|
||||
|
||||
// Skip tokens up to and including the equals sign
|
||||
parser.tokenIndex += equalsPosition + 1
|
||||
|
||||
|
||||
// Parse the right side expression
|
||||
expr, err := parser.parseExpression()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing expression in do assignment at line %d: %w", doLine, err)
|
||||
}
|
||||
|
||||
|
||||
// Make sure we have the closing tag
|
||||
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
|
||||
return nil, fmt.Errorf("expecting end of do tag at line %d", doLine)
|
||||
}
|
||||
parser.tokenIndex++
|
||||
|
||||
|
||||
// Additional validation for variable name
|
||||
if _, err := strconv.Atoi(varName); err == nil {
|
||||
return nil, fmt.Errorf("invalid variable name %q in do tag assignment at line %d", varName, doLine)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
func (p *Parser) parseFrom(parser *Parser) (Node, error) {
|
||||
// Get the line number of the from token
|
||||
fromLine := parser.tokens[parser.tokenIndex-1].Line
|
||||
|
||||
|
||||
// Debugging: Print out tokens for debugging purposes
|
||||
if IsDebugEnabled() {
|
||||
LogDebug("Parsing from tag. Next tokens (up to 10):")
|
||||
|
|
@ -22,27 +22,27 @@ func (p *Parser) parseFrom(parser *Parser) (Node, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// First try to parse in token-by-token format (from zero-allocation tokenizer)
|
||||
// Check if we have a proper sequence: [STRING/NAME, NAME="import", NAME="macroname", ...]
|
||||
if parser.tokenIndex+1 < len(parser.tokens) {
|
||||
// Check for (template path) followed by "import" keyword
|
||||
firstToken := parser.tokens[parser.tokenIndex]
|
||||
secondToken := parser.tokens[parser.tokenIndex+1]
|
||||
|
||||
|
||||
isTemplatePath := firstToken.Type == TOKEN_STRING || firstToken.Type == TOKEN_NAME
|
||||
isImportKeyword := secondToken.Type == TOKEN_NAME && secondToken.Value == "import"
|
||||
|
||||
|
||||
if isTemplatePath && isImportKeyword {
|
||||
LogDebug("Found tokenized from...import pattern")
|
||||
|
||||
|
||||
// Get template path
|
||||
templatePath := firstToken.Value
|
||||
if firstToken.Type == TOKEN_NAME {
|
||||
// For paths like ./file.twig, just use the value
|
||||
templatePath = strings.Trim(templatePath, "\"'")
|
||||
}
|
||||
|
||||
|
||||
// Create template expression
|
||||
templateExpr := &LiteralNode{
|
||||
ExpressionNode: ExpressionNode{
|
||||
|
|
@ -51,46 +51,46 @@ func (p *Parser) parseFrom(parser *Parser) (Node, error) {
|
|||
},
|
||||
value: templatePath,
|
||||
}
|
||||
|
||||
|
||||
// Skip past the template path and import keyword
|
||||
parser.tokenIndex += 2
|
||||
|
||||
|
||||
// Parse macros and aliases
|
||||
macros := []string{}
|
||||
aliases := map[string]string{}
|
||||
|
||||
|
||||
// Process tokens until end of block
|
||||
for parser.tokenIndex < len(parser.tokens) {
|
||||
token := parser.tokens[parser.tokenIndex]
|
||||
|
||||
|
||||
// Stop at block end
|
||||
if token.Type == TOKEN_BLOCK_END || token.Type == TOKEN_BLOCK_END_TRIM {
|
||||
parser.tokenIndex++
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// Skip punctuation (commas)
|
||||
if token.Type == TOKEN_PUNCTUATION {
|
||||
parser.tokenIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Handle macro name
|
||||
if token.Type == TOKEN_NAME {
|
||||
macroName := token.Value
|
||||
|
||||
|
||||
// Add to macros list
|
||||
macros = append(macros, macroName)
|
||||
|
||||
|
||||
// Check for alias
|
||||
parser.tokenIndex++
|
||||
if parser.tokenIndex < len(parser.tokens) &&
|
||||
parser.tokens[parser.tokenIndex].Type == TOKEN_NAME &&
|
||||
parser.tokens[parser.tokenIndex].Value == "as" {
|
||||
|
||||
if parser.tokenIndex < len(parser.tokens) &&
|
||||
parser.tokens[parser.tokenIndex].Type == TOKEN_NAME &&
|
||||
parser.tokens[parser.tokenIndex].Value == "as" {
|
||||
|
||||
// Skip 'as' keyword
|
||||
parser.tokenIndex++
|
||||
|
||||
|
||||
// Get alias
|
||||
if parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
|
||||
aliases[macroName] = parser.tokens[parser.tokenIndex].Value
|
||||
|
|
@ -102,14 +102,14 @@ func (p *Parser) parseFrom(parser *Parser) (Node, error) {
|
|||
parser.tokenIndex++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we found macros, return a FromImportNode
|
||||
if len(macros) > 0 {
|
||||
return NewFromImportNode(templateExpr, macros, aliases, fromLine), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fall back to the original approach (for backward compatibility)
|
||||
// We need to extract the template path, import keyword, and macro(s) from
|
||||
// the current token. The tokenizer may be combining them.
|
||||
|
|
|
|||
10
parser.go
10
parser.go
|
|
@ -62,10 +62,10 @@ func (p *Parser) Parse(source string) (Node, error) {
|
|||
// Use the optimized tokenizer for maximum performance and minimal allocations
|
||||
// This will treat everything outside twig tags as TEXT tokens
|
||||
var err error
|
||||
|
||||
|
||||
// Use zero allocation tokenizer for optimal performance
|
||||
tokenizer := GetTokenizer(p.source, 0)
|
||||
|
||||
|
||||
// Use optimized version for larger templates
|
||||
if len(p.source) > 4096 {
|
||||
// Use the optimized tag detection for large templates
|
||||
|
|
@ -74,15 +74,15 @@ func (p *Parser) Parse(source string) (Node, error) {
|
|||
// Use regular tokenization for smaller templates
|
||||
p.tokens, err = tokenizer.TokenizeHtmlPreserving()
|
||||
}
|
||||
|
||||
|
||||
// Apply whitespace control to handle whitespace trimming directives
|
||||
if err == nil {
|
||||
tokenizer.ApplyWhitespaceControl()
|
||||
}
|
||||
|
||||
|
||||
// Return the tokenizer to the pool
|
||||
ReleaseTokenizer(tokenizer)
|
||||
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tokenization error: %w", err)
|
||||
}
|
||||
|
|
|
|||
36
render.go
36
render.go
|
|
@ -17,17 +17,17 @@ import (
|
|||
|
||||
// RenderContext holds the state during template rendering
|
||||
type RenderContext struct {
|
||||
env *Environment
|
||||
context map[string]interface{}
|
||||
blocks map[string][]Node
|
||||
parentBlocks map[string][]Node // Original block content from parent templates
|
||||
macros map[string]Node
|
||||
parent *RenderContext
|
||||
engine *Engine // Reference to engine for loading templates
|
||||
extending bool // Whether this template extends another
|
||||
currentBlock *BlockNode // Current block being rendered (for parent() function)
|
||||
inParentCall bool // Flag to indicate if we're currently rendering a parent() call
|
||||
sandboxed bool // Flag indicating if this context is sandboxed
|
||||
env *Environment
|
||||
context map[string]interface{}
|
||||
blocks map[string][]Node
|
||||
parentBlocks map[string][]Node // Original block content from parent templates
|
||||
macros map[string]Node
|
||||
parent *RenderContext
|
||||
engine *Engine // Reference to engine for loading templates
|
||||
extending bool // Whether this template extends another
|
||||
currentBlock *BlockNode // Current block being rendered (for parent() function)
|
||||
inParentCall bool // Flag to indicate if we're currently rendering a parent() call
|
||||
sandboxed bool // Flag indicating if this context is sandboxed
|
||||
lastLoadedTemplate *Template // The template that created this context (for resolving relative paths)
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +331,7 @@ func (ctx *RenderContext) Clone() *RenderContext {
|
|||
|
||||
// Inherit sandbox state
|
||||
newCtx.sandboxed = ctx.sandboxed
|
||||
|
||||
|
||||
// Copy the lastLoadedTemplate reference (crucial for relative path resolution)
|
||||
newCtx.lastLoadedTemplate = ctx.lastLoadedTemplate
|
||||
|
||||
|
|
@ -782,7 +782,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
// We need to avoid pooling for arrays that might be directly used by filters like merge
|
||||
// as those filters return the slice directly to the user
|
||||
items := make([]interface{}, 0, len(n.items))
|
||||
|
||||
|
||||
for i := 0; i < len(n.items); i++ {
|
||||
val, err := ctx.EvaluateExpression(n.items[i])
|
||||
if err != nil {
|
||||
|
|
@ -802,7 +802,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
// Evaluate each key-value pair in the hash using a new map
|
||||
// We can't use pooling with defer here because the map is returned directly
|
||||
result := make(map[string]interface{}, len(n.items))
|
||||
|
||||
|
||||
for k, v := range n.items {
|
||||
// Evaluate the key
|
||||
keyVal, err := ctx.EvaluateExpression(k)
|
||||
|
|
@ -839,7 +839,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
|
||||
// Evaluate all arguments - need direct allocation
|
||||
args := make([]interface{}, len(n.args))
|
||||
|
||||
|
||||
for i := 0; i < len(n.args); i++ {
|
||||
val, err := ctx.EvaluateExpression(n.args[i])
|
||||
if err != nil {
|
||||
|
|
@ -881,7 +881,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
if macro, ok := ctx.GetMacro(n.name); ok {
|
||||
// Evaluate arguments - need direct allocation for macro calls
|
||||
args := make([]interface{}, len(n.args))
|
||||
|
||||
|
||||
// Evaluate arguments
|
||||
for i := 0; i < len(n.args); i++ {
|
||||
val, err := ctx.EvaluateExpression(n.args[i])
|
||||
|
|
@ -904,7 +904,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
// Otherwise, it's a regular function call
|
||||
// Evaluate arguments - need direct allocation for function calls
|
||||
args := make([]interface{}, len(n.args))
|
||||
|
||||
|
||||
// Evaluate arguments
|
||||
for i := 0; i < len(n.args); i++ {
|
||||
val, err := ctx.EvaluateExpression(n.args[i])
|
||||
|
|
@ -1026,7 +1026,7 @@ func (ctx *RenderContext) EvaluateExpression(node Node) (interface{}, error) {
|
|||
|
||||
// Evaluate test arguments - need direct allocation
|
||||
args := make([]interface{}, len(n.args))
|
||||
|
||||
|
||||
// Evaluate arguments
|
||||
for i := 0; i < len(n.args); i++ {
|
||||
val, err := ctx.EvaluateExpression(n.args[i])
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@ func BenchmarkRenderContextCloning(b *testing.B) {
|
|||
for i := 0; i < b.N; i++ {
|
||||
// Clone the context - this should reuse memory from pools
|
||||
child := parent.Clone()
|
||||
|
||||
|
||||
// Do some operations on the child context
|
||||
child.SetVariable("newVar", "test value")
|
||||
|
||||
|
||||
// Release the child context
|
||||
child.Release()
|
||||
}
|
||||
|
|
@ -115,10 +115,10 @@ func BenchmarkContextVariableLookup(b *testing.B) {
|
|||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Test variable lookup at different levels
|
||||
level2.GetVariable("level2Var") // Local var
|
||||
level2.GetVariable("level1Var") // Parent var
|
||||
level2.GetVariable("rootVar") // Root var
|
||||
level2.GetVariable("shared") // Shadowed var
|
||||
level2.GetVariable("level2Var") // Local var
|
||||
level2.GetVariable("level1Var") // Parent var
|
||||
level2.GetVariable("rootVar") // Root var
|
||||
level2.GetVariable("shared") // Shadowed var
|
||||
level2.GetVariable("nonExistentVar") // Missing var
|
||||
}
|
||||
|
||||
|
|
@ -126,4 +126,4 @@ func BenchmarkContextVariableLookup(b *testing.B) {
|
|||
level2.Release()
|
||||
level1.Release()
|
||||
rootCtx.Release()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ func processWhitespaceControl(tokens []Token) []Token {
|
|||
|
||||
// Modify tokens in-place to avoid allocation
|
||||
// This works because we're only changing token values, not adding/removing tokens
|
||||
|
||||
|
||||
// Process each token to apply whitespace trimming
|
||||
for i := 0; i < len(tokens); i++ {
|
||||
token := tokens[i]
|
||||
|
|
|
|||
2
twig.go
2
twig.go
|
|
@ -607,7 +607,7 @@ func (t *Template) Render(context map[string]interface{}) (string, error) {
|
|||
func (t *Template) RenderTo(w io.Writer, context map[string]interface{}) error {
|
||||
// Get a render context from the pool
|
||||
ctx := NewRenderContext(t.env, context, t.engine)
|
||||
|
||||
|
||||
// Set the template as the lastLoadedTemplate for relative path resolution
|
||||
ctx.lastLoadedTemplate = t
|
||||
|
||||
|
|
|
|||
12
utility.go
12
utility.go
|
|
@ -25,7 +25,7 @@ func WriteString(w io.Writer, s string) (int, error) {
|
|||
if sw, ok := w.(io.StringWriter); ok {
|
||||
return sw.WriteString(s)
|
||||
}
|
||||
|
||||
|
||||
// Fast path for our own Buffer type
|
||||
if buf, ok := w.(*Buffer); ok {
|
||||
return buf.WriteString(s)
|
||||
|
|
@ -46,14 +46,14 @@ func WriteFormat(w io.Writer, format string, args ...interface{}) (int, error) {
|
|||
if buf, ok := w.(*Buffer); ok {
|
||||
return buf.WriteFormat(format, args...)
|
||||
}
|
||||
|
||||
|
||||
// Use a pooled buffer for other writer types
|
||||
buf := GetBuffer()
|
||||
defer buf.Release()
|
||||
|
||||
|
||||
// Write the formatted string to the buffer
|
||||
buf.WriteFormat(format, args...)
|
||||
|
||||
|
||||
// Write the buffer to the writer
|
||||
return w.Write(buf.Bytes())
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ func FormatInt(i int) string {
|
|||
} else if i > -100 && i < 0 {
|
||||
return smallNegIntStrings[-i]
|
||||
}
|
||||
|
||||
|
||||
// Fall back to standard formatting
|
||||
return strconv.Itoa(i)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue