2010-04-26 13:58:06 -07:00
|
|
|
#!/usr/bin/env ruby
|
|
|
|
|
|
|
|
require 'less'
|
|
|
|
|
|
|
|
module Less
|
2010-04-29 19:46:50 -07:00
|
|
|
# This is the class that Treetop defines for parsing Less files.
|
|
|
|
# Since not everything gets parsed into the AST but is instead resolved at parse-time,
|
|
|
|
# we need to override some of it so that it can be converted into Sass.
|
2010-04-26 13:58:06 -07:00
|
|
|
module StyleSheet
|
2010-04-29 19:46:50 -07:00
|
|
|
# Selector mixins that don't have arguments.
|
|
|
|
# This depends only on the syntax at the call site;
|
|
|
|
# if it doesn't use parens, it hits this production,
|
|
|
|
# regardless of whether the mixin being called has arguments or not.
|
2010-04-26 13:58:06 -07:00
|
|
|
module Mixin4
|
2010-04-27 03:41:26 -07:00
|
|
|
def build_with_sass(env)
|
2010-04-26 13:58:06 -07:00
|
|
|
selectors.build(env, :mixin).each do |path|
|
|
|
|
el = path.inject(env.root) do |current, node|
|
|
|
|
current.descend(node.selector, node) or raise MixinNameError, "#{selectors.text_value} in #{env}"
|
|
|
|
end
|
|
|
|
if el.is_a?(Node::Mixin::Def)
|
2010-04-29 19:46:50 -07:00
|
|
|
# Calling a mixin with arguments, which gets compiled to a Sass mixin
|
2010-04-26 13:58:06 -07:00
|
|
|
env << Node::Mixin::Call.new(el, [], env)
|
|
|
|
else
|
2010-04-29 19:46:50 -07:00
|
|
|
# Calling a mixin without arguments, which gets compiled to @extend
|
2010-04-29 19:31:35 -07:00
|
|
|
sel = selector_str(path)
|
|
|
|
base = selector_str(selector_base(path))
|
|
|
|
if base == sel
|
|
|
|
env << Node::SassNode.new(Sass::Tree::ExtendNode.new([sel]))
|
|
|
|
else
|
2010-04-28 17:59:13 -07:00
|
|
|
Haml::Util.haml_warn <<WARNING
|
2010-04-28 17:57:22 -07:00
|
|
|
WARNING: Sass doesn't support mixing in selector sequences.
|
2010-04-29 19:31:35 -07:00
|
|
|
Replacing "#{sel}" with "@extend #{base}"
|
2010-04-28 17:57:22 -07:00
|
|
|
WARNING
|
|
|
|
env << Node::SassNode.new(Sass::Tree::CommentNode.new("// #{sel};", true))
|
2010-04-29 19:31:35 -07:00
|
|
|
env << Node::SassNode.new(Sass::Tree::ExtendNode.new([base]))
|
2010-04-28 17:57:22 -07:00
|
|
|
end
|
2010-04-26 13:58:06 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2010-04-27 03:41:26 -07:00
|
|
|
alias_method :build_without_sass, :build
|
|
|
|
alias_method :build, :build_with_sass
|
2010-04-29 19:31:35 -07:00
|
|
|
|
|
|
|
def selector_base(path)
|
|
|
|
el, i = Haml::Util.enum_with_index(path).to_a.reverse.find {|e, i| e.selector !~ /^:{1,2}$/} ||
|
|
|
|
[path.first, 0]
|
|
|
|
sel = (el.selector =~ /^:{0,2}$/ ? el.selector : "")
|
|
|
|
[Node::Element.new(el.name, sel)] + path[i+1..-1]
|
|
|
|
end
|
|
|
|
|
|
|
|
def selector_str(path)
|
|
|
|
path.map {|e| e.sass_selector_str}.join(' ').gsub(' :', ':')
|
|
|
|
end
|
2010-04-26 13:58:06 -07:00
|
|
|
end
|
|
|
|
|
2010-04-29 19:46:50 -07:00
|
|
|
# Comma-separated selectors.
|
|
|
|
# Less breaks these into completely separate nodes.
|
|
|
|
# Since we don't want this duplication in the Sass,
|
|
|
|
# we modify the production to keep track of the original group
|
|
|
|
# so we can reconstruct it later on.
|
2010-04-27 02:40:28 -07:00
|
|
|
module Selectors2
|
|
|
|
def build_with_sass(env, method)
|
|
|
|
arr = build_without_sass(env, method)
|
|
|
|
return arr if method == :mixin
|
|
|
|
rarr = arr.map {|e| e.top(env)}
|
|
|
|
rarr.each {|e| e.group = rarr}
|
|
|
|
arr
|
|
|
|
end
|
|
|
|
alias_method :build_without_sass, :build
|
|
|
|
alias_method :build, :build_with_sass
|
|
|
|
end
|
|
|
|
|
2010-04-29 19:46:50 -07:00
|
|
|
# Attribute accessors.
|
|
|
|
# Sass just flat-out doesn't support these,
|
|
|
|
# so we print a warning to that effect and compile them to comments.
|
2010-04-26 13:58:06 -07:00
|
|
|
module Accessor1
|
|
|
|
def build(env)
|
2010-04-28 17:59:13 -07:00
|
|
|
Haml::Util.haml_warn <<WARNING
|
2010-04-27 01:38:00 -07:00
|
|
|
WARNING: Sass doesn't support attribute accessors.
|
2010-04-26 13:58:06 -07:00
|
|
|
Ignoring #{text_value}
|
|
|
|
WARNING
|
|
|
|
Node::Anonymous.new("/* #{text_value} */")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-04-29 19:46:50 -07:00
|
|
|
# @import statements.
|
|
|
|
# Less handles these during parse-time,
|
|
|
|
# so we want to wrap them up as a node in the tree.
|
|
|
|
# We also include the nodes, though,
|
|
|
|
# since we want to have access to the mixins
|
|
|
|
# so we can tell if they take arguments or not.
|
|
|
|
# The included nodes are hidden so they don't appear in the output.
|
2010-04-27 03:02:34 -07:00
|
|
|
module Import1
|
2010-04-27 03:41:26 -07:00
|
|
|
def build_with_sass(env)
|
2010-04-27 04:53:40 -07:00
|
|
|
line = input.line_of(interval.first)
|
2010-04-28 17:39:34 -07:00
|
|
|
import = Sass::Tree::ImportNode.new(url.value.gsub(/\.less$/, ''))
|
|
|
|
import.line = input.line_of(interval.first)
|
|
|
|
env << Node::SassNode.new(import)
|
2010-04-27 03:41:26 -07:00
|
|
|
old_rules = env.rules.dup
|
|
|
|
build_without_sass env
|
|
|
|
(env.rules - old_rules).each {|r| r.hide_in_sass = true}
|
2010-04-27 04:53:40 -07:00
|
|
|
rescue ImportError => e
|
|
|
|
raise Sass::SyntaxError.new("File to import #{url.text_value} not found or unreadable", :line => line)
|
2010-04-27 03:02:34 -07:00
|
|
|
end
|
2010-04-27 03:41:26 -07:00
|
|
|
alias_method :build_without_sass, :build
|
|
|
|
alias_method :build, :build_with_sass
|
2010-04-27 03:02:34 -07:00
|
|
|
end
|
|
|
|
|
2010-04-29 19:46:50 -07:00
|
|
|
# The IE-specific `alpha(opacity=@var)`.
|
|
|
|
# Less manually resolves the variable here at parse-time.
|
|
|
|
# We want to keep the variable around,
|
|
|
|
# so we compile this to a function.
|
|
|
|
# Less doesn't actually have an `=` operator,
|
|
|
|
# but that's okay since it's just getting compiled to Sass anyway.
|
2010-04-26 13:58:06 -07:00
|
|
|
module Entity::Alpha1
|
|
|
|
def build(env)
|
|
|
|
Node::Function.new("alpha",
|
|
|
|
[Node::Expression.new([
|
|
|
|
Node::Keyword.new("opacity"),
|
|
|
|
Node::Operator.new("="),
|
|
|
|
variable.build])])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-04-29 19:46:50 -07:00
|
|
|
# The Less AST classes for the document,
|
|
|
|
# including both stylesheet-level nodes and expression-level nodes.
|
|
|
|
# The main purpose of overriding these is to add `#to_sass_tree` functions
|
|
|
|
# for converting to Sass.
|
2010-04-26 13:58:06 -07:00
|
|
|
module Node
|
2010-04-27 03:41:26 -07:00
|
|
|
module Entity
|
|
|
|
attr_accessor :hide_in_sass
|
|
|
|
end
|
|
|
|
|
2010-04-26 13:58:06 -07:00
|
|
|
class Element
|
2010-04-27 02:40:28 -07:00
|
|
|
attr_accessor :group
|
|
|
|
|
|
|
|
def top(env)
|
|
|
|
return self if parent.equal?(env)
|
|
|
|
return parent.top(env)
|
|
|
|
end
|
|
|
|
|
2010-04-26 13:58:06 -07:00
|
|
|
def to_sass_tree
|
|
|
|
if root?
|
|
|
|
root = Sass::Tree::RootNode.new("")
|
|
|
|
rules.each {|r| root << r.to_sass_tree}
|
|
|
|
return root
|
|
|
|
end
|
2010-04-27 03:41:26 -07:00
|
|
|
return if hide_in_sass
|
2010-04-27 02:40:28 -07:00
|
|
|
return if !self.equal?(group.first)
|
2010-04-26 13:58:06 -07:00
|
|
|
|
2010-04-27 02:40:28 -07:00
|
|
|
last_el = nil
|
|
|
|
sel = group.map do |el|
|
|
|
|
comma_sel = []
|
|
|
|
loop do
|
|
|
|
comma_sel << el.sass_selector_str
|
|
|
|
break unless el.rules.size == 1 && el.rules.first.is_a?(Element)
|
|
|
|
el = el.rules.first
|
2010-04-26 13:58:06 -07:00
|
|
|
end
|
2010-04-27 02:40:28 -07:00
|
|
|
last_el = el
|
|
|
|
comma_sel = comma_sel.join(' ').gsub(' :', ':')
|
|
|
|
comma_sel.gsub!(/^:/, '&:') unless parent.root?
|
|
|
|
comma_sel
|
|
|
|
end.join(', ')
|
|
|
|
|
2010-04-28 18:02:23 -07:00
|
|
|
rule = Sass::Tree::RuleNode.new([sel])
|
2010-04-27 02:40:28 -07:00
|
|
|
last_el.rules.each {|r| rule << r.to_sass_tree}
|
2010-04-26 13:58:06 -07:00
|
|
|
return rule
|
|
|
|
end
|
|
|
|
|
|
|
|
def sass_selector_str
|
|
|
|
case @selector
|
|
|
|
when /[+>~]/; "#{@selector} #{@name}"
|
|
|
|
else @selector + @name
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
module Mixin
|
|
|
|
class Call
|
|
|
|
def to_sass_tree
|
2010-04-27 03:41:26 -07:00
|
|
|
return if hide_in_sass
|
2010-04-26 13:58:06 -07:00
|
|
|
Sass::Tree::MixinNode.new(@mixin.name.gsub(/^\./, ''), @params.map {|v| v.to_sass_tree})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Def
|
|
|
|
def to_sass_tree
|
2010-04-27 03:41:26 -07:00
|
|
|
return if hide_in_sass
|
2010-04-26 13:58:06 -07:00
|
|
|
mixin = Sass::Tree::MixinDefNode.new(name, @params.map do |v|
|
2010-04-27 03:57:38 -07:00
|
|
|
v.value.flatten!
|
2010-04-26 13:58:06 -07:00
|
|
|
[Sass::Script::Variable.new(v), v.value.to_sass_tree]
|
|
|
|
end)
|
|
|
|
rules.each {|r| mixin << r.to_sass_tree}
|
|
|
|
mixin
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-04-28 17:39:34 -07:00
|
|
|
class SassNode
|
2010-04-27 03:02:34 -07:00
|
|
|
include Entity
|
|
|
|
|
2010-04-28 17:39:34 -07:00
|
|
|
def initialize(node)
|
|
|
|
@node = node
|
2010-04-27 03:02:34 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def to_sass_tree
|
2010-04-27 03:41:26 -07:00
|
|
|
return if hide_in_sass
|
2010-04-28 17:39:34 -07:00
|
|
|
@node
|
2010-04-27 03:02:34 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-04-26 13:58:06 -07:00
|
|
|
class Property
|
|
|
|
def to_sass_tree
|
2010-04-27 03:41:26 -07:00
|
|
|
return if hide_in_sass
|
2010-04-26 13:58:06 -07:00
|
|
|
Sass::Tree::PropNode.new([self], @value.to_sass_tree, :new)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Expression
|
|
|
|
def to_sass_tree
|
|
|
|
if first.is_a?(Array)
|
|
|
|
val = map {|e| _to_sass_tree(e)}.inject(nil) do |e, i|
|
|
|
|
next i unless e
|
|
|
|
Sass::Script::Operation.new(e, i, :comma)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
val = _to_sass_tree(self)
|
|
|
|
end
|
|
|
|
val.options = {}
|
|
|
|
val
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
LESS_TO_SASS_OPERATORS = {"-" => :minus, "+" => :plus, "*" => :times, "/" => :div, "=" => :single_eq}
|
|
|
|
def _to_sass_tree(arr)
|
|
|
|
return Sass::Script::UnaryOperation.new(_to_sass_tree(arr[1..-1]), :minus) if arr[0] == "-"
|
|
|
|
|
|
|
|
first, rest = _sass_split(arr)
|
|
|
|
return first if rest.empty?
|
|
|
|
if rest[0].is_a?(Operator)
|
|
|
|
return Sass::Script::Operation.new(first, _to_sass_tree(rest[1..-1]),
|
|
|
|
LESS_TO_SASS_OPERATORS[rest[0]])
|
|
|
|
end
|
|
|
|
|
|
|
|
Sass::Script::Operation.new(first, _to_sass_tree(rest), :concat)
|
|
|
|
end
|
|
|
|
|
|
|
|
def _sass_split(arr)
|
|
|
|
return arr[0].to_sass_tree, arr[1..-1] unless arr[0] == "("
|
|
|
|
parens = 1
|
|
|
|
i = arr[1..-1].each_with_index do |e, i|
|
|
|
|
parens += 1 if e == "("
|
|
|
|
parens -= 1 if e == ")"
|
|
|
|
break i if parens == 0
|
|
|
|
end
|
|
|
|
|
|
|
|
return _to_sass_tree(arr[1...i+1]), arr[i+2..-1]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Color
|
|
|
|
def to_sass_tree
|
|
|
|
Sass::Script::Color.new(:red => r, :green => g, :blue => b, :alpha => a)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Number
|
|
|
|
def to_sass_tree
|
|
|
|
Sass::Script::Number.new(self, [self.unit])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Variable
|
|
|
|
def to_sass_tree
|
|
|
|
if @declaration
|
2010-04-27 03:41:26 -07:00
|
|
|
return if hide_in_sass
|
2010-04-26 13:58:06 -07:00
|
|
|
Sass::Tree::VariableNode.new(self, @value.to_sass_tree, false)
|
|
|
|
else
|
|
|
|
Sass::Script::Variable.new(self)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Function
|
|
|
|
def to_sass_tree
|
|
|
|
Sass::Script::Funcall.new(self, @args.map {|a| a.to_sass_tree})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Keyword
|
|
|
|
def to_sass_tree
|
|
|
|
Sass::Script::String.new(self)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Anonymous
|
|
|
|
def to_sass_tree
|
|
|
|
Sass::Script::String.new(self)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Quoted
|
|
|
|
def to_sass_tree
|
|
|
|
Sass::Script::String.new(self, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class FontFamily
|
|
|
|
def to_sass_tree
|
|
|
|
@family.map {|f| f.to_sass_tree}.inject(nil) do |e, f|
|
|
|
|
next f unless e
|
|
|
|
Sass::Script::Operation.new(e, f, :comma)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2010-04-27 04:53:40 -07:00
|
|
|
|
2010-04-29 19:46:50 -07:00
|
|
|
# The entry point to Less.
|
|
|
|
# By default Less doesn't preserve the filename of the file being parsed,
|
|
|
|
# which is unpleasant for error reporting.
|
|
|
|
# Our monkeypatch keeps it around.
|
2010-04-27 04:53:40 -07:00
|
|
|
class Engine
|
|
|
|
def initialize_with_sass(obj, opts = {})
|
|
|
|
initialize_without_sass(obj, opts)
|
|
|
|
@filename = obj.path if obj.is_a?(File)
|
|
|
|
end
|
|
|
|
alias_method :initialize_without_sass, :initialize
|
|
|
|
alias_method :initialize, :initialize_with_sass
|
|
|
|
|
|
|
|
def parse_with_sass
|
|
|
|
parse_without_sass
|
|
|
|
rescue Sass::SyntaxError => e
|
|
|
|
e.modify_backtrace(:filename => @filename)
|
|
|
|
raise e
|
|
|
|
end
|
|
|
|
alias_method :parse_without_sass, :parse
|
|
|
|
alias_method :parse, :parse_with_sass
|
|
|
|
alias_method :to_tree, :parse
|
|
|
|
end
|
2010-04-26 13:58:06 -07:00
|
|
|
end
|