text/template/parse: respect delims in String representations
Use the same delimiters in String() representation of nodes that were
used during the parsing of a template.
This means that that when using custom delims we can roundtrip back to
the original Parse input (modulo superfluous whitespace within actions).
diff --git a/src/text/template/parse/node.go b/src/text/template/parse/node.go
index a313098..31d663c 100644
--- a/src/text/template/parse/node.go
+++ b/src/text/template/parse/node.go
@@ -171,9 +171,9 @@
}
func (c *CommentNode) writeTo(sb *strings.Builder) {
- sb.WriteString("{{")
+ sb.WriteString(c.tr.leftDelim)
sb.WriteString(c.Text)
- sb.WriteString("}}")
+ sb.WriteString(c.tr.rightDelim)
}
func (c *CommentNode) tree() *Tree {
@@ -277,9 +277,9 @@
}
func (a *ActionNode) writeTo(sb *strings.Builder) {
- sb.WriteString("{{")
+ sb.WriteString(a.tr.leftDelim)
a.Pipe.writeTo(sb)
- sb.WriteString("}}")
+ sb.WriteString(a.tr.rightDelim)
}
func (a *ActionNode) tree() *Tree {
@@ -793,7 +793,7 @@
}
func (e *endNode) String() string {
- return "{{end}}"
+ return e.tr.leftDelim + "end" + e.tr.rightDelim
}
func (e *endNode) writeTo(sb *strings.Builder) {
@@ -825,7 +825,7 @@
}
func (e *elseNode) String() string {
- return "{{else}}"
+ return e.tr.leftDelim + "else" + e.tr.rightDelim
}
func (e *elseNode) writeTo(sb *strings.Builder) {
@@ -869,17 +869,21 @@
default:
panic("unknown branch type")
}
- sb.WriteString("{{")
+ sb.WriteString(b.tr.leftDelim)
sb.WriteString(name)
sb.WriteByte(' ')
b.Pipe.writeTo(sb)
- sb.WriteString("}}")
+ sb.WriteString(b.tr.rightDelim)
b.List.writeTo(sb)
if b.ElseList != nil {
- sb.WriteString("{{else}}")
+ sb.WriteString(b.tr.leftDelim)
+ sb.WriteString("else")
+ sb.WriteString(b.tr.rightDelim)
b.ElseList.writeTo(sb)
}
- sb.WriteString("{{end}}")
+ sb.WriteString(b.tr.leftDelim)
+ sb.WriteString("end")
+ sb.WriteString(b.tr.rightDelim)
}
func (b *BranchNode) tree() *Tree {
@@ -925,9 +929,9 @@
}
func (b *BreakNode) Copy() Node { return b.tr.newBreak(b.Pos, b.Line) }
-func (b *BreakNode) String() string { return "{{break}}" }
+func (b *BreakNode) String() string { return b.tr.leftDelim + "break" + b.tr.rightDelim }
func (b *BreakNode) tree() *Tree { return b.tr }
-func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString("{{break}}") }
+func (b *BreakNode) writeTo(sb *strings.Builder) { sb.WriteString(b.String()) }
// ContinueNode represents a {{continue}} action.
type ContinueNode struct {
@@ -942,9 +946,9 @@
}
func (c *ContinueNode) Copy() Node { return c.tr.newContinue(c.Pos, c.Line) }
-func (c *ContinueNode) String() string { return "{{continue}}" }
+func (c *ContinueNode) String() string { return c.tr.leftDelim + "continue" + c.tr.rightDelim }
func (c *ContinueNode) tree() *Tree { return c.tr }
-func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString("{{continue}}") }
+func (c *ContinueNode) writeTo(sb *strings.Builder) { sb.WriteString(c.String()) }
// RangeNode represents a {{range}} action and its commands.
type RangeNode struct {
@@ -993,13 +997,14 @@
}
func (t *TemplateNode) writeTo(sb *strings.Builder) {
- sb.WriteString("{{template ")
+ sb.WriteString(t.tr.leftDelim)
+ sb.WriteString("template ")
sb.WriteString(strconv.Quote(t.Name))
if t.Pipe != nil {
sb.WriteByte(' ')
t.Pipe.writeTo(sb)
}
- sb.WriteString("}}")
+ sb.WriteString(t.tr.rightDelim)
}
func (t *TemplateNode) tree() *Tree {
diff --git a/src/text/template/parse/parse.go b/src/text/template/parse/parse.go
index 27c84f3..364c528 100644
--- a/src/text/template/parse/parse.go
+++ b/src/text/template/parse/parse.go
@@ -32,6 +32,9 @@
treeSet map[string]*Tree
actionLine int // line of left delim starting action
rangeDepth int
+
+ leftDelim string
+ rightDelim string
}
// A mode value is a set of flags (or 0). Modes control parser behavior.
@@ -48,10 +51,12 @@
return nil
}
return &Tree{
- Name: t.Name,
- ParseName: t.ParseName,
- Root: t.Root.CopyList(),
- text: t.text,
+ Name: t.Name,
+ ParseName: t.ParseName,
+ Root: t.Root.CopyList(),
+ text: t.text,
+ leftDelim: t.leftDelim,
+ rightDelim: t.rightDelim,
}
}
@@ -245,7 +250,8 @@
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]any) (tree *Tree, err error) {
defer t.recover(&err)
t.ParseName = t.Name
- lexer := lex(t.Name, text, leftDelim, rightDelim)
+ t.setDelims(leftDelim, rightDelim)
+ lexer := lex(t.Name, text, t.leftDelim, t.rightDelim)
t.startParse(funcs, lexer, treeSet)
t.text = text
t.parse()
@@ -254,6 +260,17 @@
return t, nil
}
+func (t *Tree) setDelims(lhs, rhs string) {
+ if lhs == "" {
+ lhs = leftDelim
+ }
+ t.leftDelim = lhs
+ if rhs == "" {
+ rhs = rightDelim
+ }
+ t.rightDelim = rhs
+}
+
// add adds tree to t.treeSet.
func (t *Tree) add() {
tree := t.treeSet[t.Name]
@@ -305,6 +322,8 @@
newT := New("definition") // name will be updated once we know it.
newT.text = t.text
newT.Mode = t.Mode
+ newT.leftDelim = t.leftDelim
+ newT.rightDelim = t.rightDelim
newT.ParseName = t.ParseName
newT.startParse(t.funcs, t.lex, t.treeSet)
newT.parseDefinition()
@@ -637,6 +656,8 @@
block := New(name) // name will be updated once we know it.
block.text = t.text
block.Mode = t.Mode
+ block.leftDelim = t.leftDelim
+ block.rightDelim = t.rightDelim
block.ParseName = t.ParseName
block.startParse(t.funcs, t.lex, t.treeSet)
var end Node
diff --git a/src/text/template/parse/parse_test.go b/src/text/template/parse/parse_test.go
index 26aff33..9935a22 100644
--- a/src/text/template/parse/parse_test.go
+++ b/src/text/template/parse/parse_test.go
@@ -428,6 +428,19 @@
}
}
+func TestParseCustomDelims(t *testing.T) {
+ const input = "{{{range .X}}}{{{end}}}"
+ parseTrees, err := Parse("test", input, "{{{", "}}}")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ listNode := parseTrees["test"].Root
+ expected := input
+ if result := listNode.String(); result != expected {
+ t.Errorf("got\n\t%v\nexpected\n\t%v", result, expected)
+ }
+}
+
func TestSkipFuncCheck(t *testing.T) {
oldTextFormat := textFormat
textFormat = "%q"
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
SGTM. This makes Tree slightly bigger but that doesn't feel like a problem. I would suggest that we wait to merge for 1.25 because it feels a bit too risky for 1.24 at this point in the freeze and with rc1 done; there could easily be users expecting the previous behavior.
Use the same delimiters in String() representation of nodes that werenit: drop the parentheses for consistency
This means that that when using custom delims we can roundtrip back toduplicate "that"
}this is only used once, and it's just a few lines, so personally I would inline.
you could also use `cmp.Or`:
```
t.leftDelim = cmp.Or(lhs, leftDelim)
t.rightDelim = cmp.Or(rhs, rightDelim)
```
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
}Why not extend the existing TestDelims test? You could add a few lines there to test properties about the expected String outcome, or even that it's exactly the same as the input if the input follows correct spacing etc.
If you need an extra test, call it something more specific, like TestDelimsStringRoundtrip.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
}this is only used once, and it's just a few lines, so personally I would inline.
you could also use `cmp.Or`:
```
t.leftDelim = cmp.Or(lhs, leftDelim)
t.rightDelim = cmp.Or(rhs, rightDelim)
```
no need to add a new dependency (cmp) here.
and i agree it's really not worth a separate function anyway.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Use the same delimiters in String() representation of nodes that werenit: drop the parentheses for consistency
Done
This means that that when using custom delims we can roundtrip back toPaul Jollyduplicate "that"
Done
Rob Pikethis is only used once, and it's just a few lines, so personally I would inline.
you could also use `cmp.Or`:
```
t.leftDelim = cmp.Or(lhs, leftDelim)
t.rightDelim = cmp.Or(rhs, rightDelim)
```
no need to add a new dependency (cmp) here.
and i agree it's really not worth a separate function anyway.
Done — inlined without cmp, per Rob's note.
Why not extend the existing TestDelims test? You could add a few lines there to test properties about the expected String outcome, or even that it's exactly the same as the input if the input follows correct spacing etc.
If you need an extra test, call it something more specific, like TestDelimsStringRoundtrip.
Done — extended TestDelims in exec_test.go for the action roundtrip across all delim pairs, and added TestDelimsStringRoundtrip in parse_test.go covering the remaining node types (if/else/end, range/break/continue, with, template, comment).
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
}Paul JollyWhy not extend the existing TestDelims test? You could add a few lines there to test properties about the expected String outcome, or even that it's exactly the same as the input if the input follows correct spacing etc.
If you need an extra test, call it something more specific, like TestDelimsStringRoundtrip.
Done — extended TestDelims in exec_test.go for the action roundtrip across all delim pairs, and added TestDelimsStringRoundtrip in parse_test.go covering the remaining node types (if/else/end, range/break/continue, with, template, comment).
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |