grammar Less rule primary (declaration / ruleset / import / comment)+ / declaration* / import* / comment* end rule comment ws '/*' (!'*/' . )* '*/' ws / ws '//' (!"\n" .)* "\n" ws end # # div, .class, body > p {...} # rule ruleset selectors "{" ws primary ws "}" ws { def build env # Build the ruleset for each selector selectors.build(env, :ruleset).each do |sel| primary.build sel end end } / ws selectors ';' ws { def build env selectors.build(env, :mixin).each do |path| rules = path.inject(env.root) do |current, node| current.descend(node.selector, node) or raise MixinNameError, path.join end.rules env.rules += rules end end } end rule import "@import" S url:(string / url) medias? s ';' ws { def build env path = File.join(env.root.file, url.value) path += '.less' unless path =~ /\.less$/ if File.exist? path imported = Less::Engine.new(File.new path).to_tree env.rules += imported.rules else raise ImportError, path end end } end rule url 'url(' path:(string / [-a-zA-Z0-9_%$/.&=:;#+?]+) ')' { def build env = nil Node::String.new(CGI.unescape path.text_value) end def value build end } end rule medias [-a-z]+ (s ',' s [a-z]+)* end rule selectors ws selector tail:(s ',' ws selector)* ws { def build env, method all.map do |e| e.send(method, env) if e.respond_to? method end.compact end def all [selector] + tail.elements.map {|e| e.selector } end } end # # div > p a {...} # rule selector (s select element s)+ arguments? { def ruleset env elements.inject(env) do |node, e| node << Node::Element.new(e.element.text_value, e.select.text_value) node.last end end def mixin env elements.map do |e| Node::Element.new(e.element.text_value, e.select.text_value) end end } end # # @my-var: 12px; # height: 100%; # rule declaration ws name:(ident / variable) s ':' s expression s (';'/ ws &'}') ws { def build env env << (name.text_value =~ /^@/ ? Node::Variable : Node::Property).new(name.text_value, []) expression.build env end # Empty rule } / ws ident s ':' s ';' ws end # # An operation or compound value # rule expression entity (operator / S / WS) expression / entity end # # Entity: Any whitespace delimited token # rule entity function / fonts / keyword / accessor / variable / literal / important end rule fonts font family:(s ',' s font)+ { def build env fonts = ([font] + family.elements.map {|f| f.font }).map do |font| font.build env end env.identifiers.last << Node::FontFamily.new(fonts) end } end rule font [a-zA-Z] [-a-zA-Z0-9]* { def build env Node::Keyword.new(text_value) end } / string { def build env Node::String.new(text_value) end } end # # An identifier # rule ident '-'? [-a-z0-9_]+ end rule variable '@' [-a-zA-Z0-9_]+ { def build env env.identifiers.last << Node::Variable.new(text_value) end } end # # div / .class / #id / input[type="text"] / lang(fr) # rule element (class_id / tag / ident) attribute* ('(' ident? attribute* ')')? / attribute+ / '@media' / '@font-face' end rule class_id tag? (class / id)+ end # # [type="text"] # rule attribute '[' tag ([|~*$^]? '=') (tag / string) ']' / '[' (tag / string) ']' end rule class '.' [_a-z] [-a-zA-Z0-9_]* end rule id '#' [_a-z] [-a-zA-Z0-9_]* end rule tag [a-zA-Z] [-a-zA-Z]* [0-9]? / '*' end rule select (s [+>~] s / s ':' / S)? end # TODO: Merge this with attribute rule rule accessor ident:(class_id / tag) '[' attr:(string / variable) ']' { def build env env.identifiers.last << env.nearest(ident.text_value)[attr.text_value.delete(%q["'])].evaluate end } end rule operator S [-+*/] S { def build env env.identifiers.last << Node::Operator.new(text_value.strip) end } / [-+*/] { def build env env.identifiers.last << Node::Operator.new(text_value) end } end # # Tokens which don't need to be evaluated # rule literal color / (dimension / [-a-z]+) '/' dimension { def build env env.identifiers.last << Node::Anonymous.new(text_value) end } / number unit { def build env env.identifiers.last << Node::Number.new(number.text_value, unit.text_value) end } / string { def build env env.identifiers.last << Node::String.new(text_value) end } end # !important rule important '!important' { def build env env.identifiers.last << Node::Keyword.new(text_value) end } end # # `blue`, `small`, `normal` etc. # rule keyword [-a-zA-Z]+ !ns { def build env env.identifiers.last << Node::Keyword.new(text_value) end } end # # 'hello world' / "hello world" # rule string "'" content:(!"'" . )* "'" { def value text_value[1...-1] end } / ["] content:(!["] . )* ["] { def value text_value[1...-1] end } end # # Numbers & Units # rule dimension number unit end rule number '-'? [0-9]* '.' [0-9]+ / '-'? [0-9]+ end rule unit ('px'/'em'/'pc'/'%'/'pt'/'cm'/'mm')? end # # Color # rule color '#' rgb { def build env env.identifiers.last << Node::Color.new(*rgb.build) end } / fn:(('hsl'/'rgb') 'a'?) arguments { def build env env.identifiers.last << Node::Function.new(fn.text_value, arguments.build.flatten) end } end # # 00ffdd / 0fd # rule rgb r:(hex hex) g:(hex hex) b:(hex hex) { def build [r.text_value, g.text_value, b.text_value] end } / r:hex g:hex b:hex { def build [r.text_value, g.text_value, b.text_value].map {|c| c * 2 } end } end rule hex [a-fA-F0-9] end # # Functions and arguments # rule function name:([-a-zA-Z_]+) arguments { def build env env.identifiers.last << Node::Function.new(name.text_value, [arguments.build].flatten) end } end rule arguments '(' s argument s tail:(',' s argument s)* ')' { def build all.map do |e| e.build if e.respond_to? :build end.compact end def all [argument] + tail.elements.map {|e| e.argument } end } end rule argument color { def build Node::Color.new text_value end } / number unit { def build Node::Number.new number.text_value, unit.text_value end } / string { def build Node::String.new text_value end } / [a-zA-Z]+ '=' dimension { def build Node::Anonymous.new text_value end } / [-a-zA-Z0-9_%$/.&=:;#+?]+ { def build Node::String.new text_value end } end # # Whitespace # rule s [ ]* end rule S [ ]+ end rule ws [\n ]* end rule WS [\n ]+ end # Non-space char rule ns ![ ;\n] . end end