361 lines
8.2 KiB
Go
361 lines
8.2 KiB
Go
|
package parser
|
||
|
|
||
|
import (
|
||
|
"regexp"
|
||
|
|
||
|
"github.com/aymerick/raymond/ast"
|
||
|
)
|
||
|
|
||
|
// whitespaceVisitor walks through the AST to perform whitespace control
|
||
|
//
|
||
|
// The logic was shamelessly borrowed from:
|
||
|
// https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/whitespace-control.js
|
||
|
type whitespaceVisitor struct {
|
||
|
isRootSeen bool
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
rTrimLeft = regexp.MustCompile(`^[ \t]*\r?\n?`)
|
||
|
rTrimLeftMultiple = regexp.MustCompile(`^\s+`)
|
||
|
|
||
|
rTrimRight = regexp.MustCompile(`[ \t]+$`)
|
||
|
rTrimRightMultiple = regexp.MustCompile(`\s+$`)
|
||
|
|
||
|
rPrevWhitespace = regexp.MustCompile(`\r?\n\s*?$`)
|
||
|
rPrevWhitespaceStart = regexp.MustCompile(`(^|\r?\n)\s*?$`)
|
||
|
|
||
|
rNextWhitespace = regexp.MustCompile(`^\s*?\r?\n`)
|
||
|
rNextWhitespaceEnd = regexp.MustCompile(`^\s*?(\r?\n|$)`)
|
||
|
|
||
|
rPartialIndent = regexp.MustCompile(`([ \t]+$)`)
|
||
|
)
|
||
|
|
||
|
// newWhitespaceVisitor instanciates a new whitespaceVisitor
|
||
|
func newWhitespaceVisitor() *whitespaceVisitor {
|
||
|
return &whitespaceVisitor{}
|
||
|
}
|
||
|
|
||
|
// processWhitespaces performs whitespace control on given AST
|
||
|
//
|
||
|
// WARNING: It must be called only once on AST.
|
||
|
func processWhitespaces(node ast.Node) {
|
||
|
node.Accept(newWhitespaceVisitor())
|
||
|
}
|
||
|
|
||
|
func omitRightFirst(body []ast.Node, multiple bool) {
|
||
|
omitRight(body, -1, multiple)
|
||
|
}
|
||
|
|
||
|
func omitRight(body []ast.Node, i int, multiple bool) {
|
||
|
if i+1 >= len(body) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
current := body[i+1]
|
||
|
|
||
|
node, ok := current.(*ast.ContentStatement)
|
||
|
if !ok {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if !multiple && node.RightStripped {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
original := node.Value
|
||
|
|
||
|
r := rTrimLeft
|
||
|
if multiple {
|
||
|
r = rTrimLeftMultiple
|
||
|
}
|
||
|
|
||
|
node.Value = r.ReplaceAllString(node.Value, "")
|
||
|
|
||
|
node.RightStripped = (original != node.Value)
|
||
|
}
|
||
|
|
||
|
func omitLeftLast(body []ast.Node, multiple bool) {
|
||
|
omitLeft(body, len(body), multiple)
|
||
|
}
|
||
|
|
||
|
func omitLeft(body []ast.Node, i int, multiple bool) bool {
|
||
|
if i-1 < 0 {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
current := body[i-1]
|
||
|
|
||
|
node, ok := current.(*ast.ContentStatement)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if !multiple && node.LeftStripped {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
original := node.Value
|
||
|
|
||
|
r := rTrimRight
|
||
|
if multiple {
|
||
|
r = rTrimRightMultiple
|
||
|
}
|
||
|
|
||
|
node.Value = r.ReplaceAllString(node.Value, "")
|
||
|
|
||
|
node.LeftStripped = (original != node.Value)
|
||
|
|
||
|
return node.LeftStripped
|
||
|
}
|
||
|
|
||
|
func isPrevWhitespace(body []ast.Node) bool {
|
||
|
return isPrevWhitespaceProgram(body, len(body), false)
|
||
|
}
|
||
|
|
||
|
func isPrevWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
|
||
|
if i < 1 {
|
||
|
return isRoot
|
||
|
}
|
||
|
|
||
|
prev := body[i-1]
|
||
|
|
||
|
if node, ok := prev.(*ast.ContentStatement); ok {
|
||
|
if (node.Value == "") && node.RightStripped {
|
||
|
// already stripped, so it may be an empty string not catched by regexp
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
r := rPrevWhitespaceStart
|
||
|
if (i > 1) || !isRoot {
|
||
|
r = rPrevWhitespace
|
||
|
}
|
||
|
|
||
|
return r.MatchString(node.Value)
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func isNextWhitespace(body []ast.Node) bool {
|
||
|
return isNextWhitespaceProgram(body, -1, false)
|
||
|
}
|
||
|
|
||
|
func isNextWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool {
|
||
|
if i+1 >= len(body) {
|
||
|
return isRoot
|
||
|
}
|
||
|
|
||
|
next := body[i+1]
|
||
|
|
||
|
if node, ok := next.(*ast.ContentStatement); ok {
|
||
|
if (node.Value == "") && node.LeftStripped {
|
||
|
// already stripped, so it may be an empty string not catched by regexp
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
r := rNextWhitespaceEnd
|
||
|
if (i+2 > len(body)) || !isRoot {
|
||
|
r = rNextWhitespace
|
||
|
}
|
||
|
|
||
|
return r.MatchString(node.Value)
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Visitor interface
|
||
|
//
|
||
|
|
||
|
func (v *whitespaceVisitor) VisitProgram(program *ast.Program) interface{} {
|
||
|
isRoot := !v.isRootSeen
|
||
|
v.isRootSeen = true
|
||
|
|
||
|
body := program.Body
|
||
|
for i, current := range body {
|
||
|
strip, _ := current.Accept(v).(*ast.Strip)
|
||
|
if strip == nil {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
_isPrevWhitespace := isPrevWhitespaceProgram(body, i, isRoot)
|
||
|
_isNextWhitespace := isNextWhitespaceProgram(body, i, isRoot)
|
||
|
|
||
|
openStandalone := strip.OpenStandalone && _isPrevWhitespace
|
||
|
closeStandalone := strip.CloseStandalone && _isNextWhitespace
|
||
|
inlineStandalone := strip.InlineStandalone && _isPrevWhitespace && _isNextWhitespace
|
||
|
|
||
|
if strip.Close {
|
||
|
omitRight(body, i, true)
|
||
|
}
|
||
|
|
||
|
if strip.Open && (i > 0) {
|
||
|
omitLeft(body, i, true)
|
||
|
}
|
||
|
|
||
|
if inlineStandalone {
|
||
|
omitRight(body, i, false)
|
||
|
|
||
|
if omitLeft(body, i, false) {
|
||
|
// If we are on a standalone node, save the indent info for partials
|
||
|
if partial, ok := current.(*ast.PartialStatement); ok {
|
||
|
// Pull out the whitespace from the final line
|
||
|
if i > 0 {
|
||
|
if prevContent, ok := body[i-1].(*ast.ContentStatement); ok {
|
||
|
partial.Indent = rPartialIndent.FindString(prevContent.Original)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if b, ok := current.(*ast.BlockStatement); ok {
|
||
|
if openStandalone {
|
||
|
prog := b.Program
|
||
|
if prog == nil {
|
||
|
prog = b.Inverse
|
||
|
}
|
||
|
|
||
|
omitRightFirst(prog.Body, false)
|
||
|
|
||
|
// Strip out the previous content node if it's whitespace only
|
||
|
omitLeft(body, i, false)
|
||
|
}
|
||
|
|
||
|
if closeStandalone {
|
||
|
prog := b.Inverse
|
||
|
if prog == nil {
|
||
|
prog = b.Program
|
||
|
}
|
||
|
|
||
|
// Always strip the next node
|
||
|
omitRight(body, i, false)
|
||
|
|
||
|
omitLeftLast(prog.Body, false)
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (v *whitespaceVisitor) VisitBlock(block *ast.BlockStatement) interface{} {
|
||
|
if block.Program != nil {
|
||
|
block.Program.Accept(v)
|
||
|
}
|
||
|
|
||
|
if block.Inverse != nil {
|
||
|
block.Inverse.Accept(v)
|
||
|
}
|
||
|
|
||
|
program := block.Program
|
||
|
inverse := block.Inverse
|
||
|
|
||
|
if program == nil {
|
||
|
program = inverse
|
||
|
inverse = nil
|
||
|
}
|
||
|
|
||
|
firstInverse := inverse
|
||
|
lastInverse := inverse
|
||
|
|
||
|
if (inverse != nil) && inverse.Chained {
|
||
|
b, _ := inverse.Body[0].(*ast.BlockStatement)
|
||
|
firstInverse = b.Program
|
||
|
|
||
|
for lastInverse.Chained {
|
||
|
b, _ := lastInverse.Body[len(lastInverse.Body)-1].(*ast.BlockStatement)
|
||
|
lastInverse = b.Program
|
||
|
}
|
||
|
}
|
||
|
|
||
|
closeProg := firstInverse
|
||
|
if closeProg == nil {
|
||
|
closeProg = program
|
||
|
}
|
||
|
|
||
|
strip := &ast.Strip{
|
||
|
Open: (block.OpenStrip != nil) && block.OpenStrip.Open,
|
||
|
Close: (block.CloseStrip != nil) && block.CloseStrip.Close,
|
||
|
|
||
|
OpenStandalone: isNextWhitespace(program.Body),
|
||
|
CloseStandalone: isPrevWhitespace(closeProg.Body),
|
||
|
}
|
||
|
|
||
|
if (block.OpenStrip != nil) && block.OpenStrip.Close {
|
||
|
omitRightFirst(program.Body, true)
|
||
|
}
|
||
|
|
||
|
if inverse != nil {
|
||
|
if block.InverseStrip != nil {
|
||
|
inverseStrip := block.InverseStrip
|
||
|
|
||
|
if inverseStrip.Open {
|
||
|
omitLeftLast(program.Body, true)
|
||
|
}
|
||
|
|
||
|
if inverseStrip.Close {
|
||
|
omitRightFirst(firstInverse.Body, true)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (block.CloseStrip != nil) && block.CloseStrip.Open {
|
||
|
omitLeftLast(lastInverse.Body, true)
|
||
|
}
|
||
|
|
||
|
// Find standalone else statements
|
||
|
if isPrevWhitespace(program.Body) && isNextWhitespace(firstInverse.Body) {
|
||
|
omitLeftLast(program.Body, false)
|
||
|
|
||
|
omitRightFirst(firstInverse.Body, false)
|
||
|
}
|
||
|
} else if (block.CloseStrip != nil) && block.CloseStrip.Open {
|
||
|
omitLeftLast(program.Body, true)
|
||
|
}
|
||
|
|
||
|
return strip
|
||
|
}
|
||
|
|
||
|
func (v *whitespaceVisitor) VisitMustache(mustache *ast.MustacheStatement) interface{} {
|
||
|
return mustache.Strip
|
||
|
}
|
||
|
|
||
|
func _inlineStandalone(strip *ast.Strip) interface{} {
|
||
|
return &ast.Strip{
|
||
|
Open: strip.Open,
|
||
|
Close: strip.Close,
|
||
|
InlineStandalone: true,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (v *whitespaceVisitor) VisitPartial(node *ast.PartialStatement) interface{} {
|
||
|
strip := node.Strip
|
||
|
if strip == nil {
|
||
|
strip = &ast.Strip{}
|
||
|
}
|
||
|
|
||
|
return _inlineStandalone(strip)
|
||
|
}
|
||
|
|
||
|
func (v *whitespaceVisitor) VisitComment(node *ast.CommentStatement) interface{} {
|
||
|
strip := node.Strip
|
||
|
if strip == nil {
|
||
|
strip = &ast.Strip{}
|
||
|
}
|
||
|
|
||
|
return _inlineStandalone(strip)
|
||
|
}
|
||
|
|
||
|
// NOOP
|
||
|
func (v *whitespaceVisitor) VisitContent(node *ast.ContentStatement) interface{} { return nil }
|
||
|
func (v *whitespaceVisitor) VisitExpression(node *ast.Expression) interface{} { return nil }
|
||
|
func (v *whitespaceVisitor) VisitSubExpression(node *ast.SubExpression) interface{} { return nil }
|
||
|
func (v *whitespaceVisitor) VisitPath(node *ast.PathExpression) interface{} { return nil }
|
||
|
func (v *whitespaceVisitor) VisitString(node *ast.StringLiteral) interface{} { return nil }
|
||
|
func (v *whitespaceVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} { return nil }
|
||
|
func (v *whitespaceVisitor) VisitNumber(node *ast.NumberLiteral) interface{} { return nil }
|
||
|
func (v *whitespaceVisitor) VisitHash(node *ast.Hash) interface{} { return nil }
|
||
|
func (v *whitespaceVisitor) VisitHashPair(node *ast.HashPair) interface{} { return nil }
|