1package hclsyntax
2
3import (
4	"fmt"
5	"strings"
6	"unicode"
7
8	"github.com/apparentlymart/go-textseg/textseg"
9	"github.com/hashicorp/hcl2/hcl"
10	"github.com/zclconf/go-cty/cty"
11)
12
13func (p *parser) ParseTemplate() (Expression, hcl.Diagnostics) {
14	return p.parseTemplate(TokenEOF, false)
15}
16
17func (p *parser) parseTemplate(end TokenType, flushHeredoc bool) (Expression, hcl.Diagnostics) {
18	exprs, passthru, rng, diags := p.parseTemplateInner(end, flushHeredoc)
19
20	if passthru {
21		if len(exprs) != 1 {
22			panic("passthru set with len(exprs) != 1")
23		}
24		return &TemplateWrapExpr{
25			Wrapped:  exprs[0],
26			SrcRange: rng,
27		}, diags
28	}
29
30	return &TemplateExpr{
31		Parts:    exprs,
32		SrcRange: rng,
33	}, diags
34}
35
36func (p *parser) parseTemplateInner(end TokenType, flushHeredoc bool) ([]Expression, bool, hcl.Range, hcl.Diagnostics) {
37	parts, diags := p.parseTemplateParts(end)
38	if flushHeredoc {
39		flushHeredocTemplateParts(parts) // Trim off leading spaces on lines per the flush heredoc spec
40	}
41	tp := templateParser{
42		Tokens:   parts.Tokens,
43		SrcRange: parts.SrcRange,
44	}
45	exprs, exprsDiags := tp.parseRoot()
46	diags = append(diags, exprsDiags...)
47
48	passthru := false
49	if len(parts.Tokens) == 2 { // one real token and one synthetic "end" token
50		if _, isInterp := parts.Tokens[0].(*templateInterpToken); isInterp {
51			passthru = true
52		}
53	}
54
55	return exprs, passthru, parts.SrcRange, diags
56}
57
58type templateParser struct {
59	Tokens   []templateToken
60	SrcRange hcl.Range
61
62	pos int
63}
64
65func (p *templateParser) parseRoot() ([]Expression, hcl.Diagnostics) {
66	var exprs []Expression
67	var diags hcl.Diagnostics
68
69	for {
70		next := p.Peek()
71		if _, isEnd := next.(*templateEndToken); isEnd {
72			break
73		}
74
75		expr, exprDiags := p.parseExpr()
76		diags = append(diags, exprDiags...)
77		exprs = append(exprs, expr)
78	}
79
80	return exprs, diags
81}
82
83func (p *templateParser) parseExpr() (Expression, hcl.Diagnostics) {
84	next := p.Peek()
85	switch tok := next.(type) {
86
87	case *templateLiteralToken:
88		p.Read() // eat literal
89		return &LiteralValueExpr{
90			Val:      cty.StringVal(tok.Val),
91			SrcRange: tok.SrcRange,
92		}, nil
93
94	case *templateInterpToken:
95		p.Read() // eat interp
96		return tok.Expr, nil
97
98	case *templateIfToken:
99		return p.parseIf()
100
101	case *templateForToken:
102		return p.parseFor()
103
104	case *templateEndToken:
105		p.Read() // eat erroneous token
106		return errPlaceholderExpr(tok.SrcRange), hcl.Diagnostics{
107			{
108				// This is a particularly unhelpful diagnostic, so callers
109				// should attempt to pre-empt it and produce a more helpful
110				// diagnostic that is context-aware.
111				Severity: hcl.DiagError,
112				Summary:  "Unexpected end of template",
113				Detail:   "The control directives within this template are unbalanced.",
114				Subject:  &tok.SrcRange,
115			},
116		}
117
118	case *templateEndCtrlToken:
119		p.Read() // eat erroneous token
120		return errPlaceholderExpr(tok.SrcRange), hcl.Diagnostics{
121			{
122				Severity: hcl.DiagError,
123				Summary:  fmt.Sprintf("Unexpected %s directive", tok.Name()),
124				Detail:   "The control directives within this template are unbalanced.",
125				Subject:  &tok.SrcRange,
126			},
127		}
128
129	default:
130		// should never happen, because above should be exhaustive
131		panic(fmt.Sprintf("unhandled template token type %T", next))
132	}
133}
134
135func (p *templateParser) parseIf() (Expression, hcl.Diagnostics) {
136	open := p.Read()
137	openIf, isIf := open.(*templateIfToken)
138	if !isIf {
139		// should never happen if caller is behaving
140		panic("parseIf called with peeker not pointing at if token")
141	}
142
143	var ifExprs, elseExprs []Expression
144	var diags hcl.Diagnostics
145	var endifRange hcl.Range
146
147	currentExprs := &ifExprs
148Token:
149	for {
150		next := p.Peek()
151		if end, isEnd := next.(*templateEndToken); isEnd {
152			diags = append(diags, &hcl.Diagnostic{
153				Severity: hcl.DiagError,
154				Summary:  "Unexpected end of template",
155				Detail: fmt.Sprintf(
156					"The if directive at %s is missing its corresponding endif directive.",
157					openIf.SrcRange,
158				),
159				Subject: &end.SrcRange,
160			})
161			return errPlaceholderExpr(end.SrcRange), diags
162		}
163		if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
164			p.Read() // eat end directive
165
166			switch end.Type {
167
168			case templateElse:
169				if currentExprs == &ifExprs {
170					currentExprs = &elseExprs
171					continue Token
172				}
173
174				diags = append(diags, &hcl.Diagnostic{
175					Severity: hcl.DiagError,
176					Summary:  "Unexpected else directive",
177					Detail: fmt.Sprintf(
178						"Already in the else clause for the if started at %s.",
179						openIf.SrcRange,
180					),
181					Subject: &end.SrcRange,
182				})
183
184			case templateEndIf:
185				endifRange = end.SrcRange
186				break Token
187
188			default:
189				diags = append(diags, &hcl.Diagnostic{
190					Severity: hcl.DiagError,
191					Summary:  fmt.Sprintf("Unexpected %s directive", end.Name()),
192					Detail: fmt.Sprintf(
193						"Expecting an endif directive for the if started at %s.",
194						openIf.SrcRange,
195					),
196					Subject: &end.SrcRange,
197				})
198			}
199
200			return errPlaceholderExpr(end.SrcRange), diags
201		}
202
203		expr, exprDiags := p.parseExpr()
204		diags = append(diags, exprDiags...)
205		*currentExprs = append(*currentExprs, expr)
206	}
207
208	if len(ifExprs) == 0 {
209		ifExprs = append(ifExprs, &LiteralValueExpr{
210			Val: cty.StringVal(""),
211			SrcRange: hcl.Range{
212				Filename: openIf.SrcRange.Filename,
213				Start:    openIf.SrcRange.End,
214				End:      openIf.SrcRange.End,
215			},
216		})
217	}
218	if len(elseExprs) == 0 {
219		elseExprs = append(elseExprs, &LiteralValueExpr{
220			Val: cty.StringVal(""),
221			SrcRange: hcl.Range{
222				Filename: endifRange.Filename,
223				Start:    endifRange.Start,
224				End:      endifRange.Start,
225			},
226		})
227	}
228
229	trueExpr := &TemplateExpr{
230		Parts:    ifExprs,
231		SrcRange: hcl.RangeBetween(ifExprs[0].Range(), ifExprs[len(ifExprs)-1].Range()),
232	}
233	falseExpr := &TemplateExpr{
234		Parts:    elseExprs,
235		SrcRange: hcl.RangeBetween(elseExprs[0].Range(), elseExprs[len(elseExprs)-1].Range()),
236	}
237
238	return &ConditionalExpr{
239		Condition:   openIf.CondExpr,
240		TrueResult:  trueExpr,
241		FalseResult: falseExpr,
242
243		SrcRange: hcl.RangeBetween(openIf.SrcRange, endifRange),
244	}, diags
245}
246
247func (p *templateParser) parseFor() (Expression, hcl.Diagnostics) {
248	open := p.Read()
249	openFor, isFor := open.(*templateForToken)
250	if !isFor {
251		// should never happen if caller is behaving
252		panic("parseFor called with peeker not pointing at for token")
253	}
254
255	var contentExprs []Expression
256	var diags hcl.Diagnostics
257	var endforRange hcl.Range
258
259Token:
260	for {
261		next := p.Peek()
262		if end, isEnd := next.(*templateEndToken); isEnd {
263			diags = append(diags, &hcl.Diagnostic{
264				Severity: hcl.DiagError,
265				Summary:  "Unexpected end of template",
266				Detail: fmt.Sprintf(
267					"The for directive at %s is missing its corresponding endfor directive.",
268					openFor.SrcRange,
269				),
270				Subject: &end.SrcRange,
271			})
272			return errPlaceholderExpr(end.SrcRange), diags
273		}
274		if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
275			p.Read() // eat end directive
276
277			switch end.Type {
278
279			case templateElse:
280				diags = append(diags, &hcl.Diagnostic{
281					Severity: hcl.DiagError,
282					Summary:  "Unexpected else directive",
283					Detail:   "An else clause is not expected for a for directive.",
284					Subject:  &end.SrcRange,
285				})
286
287			case templateEndFor:
288				endforRange = end.SrcRange
289				break Token
290
291			default:
292				diags = append(diags, &hcl.Diagnostic{
293					Severity: hcl.DiagError,
294					Summary:  fmt.Sprintf("Unexpected %s directive", end.Name()),
295					Detail: fmt.Sprintf(
296						"Expecting an endfor directive corresponding to the for directive at %s.",
297						openFor.SrcRange,
298					),
299					Subject: &end.SrcRange,
300				})
301			}
302
303			return errPlaceholderExpr(end.SrcRange), diags
304		}
305
306		expr, exprDiags := p.parseExpr()
307		diags = append(diags, exprDiags...)
308		contentExprs = append(contentExprs, expr)
309	}
310
311	if len(contentExprs) == 0 {
312		contentExprs = append(contentExprs, &LiteralValueExpr{
313			Val: cty.StringVal(""),
314			SrcRange: hcl.Range{
315				Filename: openFor.SrcRange.Filename,
316				Start:    openFor.SrcRange.End,
317				End:      openFor.SrcRange.End,
318			},
319		})
320	}
321
322	contentExpr := &TemplateExpr{
323		Parts:    contentExprs,
324		SrcRange: hcl.RangeBetween(contentExprs[0].Range(), contentExprs[len(contentExprs)-1].Range()),
325	}
326
327	forExpr := &ForExpr{
328		KeyVar: openFor.KeyVar,
329		ValVar: openFor.ValVar,
330
331		CollExpr: openFor.CollExpr,
332		ValExpr:  contentExpr,
333
334		SrcRange:   hcl.RangeBetween(openFor.SrcRange, endforRange),
335		OpenRange:  openFor.SrcRange,
336		CloseRange: endforRange,
337	}
338
339	return &TemplateJoinExpr{
340		Tuple: forExpr,
341	}, diags
342}
343
344func (p *templateParser) Peek() templateToken {
345	return p.Tokens[p.pos]
346}
347
348func (p *templateParser) Read() templateToken {
349	ret := p.Peek()
350	if _, end := ret.(*templateEndToken); !end {
351		p.pos++
352	}
353	return ret
354}
355
356// parseTemplateParts produces a flat sequence of "template tokens", which are
357// either literal values (with any "trimming" already applied), interpolation
358// sequences, or control flow markers.
359//
360// A further pass is required on the result to turn it into an AST.
361func (p *parser) parseTemplateParts(end TokenType) (*templateParts, hcl.Diagnostics) {
362	var parts []templateToken
363	var diags hcl.Diagnostics
364
365	startRange := p.NextRange()
366	ltrimNext := false
367	nextCanTrimPrev := false
368	var endRange hcl.Range
369
370Token:
371	for {
372		next := p.Read()
373		if next.Type == end {
374			// all done!
375			endRange = next.Range
376			break
377		}
378
379		ltrim := ltrimNext
380		ltrimNext = false
381		canTrimPrev := nextCanTrimPrev
382		nextCanTrimPrev = false
383
384		switch next.Type {
385		case TokenStringLit, TokenQuotedLit:
386			str, strDiags := p.decodeStringLit(next)
387			diags = append(diags, strDiags...)
388
389			if ltrim {
390				str = strings.TrimLeftFunc(str, unicode.IsSpace)
391			}
392
393			parts = append(parts, &templateLiteralToken{
394				Val:      str,
395				SrcRange: next.Range,
396			})
397			nextCanTrimPrev = true
398
399		case TokenTemplateInterp:
400			// if the opener is ${~ then we want to eat any trailing whitespace
401			// in the preceding literal token, assuming it is indeed a literal
402			// token.
403			if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
404				prevExpr := parts[len(parts)-1]
405				if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
406					lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
407				}
408			}
409
410			p.PushIncludeNewlines(false)
411			expr, exprDiags := p.ParseExpression()
412			diags = append(diags, exprDiags...)
413			close := p.Peek()
414			if close.Type != TokenTemplateSeqEnd {
415				if !p.recovery {
416					diags = append(diags, &hcl.Diagnostic{
417						Severity: hcl.DiagError,
418						Summary:  "Extra characters after interpolation expression",
419						Detail:   "Expected a closing brace to end the interpolation expression, but found extra characters.",
420						Subject:  &close.Range,
421						Context:  hcl.RangeBetween(startRange, close.Range).Ptr(),
422					})
423				}
424				p.recover(TokenTemplateSeqEnd)
425			} else {
426				p.Read() // eat closing brace
427
428				// If the closer is ~} then we want to eat any leading
429				// whitespace on the next token, if it turns out to be a
430				// literal token.
431				if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
432					ltrimNext = true
433				}
434			}
435			p.PopIncludeNewlines()
436			parts = append(parts, &templateInterpToken{
437				Expr:     expr,
438				SrcRange: hcl.RangeBetween(next.Range, close.Range),
439			})
440
441		case TokenTemplateControl:
442			// if the opener is %{~ then we want to eat any trailing whitespace
443			// in the preceding literal token, assuming it is indeed a literal
444			// token.
445			if canTrimPrev && len(next.Bytes) == 3 && next.Bytes[2] == '~' && len(parts) > 0 {
446				prevExpr := parts[len(parts)-1]
447				if lexpr, ok := prevExpr.(*templateLiteralToken); ok {
448					lexpr.Val = strings.TrimRightFunc(lexpr.Val, unicode.IsSpace)
449				}
450			}
451			p.PushIncludeNewlines(false)
452
453			kw := p.Peek()
454			if kw.Type != TokenIdent {
455				if !p.recovery {
456					diags = append(diags, &hcl.Diagnostic{
457						Severity: hcl.DiagError,
458						Summary:  "Invalid template directive",
459						Detail:   "A template directive keyword (\"if\", \"for\", etc) is expected at the beginning of a %{ sequence.",
460						Subject:  &kw.Range,
461						Context:  hcl.RangeBetween(next.Range, kw.Range).Ptr(),
462					})
463				}
464				p.recover(TokenTemplateSeqEnd)
465				p.PopIncludeNewlines()
466				continue Token
467			}
468			p.Read() // eat keyword token
469
470			switch {
471
472			case ifKeyword.TokenMatches(kw):
473				condExpr, exprDiags := p.ParseExpression()
474				diags = append(diags, exprDiags...)
475				parts = append(parts, &templateIfToken{
476					CondExpr: condExpr,
477					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
478				})
479
480			case elseKeyword.TokenMatches(kw):
481				parts = append(parts, &templateEndCtrlToken{
482					Type:     templateElse,
483					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
484				})
485
486			case endifKeyword.TokenMatches(kw):
487				parts = append(parts, &templateEndCtrlToken{
488					Type:     templateEndIf,
489					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
490				})
491
492			case forKeyword.TokenMatches(kw):
493				var keyName, valName string
494				if p.Peek().Type != TokenIdent {
495					if !p.recovery {
496						diags = append(diags, &hcl.Diagnostic{
497							Severity: hcl.DiagError,
498							Summary:  "Invalid 'for' directive",
499							Detail:   "For directive requires variable name after 'for'.",
500							Subject:  p.Peek().Range.Ptr(),
501						})
502					}
503					p.recover(TokenTemplateSeqEnd)
504					p.PopIncludeNewlines()
505					continue Token
506				}
507
508				valName = string(p.Read().Bytes)
509
510				if p.Peek().Type == TokenComma {
511					// What we just read was actually the key, then.
512					keyName = valName
513					p.Read() // eat comma
514
515					if p.Peek().Type != TokenIdent {
516						if !p.recovery {
517							diags = append(diags, &hcl.Diagnostic{
518								Severity: hcl.DiagError,
519								Summary:  "Invalid 'for' directive",
520								Detail:   "For directive requires value variable name after comma.",
521								Subject:  p.Peek().Range.Ptr(),
522							})
523						}
524						p.recover(TokenTemplateSeqEnd)
525						p.PopIncludeNewlines()
526						continue Token
527					}
528
529					valName = string(p.Read().Bytes)
530				}
531
532				if !inKeyword.TokenMatches(p.Peek()) {
533					if !p.recovery {
534						diags = append(diags, &hcl.Diagnostic{
535							Severity: hcl.DiagError,
536							Summary:  "Invalid 'for' directive",
537							Detail:   "For directive requires 'in' keyword after names.",
538							Subject:  p.Peek().Range.Ptr(),
539						})
540					}
541					p.recover(TokenTemplateSeqEnd)
542					p.PopIncludeNewlines()
543					continue Token
544				}
545				p.Read() // eat 'in' keyword
546
547				collExpr, collDiags := p.ParseExpression()
548				diags = append(diags, collDiags...)
549				parts = append(parts, &templateForToken{
550					KeyVar:   keyName,
551					ValVar:   valName,
552					CollExpr: collExpr,
553
554					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
555				})
556
557			case endforKeyword.TokenMatches(kw):
558				parts = append(parts, &templateEndCtrlToken{
559					Type:     templateEndFor,
560					SrcRange: hcl.RangeBetween(next.Range, p.NextRange()),
561				})
562
563			default:
564				if !p.recovery {
565					suggestions := []string{"if", "for", "else", "endif", "endfor"}
566					given := string(kw.Bytes)
567					suggestion := nameSuggestion(given, suggestions)
568					if suggestion != "" {
569						suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
570					}
571
572					diags = append(diags, &hcl.Diagnostic{
573						Severity: hcl.DiagError,
574						Summary:  "Invalid template control keyword",
575						Detail:   fmt.Sprintf("%q is not a valid template control keyword.%s", given, suggestion),
576						Subject:  &kw.Range,
577						Context:  hcl.RangeBetween(next.Range, kw.Range).Ptr(),
578					})
579				}
580				p.recover(TokenTemplateSeqEnd)
581				p.PopIncludeNewlines()
582				continue Token
583
584			}
585
586			close := p.Peek()
587			if close.Type != TokenTemplateSeqEnd {
588				if !p.recovery {
589					diags = append(diags, &hcl.Diagnostic{
590						Severity: hcl.DiagError,
591						Summary:  fmt.Sprintf("Extra characters in %s marker", kw.Bytes),
592						Detail:   "Expected a closing brace to end the sequence, but found extra characters.",
593						Subject:  &close.Range,
594						Context:  hcl.RangeBetween(startRange, close.Range).Ptr(),
595					})
596				}
597				p.recover(TokenTemplateSeqEnd)
598			} else {
599				p.Read() // eat closing brace
600
601				// If the closer is ~} then we want to eat any leading
602				// whitespace on the next token, if it turns out to be a
603				// literal token.
604				if len(close.Bytes) == 2 && close.Bytes[0] == '~' {
605					ltrimNext = true
606				}
607			}
608			p.PopIncludeNewlines()
609
610		default:
611			if !p.recovery {
612				diags = append(diags, &hcl.Diagnostic{
613					Severity: hcl.DiagError,
614					Summary:  "Unterminated template string",
615					Detail:   "No closing marker was found for the string.",
616					Subject:  &next.Range,
617					Context:  hcl.RangeBetween(startRange, next.Range).Ptr(),
618				})
619			}
620			final := p.recover(end)
621			endRange = final.Range
622			break Token
623		}
624	}
625
626	if len(parts) == 0 {
627		// If a sequence has no content, we'll treat it as if it had an
628		// empty string in it because that's what the user probably means
629		// if they write "" in configuration.
630		parts = append(parts, &templateLiteralToken{
631			Val: "",
632			SrcRange: hcl.Range{
633				// Range is the zero-character span immediately after the
634				// opening quote.
635				Filename: startRange.Filename,
636				Start:    startRange.End,
637				End:      startRange.End,
638			},
639		})
640	}
641
642	// Always end with an end token, so the parser can produce diagnostics
643	// about unclosed items with proper position information.
644	parts = append(parts, &templateEndToken{
645		SrcRange: endRange,
646	})
647
648	ret := &templateParts{
649		Tokens:   parts,
650		SrcRange: hcl.RangeBetween(startRange, endRange),
651	}
652
653	return ret, diags
654}
655
656// flushHeredocTemplateParts modifies in-place the line-leading literal strings
657// to apply the flush heredoc processing rule: find the line with the smallest
658// number of whitespace characters as prefix and then trim that number of
659// characters from all of the lines.
660//
661// This rule is applied to static tokens rather than to the rendered result,
662// so interpolating a string with leading whitespace cannot affect the chosen
663// prefix length.
664func flushHeredocTemplateParts(parts *templateParts) {
665	if len(parts.Tokens) == 0 {
666		// Nothing to do
667		return
668	}
669
670	const maxInt = int((^uint(0)) >> 1)
671
672	minSpaces := maxInt
673	newline := true
674	var adjust []*templateLiteralToken
675	for _, ttok := range parts.Tokens {
676		if newline {
677			newline = false
678			var spaces int
679			if lit, ok := ttok.(*templateLiteralToken); ok {
680				orig := lit.Val
681				trimmed := strings.TrimLeftFunc(orig, unicode.IsSpace)
682				// If a token is entirely spaces and ends with a newline
683				// then it's a "blank line" and thus not considered for
684				// space-prefix-counting purposes.
685				if len(trimmed) == 0 && strings.HasSuffix(orig, "\n") {
686					spaces = maxInt
687				} else {
688					spaceBytes := len(lit.Val) - len(trimmed)
689					spaces, _ = textseg.TokenCount([]byte(orig[:spaceBytes]), textseg.ScanGraphemeClusters)
690					adjust = append(adjust, lit)
691				}
692			} else if _, ok := ttok.(*templateEndToken); ok {
693				break // don't process the end token since it never has spaces before it
694			}
695			if spaces < minSpaces {
696				minSpaces = spaces
697			}
698		}
699		if lit, ok := ttok.(*templateLiteralToken); ok {
700			if strings.HasSuffix(lit.Val, "\n") {
701				newline = true // The following token, if any, begins a new line
702			}
703		}
704	}
705
706	for _, lit := range adjust {
707		// Since we want to count space _characters_ rather than space _bytes_,
708		// we can't just do a straightforward slice operation here and instead
709		// need to hunt for the split point with a scanner.
710		valBytes := []byte(lit.Val)
711		spaceByteCount := 0
712		for i := 0; i < minSpaces; i++ {
713			adv, _, _ := textseg.ScanGraphemeClusters(valBytes, true)
714			spaceByteCount += adv
715			valBytes = valBytes[adv:]
716		}
717		lit.Val = lit.Val[spaceByteCount:]
718		lit.SrcRange.Start.Column += minSpaces
719		lit.SrcRange.Start.Byte += spaceByteCount
720	}
721}
722
723type templateParts struct {
724	Tokens   []templateToken
725	SrcRange hcl.Range
726}
727
728// templateToken is a higher-level token that represents a single atom within
729// the template language. Our template parsing first raises the raw token
730// stream to a sequence of templateToken, and then transforms the result into
731// an expression tree.
732type templateToken interface {
733	templateToken() templateToken
734}
735
736type templateLiteralToken struct {
737	Val      string
738	SrcRange hcl.Range
739	isTemplateToken
740}
741
742type templateInterpToken struct {
743	Expr     Expression
744	SrcRange hcl.Range
745	isTemplateToken
746}
747
748type templateIfToken struct {
749	CondExpr Expression
750	SrcRange hcl.Range
751	isTemplateToken
752}
753
754type templateForToken struct {
755	KeyVar   string // empty if ignoring key
756	ValVar   string
757	CollExpr Expression
758	SrcRange hcl.Range
759	isTemplateToken
760}
761
762type templateEndCtrlType int
763
764const (
765	templateEndIf templateEndCtrlType = iota
766	templateElse
767	templateEndFor
768)
769
770type templateEndCtrlToken struct {
771	Type     templateEndCtrlType
772	SrcRange hcl.Range
773	isTemplateToken
774}
775
776func (t *templateEndCtrlToken) Name() string {
777	switch t.Type {
778	case templateEndIf:
779		return "endif"
780	case templateElse:
781		return "else"
782	case templateEndFor:
783		return "endfor"
784	default:
785		// should never happen
786		panic("invalid templateEndCtrlType")
787	}
788}
789
790type templateEndToken struct {
791	SrcRange hcl.Range
792	isTemplateToken
793}
794
795type isTemplateToken [0]int
796
797func (t isTemplateToken) templateToken() templateToken {
798	return t
799}
800