|
- package parser
-
- import (
- "errors"
- "fmt"
- "regexp"
- "strings"
-
- "github.com/gorilla/css/scanner"
-
- "github.com/aymerick/douceur/css"
- )
-
- const (
- importantSuffixRegexp = `(?i)\s*!important\s*$`
- )
-
- var (
- importantRegexp *regexp.Regexp
- )
-
- // Parser represents a CSS parser
- type Parser struct {
- scan *scanner.Scanner // Tokenizer
-
- // Tokens parsed but not consumed yet
- tokens []*scanner.Token
-
- // Rule embedding level
- embedLevel int
- }
-
- func init() {
- importantRegexp = regexp.MustCompile(importantSuffixRegexp)
- }
-
- // NewParser instanciates a new parser
- func NewParser(txt string) *Parser {
- return &Parser{
- scan: scanner.New(txt),
- }
- }
-
- // Parse parses a whole stylesheet
- func Parse(text string) (*css.Stylesheet, error) {
- result, err := NewParser(text).ParseStylesheet()
- if err != nil {
- return nil, err
- }
-
- return result, nil
- }
-
- // ParseDeclarations parses CSS declarations
- func ParseDeclarations(text string) ([]*css.Declaration, error) {
- result, err := NewParser(text).ParseDeclarations()
- if err != nil {
- return nil, err
- }
-
- return result, nil
- }
-
- // ParseStylesheet parses a stylesheet
- func (parser *Parser) ParseStylesheet() (*css.Stylesheet, error) {
- result := css.NewStylesheet()
-
- // Parse BOM
- if _, err := parser.parseBOM(); err != nil {
- return result, err
- }
-
- // Parse list of rules
- rules, err := parser.ParseRules()
- if err != nil {
- return result, err
- }
-
- result.Rules = rules
-
- return result, nil
- }
-
- // ParseRules parses a list of rules
- func (parser *Parser) ParseRules() ([]*css.Rule, error) {
- result := []*css.Rule{}
-
- inBlock := false
- if parser.tokenChar("{") {
- // parsing a block of rules
- inBlock = true
- parser.embedLevel++
-
- parser.shiftToken()
- }
-
- for parser.tokenParsable() {
- if parser.tokenIgnorable() {
- parser.shiftToken()
- } else if parser.tokenChar("}") {
- if !inBlock {
- errMsg := fmt.Sprintf("Unexpected } character: %s", parser.nextToken().String())
- return result, errors.New(errMsg)
- }
-
- parser.shiftToken()
- parser.embedLevel--
-
- // finished
- break
- } else {
- rule, err := parser.ParseRule()
- if err != nil {
- return result, err
- }
-
- rule.EmbedLevel = parser.embedLevel
- result = append(result, rule)
- }
- }
-
- return result, parser.err()
- }
-
- // ParseRule parses a rule
- func (parser *Parser) ParseRule() (*css.Rule, error) {
- if parser.tokenAtKeyword() {
- return parser.parseAtRule()
- }
-
- return parser.parseQualifiedRule()
- }
-
- // ParseDeclarations parses a list of declarations
- func (parser *Parser) ParseDeclarations() ([]*css.Declaration, error) {
- result := []*css.Declaration{}
-
- if parser.tokenChar("{") {
- parser.shiftToken()
- }
-
- for parser.tokenParsable() {
- if parser.tokenIgnorable() {
- parser.shiftToken()
- } else if parser.tokenChar("}") {
- // end of block
- parser.shiftToken()
- break
- } else {
- declaration, err := parser.ParseDeclaration()
- if err != nil {
- return result, err
- }
-
- result = append(result, declaration)
- }
- }
-
- return result, parser.err()
- }
-
- // ParseDeclaration parses a declaration
- func (parser *Parser) ParseDeclaration() (*css.Declaration, error) {
- result := css.NewDeclaration()
- curValue := ""
-
- for parser.tokenParsable() {
- if parser.tokenChar(":") {
- result.Property = strings.TrimSpace(curValue)
- curValue = ""
-
- parser.shiftToken()
- } else if parser.tokenChar(";") || parser.tokenChar("}") {
- if result.Property == "" {
- errMsg := fmt.Sprintf("Unexpected ; character: %s", parser.nextToken().String())
- return result, errors.New(errMsg)
- }
-
- if importantRegexp.MatchString(curValue) {
- result.Important = true
- curValue = importantRegexp.ReplaceAllString(curValue, "")
- }
-
- result.Value = strings.TrimSpace(curValue)
-
- if parser.tokenChar(";") {
- parser.shiftToken()
- }
-
- // finished
- break
- } else {
- token := parser.shiftToken()
- curValue += token.Value
- }
- }
-
- // log.Printf("[parsed] Declaration: %s", result.String())
-
- return result, parser.err()
- }
-
- // Parse an At Rule
- func (parser *Parser) parseAtRule() (*css.Rule, error) {
- // parse rule name (eg: "@import")
- token := parser.shiftToken()
-
- result := css.NewRule(css.AtRule)
- result.Name = token.Value
-
- for parser.tokenParsable() {
- if parser.tokenChar(";") {
- parser.shiftToken()
-
- // finished
- break
- } else if parser.tokenChar("{") {
- if result.EmbedsRules() {
- // parse rules block
- rules, err := parser.ParseRules()
- if err != nil {
- return result, err
- }
-
- result.Rules = rules
- } else {
- // parse declarations block
- declarations, err := parser.ParseDeclarations()
- if err != nil {
- return result, err
- }
-
- result.Declarations = declarations
- }
-
- // finished
- break
- } else {
- // parse prelude
- prelude, err := parser.parsePrelude()
- if err != nil {
- return result, err
- }
-
- result.Prelude = prelude
- }
- }
-
- // log.Printf("[parsed] Rule: %s", result.String())
-
- return result, parser.err()
- }
-
- // Parse a Qualified Rule
- func (parser *Parser) parseQualifiedRule() (*css.Rule, error) {
- result := css.NewRule(css.QualifiedRule)
-
- for parser.tokenParsable() {
- if parser.tokenChar("{") {
- if result.Prelude == "" {
- errMsg := fmt.Sprintf("Unexpected { character: %s", parser.nextToken().String())
- return result, errors.New(errMsg)
- }
-
- // parse declarations block
- declarations, err := parser.ParseDeclarations()
- if err != nil {
- return result, err
- }
-
- result.Declarations = declarations
-
- // finished
- break
- } else {
- // parse prelude
- prelude, err := parser.parsePrelude()
- if err != nil {
- return result, err
- }
-
- result.Prelude = prelude
- }
- }
-
- result.Selectors = strings.Split(result.Prelude, ",")
- for i, sel := range result.Selectors {
- result.Selectors[i] = strings.TrimSpace(sel)
- }
-
- // log.Printf("[parsed] Rule: %s", result.String())
-
- return result, parser.err()
- }
-
- // Parse Rule prelude
- func (parser *Parser) parsePrelude() (string, error) {
- result := ""
-
- for parser.tokenParsable() && !parser.tokenEndOfPrelude() {
- token := parser.shiftToken()
- result += token.Value
- }
-
- result = strings.TrimSpace(result)
-
- // log.Printf("[parsed] prelude: %s", result)
-
- return result, parser.err()
- }
-
- // Parse BOM
- func (parser *Parser) parseBOM() (bool, error) {
- if parser.nextToken().Type == scanner.TokenBOM {
- parser.shiftToken()
- return true, nil
- }
-
- return false, parser.err()
- }
-
- // Returns next token without removing it from tokens buffer
- func (parser *Parser) nextToken() *scanner.Token {
- if len(parser.tokens) == 0 {
- // fetch next token
- nextToken := parser.scan.Next()
-
- // log.Printf("[token] %s => %v", nextToken.Type.String(), nextToken.Value)
-
- // queue it
- parser.tokens = append(parser.tokens, nextToken)
- }
-
- return parser.tokens[0]
- }
-
- // Returns next token and remove it from the tokens buffer
- func (parser *Parser) shiftToken() *scanner.Token {
- var result *scanner.Token
-
- result, parser.tokens = parser.tokens[0], parser.tokens[1:]
- return result
- }
-
- // Returns tokenizer error, or nil if no error
- func (parser *Parser) err() error {
- if parser.tokenError() {
- token := parser.nextToken()
- return fmt.Errorf("Tokenizer error: %s", token.String())
- }
-
- return nil
- }
-
- // Returns true if next token is Error
- func (parser *Parser) tokenError() bool {
- return parser.nextToken().Type == scanner.TokenError
- }
-
- // Returns true if next token is EOF
- func (parser *Parser) tokenEOF() bool {
- return parser.nextToken().Type == scanner.TokenEOF
- }
-
- // Returns true if next token is a whitespace
- func (parser *Parser) tokenWS() bool {
- return parser.nextToken().Type == scanner.TokenS
- }
-
- // Returns true if next token is a comment
- func (parser *Parser) tokenComment() bool {
- return parser.nextToken().Type == scanner.TokenComment
- }
-
- // Returns true if next token is a CDO or a CDC
- func (parser *Parser) tokenCDOorCDC() bool {
- switch parser.nextToken().Type {
- case scanner.TokenCDO, scanner.TokenCDC:
- return true
- default:
- return false
- }
- }
-
- // Returns true if next token is ignorable
- func (parser *Parser) tokenIgnorable() bool {
- return parser.tokenWS() || parser.tokenComment() || parser.tokenCDOorCDC()
- }
-
- // Returns true if next token is parsable
- func (parser *Parser) tokenParsable() bool {
- return !parser.tokenEOF() && !parser.tokenError()
- }
-
- // Returns true if next token is an At Rule keyword
- func (parser *Parser) tokenAtKeyword() bool {
- return parser.nextToken().Type == scanner.TokenAtKeyword
- }
-
- // Returns true if next token is given character
- func (parser *Parser) tokenChar(value string) bool {
- token := parser.nextToken()
- return (token.Type == scanner.TokenChar) && (token.Value == value)
- }
-
- // Returns true if next token marks the end of a prelude
- func (parser *Parser) tokenEndOfPrelude() bool {
- return parser.tokenChar(";") || parser.tokenChar("{")
- }
|