2009-12-20 05:57:35 -08:00
|
|
|
require 'sass/scss/rx'
|
|
|
|
require 'sass/css'
|
|
|
|
|
2009-12-20 01:13:38 -08:00
|
|
|
require 'strscan'
|
|
|
|
|
|
|
|
module Sass
|
|
|
|
module SCSS
|
|
|
|
class Parser
|
|
|
|
def initialize(str)
|
|
|
|
@scanner = StringScanner.new(str)
|
|
|
|
end
|
|
|
|
|
|
|
|
def parse
|
2009-12-20 05:57:35 -08:00
|
|
|
root = stylesheet
|
|
|
|
raise "Invalid CSS at #{@scanner.rest.inspect}" unless @scanner.eos?
|
|
|
|
root
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def stylesheet
|
2009-12-20 05:57:35 -08:00
|
|
|
root = Sass::Tree::RootNode.new(@scanner.string)
|
|
|
|
|
2009-12-20 05:45:58 -08:00
|
|
|
if tok :charset
|
|
|
|
ss
|
2009-12-20 05:57:35 -08:00
|
|
|
root << Sass::Tree::DirectiveNode.new("@charset #{tok!(:string).strip}")
|
|
|
|
ss
|
2009-12-20 05:45:58 -08:00
|
|
|
raw! ';'
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
s
|
|
|
|
|
2009-12-20 05:57:35 -08:00
|
|
|
while child = import || namespace || ruleset || media ||
|
|
|
|
page || font_face
|
|
|
|
root << child
|
|
|
|
s
|
|
|
|
end
|
|
|
|
root
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def s
|
2009-12-20 05:56:28 -08:00
|
|
|
nil while tok(:s) || tok(:cdc) || tok(:cdo) || tok(:comment)
|
2009-12-20 05:45:58 -08:00
|
|
|
true
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def ss
|
2009-12-20 05:56:28 -08:00
|
|
|
nil while tok(:s) || tok(:comment)
|
2009-12-20 05:45:58 -08:00
|
|
|
true
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def import
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok(:import)
|
|
|
|
ss
|
2009-12-20 05:57:35 -08:00
|
|
|
val = str do
|
2009-12-20 15:03:18 -08:00
|
|
|
@expected = "string or url()"
|
2009-12-20 05:57:35 -08:00
|
|
|
tok(:string) || tok!(:uri); ss
|
|
|
|
if medium
|
|
|
|
while raw ','
|
|
|
|
ss; expr! :medium
|
|
|
|
end
|
2009-12-20 05:45:58 -08:00
|
|
|
end
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
2009-12-20 05:45:58 -08:00
|
|
|
raw! ';'; ss
|
2009-12-20 05:57:35 -08:00
|
|
|
Sass::Tree::DirectiveNode.new("@import #{val.strip}")
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def namespace
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok(:namespace)
|
|
|
|
ss
|
2009-12-20 05:57:35 -08:00
|
|
|
val = str do
|
|
|
|
ss if namespace_prefix
|
2009-12-20 15:03:18 -08:00
|
|
|
@expected = "string or url()"
|
2009-12-20 05:57:35 -08:00
|
|
|
tok(:string) || tok!(:uri); ss
|
|
|
|
end
|
2009-12-20 05:45:58 -08:00
|
|
|
raw! ';'; ss
|
2009-12-20 05:57:35 -08:00
|
|
|
Sass::Tree::DirectiveNode.new("@namespace #{val.strip}")
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def namespace_prefix
|
|
|
|
tok :ident
|
|
|
|
end
|
|
|
|
|
|
|
|
def media
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok :media
|
|
|
|
ss
|
2009-12-20 05:57:35 -08:00
|
|
|
val = str do
|
|
|
|
expr! :medium
|
|
|
|
while raw ','
|
|
|
|
ss; expr! :medium
|
|
|
|
end
|
2009-12-20 05:45:58 -08:00
|
|
|
end
|
2009-12-20 05:57:35 -08:00
|
|
|
node = Sass::Tree::DirectiveNode.new("@media #{val.strip}")
|
2009-12-20 05:45:58 -08:00
|
|
|
|
|
|
|
raw! '{'; ss
|
2009-12-20 05:57:35 -08:00
|
|
|
while child = ruleset
|
|
|
|
node << child
|
|
|
|
end
|
2009-12-20 05:45:58 -08:00
|
|
|
raw! '}'; ss
|
2009-12-20 05:57:35 -08:00
|
|
|
node
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def medium
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok :ident
|
|
|
|
ss
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def page
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok :page
|
|
|
|
ss
|
2009-12-20 05:57:35 -08:00
|
|
|
val = str do
|
|
|
|
tok :ident
|
|
|
|
pseudo_page; ss
|
2009-12-20 05:45:58 -08:00
|
|
|
end
|
2009-12-20 05:57:35 -08:00
|
|
|
declarations(Sass::Tree::DirectiveNode.new("@page #{val.strip}"))
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def pseudo_page
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless raw ':'
|
|
|
|
tok! :ident
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def font_face
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok :font_face
|
|
|
|
ss
|
2009-12-20 05:57:35 -08:00
|
|
|
declarations(Sass::Tree::DirectiveNode.new("@font-face"))
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def operator
|
2009-12-21 01:22:45 -08:00
|
|
|
# Many of these operators (all except / and ,)
|
|
|
|
# are disallowed by the CSS spec,
|
|
|
|
# but they're included here for compatibility
|
|
|
|
# with some proprietary MS properties
|
|
|
|
ss if raw('/') || raw(',') || raw(':') || raw('.') || raw('=')
|
2009-12-20 05:45:58 -08:00
|
|
|
true
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def unary_operator
|
2009-12-20 05:45:58 -08:00
|
|
|
raw('-') || raw('+')
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def property
|
2009-12-20 05:57:35 -08:00
|
|
|
return unless name = tok(:ident)
|
2009-12-20 05:45:58 -08:00
|
|
|
ss
|
2009-12-20 05:57:35 -08:00
|
|
|
name
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def ruleset
|
2009-12-20 05:57:35 -08:00
|
|
|
rules = str do
|
|
|
|
return unless selector
|
|
|
|
while raw ','
|
|
|
|
ss; expr!(:selector)
|
|
|
|
end
|
2009-12-20 05:45:58 -08:00
|
|
|
end
|
2009-12-20 05:57:35 -08:00
|
|
|
declarations(Sass::Tree::RuleNode.new(rules.strip))
|
|
|
|
end
|
|
|
|
|
|
|
|
def declarations(node)
|
2009-12-20 05:45:58 -08:00
|
|
|
raw! '{'; ss
|
2009-12-20 05:57:35 -08:00
|
|
|
node << declaration
|
2009-12-20 05:45:58 -08:00
|
|
|
while raw ';'
|
2009-12-20 05:57:35 -08:00
|
|
|
ss
|
|
|
|
node << declaration
|
2009-12-20 05:45:58 -08:00
|
|
|
end
|
|
|
|
raw! '}'; ss
|
2009-12-20 05:57:35 -08:00
|
|
|
node
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def selector
|
2009-12-21 02:15:10 -08:00
|
|
|
# The combinator here allows the "> E" hack
|
|
|
|
return unless combinator || simple_selector_sequence
|
2009-12-20 03:57:33 -08:00
|
|
|
simple_selector_sequence while combinator
|
|
|
|
true
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
2009-12-20 03:57:33 -08:00
|
|
|
def combinator
|
|
|
|
tok(:plus) || tok(:greater) || tok(:tilde) || tok(:s)
|
|
|
|
end
|
|
|
|
|
|
|
|
def simple_selector_sequence
|
2009-12-20 04:20:10 -08:00
|
|
|
unless element_name || tok(:hash) || class_expr ||
|
|
|
|
attrib || negation || pseudo
|
|
|
|
# This allows for stuff like http://www.w3.org/TR/css3-animations/#keyframes-
|
|
|
|
return expr
|
|
|
|
end
|
|
|
|
|
2009-12-21 02:15:10 -08:00
|
|
|
# The raw('*') allows the "E*" hack
|
|
|
|
nil while tok(:hash) || class_expr || attrib ||
|
|
|
|
negation || pseudo || raw('*')
|
2009-12-20 03:57:33 -08:00
|
|
|
true
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def class_expr
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless raw '.'
|
|
|
|
tok! :ident
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def element_name
|
2009-12-20 03:57:33 -08:00
|
|
|
res = tok(:ident) || raw('*')
|
2009-12-20 15:03:18 -08:00
|
|
|
if raw '|'
|
|
|
|
@expected = "element name or *"
|
|
|
|
res = tok(:ident) || raw!('*')
|
|
|
|
end
|
2009-12-20 03:57:33 -08:00
|
|
|
res
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def attrib
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless raw('[')
|
|
|
|
ss
|
2009-12-20 03:57:33 -08:00
|
|
|
attrib_name!; ss
|
2009-12-20 05:45:58 -08:00
|
|
|
if raw('=') ||
|
|
|
|
tok(:includes) ||
|
|
|
|
tok(:dashmatch) ||
|
|
|
|
tok(:prefixmatch) ||
|
|
|
|
tok(:suffixmatch) ||
|
2009-12-20 01:13:38 -08:00
|
|
|
tok(:substringmatch)
|
|
|
|
ss
|
2009-12-20 15:03:18 -08:00
|
|
|
@expected = "identifier or string"
|
2009-12-20 05:45:58 -08:00
|
|
|
tok(:ident) || tok!(:string); ss
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
2009-12-20 05:45:58 -08:00
|
|
|
raw! ']'
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
2009-12-20 03:57:33 -08:00
|
|
|
def attrib_name!
|
|
|
|
if tok(:ident)
|
|
|
|
# E or E|E
|
|
|
|
tok! :ident if raw('|')
|
|
|
|
elsif raw('*')
|
|
|
|
# *|E
|
|
|
|
raw! '|'
|
|
|
|
tok! :ident
|
|
|
|
else
|
|
|
|
# |E or E
|
|
|
|
raw '|'
|
|
|
|
tok! :ident
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-12-20 01:13:38 -08:00
|
|
|
def pseudo
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless raw ':'
|
2009-12-20 03:57:33 -08:00
|
|
|
raw ':'
|
|
|
|
|
2009-12-20 15:03:18 -08:00
|
|
|
@expected = "pseudoclass or pseudoelement"
|
2009-12-20 03:57:33 -08:00
|
|
|
functional_pseudo || tok!(:ident)
|
|
|
|
end
|
|
|
|
|
|
|
|
def functional_pseudo
|
|
|
|
return unless tok :function
|
|
|
|
ss
|
|
|
|
expr! :pseudo_expr
|
2009-12-20 05:45:58 -08:00
|
|
|
raw! ')'
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
2009-12-20 03:57:33 -08:00
|
|
|
def pseudo_expr
|
|
|
|
return unless tok(:plus) || raw('-') || tok(:number) ||
|
|
|
|
tok(:string) || tok(:ident)
|
|
|
|
ss
|
|
|
|
ss while tok(:plus) || raw('-') || tok(:number) ||
|
|
|
|
tok(:string) || tok(:ident)
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
|
|
|
def negation
|
|
|
|
return unless tok(:not)
|
|
|
|
ss
|
2009-12-20 15:03:18 -08:00
|
|
|
@expected = "selector"
|
2009-12-20 03:57:33 -08:00
|
|
|
element_name || tok(:hash) || class_expr || attrib || expr!(:pseudo)
|
|
|
|
end
|
|
|
|
|
2009-12-20 01:13:38 -08:00
|
|
|
def declaration
|
2009-12-21 02:15:10 -08:00
|
|
|
# The raw('*') allows the "*prop: val" hack
|
|
|
|
if raw '*'
|
|
|
|
name = expr!(:property)
|
|
|
|
else
|
|
|
|
return unless name = property
|
|
|
|
end
|
|
|
|
|
2009-12-20 05:45:58 -08:00
|
|
|
raw! ':'; ss
|
2009-12-20 05:57:35 -08:00
|
|
|
value = str do
|
|
|
|
expr! :expr
|
|
|
|
prio
|
|
|
|
end
|
|
|
|
Sass::Tree::PropNode.new(name, value.strip, :new)
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def prio
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok :important
|
|
|
|
ss
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def expr
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless term
|
|
|
|
nil while operator && term
|
|
|
|
true
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def term
|
2009-12-21 01:34:46 -08:00
|
|
|
unless tok(:number) ||
|
|
|
|
tok(:uri) ||
|
|
|
|
function ||
|
|
|
|
tok(:string) ||
|
|
|
|
tok(:ident) ||
|
|
|
|
tok(:unicoderange) ||
|
|
|
|
hexcolor
|
|
|
|
return unless unary_operator
|
2009-12-20 15:03:18 -08:00
|
|
|
@expected = "number or function"
|
2009-12-20 05:45:58 -08:00
|
|
|
tok(:number) || expr!(:function)
|
|
|
|
end
|
|
|
|
ss
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def function
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok :function
|
|
|
|
ss
|
|
|
|
expr! :expr
|
|
|
|
raw! ')'; ss
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
# There is a constraint on the color that it must
|
|
|
|
# have either 3 or 6 hex-digits (i.e., [0-9a-fA-F])
|
|
|
|
# after the "#"; e.g., "#000" is OK, but "#abcd" is not.
|
|
|
|
def hexcolor
|
2009-12-20 05:45:58 -08:00
|
|
|
return unless tok :hash
|
|
|
|
ss
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
2009-12-20 05:57:35 -08:00
|
|
|
def str
|
|
|
|
@str = ""
|
|
|
|
yield
|
|
|
|
@str
|
|
|
|
ensure
|
|
|
|
@str = nil
|
|
|
|
end
|
|
|
|
|
2009-12-20 15:03:18 -08:00
|
|
|
EXPR_NAMES = {
|
|
|
|
:medium => "medium (e.g. print, screen)",
|
|
|
|
:pseudo_expr => "expression (e.g. fr, 2n+1)",
|
|
|
|
:expr => "expression (e.g. 1px, bold)",
|
|
|
|
}
|
|
|
|
|
|
|
|
TOK_NAMES = {
|
|
|
|
:ident => "identifier",
|
|
|
|
}
|
|
|
|
|
2009-12-20 05:45:58 -08:00
|
|
|
def expr!(name)
|
2009-12-20 05:57:35 -08:00
|
|
|
(e = send(name)) && (return e)
|
2009-12-20 15:09:10 -08:00
|
|
|
expected(EXPR_NAMES[name] || name.to_s)
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
2009-12-20 05:45:58 -08:00
|
|
|
def tok!(name)
|
2009-12-20 05:57:35 -08:00
|
|
|
(t = tok(name)) && (return t)
|
2009-12-20 15:09:10 -08:00
|
|
|
expected(TOK_NAMES[name] || name.to_s)
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
2009-12-20 05:45:58 -08:00
|
|
|
def raw!(chr)
|
|
|
|
return true if raw(chr)
|
2009-12-20 15:09:10 -08:00
|
|
|
expected(chr.inspect)
|
|
|
|
end
|
|
|
|
|
|
|
|
def expected(name)
|
|
|
|
pos = @scanner.pos
|
|
|
|
line = @scanner.string[0...pos].count("\n") + 1
|
|
|
|
|
|
|
|
after = @scanner.string[[pos - 15, 0].max...pos].gsub(/.*\n/m, '')
|
|
|
|
after = "..." + after if pos >= 15
|
|
|
|
|
|
|
|
expected = @expected || name
|
|
|
|
|
|
|
|
was = @scanner.rest[0...15].gsub(/\n.*/m, '')
|
|
|
|
was += "..." if @scanner.rest.size >= 15
|
|
|
|
raise Sass::SyntaxError.new(
|
|
|
|
"Invalid CSS after #{after.inspect}: expected #{expected}, was #{was.inspect}",
|
|
|
|
:line => line)
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def tok(name)
|
2009-12-20 05:57:35 -08:00
|
|
|
res = @scanner.scan(RX.const_get(name.to_s.upcase))
|
2009-12-20 15:03:18 -08:00
|
|
|
@expected = nil if res
|
2009-12-20 05:57:35 -08:00
|
|
|
@str << res if res && @str && name != :comment
|
|
|
|
res
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def raw(chr)
|
2009-12-20 05:57:35 -08:00
|
|
|
res = @scanner.scan(RX.quote(chr))
|
2009-12-20 15:03:18 -08:00
|
|
|
@expected = nil if res
|
2009-12-20 05:57:35 -08:00
|
|
|
@str << res if res && @str
|
|
|
|
res
|
2009-12-20 01:13:38 -08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|