func isMarkdownStartURL(s []byte) bool {
	return bytes.HasPrefix(s, https) || bytes.HasPrefix(s, http)
}
isMarkdownStartURL reports whether s is the start of an URL in markdown
context.
package compiler
import (
	"bytes"
	"strings"
	"unicode"
	"unicode/utf8"
	"github.com/open2b/scriggo/ast"
)
const BOM rune = 0xfeff
const bomErrorMsg = "invalid BOM in the middle of the file"
func scanProgram(text []byte) *lexer {
	tokens := make(chan token, 20)
	lex := &lexer{
		text:   text,
		src:    text,
		line:   1,
		column: 1,
		ctx:    ast.ContextText,
		tokens: tokens,
	}
	go lex.scan()
	return lex
}
func scanScript(text []byte) *lexer {
	tokens := make(chan token, 20)
	lex := &lexer{
		text:           text,
		src:            text,
		line:           1,
		column:         1,
		ctx:            ast.ContextText,
		tokens:         tokens,
		extendedSyntax: true,
		parseShebang:   true,
	}
	go lex.scan()
	return lex
}
func scanTemplate(text []byte, format ast.Format, parseShebang, noParseShow, dollarIdentifier bool) *lexer {
	tokens := make(chan token, 20)
	lex := &lexer{
		text:             text,
		src:              text,
		line:             1,
		column:           1,
		ctx:              ast.Context(format),
		tokens:           tokens,
		templateSyntax:   true,
		extendedSyntax:   true,
		parseShebang:     parseShebang,
		dollarIdentifier: dollarIdentifier,
		noParseShow:      noParseShow,
	}
	lex.tag.ctx = ast.ContextHTML
	if lex.ctx == ast.ContextMarkdown {
		lex.tag.ctx = ast.ContextMarkdown
	}
	go lex.scan()
	return lex
}
func (l *lexer) Tokens() <-chan token {
	return l.tokens
}
func (l *lexer) error() error {
	return l.err
}
func (l *lexer) Stop() {
	for range l.tokens {
	}
}
var cdataStart = []byte("<![CDATA[")
var cdataEnd = []byte("]]>")
var emptyMarker = []byte{}
type lexer struct {
	text     []byte         
	src      []byte         
	line     int            
	column   int            
	ctx      ast.Context    
	contexts []ast.Context  
	tag      struct {       
		name  string       
		attr  string       
		index int          
		ctx   ast.Context  
	}
	rawMarker        []byte      
	tokens           chan token  
	lastTokenType    tokenTyp    
	totals           int         
	err              error       
	templateSyntax   bool        
	extendedSyntax   bool        
	parseShebang     bool        
	dollarIdentifier bool        
	noParseShow      bool        
}
func (l *lexer) newline() {
	l.line++
	l.column = 1
}
func (l *lexer) errorf(format string, a ...interface{}) *SyntaxError {
	pos := ast.Position{
		Line:   l.line,
		Column: l.column,
		Start:  len(l.text) - len(l.src),
		End:    len(l.text) - len(l.src),
	}
	return syntaxError(&pos, format, a...)
}
func (l *lexer) emit(typ tokenTyp, length int) {
	l.emitAtLineColumn(l.line, l.column, typ, length)
}
func (l *lexer) emitAtLineColumn(line, column int, typ tokenTyp, length int) {
	var txt []byte
	if length > 0 {
		txt = l.src[0:length]
	}
	ctx := l.ctx
	if typ == tokenText {
		ctx = ast.ContextText
	}
	l.totals++
	start := len(l.text) - len(l.src)
	end := start + length - 1
	if length == 0 {
		if typ == tokenSemicolon {
			start--
			l.totals--
		}
		end = start
	}
	l.tokens <- token{
		typ: typ,
		pos: &ast.Position{
			Line:   line,
			Column: column,
			Start:  start,
			End:    end,
		},
		txt: txt,
		lin: l.line,
		ctx: ctx,
		tag: l.tag.name,
		att: l.tag.attr,
	}
	if l.templateSyntax {
		switch typ {
		case tokenRaw:
			if l.lastTokenType == tokenStartStatement {
				l.rawMarker = emptyMarker
			}
		case tokenIdentifier:
			if l.lastTokenType == tokenRaw && l.rawMarker != nil {
				l.rawMarker = txt
			}
		case tokenEnd:
			l.rawMarker = nil
		}
	}
	if length > 0 {
		l.lastTokenType = typ
		l.src = l.src[length:]
	}
}
var jsMimeType = []byte("text/javascript")
var jsonLDMimeType = []byte("application/ld+json")
var cssMimeType = []byte("text/css")
var moduleType = []byte("module")
func (l *lexer) scan() {
	if l.parseShebang && len(l.src) > 1 {
		if l.src[0] == '#' && l.src[1] == '!' {
			t := bytes.IndexByte(l.src, '\n')
			if t == -1 {
				t = len(l.src) - 1
			}
			l.emit(tokenShebangLine, t+1)
			l.line++
		}
	}
	if l.templateSyntax {
		p := 0  
		lin := l.line    
		col := l.column  
		var quote = byte(0)
		var emittedURL bool
		fileContext := l.ctx
		spacesOnlyLine := true
		isHTML := l.ctx == ast.ContextHTML || l.ctx == ast.ContextMarkdown
		if l.ctx == ast.ContextMarkdown {
			p, l.ctx = l.scanCodeBlock(0)
		}
	LOOP:
		for p < len(l.src) {
			c := l.src[p]
			if l.ctx == ast.ContextMarkdown {
				spacesOnlyLine = spacesOnlyLine && isSpace(c)
				if c == '\\' {
					p++
					l.column++
					if p < len(l.src) && l.src[p] != '\n' && l.src[p] != 'h' {
						_, s := utf8.DecodeRune(l.src[p:])
						p += s
						l.column++
					}
					continue
				}
			}
			if c == '{' && p+1 < len(l.src) {
				switch l.src[p+1] {
				case '{':
					if l.noParseShow {
						break
					}
					if p > 0 {
						l.emitAtLineColumn(lin, col, tokenText, p)
						p = 0
					}
					err := l.lexShow()
					if err != nil {
						l.err = err
						break LOOP
					}
					lin = l.line
					col = l.column
					continue
				case '%':
					if p > 0 {
						l.emitAtLineColumn(lin, col, tokenText, p)
						p = 0
					}
					var err error
					if len(l.src) > 2 && l.src[2] == '%' {
						err = l.lexStatements()
					} else {
						err = l.lexStatement()
					}
					if err != nil {
						l.err = err
						break LOOP
					}
					lin = l.line
					col = l.column
					if l.rawMarker != nil {
						p = l.skipRawContent()
					}
					continue
				case '#':
					if p > 0 {
						l.emitAtLineColumn(lin, col, tokenText, p)
						p = 0
					}
					err := l.lexComment()
					if err != nil {
						l.err = err
						break LOOP
					}
					lin = l.line
					col = l.column
					continue
				}
			} else if c == '#' && p+1 < len(l.src) && l.src[p+1] == '}' {
				l.err = l.errorf("unexpected #}")
				break LOOP
			}
			switch l.ctx {
			case ast.ContextMarkdown:
				if emittedURL {
					if isMarkdownEndURL(l.src[p:]) {
						if p > 0 {
							l.emitAtLineColumn(lin, col, tokenText, p)
						}
						l.emit(tokenEndURL, 0)
						emittedURL = false
						p = 0
						lin = l.line
						col = l.column
					}
				} else if (p == 0 || !isAlpha(l.src[p-1])) && isMarkdownStartURL(l.src[p:]) {
					if p > 0 {
						l.emitAtLineColumn(lin, col, tokenText, p)
					}
					l.emit(tokenStartURL, 0)
					emittedURL = true
					if l.src[4] == 's' {
						p = 8  
					} else {
						p = 7  
					}
					lin = l.line
					col = l.column + p
					continue
				}
				fallthrough
			case ast.ContextHTML:
				if c == '<' {
					if l.ctx == ast.ContextHTML && p+8 < len(l.src) && l.src[p+1] == '!' {
						if bytes.HasPrefix(l.src[p:], cdataStart) {
							p += 6
							l.column += 6
							var t int
							if i := bytes.Index(l.src[p:], cdataEnd); i < 0 {
								t = len(l.src)
							} else {
								t = p + i + 2
							}
							for ; p < t; p++ {
								if c = l.src[p]; c == '\n' {
									l.newline()
								} else if isStartChar(c) {
									l.column++
								}
							}
							continue
						}
					}
					p++
					l.column++
					l.tag.name, p = l.scanTag(p)
					if l.tag.name != "" {
						l.ctx = ast.ContextTag
						switch l.tag.name {
						case "script":
							l.tag.ctx = ast.ContextJS
						case "style":
							l.tag.ctx = ast.ContextCSS
						}
					}
					continue
				}
			case ast.ContextTag:
				if c == '>' || c == '/' && p < len(l.src) && l.src[p] == '>' {
					l.ctx = l.tag.ctx
					l.tag.name = ""
					l.tag.ctx = fileContext
					if c == '/' {
						p++
						l.column++
					}
				} else if !isASCIISpace(c) {
					var next int
					if l.tag.attr, next = l.scanAttribute(p); next > p {
						p = next
						if l.tag.attr != "" && p < len(l.src) {
							if c := l.src[p]; c == '"' || c == '\'' {
								quote = c
								p++
								l.column++
							}
							if containsURL(l.tag.name, l.tag.attr) {
								l.emitAtLineColumn(lin, col, tokenText, p)
								if quote == 0 {
									l.ctx = ast.ContextUnquotedAttr
								} else {
									l.ctx = ast.ContextQuotedAttr
								}
								l.emit(tokenStartURL, 0)
								emittedURL = true
								p = 0
								lin = l.line
								col = l.column
							} else {
								l.tag.index = p
								if quote == 0 {
									l.ctx = ast.ContextUnquotedAttr
								} else {
									l.ctx = ast.ContextQuotedAttr
								}
							}
						}
						continue
					}
				}
			case ast.ContextQuotedAttr, ast.ContextUnquotedAttr:
				if l.ctx == ast.ContextQuotedAttr && c == quote ||
					l.ctx == ast.ContextUnquotedAttr && (c == '>' || isASCIISpace(c)) {
					quote = 0
					if emittedURL {
						if p > 0 {
							l.emitAtLineColumn(lin, col, tokenText, p)
						}
						l.emit(tokenEndURL, 0)
						emittedURL = false
						p = 0
						lin = l.line
						col = l.column
					} else if l.tag.attr == "type" {
						switch l.tag.name {
						case "script":
							typ := l.src[l.tag.index:p]
							if bytes.Equal(typ, moduleType) {
								break
							}
							typ = bytes.TrimSpace(typ)
							if len(typ) > 0 {
								if bytes.EqualFold(typ, jsonLDMimeType) {
									l.tag.ctx = ast.ContextJSON
								} else if !bytes.EqualFold(typ, jsMimeType) {
									l.tag.ctx = fileContext
								}
							}
						case "style":
							if typ := bytes.TrimSpace(l.src[l.tag.index:p]); len(typ) > 0 {
								if !bytes.EqualFold(typ, cssMimeType) {
									l.tag.ctx = fileContext
								}
							}
						}
					}
					l.ctx = ast.ContextTag
					l.tag.attr = ""
					l.tag.index = 0
					if c == '>' {
						continue
					}
				}
			case ast.ContextCSS:
				if isHTML && c == '<' && isEndStyle(l.src[p:]) {
					l.ctx = fileContext
					p += 7
					l.column += 7
				} else if c == '"' || c == '\'' {
					l.ctx = ast.ContextCSSString
					quote = c
				}
			case ast.ContextCSSString:
				switch c {
				case '\\':
					if p+1 < len(l.src) && l.src[p+1] == quote {
						p++
						l.column++
					}
				case quote:
					l.ctx = ast.ContextCSS
					quote = 0
				case '<':
					if isHTML && isEndStyle(l.src[p:]) {
						l.ctx = fileContext
						quote = 0
						p += 7
						l.column += 7
					}
				}
			case ast.ContextJS:
				if isHTML && c == '<' && isEndScript(l.src[p:]) {
					l.ctx = fileContext
					p += 8
					l.column += 8
				} else if c == '"' || c == '\'' {
					l.ctx = ast.ContextJSString
					quote = c
				}
			case ast.ContextJSString:
				switch c {
				case '\\':
					if p+1 < len(l.src) && l.src[p+1] == quote {
						p++
						l.column++
					}
				case quote:
					l.ctx = ast.ContextJS
					quote = 0
				case '<':
					if isHTML && isEndScript(l.src[p:]) {
						l.ctx = fileContext
						quote = 0
						p += 8
						l.column += 8
					}
				}
			case ast.ContextJSON:
				if isHTML && c == '<' && isEndScript(l.src[p:]) {
					l.ctx = fileContext
					p += 8
					l.column += 8
				} else if c == '"' {
					l.ctx = ast.ContextJSONString
					quote = '"'
				}
			case ast.ContextJSONString:
				switch c {
				case '\\':
					if p+1 < len(l.src) && l.src[p+1] == '"' {
						p++
						l.column++
					}
				case '"':
					l.ctx = ast.ContextJSON
					quote = 0
				case '<':
					if isHTML && isEndScript(l.src[p:]) {
						l.ctx = fileContext
						quote = 0
						p += 8
						l.column += 8
					}
				}
			}
			p++
			if c == '\n' {
				l.newline()
				if p < len(l.src) && l.src[p] == '\r' {
					p++
				}
				switch l.ctx {
				case ast.ContextTabCodeBlock, ast.ContextSpacesCodeBlock:
					p, l.ctx = l.scanCodeBlock(p)
				case ast.ContextMarkdown:
					if spacesOnlyLine {
						p, l.ctx = l.scanCodeBlock(p)
					} else {
						spacesOnlyLine = true
					}
				}
				continue
			}
			if isStartChar(c) {
				l.column++
			}
		}
		if l.err == nil {
			if len(l.src) > 0 {
				l.emitAtLineColumn(lin, col, tokenText, p)
			}
			if l.ctx == ast.ContextMarkdown && emittedURL {
				l.emit(tokenEndURL, 0)
			}
		}
	} else {
		err := l.lexCode(tokenEOF)
		if err != nil {
			l.err = err
		}
	}
	if l.err == nil {
		l.emit(tokenEOF, 0)
	}
	l.text = nil
	l.src = nil
	close(l.tokens)
}
func (l *lexer) scanCodeBlock(p int) (int, ast.Context) {
	if p < len(l.src) {
		switch l.src[p] {
		case '\t':
			return p + 1, ast.ContextTabCodeBlock
		case ' ':
			if p+3 < len(l.src) && l.src[p+1] == ' ' && l.src[p+2] == ' ' && l.src[p+3] == ' ' {
				return p + 4, ast.ContextSpacesCodeBlock
			}
		}
	}
	return p, ast.ContextMarkdown
}
func containsURL(tag string, attr string) bool {
	if p := strings.IndexByte(attr, ':'); p != -1 {
		if attr[:p] == "xmlns" {
			return true
		}
		attr = attr[p+1:]
	} else if strings.HasPrefix(attr, "data-") {
		attr = attr[5:]
		if strings.Contains(attr, "src") ||
			strings.Contains(attr, "url") ||
			strings.Contains(attr, "uri") {
			return true
		}
	}
	switch attr {
	case "action":
		return tag == "form"
	case "cite":
		switch tag {
		case "blockquote", "del", "ins", "q":
			return true
		}
	case "data":
		return tag == "object"
	case "formaction":
		return tag == "button" || tag == "input"
	case "href":
		switch tag {
		case "a", "area", "link", "base":
			return true
		}
	case "longdesc":
		return tag == "img"
	case "manifest":
		return tag == "html"
	case "poster":
		return tag == "video"
	case "src":
		switch tag {
		case "audio", "embed", "iframe", "img", "input", "script", "source", "track", "video":
			return true
		}
	case "srcset":
		return tag == "img" || tag == "source"
	case "xmlns":
		return true
	}
	return false
}
func (l *lexer) scanTag(p int) (string, int) {
	if p == len(l.src) || !isAlpha(l.src[p]) {
		return "", p
	}
	l.column++
	s := p
	p++
	for p < len(l.src) {
		c := l.src[p]
		if c == '>' || c == '/' || isASCIISpace(c) || c == '{' {
			break
		}
		l.column++
		if c < utf8.RuneSelf {
			p++
			continue
		}
		_, size := utf8.DecodeRune(l.src[p:])
		p += size
	}
	return string(bytes.ToLower(l.src[s:p])), p
}
func (l *lexer) scanAttribute(p int) (string, int) {
	s := p
	for ; p < len(l.src); p++ {
		c := l.src[p]
		if c == '=' || isASCIISpace(c) {
			break
		}
		const DEL = 0x7F
		if c <= 0x1F || c == '"' || c == '\'' || c == '>' || c == '/' || c == DEL {
			return "", p
		}
		if c >= 0x80 {
			r, size := utf8.DecodeRune(l.src[p:])
			if r == utf8.RuneError && size == 1 {
				return "", p
			}
			if 0x7F <= r && r <= 0x9F || unicode.Is(unicode.Noncharacter_Code_Point, r) {
				return "", p
			}
			p += size - 1
		}
		if c == '{' && p+1 < len(l.src) {
			switch l.src[p+1] {
			case '{', '%', '#':
				return "", p
			}
		}
		l.column++
	}
	if p == s || p == len(l.src) {
		return "", p
	}
	name := string(bytes.ToLower(l.src[s:p]))
	for ; p < len(l.src); p++ {
		if c := l.src[p]; c == '=' {
			p++
			l.column++
			break
		} else if isASCIISpace(c) {
			if c == '\n' {
				l.newline()
			} else {
				l.column++
			}
		} else {
			return "", p
		}
	}
	for ; p < len(l.src); p++ {
		if c := l.src[p]; c == '>' {
			return "", p
		} else if isASCIISpace(c) {
			if c == '\n' {
				l.newline()
			} else {
				l.column++
			}
		} else {
			break
		}
	}
	if p == len(l.src) {
		return "", p
	}
	return name, p
}
var http = []byte("http://")
var https = []byte("https://")