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