switch glide to govendor (#43)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
		
							
								
								
									
										846
									
								
								vendor/github.com/aymerick/raymond/parser/parser.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										846
									
								
								vendor/github.com/aymerick/raymond/parser/parser.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,846 @@ | ||||
| // Package parser provides a handlebars syntax analyser. It consumes the tokens provided by the lexer to build an AST. | ||||
| package parser | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"regexp" | ||||
| 	"runtime" | ||||
| 	"strconv" | ||||
|  | ||||
| 	"github.com/aymerick/raymond/ast" | ||||
| 	"github.com/aymerick/raymond/lexer" | ||||
| ) | ||||
|  | ||||
| // References: | ||||
| //   - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.yy | ||||
| //   - https://github.com/golang/go/blob/master/src/text/template/parse/parse.go | ||||
|  | ||||
| // parser is a syntax analyzer. | ||||
| type parser struct { | ||||
| 	// Lexer | ||||
| 	lex *lexer.Lexer | ||||
|  | ||||
| 	// Root node | ||||
| 	root ast.Node | ||||
|  | ||||
| 	// Tokens parsed but not consumed yet | ||||
| 	tokens []*lexer.Token | ||||
|  | ||||
| 	// All tokens have been retreieved from lexer | ||||
| 	lexOver bool | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	rOpenComment  = regexp.MustCompile(`^\{\{~?!-?-?`) | ||||
| 	rCloseComment = regexp.MustCompile(`-?-?~?\}\}$`) | ||||
| 	rOpenAmp      = regexp.MustCompile(`^\{\{~?&`) | ||||
| ) | ||||
|  | ||||
| // new instanciates a new parser | ||||
| func new(input string) *parser { | ||||
| 	return &parser{ | ||||
| 		lex: lexer.Scan(input), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Parse analyzes given input and returns the AST root node. | ||||
| func Parse(input string) (result *ast.Program, err error) { | ||||
| 	// recover error | ||||
| 	defer errRecover(&err) | ||||
|  | ||||
| 	parser := new(input) | ||||
|  | ||||
| 	// parse | ||||
| 	result = parser.parseProgram() | ||||
|  | ||||
| 	// check last token | ||||
| 	token := parser.shift() | ||||
| 	if token.Kind != lexer.TokenEOF { | ||||
| 		// Parsing ended before EOF | ||||
| 		errToken(token, "Syntax error") | ||||
| 	} | ||||
|  | ||||
| 	// fix whitespaces | ||||
| 	processWhitespaces(result) | ||||
|  | ||||
| 	// named returned values | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // errRecover recovers parsing panic | ||||
| func errRecover(errp *error) { | ||||
| 	e := recover() | ||||
| 	if e != nil { | ||||
| 		switch err := e.(type) { | ||||
| 		case runtime.Error: | ||||
| 			panic(e) | ||||
| 		case error: | ||||
| 			*errp = err | ||||
| 		default: | ||||
| 			panic(e) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // errPanic panics | ||||
| func errPanic(err error, line int) { | ||||
| 	panic(fmt.Errorf("Parse error on line %d:\n%s", line, err)) | ||||
| } | ||||
|  | ||||
| // errNode panics with given node infos | ||||
| func errNode(node ast.Node, msg string) { | ||||
| 	errPanic(fmt.Errorf("%s\nNode: %s", msg, node), node.Location().Line) | ||||
| } | ||||
|  | ||||
| // errNode panics with given Token infos | ||||
| func errToken(tok *lexer.Token, msg string) { | ||||
| 	errPanic(fmt.Errorf("%s\nToken: %s", msg, tok), tok.Line) | ||||
| } | ||||
|  | ||||
| // errNode panics because of an unexpected Token kind | ||||
| func errExpected(expect lexer.TokenKind, tok *lexer.Token) { | ||||
| 	errPanic(fmt.Errorf("Expecting %s, got: '%s'", expect, tok), tok.Line) | ||||
| } | ||||
|  | ||||
| // program : statement* | ||||
| func (p *parser) parseProgram() *ast.Program { | ||||
| 	result := ast.NewProgram(p.next().Pos, p.next().Line) | ||||
|  | ||||
| 	for p.isStatement() { | ||||
| 		result.AddStatement(p.parseStatement()) | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // statement : mustache | block | rawBlock | partial | content | COMMENT | ||||
| func (p *parser) parseStatement() ast.Node { | ||||
| 	var result ast.Node | ||||
|  | ||||
| 	tok := p.next() | ||||
|  | ||||
| 	switch tok.Kind { | ||||
| 	case lexer.TokenOpen, lexer.TokenOpenUnescaped: | ||||
| 		// mustache | ||||
| 		result = p.parseMustache() | ||||
| 	case lexer.TokenOpenBlock: | ||||
| 		// block | ||||
| 		result = p.parseBlock() | ||||
| 	case lexer.TokenOpenInverse: | ||||
| 		// block | ||||
| 		result = p.parseInverse() | ||||
| 	case lexer.TokenOpenRawBlock: | ||||
| 		// rawBlock | ||||
| 		result = p.parseRawBlock() | ||||
| 	case lexer.TokenOpenPartial: | ||||
| 		// partial | ||||
| 		result = p.parsePartial() | ||||
| 	case lexer.TokenContent: | ||||
| 		// content | ||||
| 		result = p.parseContent() | ||||
| 	case lexer.TokenComment: | ||||
| 		// COMMENT | ||||
| 		result = p.parseComment() | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // isStatement returns true if next token starts a statement | ||||
| func (p *parser) isStatement() bool { | ||||
| 	if !p.have(1) { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	switch p.next().Kind { | ||||
| 	case lexer.TokenOpen, lexer.TokenOpenUnescaped, lexer.TokenOpenBlock, | ||||
| 		lexer.TokenOpenInverse, lexer.TokenOpenRawBlock, lexer.TokenOpenPartial, | ||||
| 		lexer.TokenContent, lexer.TokenComment: | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // content : CONTENT | ||||
| func (p *parser) parseContent() *ast.ContentStatement { | ||||
| 	// CONTENT | ||||
| 	tok := p.shift() | ||||
| 	if tok.Kind != lexer.TokenContent { | ||||
| 		// @todo This check can be removed if content is optional in a raw block | ||||
| 		errExpected(lexer.TokenContent, tok) | ||||
| 	} | ||||
|  | ||||
| 	return ast.NewContentStatement(tok.Pos, tok.Line, tok.Val) | ||||
| } | ||||
|  | ||||
| // COMMENT | ||||
| func (p *parser) parseComment() *ast.CommentStatement { | ||||
| 	// COMMENT | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	value := rOpenComment.ReplaceAllString(tok.Val, "") | ||||
| 	value = rCloseComment.ReplaceAllString(value, "") | ||||
|  | ||||
| 	result := ast.NewCommentStatement(tok.Pos, tok.Line, value) | ||||
| 	result.Strip = ast.NewStripForStr(tok.Val) | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // param* hash? | ||||
| func (p *parser) parseExpressionParamsHash() ([]ast.Node, *ast.Hash) { | ||||
| 	var params []ast.Node | ||||
| 	var hash *ast.Hash | ||||
|  | ||||
| 	// params* | ||||
| 	if p.isParam() { | ||||
| 		params = p.parseParams() | ||||
| 	} | ||||
|  | ||||
| 	// hash? | ||||
| 	if p.isHashSegment() { | ||||
| 		hash = p.parseHash() | ||||
| 	} | ||||
|  | ||||
| 	return params, hash | ||||
| } | ||||
|  | ||||
| // helperName param* hash? | ||||
| func (p *parser) parseExpression(tok *lexer.Token) *ast.Expression { | ||||
| 	result := ast.NewExpression(tok.Pos, tok.Line) | ||||
|  | ||||
| 	// helperName | ||||
| 	result.Path = p.parseHelperName() | ||||
|  | ||||
| 	// param* hash? | ||||
| 	result.Params, result.Hash = p.parseExpressionParamsHash() | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // rawBlock : openRawBlock content endRawBlock | ||||
| // openRawBlock : OPEN_RAW_BLOCK helperName param* hash? CLOSE_RAW_BLOCK | ||||
| // endRawBlock : OPEN_END_RAW_BLOCK helperName CLOSE_RAW_BLOCK | ||||
| func (p *parser) parseRawBlock() *ast.BlockStatement { | ||||
| 	// OPEN_RAW_BLOCK | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	result := ast.NewBlockStatement(tok.Pos, tok.Line) | ||||
|  | ||||
| 	// helperName param* hash? | ||||
| 	result.Expression = p.parseExpression(tok) | ||||
|  | ||||
| 	openName := result.Expression.Canonical() | ||||
|  | ||||
| 	// CLOSE_RAW_BLOCK | ||||
| 	tok = p.shift() | ||||
| 	if tok.Kind != lexer.TokenCloseRawBlock { | ||||
| 		errExpected(lexer.TokenCloseRawBlock, tok) | ||||
| 	} | ||||
|  | ||||
| 	// content | ||||
| 	// @todo Is content mandatory in a raw block ? | ||||
| 	content := p.parseContent() | ||||
|  | ||||
| 	program := ast.NewProgram(tok.Pos, tok.Line) | ||||
| 	program.AddStatement(content) | ||||
|  | ||||
| 	result.Program = program | ||||
|  | ||||
| 	// OPEN_END_RAW_BLOCK | ||||
| 	tok = p.shift() | ||||
| 	if tok.Kind != lexer.TokenOpenEndRawBlock { | ||||
| 		// should never happen as it is caught by lexer | ||||
| 		errExpected(lexer.TokenOpenEndRawBlock, tok) | ||||
| 	} | ||||
|  | ||||
| 	// helperName | ||||
| 	endID := p.parseHelperName() | ||||
|  | ||||
| 	closeName, ok := ast.HelperNameStr(endID) | ||||
| 	if !ok { | ||||
| 		errNode(endID, "Erroneous closing expression") | ||||
| 	} | ||||
|  | ||||
| 	if openName != closeName { | ||||
| 		errNode(endID, fmt.Sprintf("%s doesn't match %s", openName, closeName)) | ||||
| 	} | ||||
|  | ||||
| 	// CLOSE_RAW_BLOCK | ||||
| 	tok = p.shift() | ||||
| 	if tok.Kind != lexer.TokenCloseRawBlock { | ||||
| 		errExpected(lexer.TokenCloseRawBlock, tok) | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // block : openBlock program inverseChain? closeBlock | ||||
| func (p *parser) parseBlock() *ast.BlockStatement { | ||||
| 	// openBlock | ||||
| 	result, blockParams := p.parseOpenBlock() | ||||
|  | ||||
| 	// program | ||||
| 	program := p.parseProgram() | ||||
| 	program.BlockParams = blockParams | ||||
| 	result.Program = program | ||||
|  | ||||
| 	// inverseChain? | ||||
| 	if p.isInverseChain() { | ||||
| 		result.Inverse = p.parseInverseChain() | ||||
| 	} | ||||
|  | ||||
| 	// closeBlock | ||||
| 	p.parseCloseBlock(result) | ||||
|  | ||||
| 	setBlockInverseStrip(result) | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // setBlockInverseStrip is called when parsing `block` (openBlock | openInverse) and `inverseChain` | ||||
| // | ||||
| // TODO: This was totally cargo culted ! CHECK THAT ! | ||||
| // | ||||
| // cf. prepareBlock() in: | ||||
| //   https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/helper.js | ||||
| func setBlockInverseStrip(block *ast.BlockStatement) { | ||||
| 	if block.Inverse == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if block.Inverse.Chained { | ||||
| 		b, _ := block.Inverse.Body[0].(*ast.BlockStatement) | ||||
| 		b.CloseStrip = block.CloseStrip | ||||
| 	} | ||||
|  | ||||
| 	block.InverseStrip = block.Inverse.Strip | ||||
| } | ||||
|  | ||||
| // block : openInverse program inverseAndProgram? closeBlock | ||||
| func (p *parser) parseInverse() *ast.BlockStatement { | ||||
| 	// openInverse | ||||
| 	result, blockParams := p.parseOpenBlock() | ||||
|  | ||||
| 	// program | ||||
| 	program := p.parseProgram() | ||||
|  | ||||
| 	program.BlockParams = blockParams | ||||
| 	result.Inverse = program | ||||
|  | ||||
| 	// inverseAndProgram? | ||||
| 	if p.isInverse() { | ||||
| 		result.Program = p.parseInverseAndProgram() | ||||
| 	} | ||||
|  | ||||
| 	// closeBlock | ||||
| 	p.parseCloseBlock(result) | ||||
|  | ||||
| 	setBlockInverseStrip(result) | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // helperName param* hash? blockParams? | ||||
| func (p *parser) parseOpenBlockExpression(tok *lexer.Token) (*ast.BlockStatement, []string) { | ||||
| 	var blockParams []string | ||||
|  | ||||
| 	result := ast.NewBlockStatement(tok.Pos, tok.Line) | ||||
|  | ||||
| 	// helperName param* hash? | ||||
| 	result.Expression = p.parseExpression(tok) | ||||
|  | ||||
| 	// blockParams? | ||||
| 	if p.isBlockParams() { | ||||
| 		blockParams = p.parseBlockParams() | ||||
| 	} | ||||
|  | ||||
| 	// named returned values | ||||
| 	return result, blockParams | ||||
| } | ||||
|  | ||||
| // inverseChain : openInverseChain program inverseChain? | ||||
| //              | inverseAndProgram | ||||
| func (p *parser) parseInverseChain() *ast.Program { | ||||
| 	if p.isInverse() { | ||||
| 		// inverseAndProgram | ||||
| 		return p.parseInverseAndProgram() | ||||
| 	} | ||||
|  | ||||
| 	result := ast.NewProgram(p.next().Pos, p.next().Line) | ||||
|  | ||||
| 	// openInverseChain | ||||
| 	block, blockParams := p.parseOpenBlock() | ||||
|  | ||||
| 	// program | ||||
| 	program := p.parseProgram() | ||||
|  | ||||
| 	program.BlockParams = blockParams | ||||
| 	block.Program = program | ||||
|  | ||||
| 	// inverseChain? | ||||
| 	if p.isInverseChain() { | ||||
| 		block.Inverse = p.parseInverseChain() | ||||
| 	} | ||||
|  | ||||
| 	setBlockInverseStrip(block) | ||||
|  | ||||
| 	result.Chained = true | ||||
| 	result.AddStatement(block) | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // Returns true if current token starts an inverse chain | ||||
| func (p *parser) isInverseChain() bool { | ||||
| 	return p.isOpenInverseChain() || p.isInverse() | ||||
| } | ||||
|  | ||||
| // inverseAndProgram : INVERSE program | ||||
| func (p *parser) parseInverseAndProgram() *ast.Program { | ||||
| 	// INVERSE | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	// program | ||||
| 	result := p.parseProgram() | ||||
| 	result.Strip = ast.NewStripForStr(tok.Val) | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // openBlock : OPEN_BLOCK helperName param* hash? blockParams? CLOSE | ||||
| // openInverse : OPEN_INVERSE helperName param* hash? blockParams? CLOSE | ||||
| // openInverseChain: OPEN_INVERSE_CHAIN helperName param* hash? blockParams? CLOSE | ||||
| func (p *parser) parseOpenBlock() (*ast.BlockStatement, []string) { | ||||
| 	// OPEN_BLOCK | OPEN_INVERSE | OPEN_INVERSE_CHAIN | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	// helperName param* hash? blockParams? | ||||
| 	result, blockParams := p.parseOpenBlockExpression(tok) | ||||
|  | ||||
| 	// CLOSE | ||||
| 	tokClose := p.shift() | ||||
| 	if tokClose.Kind != lexer.TokenClose { | ||||
| 		errExpected(lexer.TokenClose, tokClose) | ||||
| 	} | ||||
|  | ||||
| 	result.OpenStrip = ast.NewStrip(tok.Val, tokClose.Val) | ||||
|  | ||||
| 	// named returned values | ||||
| 	return result, blockParams | ||||
| } | ||||
|  | ||||
| // closeBlock : OPEN_ENDBLOCK helperName CLOSE | ||||
| func (p *parser) parseCloseBlock(block *ast.BlockStatement) { | ||||
| 	// OPEN_ENDBLOCK | ||||
| 	tok := p.shift() | ||||
| 	if tok.Kind != lexer.TokenOpenEndBlock { | ||||
| 		errExpected(lexer.TokenOpenEndBlock, tok) | ||||
| 	} | ||||
|  | ||||
| 	// helperName | ||||
| 	endID := p.parseHelperName() | ||||
|  | ||||
| 	closeName, ok := ast.HelperNameStr(endID) | ||||
| 	if !ok { | ||||
| 		errNode(endID, "Erroneous closing expression") | ||||
| 	} | ||||
|  | ||||
| 	openName := block.Expression.Canonical() | ||||
| 	if openName != closeName { | ||||
| 		errNode(endID, fmt.Sprintf("%s doesn't match %s", openName, closeName)) | ||||
| 	} | ||||
|  | ||||
| 	// CLOSE | ||||
| 	tokClose := p.shift() | ||||
| 	if tokClose.Kind != lexer.TokenClose { | ||||
| 		errExpected(lexer.TokenClose, tokClose) | ||||
| 	} | ||||
|  | ||||
| 	block.CloseStrip = ast.NewStrip(tok.Val, tokClose.Val) | ||||
| } | ||||
|  | ||||
| // mustache : OPEN helperName param* hash? CLOSE | ||||
| //          | OPEN_UNESCAPED helperName param* hash? CLOSE_UNESCAPED | ||||
| func (p *parser) parseMustache() *ast.MustacheStatement { | ||||
| 	// OPEN | OPEN_UNESCAPED | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	closeToken := lexer.TokenClose | ||||
| 	if tok.Kind == lexer.TokenOpenUnescaped { | ||||
| 		closeToken = lexer.TokenCloseUnescaped | ||||
| 	} | ||||
|  | ||||
| 	unescaped := false | ||||
| 	if (tok.Kind == lexer.TokenOpenUnescaped) || (rOpenAmp.MatchString(tok.Val)) { | ||||
| 		unescaped = true | ||||
| 	} | ||||
|  | ||||
| 	result := ast.NewMustacheStatement(tok.Pos, tok.Line, unescaped) | ||||
|  | ||||
| 	// helperName param* hash? | ||||
| 	result.Expression = p.parseExpression(tok) | ||||
|  | ||||
| 	// CLOSE | CLOSE_UNESCAPED | ||||
| 	tokClose := p.shift() | ||||
| 	if tokClose.Kind != closeToken { | ||||
| 		errExpected(closeToken, tokClose) | ||||
| 	} | ||||
|  | ||||
| 	result.Strip = ast.NewStrip(tok.Val, tokClose.Val) | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // partial : OPEN_PARTIAL partialName param* hash? CLOSE | ||||
| func (p *parser) parsePartial() *ast.PartialStatement { | ||||
| 	// OPEN_PARTIAL | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	result := ast.NewPartialStatement(tok.Pos, tok.Line) | ||||
|  | ||||
| 	// partialName | ||||
| 	result.Name = p.parsePartialName() | ||||
|  | ||||
| 	// param* hash? | ||||
| 	result.Params, result.Hash = p.parseExpressionParamsHash() | ||||
|  | ||||
| 	// CLOSE | ||||
| 	tokClose := p.shift() | ||||
| 	if tokClose.Kind != lexer.TokenClose { | ||||
| 		errExpected(lexer.TokenClose, tokClose) | ||||
| 	} | ||||
|  | ||||
| 	result.Strip = ast.NewStrip(tok.Val, tokClose.Val) | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // helperName | sexpr | ||||
| func (p *parser) parseHelperNameOrSexpr() ast.Node { | ||||
| 	if p.isSexpr() { | ||||
| 		// sexpr | ||||
| 		return p.parseSexpr() | ||||
| 	} | ||||
|  | ||||
| 	// helperName | ||||
| 	return p.parseHelperName() | ||||
| } | ||||
|  | ||||
| // param : helperName | sexpr | ||||
| func (p *parser) parseParam() ast.Node { | ||||
| 	return p.parseHelperNameOrSexpr() | ||||
| } | ||||
|  | ||||
| // Returns true if next tokens represent a `param` | ||||
| func (p *parser) isParam() bool { | ||||
| 	return (p.isSexpr() || p.isHelperName()) && !p.isHashSegment() | ||||
| } | ||||
|  | ||||
| // param* | ||||
| func (p *parser) parseParams() []ast.Node { | ||||
| 	var result []ast.Node | ||||
|  | ||||
| 	for p.isParam() { | ||||
| 		result = append(result, p.parseParam()) | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // sexpr : OPEN_SEXPR helperName param* hash? CLOSE_SEXPR | ||||
| func (p *parser) parseSexpr() *ast.SubExpression { | ||||
| 	// OPEN_SEXPR | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	result := ast.NewSubExpression(tok.Pos, tok.Line) | ||||
|  | ||||
| 	// helperName param* hash? | ||||
| 	result.Expression = p.parseExpression(tok) | ||||
|  | ||||
| 	// CLOSE_SEXPR | ||||
| 	tok = p.shift() | ||||
| 	if tok.Kind != lexer.TokenCloseSexpr { | ||||
| 		errExpected(lexer.TokenCloseSexpr, tok) | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // hash : hashSegment+ | ||||
| func (p *parser) parseHash() *ast.Hash { | ||||
| 	var pairs []*ast.HashPair | ||||
|  | ||||
| 	for p.isHashSegment() { | ||||
| 		pairs = append(pairs, p.parseHashSegment()) | ||||
| 	} | ||||
|  | ||||
| 	firstLoc := pairs[0].Location() | ||||
|  | ||||
| 	result := ast.NewHash(firstLoc.Pos, firstLoc.Line) | ||||
| 	result.Pairs = pairs | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // returns true if next tokens represents a `hashSegment` | ||||
| func (p *parser) isHashSegment() bool { | ||||
| 	return p.have(2) && (p.next().Kind == lexer.TokenID) && (p.nextAt(1).Kind == lexer.TokenEquals) | ||||
| } | ||||
|  | ||||
| // hashSegment : ID EQUALS param | ||||
| func (p *parser) parseHashSegment() *ast.HashPair { | ||||
| 	// ID | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	// EQUALS | ||||
| 	p.shift() | ||||
|  | ||||
| 	// param | ||||
| 	param := p.parseParam() | ||||
|  | ||||
| 	result := ast.NewHashPair(tok.Pos, tok.Line) | ||||
| 	result.Key = tok.Val | ||||
| 	result.Val = param | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // blockParams : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS | ||||
| func (p *parser) parseBlockParams() []string { | ||||
| 	var result []string | ||||
|  | ||||
| 	// OPEN_BLOCK_PARAMS | ||||
| 	tok := p.shift() | ||||
|  | ||||
| 	// ID+ | ||||
| 	for p.isID() { | ||||
| 		result = append(result, p.shift().Val) | ||||
| 	} | ||||
|  | ||||
| 	if len(result) == 0 { | ||||
| 		errExpected(lexer.TokenID, p.next()) | ||||
| 	} | ||||
|  | ||||
| 	// CLOSE_BLOCK_PARAMS | ||||
| 	tok = p.shift() | ||||
| 	if tok.Kind != lexer.TokenCloseBlockParams { | ||||
| 		errExpected(lexer.TokenCloseBlockParams, tok) | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // helperName : path | dataName | STRING | NUMBER | BOOLEAN | UNDEFINED | NULL | ||||
| func (p *parser) parseHelperName() ast.Node { | ||||
| 	var result ast.Node | ||||
|  | ||||
| 	tok := p.next() | ||||
|  | ||||
| 	switch tok.Kind { | ||||
| 	case lexer.TokenBoolean: | ||||
| 		// BOOLEAN | ||||
| 		p.shift() | ||||
| 		result = ast.NewBooleanLiteral(tok.Pos, tok.Line, (tok.Val == "true"), tok.Val) | ||||
| 	case lexer.TokenNumber: | ||||
| 		// NUMBER | ||||
| 		p.shift() | ||||
|  | ||||
| 		val, isInt := parseNumber(tok) | ||||
| 		result = ast.NewNumberLiteral(tok.Pos, tok.Line, val, isInt, tok.Val) | ||||
| 	case lexer.TokenString: | ||||
| 		// STRING | ||||
| 		p.shift() | ||||
| 		result = ast.NewStringLiteral(tok.Pos, tok.Line, tok.Val) | ||||
| 	case lexer.TokenData: | ||||
| 		// dataName | ||||
| 		result = p.parseDataName() | ||||
| 	default: | ||||
| 		// path | ||||
| 		result = p.parsePath(false) | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // parseNumber parses a number | ||||
| func parseNumber(tok *lexer.Token) (result float64, isInt bool) { | ||||
| 	var valInt int | ||||
| 	var err error | ||||
|  | ||||
| 	valInt, err = strconv.Atoi(tok.Val) | ||||
| 	if err == nil { | ||||
| 		isInt = true | ||||
|  | ||||
| 		result = float64(valInt) | ||||
| 	} else { | ||||
| 		isInt = false | ||||
|  | ||||
| 		result, err = strconv.ParseFloat(tok.Val, 64) | ||||
| 		if err != nil { | ||||
| 			errToken(tok, fmt.Sprintf("Failed to parse number: %s", tok.Val)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// named returned values | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // Returns true if next tokens represent a `helperName` | ||||
| func (p *parser) isHelperName() bool { | ||||
| 	switch p.next().Kind { | ||||
| 	case lexer.TokenBoolean, lexer.TokenNumber, lexer.TokenString, lexer.TokenData, lexer.TokenID: | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // partialName : helperName | sexpr | ||||
| func (p *parser) parsePartialName() ast.Node { | ||||
| 	return p.parseHelperNameOrSexpr() | ||||
| } | ||||
|  | ||||
| // dataName : DATA pathSegments | ||||
| func (p *parser) parseDataName() *ast.PathExpression { | ||||
| 	// DATA | ||||
| 	p.shift() | ||||
|  | ||||
| 	// pathSegments | ||||
| 	return p.parsePath(true) | ||||
| } | ||||
|  | ||||
| // path : pathSegments | ||||
| // pathSegments : pathSegments SEP ID | ||||
| //              | ID | ||||
| func (p *parser) parsePath(data bool) *ast.PathExpression { | ||||
| 	var tok *lexer.Token | ||||
|  | ||||
| 	// ID | ||||
| 	tok = p.shift() | ||||
| 	if tok.Kind != lexer.TokenID { | ||||
| 		errExpected(lexer.TokenID, tok) | ||||
| 	} | ||||
|  | ||||
| 	result := ast.NewPathExpression(tok.Pos, tok.Line, data) | ||||
| 	result.Part(tok.Val) | ||||
|  | ||||
| 	for p.isPathSep() { | ||||
| 		// SEP | ||||
| 		tok = p.shift() | ||||
| 		result.Sep(tok.Val) | ||||
|  | ||||
| 		// ID | ||||
| 		tok = p.shift() | ||||
| 		if tok.Kind != lexer.TokenID { | ||||
| 			errExpected(lexer.TokenID, tok) | ||||
| 		} | ||||
|  | ||||
| 		result.Part(tok.Val) | ||||
|  | ||||
| 		if len(result.Parts) > 0 { | ||||
| 			switch tok.Val { | ||||
| 			case "..", ".", "this": | ||||
| 				errToken(tok, "Invalid path: "+result.Original) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // Ensures there is token to parse at given index | ||||
| func (p *parser) ensure(index int) { | ||||
| 	if p.lexOver { | ||||
| 		// nothing more to grab | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	nb := index + 1 | ||||
|  | ||||
| 	for len(p.tokens) < nb { | ||||
| 		// fetch next token | ||||
| 		tok := p.lex.NextToken() | ||||
|  | ||||
| 		// queue it | ||||
| 		p.tokens = append(p.tokens, &tok) | ||||
|  | ||||
| 		if (tok.Kind == lexer.TokenEOF) || (tok.Kind == lexer.TokenError) { | ||||
| 			p.lexOver = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // have returns true is there are a list given number of tokens to consume left | ||||
| func (p *parser) have(nb int) bool { | ||||
| 	p.ensure(nb - 1) | ||||
|  | ||||
| 	return len(p.tokens) >= nb | ||||
| } | ||||
|  | ||||
| // nextAt returns next token at given index, without consuming it | ||||
| func (p *parser) nextAt(index int) *lexer.Token { | ||||
| 	p.ensure(index) | ||||
|  | ||||
| 	return p.tokens[index] | ||||
| } | ||||
|  | ||||
| // next returns next token without consuming it | ||||
| func (p *parser) next() *lexer.Token { | ||||
| 	return p.nextAt(0) | ||||
| } | ||||
|  | ||||
| // shift returns next token and remove it from the tokens buffer | ||||
| // | ||||
| // Panics if next token is `TokenError` | ||||
| func (p *parser) shift() *lexer.Token { | ||||
| 	var result *lexer.Token | ||||
|  | ||||
| 	p.ensure(0) | ||||
|  | ||||
| 	result, p.tokens = p.tokens[0], p.tokens[1:] | ||||
|  | ||||
| 	// check error token | ||||
| 	if result.Kind == lexer.TokenError { | ||||
| 		errToken(result, "Lexer error") | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // isToken returns true if next token is of given type | ||||
| func (p *parser) isToken(kind lexer.TokenKind) bool { | ||||
| 	return p.have(1) && p.next().Kind == kind | ||||
| } | ||||
|  | ||||
| // isSexpr returns true if next token starts a sexpr | ||||
| func (p *parser) isSexpr() bool { | ||||
| 	return p.isToken(lexer.TokenOpenSexpr) | ||||
| } | ||||
|  | ||||
| // isPathSep returns true if next token is a path separator | ||||
| func (p *parser) isPathSep() bool { | ||||
| 	return p.isToken(lexer.TokenSep) | ||||
| } | ||||
|  | ||||
| // isID returns true if next token is an ID | ||||
| func (p *parser) isID() bool { | ||||
| 	return p.isToken(lexer.TokenID) | ||||
| } | ||||
|  | ||||
| // isBlockParams returns true if next token starts a block params | ||||
| func (p *parser) isBlockParams() bool { | ||||
| 	return p.isToken(lexer.TokenOpenBlockParams) | ||||
| } | ||||
|  | ||||
| // isInverse returns true if next token starts an INVERSE sequence | ||||
| func (p *parser) isInverse() bool { | ||||
| 	return p.isToken(lexer.TokenInverse) | ||||
| } | ||||
|  | ||||
| // isOpenInverseChain returns true if next token is OPEN_INVERSE_CHAIN | ||||
| func (p *parser) isOpenInverseChain() bool { | ||||
| 	return p.isToken(lexer.TokenOpenInverseChain) | ||||
| } | ||||
							
								
								
									
										360
									
								
								vendor/github.com/aymerick/raymond/parser/whitespace.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								vendor/github.com/aymerick/raymond/parser/whitespace.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,360 @@ | ||||
| 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 } | ||||
		Reference in New Issue
	
	Block a user