parser.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. package css
  2. import (
  3. "log"
  4. "github.com/gorilla/css/scanner"
  5. )
  6. /*
  7. stylesheet : [ CDO | CDC | S | statement ]*;
  8. statement : ruleset | at-rule;
  9. at-rule : ATKEYWORD S* any* [ block | ';' S* ];
  10. block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
  11. ruleset : selector? '{' S* declaration? [ ';' S* declaration? ]* '}' S*;
  12. selector : any+;
  13. declaration : property S* ':' S* value;
  14. property : IDENT;
  15. value : [ any | block | ATKEYWORD S* ]+;
  16. any : [ IDENT | NUMBER | PERCENTAGE | DIMENSION | STRING
  17. | DELIM | URI | HASH | UNICODE-RANGE | INCLUDES
  18. | DASHMATCH | ':' | FUNCTION S* [any|unused]* ')'
  19. | '(' S* [any|unused]* ')' | '[' S* [any|unused]* ']'
  20. ] S*;
  21. unused : block | ATKEYWORD S* | ';' S* | CDO S* | CDC S*;
  22. */
  23. type State int
  24. const (
  25. STATE_NONE State = iota
  26. STATE_SELECTOR
  27. STATE_PROPERTY
  28. STATE_VALUE
  29. )
  30. type parserContext struct {
  31. State State
  32. NowSelector []*scanner.Token
  33. NowRuleType RuleType
  34. CurrentNestedRule *CSSRule
  35. }
  36. func (context *parserContext) resetContextStyleRule() {
  37. context.NowSelector = make([]*scanner.Token, 0)
  38. context.NowRuleType = STYLE_RULE
  39. context.State = STATE_NONE
  40. }
  41. func parseRule(context *parserContext, s *scanner.Scanner, css *CSSStyleSheet) {
  42. rule := NewRule(context.NowRuleType)
  43. selector := append(context.NowSelector, parseSelector(s)...)
  44. rule.Style.Selector = &CSSValue{Tokens: selector}
  45. rule.Style.Styles = parseBlock(s)
  46. if context.CurrentNestedRule != nil {
  47. context.CurrentNestedRule.Rules = append(context.CurrentNestedRule.Rules, rule)
  48. } else {
  49. css.CssRuleList = append(css.CssRuleList, rule)
  50. }
  51. context.resetContextStyleRule()
  52. }
  53. // Parse takes a string of valid css rules, stylesheet,
  54. // and parses it. Be aware this function has poor error handling
  55. // so you should have valid syntax in your css
  56. func Parse(csstext string) *CSSStyleSheet {
  57. context := &parserContext{
  58. State: STATE_NONE,
  59. NowSelector: make([]*scanner.Token, 0),
  60. NowRuleType: STYLE_RULE,
  61. CurrentNestedRule: nil,
  62. }
  63. css := &CSSStyleSheet{}
  64. css.CssRuleList = make([]*CSSRule, 0)
  65. s := scanner.New(csstext)
  66. for {
  67. token := s.Next()
  68. if token.Type == scanner.TokenEOF || token.Type == scanner.TokenError {
  69. break
  70. }
  71. switch token.Type {
  72. case scanner.TokenCDO:
  73. break
  74. case scanner.TokenCDC:
  75. break
  76. case scanner.TokenComment:
  77. break
  78. case scanner.TokenS:
  79. break
  80. case scanner.TokenAtKeyword:
  81. switch token.Value {
  82. case "@media":
  83. context.NowRuleType = MEDIA_RULE
  84. case "@font-face":
  85. // Parse as normal rule, would be nice to parse according to syntax
  86. // https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face
  87. context.NowRuleType = FONT_FACE_RULE
  88. parseRule(context, s, css)
  89. case "@import":
  90. // No validation
  91. // https://developer.mozilla.org/en-US/docs/Web/CSS/@import
  92. rule := parseAtNoBody(s, IMPORT_RULE)
  93. if rule != nil {
  94. css.CssRuleList = append(css.CssRuleList, rule)
  95. }
  96. context.resetContextStyleRule()
  97. case "@charset":
  98. // No validation
  99. // https://developer.mozilla.org/en-US/docs/Web/CSS/@charset
  100. rule := parseAtNoBody(s, CHARSET_RULE)
  101. if rule != nil {
  102. css.CssRuleList = append(css.CssRuleList, rule)
  103. }
  104. context.resetContextStyleRule()
  105. case "@page":
  106. context.NowRuleType = PAGE_RULE
  107. parseRule(context, s, css)
  108. case "@keyframes":
  109. context.NowRuleType = KEYFRAMES_RULE
  110. case "@-webkit-keyframes":
  111. context.NowRuleType = WEBKIT_KEYFRAMES_RULE
  112. case "@counter-style":
  113. context.NowRuleType = COUNTER_STYLE_RULE
  114. parseRule(context, s, css)
  115. default:
  116. log.Printf("Skip unsupported atrule: %s", token.Value)
  117. skipRules(s)
  118. context.resetContextStyleRule()
  119. }
  120. default:
  121. if context.State == STATE_NONE {
  122. if token.Value == "}" && context.CurrentNestedRule != nil {
  123. // close media/keyframe/… rule
  124. css.CssRuleList = append(css.CssRuleList, context.CurrentNestedRule)
  125. context.CurrentNestedRule = nil
  126. break
  127. }
  128. }
  129. if context.NowRuleType == MEDIA_RULE || context.NowRuleType == KEYFRAMES_RULE || context.NowRuleType == WEBKIT_KEYFRAMES_RULE {
  130. context.CurrentNestedRule = NewRule(context.NowRuleType)
  131. sel := append([]*scanner.Token{token}, parseSelector(s)...)
  132. context.CurrentNestedRule.Style.Selector = &CSSValue{Tokens: sel}
  133. context.resetContextStyleRule()
  134. break
  135. } else {
  136. context.NowSelector = append(context.NowSelector, token)
  137. parseRule(context, s, css)
  138. break
  139. }
  140. }
  141. }
  142. return css
  143. }