less.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. #!/usr/bin/env ruby
  2. require 'less'
  3. module Less
  4. # This is the class that Treetop defines for parsing Less files.
  5. # Since not everything gets parsed into the AST but is instead resolved at parse-time,
  6. # we need to override some of it so that it can be converted into Sass.
  7. module StyleSheet
  8. # Selector mixins that don't have arguments.
  9. # This depends only on the syntax at the call site;
  10. # if it doesn't use parens, it hits this production,
  11. # regardless of whether the mixin being called has arguments or not.
  12. module Mixin4
  13. def build_with_sass(env)
  14. selectors.build(env, :mixin).each do |path|
  15. el = path.inject(env.root) do |current, node|
  16. current.descend(node.selector, node) or raise MixinNameError, "#{selectors.text_value} in #{env}"
  17. end
  18. if el.is_a?(Node::Mixin::Def)
  19. # Calling a mixin with arguments, which gets compiled to a Sass mixin
  20. env << Node::Mixin::Call.new(el, [], env)
  21. else
  22. # Calling a mixin without arguments, which gets compiled to @extend
  23. sel = selector_str(path)
  24. base = selector_str(selector_base(path))
  25. if base == sel
  26. env << Node::SassNode.new(Sass::Tree::ExtendNode.new([sel]))
  27. else
  28. Sass::Util.sass_warn <<WARNING
  29. WARNING: Sass doesn't support mixing in selector sequences.
  30. Replacing "#{sel}" with "@extend #{base}"
  31. WARNING
  32. env << Node::SassNode.new(Sass::Tree::CommentNode.new(["// #{sel};"], true, false))
  33. env << Node::SassNode.new(Sass::Tree::ExtendNode.new([base]))
  34. end
  35. end
  36. end
  37. end
  38. alias_method :build_without_sass, :build
  39. alias_method :build, :build_with_sass
  40. def selector_base(path)
  41. el, i = Sass::Util.enum_with_index(path).to_a.reverse.find {|e, i| e.selector !~ /^:{1,2}$/} ||
  42. [path.first, 0]
  43. sel = (el.selector =~ /^:{0,2}$/ ? el.selector : "")
  44. [Node::Element.new(el.name, sel)] + path[i+1..-1]
  45. end
  46. def selector_str(path)
  47. path.map {|e| e.sass_selector_str}.join(' ').gsub(' :', ':')
  48. end
  49. end
  50. # Property and variable declarations.
  51. # We want to keep track of the line number
  52. # so we don't space out the variables too much in the generated Sass.
  53. module Declaration3
  54. def build_with_sass(env)
  55. build_without_sass(env)
  56. env.rules.last.src_line = input.line_of(interval.first)
  57. end
  58. alias_method :build_without_sass, :build
  59. alias_method :build, :build_with_sass
  60. end
  61. # Comma-separated selectors.
  62. # Less breaks these into completely separate nodes.
  63. # Since we don't want this duplication in the Sass,
  64. # we modify the production to keep track of the original group
  65. # so we can reconstruct it later on.
  66. module Selectors2
  67. def build_with_sass(env, method)
  68. arr = build_without_sass(env, method)
  69. return arr if method == :mixin
  70. rarr = arr.map {|e| e.top(env)}
  71. rarr.each {|e| e.group = rarr}
  72. arr
  73. end
  74. alias_method :build_without_sass, :build
  75. alias_method :build, :build_with_sass
  76. end
  77. # Attribute accessors.
  78. # Sass just flat-out doesn't support these,
  79. # so we print a warning to that effect and compile them to comments.
  80. module Accessor1
  81. def build(env)
  82. Sass::Util.sass_warn <<WARNING
  83. WARNING: Sass doesn't support attribute accessors.
  84. Ignoring #{text_value}
  85. WARNING
  86. Node::Anonymous.new("/* #{text_value} */")
  87. end
  88. end
  89. # @import statements.
  90. # Less handles these during parse-time,
  91. # so we want to wrap them up as a node in the tree.
  92. # We also include the nodes, though,
  93. # since we want to have access to the mixins
  94. # so we can tell if they take arguments or not.
  95. # The included nodes are hidden so they don't appear in the output.
  96. module Import1
  97. def build_with_sass(env)
  98. line = input.line_of(interval.first)
  99. import = Sass::Tree::ImportNode.new(url.value.gsub(/\.less$/, ''))
  100. import.line = input.line_of(interval.first)
  101. env << Node::SassNode.new(import)
  102. old_rules = env.rules.dup
  103. build_without_sass env
  104. (env.rules - old_rules).each {|r| r.hide_in_sass = true}
  105. rescue ImportError => e
  106. raise Sass::SyntaxError.new("File to import #{url.text_value} not found or unreadable", :line => line)
  107. end
  108. alias_method :build_without_sass, :build
  109. alias_method :build, :build_with_sass
  110. end
  111. # The IE-specific `alpha(opacity=@var)`.
  112. # Less manually resolves the variable here at parse-time.
  113. # We want to keep the variable around,
  114. # so we compile this to a function.
  115. # Less doesn't actually have an `=` operator,
  116. # but that's okay since it's just getting compiled to Sass anyway.
  117. module Entity::Alpha1
  118. def build(env)
  119. Node::Function.new("alpha",
  120. [Node::Expression.new([
  121. Node::Keyword.new("opacity"),
  122. Node::Operator.new("="),
  123. variable.build])])
  124. end
  125. end
  126. end
  127. # The Less AST classes for the document,
  128. # including both stylesheet-level nodes and expression-level nodes.
  129. # The main purpose of overriding these is to add `#to_sass_tree` functions
  130. # for converting to Sass.
  131. module Node
  132. module Entity
  133. attr_accessor :hide_in_sass
  134. attr_accessor :src_line
  135. end
  136. class Element
  137. attr_accessor :group
  138. def top(env)
  139. return self if parent.equal?(env)
  140. return parent.top(env)
  141. end
  142. def to_sass_tree
  143. if root?
  144. root = Sass::Tree::RootNode.new("")
  145. rules.each {|r| root << r.to_sass_tree}
  146. return root
  147. end
  148. return if hide_in_sass
  149. return if !self.equal?(group.first)
  150. last_el = nil
  151. sel = group.map do |el|
  152. comma_sel = []
  153. loop do
  154. comma_sel << el.sass_selector_str
  155. break unless el.rules.size == 1 && el.rules.first.is_a?(Element)
  156. el = el.rules.first
  157. end
  158. last_el = el
  159. comma_sel = comma_sel.join(' ').gsub(' :', ':')
  160. comma_sel.gsub!(/^:/, '&:') unless parent.root?
  161. comma_sel
  162. end.join(', ')
  163. rule = Sass::Tree::RuleNode.new([sel])
  164. last_el.rules.each {|r| rule << r.to_sass_tree}
  165. return rule
  166. end
  167. def sass_selector_str
  168. case @selector
  169. when /[+>~]/; "#{@selector} #{@name}"
  170. else @selector + @name
  171. end
  172. end
  173. end
  174. module Mixin
  175. class Call
  176. def to_sass_tree
  177. return if hide_in_sass
  178. Sass::Tree::MixinNode.new(@mixin.name.gsub(/^\./, ''), @params.map {|v| v.to_sass_tree}, {})
  179. end
  180. end
  181. class Def
  182. def to_sass_tree
  183. return if hide_in_sass
  184. mixin = Sass::Tree::MixinDefNode.new(name, @params.map do |v|
  185. v.value.flatten!
  186. [Sass::Script::Variable.new(v), v.value.to_sass_tree]
  187. end)
  188. rules.each {|r| mixin << r.to_sass_tree}
  189. mixin
  190. end
  191. end
  192. end
  193. class SassNode
  194. include Entity
  195. def initialize(node)
  196. @node = node
  197. end
  198. def to_sass_tree
  199. return if hide_in_sass
  200. @node
  201. end
  202. end
  203. class Property
  204. def to_sass_tree
  205. return if hide_in_sass
  206. Sass::Tree::PropNode.new([self], @value.to_sass_tree, :new)
  207. end
  208. end
  209. class Expression
  210. def to_sass_tree
  211. if first.is_a?(Array)
  212. val = map {|e| _to_sass_tree(e)}.inject(nil) do |e, i|
  213. next i unless e
  214. Sass::Script::Operation.new(e, i, :comma)
  215. end
  216. else
  217. val = _to_sass_tree(self)
  218. end
  219. val.options = {}
  220. val
  221. end
  222. private
  223. LESS_TO_SASS_OPERATORS = {"-" => :minus, "+" => :plus, "*" => :times, "/" => :div, "=" => :single_eq}
  224. def _to_sass_tree(arr)
  225. e, rest = _to_sass_tree_plus_minus_eq(arr)
  226. until rest.empty?
  227. e2, rest = _to_sass_tree_plus_minus_eq(rest)
  228. e = Sass::Script::Operation.new(e, e2, :space)
  229. end
  230. return e
  231. end
  232. def _to_sass_tree_plus_minus_eq(arr)
  233. e, rest = _to_sass_tree_times_div(arr)
  234. while rest[0] && rest[0].is_a?(Operator) && %w[+ - =].include?(rest[0])
  235. op = LESS_TO_SASS_OPERATORS[rest[0]]
  236. e2, rest = _to_sass_tree_times_div(rest[1..-1])
  237. e = Sass::Script::Operation.new(e, e2, op)
  238. end
  239. return e, rest
  240. end
  241. def _to_sass_tree_times_div(arr)
  242. e, rest = _to_sass_tree_unary(arr)
  243. while rest[0] && rest[0].is_a?(Operator) && %w[* /].include?(rest[0])
  244. op = LESS_TO_SASS_OPERATORS[rest[0]]
  245. e2, rest = _to_sass_tree_unary(rest[1..-1])
  246. e = Sass::Script::Operation.new(e, e2, op)
  247. end
  248. return e, rest
  249. end
  250. def _to_sass_tree_unary(arr)
  251. if arr[0] == "-"
  252. first, rest = _sass_split(arr[1..-1])
  253. return Sass::Script::UnaryOperation.new(first, :minus), rest
  254. else
  255. return _sass_split(arr[0..-1])
  256. end
  257. end
  258. def _sass_split(arr)
  259. return arr[0].to_sass_tree, arr[1..-1] unless arr[0] == "("
  260. parens = 1
  261. i = arr[1..-1].each_with_index do |e, i|
  262. parens += 1 if e == "("
  263. parens -= 1 if e == ")"
  264. break i if parens == 0
  265. end
  266. return _to_sass_tree(arr[1...i+1]), arr[i+2..-1]
  267. end
  268. end
  269. class Color
  270. def to_sass_tree
  271. Sass::Script::Color.new(:red => r, :green => g, :blue => b, :alpha => a)
  272. end
  273. end
  274. class Number
  275. def to_sass_tree
  276. Sass::Script::Number.new(self, [self.unit])
  277. end
  278. end
  279. class Variable
  280. def to_sass_tree
  281. if @declaration
  282. return if hide_in_sass
  283. node = Sass::Tree::VariableNode.new(self, @value.to_sass_tree, false)
  284. node.line = self.src_line
  285. node
  286. else
  287. Sass::Script::Variable.new(self)
  288. end
  289. end
  290. end
  291. class Function
  292. def to_sass_tree
  293. Sass::Script::Funcall.new(self, @args.map {|a| a.to_sass_tree}, {})
  294. end
  295. end
  296. class Keyword
  297. def to_sass_tree
  298. Sass::Script::String.new(self)
  299. end
  300. end
  301. class Anonymous
  302. def to_sass_tree
  303. Sass::Script::String.new(self)
  304. end
  305. end
  306. class Quoted
  307. def to_sass_tree
  308. Sass::Script::String.new(self, true)
  309. end
  310. end
  311. class FontFamily
  312. def to_sass_tree
  313. @family.map {|f| f.to_sass_tree}.inject(nil) do |e, f|
  314. next f unless e
  315. Sass::Script::Operation.new(e, f, :comma)
  316. end
  317. end
  318. end
  319. end
  320. # The entry point to Less.
  321. # By default Less doesn't preserve the filename of the file being parsed,
  322. # which is unpleasant for error reporting.
  323. # Our monkeypatch keeps it around.
  324. class Engine
  325. def initialize_with_sass(obj, opts = {})
  326. initialize_without_sass(obj, opts)
  327. @filename = obj.path if obj.is_a?(File)
  328. end
  329. alias_method :initialize_without_sass, :initialize
  330. alias_method :initialize, :initialize_with_sass
  331. def parse_with_sass
  332. parse_without_sass
  333. rescue Sass::SyntaxError => e
  334. e.modify_backtrace(:filename => @filename)
  335. raise e
  336. end
  337. alias_method :parse_without_sass, :parse
  338. alias_method :parse, :parse_with_sass
  339. alias_method :to_tree, :parse
  340. end
  341. end