mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			365 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			365 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2018 The Hugo Authors. All rights reserved.
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| // http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package pageparser
 | |
| 
 | |
| type lexerShortcodeState struct {
 | |
| 	currLeftDelimItem  ItemType
 | |
| 	currRightDelimItem ItemType
 | |
| 	isInline           bool
 | |
| 	currShortcodeName  string          // is only set when a shortcode is in opened state
 | |
| 	closingState       int             // > 0 = on its way to be closed
 | |
| 	elementStepNum     int             // step number in element
 | |
| 	paramElements      int             // number of elements (name + value = 2) found first
 | |
| 	openShortcodes     map[string]bool // set of shortcodes in open state
 | |
| 
 | |
| }
 | |
| 
 | |
| // Shortcode syntax
 | |
| var (
 | |
| 	leftDelimSc            = []byte("{{")
 | |
| 	leftDelimScNoMarkup    = []byte("{{<")
 | |
| 	rightDelimScNoMarkup   = []byte(">}}")
 | |
| 	leftDelimScWithMarkup  = []byte("{{%")
 | |
| 	rightDelimScWithMarkup = []byte("%}}")
 | |
| 	leftComment            = []byte("/*") // comments in this context us used to to mark shortcodes as "not really a shortcode"
 | |
| 	rightComment           = []byte("*/")
 | |
| )
 | |
| 
 | |
| func (l *pageLexer) isShortCodeStart() bool {
 | |
| 	return l.hasPrefix(leftDelimScWithMarkup) || l.hasPrefix(leftDelimScNoMarkup)
 | |
| }
 | |
| 
 | |
| func lexShortcodeLeftDelim(l *pageLexer) stateFunc {
 | |
| 	l.pos += len(l.currentLeftShortcodeDelim())
 | |
| 	if l.hasPrefix(leftComment) {
 | |
| 		return lexShortcodeComment
 | |
| 	}
 | |
| 	l.emit(l.currentLeftShortcodeDelimItem())
 | |
| 	l.elementStepNum = 0
 | |
| 	l.paramElements = 0
 | |
| 	return lexInsideShortcode
 | |
| }
 | |
| 
 | |
| func lexShortcodeComment(l *pageLexer) stateFunc {
 | |
| 	posRightComment := l.index(append(rightComment, l.currentRightShortcodeDelim()...))
 | |
| 	if posRightComment <= 1 {
 | |
| 		return l.errorf("comment must be closed")
 | |
| 	}
 | |
| 	// we emit all as text, except the comment markers
 | |
| 	l.emit(tText)
 | |
| 	l.pos += len(leftComment)
 | |
| 	l.ignore()
 | |
| 	l.pos += posRightComment - len(leftComment)
 | |
| 	l.emit(tText)
 | |
| 	l.pos += len(rightComment)
 | |
| 	l.ignore()
 | |
| 	l.pos += len(l.currentRightShortcodeDelim())
 | |
| 	l.emit(tText)
 | |
| 	return lexMainSection
 | |
| }
 | |
| 
 | |
| func lexShortcodeRightDelim(l *pageLexer) stateFunc {
 | |
| 	l.closingState = 0
 | |
| 	l.pos += len(l.currentRightShortcodeDelim())
 | |
| 	l.emit(l.currentRightShortcodeDelimItem())
 | |
| 	return lexMainSection
 | |
| }
 | |
| 
 | |
| // either:
 | |
| // 1. param
 | |
| // 2. "param" or "param\"
 | |
| // 3. param="123" or param="123\"
 | |
| // 4. param="Some \"escaped\" text"
 | |
| // 5. `param`
 | |
| // 6. param=`123`
 | |
| func lexShortcodeParam(l *pageLexer, escapedQuoteStart bool) stateFunc {
 | |
| 	first := true
 | |
| 	nextEq := false
 | |
| 
 | |
| 	var r rune
 | |
| 
 | |
| 	for {
 | |
| 		r = l.next()
 | |
| 		if first {
 | |
| 			if r == '"' || (r == '`' && !escapedQuoteStart) {
 | |
| 				// a positional param with quotes
 | |
| 				if l.paramElements == 2 {
 | |
| 					return l.errorf("got quoted positional parameter. Cannot mix named and positional parameters")
 | |
| 				}
 | |
| 				l.paramElements = 1
 | |
| 				l.backup()
 | |
| 				if r == '"' {
 | |
| 					return lexShortcodeQuotedParamVal(l, !escapedQuoteStart, tScParam)
 | |
| 				}
 | |
| 				return lexShortCodeParamRawStringVal(l, tScParam)
 | |
| 
 | |
| 			} else if r == '`' && escapedQuoteStart {
 | |
| 				return l.errorf("unrecognized escape character")
 | |
| 			}
 | |
| 			first = false
 | |
| 		} else if r == '=' {
 | |
| 			// a named param
 | |
| 			l.backup()
 | |
| 			nextEq = true
 | |
| 			break
 | |
| 		}
 | |
| 
 | |
| 		if !isAlphaNumericOrHyphen(r) && r != '.' { // Floats have period
 | |
| 			l.backup()
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if l.paramElements == 0 {
 | |
| 		l.paramElements++
 | |
| 
 | |
| 		if nextEq {
 | |
| 			l.paramElements++
 | |
| 		}
 | |
| 	} else {
 | |
| 		if nextEq && l.paramElements == 1 {
 | |
| 			return l.errorf("got named parameter '%s'. Cannot mix named and positional parameters", l.current())
 | |
| 		} else if !nextEq && l.paramElements == 2 {
 | |
| 			return l.errorf("got positional parameter '%s'. Cannot mix named and positional parameters", l.current())
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	l.emit(tScParam)
 | |
| 	return lexInsideShortcode
 | |
| }
 | |
| 
 | |
| func lexShortcodeParamVal(l *pageLexer) stateFunc {
 | |
| 	l.consumeToSpace()
 | |
| 	l.emit(tScParamVal)
 | |
| 	return lexInsideShortcode
 | |
| }
 | |
| 
 | |
| func lexShortCodeParamRawStringVal(l *pageLexer, typ ItemType) stateFunc {
 | |
| 	openBacktickFound := false
 | |
| 
 | |
| Loop:
 | |
| 	for {
 | |
| 		switch r := l.next(); {
 | |
| 		case r == '`':
 | |
| 			if openBacktickFound {
 | |
| 				l.backup()
 | |
| 				break Loop
 | |
| 			} else {
 | |
| 				openBacktickFound = true
 | |
| 				l.ignore()
 | |
| 			}
 | |
| 		case r == eof:
 | |
| 			return l.errorf("unterminated raw string in shortcode parameter-argument: '%s'", l.current())
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	l.emitString(typ)
 | |
| 	l.next()
 | |
| 	l.ignore()
 | |
| 
 | |
| 	return lexInsideShortcode
 | |
| }
 | |
| 
 | |
| func lexShortcodeQuotedParamVal(l *pageLexer, escapedQuotedValuesAllowed bool, typ ItemType) stateFunc {
 | |
| 	openQuoteFound := false
 | |
| 	escapedInnerQuoteFound := false
 | |
| 	escapedQuoteState := 0
 | |
| 
 | |
| Loop:
 | |
| 	for {
 | |
| 		switch r := l.next(); {
 | |
| 		case r == '\\':
 | |
| 			if l.peek() == '"' {
 | |
| 				if openQuoteFound && !escapedQuotedValuesAllowed {
 | |
| 					l.backup()
 | |
| 					break Loop
 | |
| 				} else if openQuoteFound {
 | |
| 					// the coming quote is inside
 | |
| 					escapedInnerQuoteFound = true
 | |
| 					escapedQuoteState = 1
 | |
| 				}
 | |
| 			} else if l.peek() == '`' {
 | |
| 				return l.errorf("unrecognized escape character")
 | |
| 			}
 | |
| 		case r == eof, r == '\n':
 | |
| 			return l.errorf("unterminated quoted string in shortcode parameter-argument: '%s'", l.current())
 | |
| 		case r == '"':
 | |
| 			if escapedQuoteState == 0 {
 | |
| 				if openQuoteFound {
 | |
| 					l.backup()
 | |
| 					break Loop
 | |
| 
 | |
| 				} else {
 | |
| 					openQuoteFound = true
 | |
| 					l.ignore()
 | |
| 				}
 | |
| 			} else {
 | |
| 				escapedQuoteState = 0
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if escapedInnerQuoteFound {
 | |
| 		l.ignoreEscapesAndEmit(typ, true)
 | |
| 	} else {
 | |
| 		l.emitString(typ)
 | |
| 	}
 | |
| 
 | |
| 	r := l.next()
 | |
| 
 | |
| 	if r == '\\' {
 | |
| 		if l.peek() == '"' {
 | |
| 			// ignore the escaped closing quote
 | |
| 			l.ignore()
 | |
| 			l.next()
 | |
| 			l.ignore()
 | |
| 		}
 | |
| 	} else if r == '"' {
 | |
| 		// ignore closing quote
 | |
| 		l.ignore()
 | |
| 	} else {
 | |
| 		// handled by next state
 | |
| 		l.backup()
 | |
| 	}
 | |
| 
 | |
| 	return lexInsideShortcode
 | |
| }
 | |
| 
 | |
| // Inline shortcodes has the form {{< myshortcode.inline >}}
 | |
| var inlineIdentifier = []byte("inline ")
 | |
| 
 | |
| // scans an alphanumeric inside shortcode
 | |
| func lexIdentifierInShortcode(l *pageLexer) stateFunc {
 | |
| 	lookForEnd := false
 | |
| Loop:
 | |
| 	for {
 | |
| 		switch r := l.next(); {
 | |
| 		case isAlphaNumericOrHyphen(r):
 | |
| 		// Allow forward slash inside names to make it possible to create namespaces.
 | |
| 		case r == '/':
 | |
| 		case r == '.':
 | |
| 			l.isInline = l.hasPrefix(inlineIdentifier)
 | |
| 			if !l.isInline {
 | |
| 				return l.errorf("period in shortcode name only allowed for inline identifiers")
 | |
| 			}
 | |
| 		default:
 | |
| 			l.backup()
 | |
| 			word := string(l.input[l.start:l.pos])
 | |
| 			if l.closingState > 0 && !l.openShortcodes[word] {
 | |
| 				return l.errorf("closing tag for shortcode '%s' does not match start tag", word)
 | |
| 			} else if l.closingState > 0 {
 | |
| 				l.openShortcodes[word] = false
 | |
| 				lookForEnd = true
 | |
| 			}
 | |
| 
 | |
| 			l.closingState = 0
 | |
| 			l.currShortcodeName = word
 | |
| 			l.openShortcodes[word] = true
 | |
| 			l.elementStepNum++
 | |
| 			if l.isInline {
 | |
| 				l.emit(tScNameInline)
 | |
| 			} else {
 | |
| 				l.emit(tScName)
 | |
| 			}
 | |
| 			break Loop
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if lookForEnd {
 | |
| 		return lexEndOfShortcode
 | |
| 	}
 | |
| 	return lexInsideShortcode
 | |
| }
 | |
| 
 | |
| func lexEndOfShortcode(l *pageLexer) stateFunc {
 | |
| 	l.isInline = false
 | |
| 	if l.hasPrefix(l.currentRightShortcodeDelim()) {
 | |
| 		return lexShortcodeRightDelim
 | |
| 	}
 | |
| 	switch r := l.next(); {
 | |
| 	case isSpace(r):
 | |
| 		l.ignore()
 | |
| 	default:
 | |
| 		return l.errorf("unclosed shortcode")
 | |
| 	}
 | |
| 	return lexEndOfShortcode
 | |
| }
 | |
| 
 | |
| // scans the elements inside shortcode tags
 | |
| func lexInsideShortcode(l *pageLexer) stateFunc {
 | |
| 	if l.hasPrefix(l.currentRightShortcodeDelim()) {
 | |
| 		return lexShortcodeRightDelim
 | |
| 	}
 | |
| 	switch r := l.next(); {
 | |
| 	case r == eof:
 | |
| 		// eol is allowed inside shortcodes; this may go to end of document before it fails
 | |
| 		return l.errorf("unclosed shortcode action")
 | |
| 	case isSpace(r), isEndOfLine(r):
 | |
| 		l.ignore()
 | |
| 	case r == '=':
 | |
| 		l.consumeSpace()
 | |
| 		l.ignore()
 | |
| 		peek := l.peek()
 | |
| 		if peek == '"' || peek == '\\' {
 | |
| 			return lexShortcodeQuotedParamVal(l, peek != '\\', tScParamVal)
 | |
| 		} else if peek == '`' {
 | |
| 			return lexShortCodeParamRawStringVal(l, tScParamVal)
 | |
| 		}
 | |
| 		return lexShortcodeParamVal
 | |
| 	case r == '/':
 | |
| 		if l.currShortcodeName == "" {
 | |
| 			return l.errorf("got closing shortcode, but none is open")
 | |
| 		}
 | |
| 		l.closingState++
 | |
| 		l.isInline = false
 | |
| 		l.emit(tScClose)
 | |
| 	case r == '\\':
 | |
| 		l.ignore()
 | |
| 		if l.peek() == '"' || l.peek() == '`' {
 | |
| 			return lexShortcodeParam(l, true)
 | |
| 		}
 | |
| 	case l.elementStepNum > 0 && (isAlphaNumericOrHyphen(r) || r == '"' || r == '`'): // positional params can have quotes
 | |
| 		l.backup()
 | |
| 		return lexShortcodeParam(l, false)
 | |
| 	case isAlphaNumeric(r):
 | |
| 		l.backup()
 | |
| 		return lexIdentifierInShortcode
 | |
| 	default:
 | |
| 		return l.errorf("unrecognized character in shortcode action: %#U. Note: Parameters with non-alphanumeric args must be quoted", r)
 | |
| 	}
 | |
| 	return lexInsideShortcode
 | |
| }
 | |
| 
 | |
| func (l *pageLexer) currentLeftShortcodeDelimItem() ItemType {
 | |
| 	return l.currLeftDelimItem
 | |
| }
 | |
| 
 | |
| func (l *pageLexer) currentRightShortcodeDelimItem() ItemType {
 | |
| 	return l.currRightDelimItem
 | |
| }
 | |
| 
 | |
| func (l *pageLexer) currentLeftShortcodeDelim() []byte {
 | |
| 	if l.currLeftDelimItem == tLeftDelimScWithMarkup {
 | |
| 		return leftDelimScWithMarkup
 | |
| 	}
 | |
| 	return leftDelimScNoMarkup
 | |
| }
 | |
| 
 | |
| func (l *pageLexer) currentRightShortcodeDelim() []byte {
 | |
| 	if l.currRightDelimItem == tRightDelimScWithMarkup {
 | |
| 		return rightDelimScWithMarkup
 | |
| 	}
 | |
| 	return rightDelimScNoMarkup
 | |
| }
 |