require 'grammar/common' require 'grammar/entity' module Less grammar StyleSheet include Common include Entity rule primary (import / declaration / ruleset / mixin / comment)* { def build env = Less::Element.new elements.map do |e| e.build env if e.respond_to? :build end; env end } end rule comment ws '/*' (!'*/' . )* '*/' ws / ws '//' (!"\n" .)* "\n" ws end # # div, .class, body > p {...} # rule ruleset selectors "{" ws primary ws "}" s hide:(';'?) ws { def build env # Build the ruleset for each selector selectors.build(env, :ruleset).each do |sel| sel.hide unless hide.empty? primary.build sel end end # Mixin Declaration } / ws '.' name:[-a-zA-Z0-9_]+ ws parameters ws "{" ws primary ws "}" ws { def build env env << Node::Mixin::Def.new(name.text_value, parameters.build(env)) primary.build env.last end } end rule mixin name:('.' [-a-zA-Z0-9_]+) args:(arguments) s ';' ws { def build env definition = env.nearest(name.text_value, :mixin) or raise MixinNameError, "#{name.text_value}() in #{env}" params = args.build.map {|i| Node::Expression.new i } unless args.empty? env << Node::Mixin::Call.new(definition, params || [], env) 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, "#{selectors.text_value} in #{env}" end.rules env.rules += rules end end } 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 sel:(s select element s)+ '' { def ruleset env sel.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 sel.elements.map do |e| Node::Element.new(e.element.text_value, e.select.text_value) end end } end rule parameters '(' s ')' { def build env [] end } / '(' parameter tail:(s ',' s parameter)* ')' { def build env all.map do |e| e.build(env) end end def all [parameter] + tail.elements.map {|e| e.parameter } end } end rule parameter variable s ':' s expressions { def build env Node::Variable.new(variable.text_value, expressions.build(env), env) end } end rule import ws "@import" S url:(string / url) medias? s ';' ws { def build env standard_path = File.join(env.root.file || Dir.pwd, url.value) # Compile a list of possible paths for this file paths = $LESS_LOAD_PATH.map { |p| File.join(p, url.value) } + [standard_path] # Standardize and make uniq paths = paths.map do |p| p = File.expand_path(p) p += '.less' unless p =~ /\.(le|c)ss$/ p end.uniq # Use the first that exists if any if path = paths.detect {|p| File.exists?(p)} unless env.root.imported.include?(path) env.root.imported << path env.rules += Less::Engine.new(File.new(path)).to_tree.rules end else raise ImportError, standard_path end end } end rule url 'url(' path:(string / [-a-zA-Z0-9_%$/.&=:;#+?]+) ')' { def build env = nil Node::Function.new('url', value) end def value Node::Quoted.new CGI.unescape(path.text_value) end } end rule medias [-a-z]+ (s ',' s [a-z]+)* end # # @my-var: 12px; # height: 100%; # rule declaration ws name:(ident / variable) s ':' ws expressions tail:(ws ',' ws expressions)* s (';'/ ws &'}') ws { def build env result = all.map {|e| e.build(env) if e.respond_to? :build }.compact env << (name.text_value =~ /^@/ ? Node::Variable : Node::Property).new(name.text_value, result, env) end def all [expressions] + tail.elements.map {|f| f.expressions } end # Empty rule } / ws ident s ':' s ';' ws end # # An operation or compound value # rule expressions # Operation expression tail:(operator expression)+ { def build env = nil all.map {|e| e.build(env) }.dissolve end def all [expression] + tail.elements.map {|i| [i.operator, i.expression] }.flatten.compact end # Space-delimited expressions } / expression tail:(WS expression)* i:important? { def build env = nil all.map {|e| e.build(env) if e.respond_to? :build }.compact end def all [expression] + tail.elements.map {|f| f.expression } + [i] end # Catch-all rule } / [-a-zA-Z0-9_.&*/=:,+? \[\]()#%]+ { def build env [Node::Anonymous.new(text_value)] end } end rule expression '(' s expressions s ')' { def build env = nil Node::Expression.new(['('] + expressions.build(env).flatten + [')']) end } / entity '' { def build env = nil e = entity.method(:build).arity.zero?? entity.build : entity.build(env) e.respond_to?(:dissolve) ? e.dissolve : e end } end # !important rule important s '!' s 'important' { def build env = nil Node::Keyword.new(text_value.strip) end } end # # An identifier # rule ident '*'? '-'? [-a-z_] [-a-z0-9_]* end rule variable '@' [-a-zA-Z0-9_]+ { def build Node::Variable.new(text_value) end } end # # div / .class / #id / input[type="text"] / lang(fr) # rule element ((class / id / tag / ident) attribute* ('(' [a-zA-Z]+ ')' / '(' (pseudo_exp / selector / [0-9]+) ')' )?)+ / attribute+ / '@media' / '@font-face' end # # 4n+1 # rule pseudo_exp '-'? ([0-9]+)? 'n' ([-+] [0-9]+)? end # # [type="text"] # rule attribute '[' tag ([|~*$^]? '=') (string / [-a-zA-Z_0-9]+) ']' / '[' (tag / string) ']' end rule class '.' [_a-zA-Z] [-a-zA-Z0-9_]* end rule id '#' [_a-zA-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.nearest(ident.text_value)[attr.text_value.delete(%q["'])].evaluate end } end rule operator S [-+*/] S { def build env Node::Operator.new(text_value.strip) end } / [-+*/] { def build env Node::Operator.new(text_value) end } end # # Functions and arguments # rule function name:([-a-zA-Z_]+) arguments { def build Node::Function.new(name.text_value, arguments.build) end } end rule arguments '(' s expressions s tail:(',' s expressions s)* ')' { def build all.map do |e| e.build if e.respond_to? :build end.compact end def all [expressions] + tail.elements.map {|e| e.expressions } end } / '(' s ')' { def build [] end } end end end