ViewVC Help
View File | Revision Log | Show Annotations | Revision Graph | Root Listing
root/aya/vendor/github.com/eknkc/amber/compiler.go
Revision: 1.1
Committed: Mon Sep 30 00:42:06 2024 UTC (6 weeks, 4 days ago) by yakumo_izuru
Branch: MAIN
CVS Tags: HEAD
Log Message:
Mirrored from https://git.chaotic.ninja/git/yakumo_izuru/aya

File Contents

# Content
1 package amber
2
3 import (
4 "bytes"
5 "container/list"
6 "errors"
7 "fmt"
8 "go/ast"
9 gp "go/parser"
10 gt "go/token"
11 "html/template"
12 "io"
13 "net/http"
14 "os"
15 "path/filepath"
16 "reflect"
17 "regexp"
18 "sort"
19 "strconv"
20 "strings"
21
22 "github.com/eknkc/amber/parser"
23 )
24
25 var builtinFunctions = [...]string{
26 "len",
27 "print",
28 "printf",
29 "println",
30 "urlquery",
31 "js",
32 "json",
33 "index",
34 "html",
35 "unescaped",
36 }
37
38 const (
39 dollar = "__DOLLAR__"
40 )
41
42 // Compiler is the main interface of Amber Template Engine.
43 // In order to use an Amber template, it is required to create a Compiler and
44 // compile an Amber source to native Go template.
45 // compiler := amber.New()
46 // // Parse the input file
47 // err := compiler.ParseFile("./input.amber")
48 // if err == nil {
49 // // Compile input file to Go template
50 // tpl, err := compiler.Compile()
51 // if err == nil {
52 // // Check built in html/template documentation for further details
53 // tpl.Execute(os.Stdout, somedata)
54 // }
55 // }
56 type Compiler struct {
57 // Compiler options
58 Options
59 filename string
60 node parser.Node
61 indentLevel int
62 newline bool
63 buffer *bytes.Buffer
64 tempvarIndex int
65 mixins map[string]*parser.Mixin
66 }
67
68 // New creates and initialize a new Compiler.
69 func New() *Compiler {
70 compiler := new(Compiler)
71 compiler.filename = ""
72 compiler.tempvarIndex = 0
73 compiler.PrettyPrint = true
74 compiler.Options = DefaultOptions
75 compiler.mixins = make(map[string]*parser.Mixin)
76
77 return compiler
78 }
79
80 // Options defines template output behavior.
81 type Options struct {
82 // Setting if pretty printing is enabled.
83 // Pretty printing ensures that the output html is properly indented and in human readable form.
84 // If disabled, produced HTML is compact. This might be more suitable in production environments.
85 // Default: true
86 PrettyPrint bool
87 // Setting if line number emitting is enabled
88 // In this form, Amber emits line number comments in the output template. It is usable in debugging environments.
89 // Default: false
90 LineNumbers bool
91 // Setting the virtual filesystem to use
92 // If set, will attempt to use a virtual filesystem provided instead of os.
93 // Default: nil
94 VirtualFilesystem http.FileSystem
95 }
96
97 // DirOptions is used to provide options to directory compilation.
98 type DirOptions struct {
99 // File extension to match for compilation
100 Ext string
101 // Whether or not to walk subdirectories
102 Recursive bool
103 }
104
105 // DefaultOptions sets pretty-printing to true and line numbering to false.
106 var DefaultOptions = Options{true, false, nil}
107
108 // DefaultDirOptions sets expected file extension to ".amber" and recursive search for templates within a directory to true.
109 var DefaultDirOptions = DirOptions{".amber", true}
110
111 // Compile parses and compiles the supplied amber template string. Returns corresponding Go Template (html/templates) instance.
112 // Necessary runtime functions will be injected and the template will be ready to be executed.
113 func Compile(input string, options Options) (*template.Template, error) {
114 comp := New()
115 comp.Options = options
116
117 err := comp.Parse(input)
118 if err != nil {
119 return nil, err
120 }
121
122 return comp.Compile()
123 }
124
125 // Compile parses and compiles the supplied amber template []byte.
126 // Returns corresponding Go Template (html/templates) instance.
127 // Necessary runtime functions will be injected and the template will be ready to be executed.
128 func CompileData(input []byte, filename string, options Options) (*template.Template, error) {
129 comp := New()
130 comp.Options = options
131
132 err := comp.ParseData(input, filename)
133 if err != nil {
134 return nil, err
135 }
136
137 return comp.Compile()
138 }
139
140 // MustCompile is the same as Compile, except the input is assumed error free. If else, panic.
141 func MustCompile(input string, options Options) *template.Template {
142 t, err := Compile(input, options)
143 if err != nil {
144 panic(err)
145 }
146 return t
147 }
148
149 // CompileFile parses and compiles the contents of supplied filename. Returns corresponding Go Template (html/templates) instance.
150 // Necessary runtime functions will be injected and the template will be ready to be executed.
151 func CompileFile(filename string, options Options) (*template.Template, error) {
152 comp := New()
153 comp.Options = options
154
155 err := comp.ParseFile(filename)
156 if err != nil {
157 return nil, err
158 }
159
160 return comp.Compile()
161 }
162
163 // MustCompileFile is the same as CompileFile, except the input is assumed error free. If else, panic.
164 func MustCompileFile(filename string, options Options) *template.Template {
165 t, err := CompileFile(filename, options)
166 if err != nil {
167 panic(err)
168 }
169 return t
170 }
171
172 // CompileDir parses and compiles the contents of a supplied directory path, with options.
173 // Returns a map of a template identifier (key) to a Go Template instance.
174 // Ex: if the dirname="templates/" had a file "index.amber" the key would be "index"
175 // If option for recursive is True, this parses every file of relevant extension
176 // in all subdirectories. The key then is the path e.g: "layouts/layout"
177 func CompileDir(dirname string, dopt DirOptions, opt Options) (map[string]*template.Template, error) {
178 dir, err := os.Open(dirname)
179 if err != nil && opt.VirtualFilesystem != nil {
180 vdir, err := opt.VirtualFilesystem.Open(dirname)
181 if err != nil {
182 return nil, err
183 }
184 dir = vdir.(*os.File)
185 } else if err != nil {
186 return nil, err
187 }
188 defer dir.Close()
189
190 files, err := dir.Readdir(0)
191 if err != nil {
192 return nil, err
193 }
194
195 compiled := make(map[string]*template.Template)
196 for _, file := range files {
197 // filename is for example "index.amber"
198 filename := file.Name()
199 fileext := filepath.Ext(filename)
200
201 // If recursive is true and there's a subdirectory, recurse
202 if dopt.Recursive && file.IsDir() {
203 dirpath := filepath.Join(dirname, filename)
204 subcompiled, err := CompileDir(dirpath, dopt, opt)
205 if err != nil {
206 return nil, err
207 }
208 // Copy templates from subdirectory into parent template mapping
209 for k, v := range subcompiled {
210 // Concat with parent directory name for unique paths
211 key := filepath.Join(filename, k)
212 compiled[key] = v
213 }
214 } else if fileext == dopt.Ext {
215 // Otherwise compile the file and add to mapping
216 fullpath := filepath.Join(dirname, filename)
217 tmpl, err := CompileFile(fullpath, opt)
218 if err != nil {
219 return nil, err
220 }
221 // Strip extension
222 key := filename[0 : len(filename)-len(fileext)]
223 compiled[key] = tmpl
224 }
225 }
226
227 return compiled, nil
228 }
229
230 // MustCompileDir is the same as CompileDir, except input is assumed error free. If else, panic.
231 func MustCompileDir(dirname string, dopt DirOptions, opt Options) map[string]*template.Template {
232 m, err := CompileDir(dirname, dopt, opt)
233 if err != nil {
234 panic(err)
235 }
236 return m
237 }
238
239 // Parse given raw amber template string.
240 func (c *Compiler) Parse(input string) (err error) {
241 defer func() {
242 if r := recover(); r != nil {
243 err = errors.New(r.(string))
244 }
245 }()
246
247 parser, err := parser.StringParser(input)
248
249 if err != nil {
250 return
251 }
252
253 c.node = parser.Parse()
254 return
255 }
256
257 // Parse given raw amber template bytes, and the filename that belongs with it
258 func (c *Compiler) ParseData(input []byte, filename string) (err error) {
259 defer func() {
260 if r := recover(); r != nil {
261 err = errors.New(r.(string))
262 }
263 }()
264
265 parser, err := parser.ByteParser(input)
266 parser.SetFilename(filename)
267 if c.VirtualFilesystem != nil {
268 parser.SetVirtualFilesystem(c.VirtualFilesystem)
269 }
270
271 if err != nil {
272 return
273 }
274
275 c.node = parser.Parse()
276 return
277 }
278
279 // ParseFile parses the amber template file in given path.
280 func (c *Compiler) ParseFile(filename string) (err error) {
281 defer func() {
282 if r := recover(); r != nil {
283 err = errors.New(r.(string))
284 }
285 }()
286
287 p, err := parser.FileParser(filename)
288 if err != nil && c.VirtualFilesystem != nil {
289 p, err = parser.VirtualFileParser(filename, c.VirtualFilesystem)
290 }
291 if err != nil {
292 return
293 }
294
295 c.node = p.Parse()
296 c.filename = filename
297 return
298 }
299
300 // Compile amber and create a Go Template (html/templates) instance.
301 // Necessary runtime functions will be injected and the template will be ready to be executed.
302 func (c *Compiler) Compile() (*template.Template, error) {
303 return c.CompileWithName(filepath.Base(c.filename))
304 }
305
306 // CompileWithName is the same as Compile, but allows to specify a name for the template.
307 func (c *Compiler) CompileWithName(name string) (*template.Template, error) {
308 return c.CompileWithTemplate(template.New(name))
309 }
310
311 // CompileWithTemplate is the same as Compile but allows to specify a template.
312 func (c *Compiler) CompileWithTemplate(t *template.Template) (*template.Template, error) {
313 data, err := c.CompileString()
314
315 if err != nil {
316 return nil, err
317 }
318
319 tpl, err := t.Funcs(FuncMap).Parse(data)
320
321 if err != nil {
322 return nil, err
323 }
324
325 return tpl, nil
326 }
327
328 // CompileWriter compiles amber and writes the Go Template source into given io.Writer instance.
329 // You would not be using this unless debugging / checking the output. Please use Compile
330 // method to obtain a template instance directly.
331 func (c *Compiler) CompileWriter(out io.Writer) (err error) {
332 defer func() {
333 if r := recover(); r != nil {
334 err = errors.New(r.(string))
335 }
336 }()
337
338 c.buffer = new(bytes.Buffer)
339 c.visit(c.node)
340
341 if c.buffer.Len() > 0 {
342 c.write("\n")
343 }
344
345 _, err = c.buffer.WriteTo(out)
346 return
347 }
348
349 // CompileString compiles the template and returns the Go Template source.
350 // You would not be using this unless debugging / checking the output. Please use Compile
351 // method to obtain a template instance directly.
352 func (c *Compiler) CompileString() (string, error) {
353 var buf bytes.Buffer
354
355 if err := c.CompileWriter(&buf); err != nil {
356 return "", err
357 }
358
359 result := buf.String()
360
361 return result, nil
362 }
363
364 func (c *Compiler) visit(node parser.Node) {
365 defer func() {
366 if r := recover(); r != nil {
367 if rs, ok := r.(string); ok && rs[:len("Amber Error")] == "Amber Error" {
368 panic(r)
369 }
370
371 pos := node.Pos()
372
373 if len(pos.Filename) > 0 {
374 panic(fmt.Sprintf("Amber Error in <%s>: %v - Line: %d, Column: %d, Length: %d", pos.Filename, r, pos.LineNum, pos.ColNum, pos.TokenLength))
375 } else {
376 panic(fmt.Sprintf("Amber Error: %v - Line: %d, Column: %d, Length: %d", r, pos.LineNum, pos.ColNum, pos.TokenLength))
377 }
378 }
379 }()
380
381 switch node.(type) {
382 case *parser.Block:
383 c.visitBlock(node.(*parser.Block))
384 case *parser.Doctype:
385 c.visitDoctype(node.(*parser.Doctype))
386 case *parser.Comment:
387 c.visitComment(node.(*parser.Comment))
388 case *parser.Tag:
389 c.visitTag(node.(*parser.Tag))
390 case *parser.Text:
391 c.visitText(node.(*parser.Text))
392 case *parser.Condition:
393 c.visitCondition(node.(*parser.Condition))
394 case *parser.Each:
395 c.visitEach(node.(*parser.Each))
396 case *parser.Assignment:
397 c.visitAssignment(node.(*parser.Assignment))
398 case *parser.Mixin:
399 c.visitMixin(node.(*parser.Mixin))
400 case *parser.MixinCall:
401 c.visitMixinCall(node.(*parser.MixinCall))
402 }
403 }
404
405 func (c *Compiler) write(value string) {
406 c.buffer.WriteString(value)
407 }
408
409 func (c *Compiler) indent(offset int, newline bool) {
410 if !c.PrettyPrint {
411 return
412 }
413
414 if newline && c.buffer.Len() > 0 {
415 c.write("\n")
416 }
417
418 for i := 0; i < c.indentLevel+offset; i++ {
419 c.write("\t")
420 }
421 }
422
423 func (c *Compiler) tempvar() string {
424 c.tempvarIndex++
425 return "$__amber_" + strconv.Itoa(c.tempvarIndex)
426 }
427
428 func (c *Compiler) escape(input string) string {
429 return strings.Replace(strings.Replace(input, `\`, `\\`, -1), `"`, `\"`, -1)
430 }
431
432 func (c *Compiler) visitBlock(block *parser.Block) {
433 for _, node := range block.Children {
434 if _, ok := node.(*parser.Text); !block.CanInline() && ok {
435 c.indent(0, true)
436 }
437
438 c.visit(node)
439 }
440 }
441
442 func (c *Compiler) visitDoctype(doctype *parser.Doctype) {
443 c.write(doctype.String())
444 }
445
446 func (c *Compiler) visitComment(comment *parser.Comment) {
447 if comment.Silent {
448 return
449 }
450
451 c.indent(0, false)
452
453 if comment.Block == nil {
454 c.write(`{{unescaped "<!-- ` + c.escape(comment.Value) + ` -->"}}`)
455 } else {
456 c.write(`<!-- ` + comment.Value)
457 c.visitBlock(comment.Block)
458 c.write(` -->`)
459 }
460 }
461
462 func (c *Compiler) visitCondition(condition *parser.Condition) {
463 c.write(`{{if ` + c.visitRawInterpolation(condition.Expression) + `}}`)
464 c.visitBlock(condition.Positive)
465 if condition.Negative != nil {
466 c.write(`{{else}}`)
467 c.visitBlock(condition.Negative)
468 }
469 c.write(`{{end}}`)
470 }
471
472 func (c *Compiler) visitEach(each *parser.Each) {
473 if each.Block == nil {
474 return
475 }
476
477 if len(each.Y) == 0 {
478 c.write(`{{range ` + each.X + ` := ` + c.visitRawInterpolation(each.Expression) + `}}`)
479 } else {
480 c.write(`{{range ` + each.X + `, ` + each.Y + ` := ` + c.visitRawInterpolation(each.Expression) + `}}`)
481 }
482 c.visitBlock(each.Block)
483 c.write(`{{end}}`)
484 }
485
486 func (c *Compiler) visitAssignment(assgn *parser.Assignment) {
487 c.write(`{{` + assgn.X + ` := ` + c.visitRawInterpolation(assgn.Expression) + `}}`)
488 }
489
490 func (c *Compiler) visitTag(tag *parser.Tag) {
491 type attrib struct {
492 name string
493 value func() string
494 condition string
495 }
496
497 attribs := make(map[string]*attrib)
498
499 for _, item := range tag.Attributes {
500 attritem := item
501 attr := new(attrib)
502 attr.name = item.Name
503
504 attr.value = func() string {
505 if !attritem.IsRaw {
506 return c.visitInterpolation(attritem.Value)
507 } else if attritem.Value == "" {
508 return ""
509 } else {
510 return attritem.Value
511 }
512 }
513
514 if len(attritem.Condition) != 0 {
515 attr.condition = c.visitRawInterpolation(attritem.Condition)
516 }
517
518 if attr.name == "class" && attribs["class"] != nil {
519 prevclass := attribs["class"]
520 prevvalue := prevclass.value
521
522 prevclass.value = func() string {
523 aval := attr.value()
524
525 if len(attr.condition) > 0 {
526 aval = `{{if ` + attr.condition + `}}` + aval + `{{end}}`
527 }
528
529 if len(prevclass.condition) > 0 {
530 return `{{if ` + prevclass.condition + `}}` + prevvalue() + `{{end}} ` + aval
531 }
532
533 return prevvalue() + " " + aval
534 }
535 } else {
536 attribs[attritem.Name] = attr
537 }
538 }
539
540 keys := make([]string, 0, len(attribs))
541 for key := range attribs {
542 keys = append(keys, key)
543 }
544 sort.Strings(keys)
545
546 c.indent(0, true)
547 c.write("<" + tag.Name)
548
549 for _, name := range keys {
550 value := attribs[name]
551
552 if len(value.condition) > 0 {
553 c.write(`{{if ` + value.condition + `}}`)
554 }
555
556 val := value.value()
557
558 if val == "" {
559 c.write(` ` + name)
560 } else {
561 c.write(` ` + name + `="` + val + `"`)
562 }
563
564 if len(value.condition) > 0 {
565 c.write(`{{end}}`)
566 }
567 }
568
569 if tag.IsSelfClosing() {
570 c.write(` />`)
571 } else {
572 c.write(`>`)
573
574 if tag.Block != nil {
575 if !tag.Block.CanInline() {
576 c.indentLevel++
577 }
578
579 c.visitBlock(tag.Block)
580
581 if !tag.Block.CanInline() {
582 c.indentLevel--
583 c.indent(0, true)
584 }
585 }
586
587 c.write(`</` + tag.Name + `>`)
588 }
589 }
590
591 var textInterpolateRegexp = regexp.MustCompile(`#\{(.*?)\}`)
592 var textEscapeRegexp = regexp.MustCompile(`\{\{(.*?)\}\}`)
593
594 func (c *Compiler) visitText(txt *parser.Text) {
595 value := textEscapeRegexp.ReplaceAllStringFunc(txt.Value, func(value string) string {
596 return `{{"{{"}}` + value[2:len(value)-2] + `{{"}}"}}`
597 })
598
599 value = textInterpolateRegexp.ReplaceAllStringFunc(value, func(value string) string {
600 return c.visitInterpolation(value[2 : len(value)-1])
601 })
602
603 lines := strings.Split(value, "\n")
604 for i := 0; i < len(lines); i++ {
605 c.write(lines[i])
606
607 if i < len(lines)-1 {
608 c.write("\n")
609 c.indent(0, false)
610 }
611 }
612 }
613
614 func (c *Compiler) visitInterpolation(value string) string {
615 return `{{` + c.visitRawInterpolation(value) + `}}`
616 }
617
618 func (c *Compiler) visitRawInterpolation(value string) string {
619 if value == "" {
620 value = "\"\""
621 }
622
623 value = strings.Replace(value, "$", dollar, -1)
624 expr, err := gp.ParseExpr(value)
625 if err != nil {
626 panic("Unable to parse expression.")
627 }
628 value = strings.Replace(c.visitExpression(expr), dollar, "$", -1)
629 return value
630 }
631
632 func (c *Compiler) visitExpression(outerexpr ast.Expr) string {
633 stack := list.New()
634
635 pop := func() string {
636 if stack.Front() == nil {
637 return ""
638 }
639
640 val := stack.Front().Value.(string)
641 stack.Remove(stack.Front())
642 return val
643 }
644
645 var exec func(ast.Expr)
646
647 exec = func(expr ast.Expr) {
648 switch expr := expr.(type) {
649 case *ast.BinaryExpr:
650 {
651 be := expr
652
653 exec(be.Y)
654 exec(be.X)
655
656 negate := false
657 name := c.tempvar()
658 c.write(`{{` + name + ` := `)
659
660 switch be.Op {
661 case gt.ADD:
662 c.write("__amber_add ")
663 case gt.SUB:
664 c.write("__amber_sub ")
665 case gt.MUL:
666 c.write("__amber_mul ")
667 case gt.QUO:
668 c.write("__amber_quo ")
669 case gt.REM:
670 c.write("__amber_rem ")
671 case gt.LAND:
672 c.write("and ")
673 case gt.LOR:
674 c.write("or ")
675 case gt.EQL:
676 c.write("__amber_eql ")
677 case gt.NEQ:
678 c.write("__amber_eql ")
679 negate = true
680 case gt.LSS:
681 c.write("__amber_lss ")
682 case gt.GTR:
683 c.write("__amber_gtr ")
684 case gt.LEQ:
685 c.write("__amber_gtr ")
686 negate = true
687 case gt.GEQ:
688 c.write("__amber_lss ")
689 negate = true
690 default:
691 panic("Unexpected operator!")
692 }
693
694 c.write(pop() + ` ` + pop() + `}}`)
695
696 if !negate {
697 stack.PushFront(name)
698 } else {
699 negname := c.tempvar()
700 c.write(`{{` + negname + ` := not ` + name + `}}`)
701 stack.PushFront(negname)
702 }
703 }
704 case *ast.UnaryExpr:
705 {
706 ue := expr
707
708 exec(ue.X)
709
710 name := c.tempvar()
711 c.write(`{{` + name + ` := `)
712
713 switch ue.Op {
714 case gt.SUB:
715 c.write("__amber_minus ")
716 case gt.ADD:
717 c.write("__amber_plus ")
718 case gt.NOT:
719 c.write("not ")
720 default:
721 panic("Unexpected operator!")
722 }
723
724 c.write(pop() + `}}`)
725 stack.PushFront(name)
726 }
727 case *ast.ParenExpr:
728 exec(expr.X)
729 case *ast.BasicLit:
730 stack.PushFront(strings.Replace(expr.Value, dollar, "$", -1))
731 case *ast.Ident:
732 name := expr.Name
733 if len(name) >= len(dollar) && name[:len(dollar)] == dollar {
734 if name == dollar {
735 stack.PushFront(`.`)
736 } else {
737 stack.PushFront(`$` + expr.Name[len(dollar):])
738 }
739 } else {
740 stack.PushFront(`.` + expr.Name)
741 }
742 case *ast.SelectorExpr:
743 se := expr
744 exec(se.X)
745 x := pop()
746
747 if x == "." {
748 x = ""
749 }
750
751 name := c.tempvar()
752 c.write(`{{` + name + ` := ` + x + `.` + se.Sel.Name + `}}`)
753 stack.PushFront(name)
754 case *ast.CallExpr:
755 ce := expr
756
757 for i := len(ce.Args) - 1; i >= 0; i-- {
758 exec(ce.Args[i])
759 }
760
761 name := c.tempvar()
762 builtin := false
763
764 if ident, ok := ce.Fun.(*ast.Ident); ok {
765 for _, fname := range builtinFunctions {
766 if fname == ident.Name {
767 builtin = true
768 break
769 }
770 }
771 for fname, _ := range FuncMap {
772 if fname == ident.Name {
773 builtin = true
774 break
775 }
776 }
777 }
778
779 if builtin {
780 stack.PushFront(ce.Fun.(*ast.Ident).Name)
781 c.write(`{{` + name + ` := ` + pop())
782 } else if se, ok := ce.Fun.(*ast.SelectorExpr); ok {
783 exec(se.X)
784 x := pop()
785
786 if x == "." {
787 x = ""
788 }
789 stack.PushFront(se.Sel.Name)
790 c.write(`{{` + name + ` := ` + x + `.` + pop())
791 } else {
792 exec(ce.Fun)
793 c.write(`{{` + name + ` := call ` + pop())
794 }
795
796 for i := 0; i < len(ce.Args); i++ {
797 c.write(` `)
798 c.write(pop())
799 }
800
801 c.write(`}}`)
802
803 stack.PushFront(name)
804 default:
805 panic("Unable to parse expression. Unsupported: " + reflect.TypeOf(expr).String())
806 }
807 }
808
809 exec(outerexpr)
810 return pop()
811 }
812
813 func (c *Compiler) visitMixin(mixin *parser.Mixin) {
814 c.mixins[mixin.Name] = mixin
815 }
816
817 func (c *Compiler) visitMixinCall(mixinCall *parser.MixinCall) {
818 mixin := c.mixins[mixinCall.Name]
819
820 switch {
821 case mixin == nil:
822 panic(fmt.Sprintf("unknown mixin %q", mixinCall.Name))
823
824 case len(mixinCall.Args) < len(mixin.Args):
825 panic(fmt.Sprintf(
826 "not enough arguments in call to mixin %q (have: %d, want: %d)",
827 mixinCall.Name,
828 len(mixinCall.Args),
829 len(mixin.Args),
830 ))
831 case len(mixinCall.Args) > len(mixin.Args):
832 panic(fmt.Sprintf(
833 "too many arguments in call to mixin %q (have: %d, want: %d)",
834 mixinCall.Name,
835 len(mixinCall.Args),
836 len(mixin.Args),
837 ))
838 }
839
840 for i, arg := range mixin.Args {
841 c.write(fmt.Sprintf(`{{%s := %s}}`, arg, c.visitRawInterpolation(mixinCall.Args[i])))
842 }
843 c.visitBlock(mixin.Block)
844 }