exec.rb 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. require 'optparse'
  2. require 'fileutils'
  3. module Sass
  4. # This module handles the various Sass executables (`sass` and `sass-convert`).
  5. module Exec
  6. # An abstract class that encapsulates the executable code for all three executables.
  7. class Generic
  8. # @param args [Array<String>] The command-line arguments
  9. def initialize(args)
  10. @args = args
  11. @options = {}
  12. end
  13. # Parses the command-line arguments and runs the executable.
  14. # Calls `Kernel#exit` at the end, so it never returns.
  15. #
  16. # @see #parse
  17. def parse!
  18. begin
  19. parse
  20. rescue Exception => e
  21. raise e if @options[:trace] || e.is_a?(SystemExit)
  22. $stderr.print "#{e.class}: " unless e.class == RuntimeError
  23. $stderr.puts "#{e.message}"
  24. $stderr.puts " Use --trace for backtrace."
  25. exit 1
  26. end
  27. exit 0
  28. end
  29. # Parses the command-line arguments and runs the executable.
  30. # This does not handle exceptions or exit the program.
  31. #
  32. # @see #parse!
  33. def parse
  34. @opts = OptionParser.new(&method(:set_opts))
  35. @opts.parse!(@args)
  36. process_result
  37. @options
  38. end
  39. # @return [String] A description of the executable
  40. def to_s
  41. @opts.to_s
  42. end
  43. protected
  44. # Finds the line of the source template
  45. # on which an exception was raised.
  46. #
  47. # @param exception [Exception] The exception
  48. # @return [String] The line number
  49. def get_line(exception)
  50. # SyntaxErrors have weird line reporting
  51. # when there's trailing whitespace
  52. return (exception.message.scan(/:(\d+)/).first || ["??"]).first if exception.is_a?(::SyntaxError)
  53. (exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first
  54. end
  55. # Tells optparse how to parse the arguments
  56. # available for all executables.
  57. #
  58. # This is meant to be overridden by subclasses
  59. # so they can add their own options.
  60. #
  61. # @param opts [OptionParser]
  62. def set_opts(opts)
  63. opts.on('-s', '--stdin', :NONE, 'Read input from standard input instead of an input file') do
  64. @options[:input] = $stdin
  65. end
  66. opts.on('--trace', :NONE, 'Show a full traceback on error') do
  67. @options[:trace] = true
  68. end
  69. opts.on('--unix-newlines', 'Use Unix-style newlines in written files.') do
  70. @options[:unix_newlines] = true if ::Sass::Util.windows?
  71. end
  72. opts.on_tail("-?", "-h", "--help", "Show this message") do
  73. puts opts
  74. exit
  75. end
  76. opts.on_tail("-v", "--version", "Print version") do
  77. puts("Sass #{::Sass.version[:string]}")
  78. exit
  79. end
  80. end
  81. # Processes the options set by the command-line arguments.
  82. # In particular, sets `@options[:input]` and `@options[:output]`
  83. # to appropriate IO streams.
  84. #
  85. # This is meant to be overridden by subclasses
  86. # so they can run their respective programs.
  87. def process_result
  88. input, output = @options[:input], @options[:output]
  89. args = @args.dup
  90. input ||=
  91. begin
  92. filename = args.shift
  93. @options[:filename] = filename
  94. open_file(filename) || $stdin
  95. end
  96. output ||= open_file(args.shift, 'w') || $stdout
  97. @options[:input], @options[:output] = input, output
  98. end
  99. COLORS = { :red => 31, :green => 32, :yellow => 33 }
  100. # Prints a status message about performing the given action,
  101. # colored using the given color (via terminal escapes) if possible.
  102. #
  103. # @param name [#to_s] A short name for the action being performed.
  104. # Shouldn't be longer than 11 characters.
  105. # @param color [Symbol] The name of the color to use for this action.
  106. # Can be `:red`, `:green`, or `:yellow`.
  107. def puts_action(name, color, arg)
  108. return if @options[:for_engine][:quiet]
  109. printf color(color, "%11s %s\n"), name, arg
  110. end
  111. # Same as \{Kernel.puts}, but doesn't print anything if the `--quiet` option is set.
  112. #
  113. # @param args [Array] Passed on to \{Kernel.puts}
  114. def puts(*args)
  115. return if @options[:for_engine][:quiet]
  116. Kernel.puts(*args)
  117. end
  118. # Wraps the given string in terminal escapes
  119. # causing it to have the given color.
  120. # If terminal esapes aren't supported on this platform,
  121. # just returns the string instead.
  122. #
  123. # @param color [Symbol] The name of the color to use.
  124. # Can be `:red`, `:green`, or `:yellow`.
  125. # @param str [String] The string to wrap in the given color.
  126. # @return [String] The wrapped string.
  127. def color(color, str)
  128. raise "[BUG] Unrecognized color #{color}" unless COLORS[color]
  129. # Almost any real Unix terminal will support color,
  130. # so we just filter for Windows terms (which don't set TERM)
  131. # and not-real terminals, which aren't ttys.
  132. return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty?
  133. return "\e[#{COLORS[color]}m#{str}\e[0m"
  134. end
  135. private
  136. def open_file(filename, flag = 'r')
  137. return if filename.nil?
  138. flag = 'wb' if @options[:unix_newlines] && flag == 'w'
  139. File.open(filename, flag)
  140. end
  141. def handle_load_error(err)
  142. dep = err.message[/^no such file to load -- (.*)/, 1]
  143. raise err if @options[:trace] || dep.nil? || dep.empty?
  144. $stderr.puts <<MESSAGE
  145. Required dependency #{dep} not found!
  146. Run "gem install #{dep}" to get it.
  147. Use --trace for backtrace.
  148. MESSAGE
  149. exit 1
  150. end
  151. end
  152. # The `sass` executable.
  153. class Sass < Generic
  154. attr_reader :default_syntax
  155. # @param args [Array<String>] The command-line arguments
  156. def initialize(args)
  157. super
  158. @options[:for_engine] = {
  159. :load_paths => ['.'] + (ENV['SASSPATH'] || '').split(File::PATH_SEPARATOR)
  160. }
  161. @default_syntax = :sass
  162. end
  163. protected
  164. # Tells optparse how to parse the arguments.
  165. #
  166. # @param opts [OptionParser]
  167. def set_opts(opts)
  168. super
  169. opts.banner = <<END
  170. Usage: #{default_syntax} [options] [INPUT] [OUTPUT]
  171. Description:
  172. Converts SCSS or Sass files to CSS.
  173. Options:
  174. END
  175. if @default_syntax == :sass
  176. opts.on('--scss',
  177. 'Use the CSS-superset SCSS syntax.') do
  178. @options[:for_engine][:syntax] = :scss
  179. end
  180. else
  181. opts.on('--sass',
  182. 'Use the Indented syntax.') do
  183. @options[:for_engine][:syntax] = :sass
  184. end
  185. end
  186. opts.on('--watch', 'Watch files or directories for changes.',
  187. 'The location of the generated CSS can be set using a colon:',
  188. " #{@default_syntax} --watch input.#{@default_syntax}:output.css",
  189. " #{@default_syntax} --watch input-dir:output-dir") do
  190. @options[:watch] = true
  191. end
  192. opts.on('--update', 'Compile files or directories to CSS.',
  193. 'Locations are set like --watch.') do
  194. @options[:update] = true
  195. end
  196. opts.on('--stop-on-error', 'If a file fails to compile, exit immediately.',
  197. 'Only meaningful for --watch and --update.') do
  198. @options[:stop_on_error] = true
  199. end
  200. opts.on('-f', '--force', 'Recompile all Sass files, even if the CSS file is newer.',
  201. 'Only meaningful for --update.') do
  202. @options[:force] = true
  203. end
  204. opts.on('-c', '--check', "Just check syntax, don't evaluate.") do
  205. require 'stringio'
  206. @options[:check_syntax] = true
  207. @options[:output] = StringIO.new
  208. end
  209. opts.on('-t', '--style NAME',
  210. 'Output style. Can be nested (default), compact, compressed, or expanded.') do |name|
  211. @options[:for_engine][:style] = name.to_sym
  212. end
  213. opts.on('--precision NUMBER_OF_DIGITS', Integer,
  214. 'How many digits of precision to use when outputting decimal numbers. Defaults to 3.') do |precision|
  215. ::Sass::Script::Number.precision = precision
  216. end
  217. opts.on('-q', '--quiet', 'Silence warnings and status messages during compilation.') do
  218. @options[:for_engine][:quiet] = true
  219. end
  220. opts.on('--compass', 'Make Compass imports available and load project configuration.') do
  221. @options[:compass] = true
  222. end
  223. opts.on('-g', '--debug-info',
  224. 'Emit extra information in the generated CSS that can be used by the FireSass Firebug plugin.') do
  225. @options[:for_engine][:debug_info] = true
  226. end
  227. opts.on('-l', '--line-numbers', '--line-comments',
  228. 'Emit comments in the generated CSS indicating the corresponding source line.') do
  229. @options[:for_engine][:line_numbers] = true
  230. end
  231. opts.on('-i', '--interactive',
  232. 'Run an interactive SassScript shell.') do
  233. @options[:interactive] = true
  234. end
  235. opts.on('-I', '--load-path PATH', 'Add a sass import path.') do |path|
  236. @options[:for_engine][:load_paths] << path
  237. end
  238. opts.on('-r', '--require LIB', 'Require a Ruby library before running Sass.') do |lib|
  239. require lib
  240. end
  241. opts.on('--cache-location PATH', 'The path to put cached Sass files. Defaults to .sass-cache.') do |loc|
  242. @options[:for_engine][:cache_location] = loc
  243. end
  244. opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
  245. @options[:for_engine][:cache] = false
  246. end
  247. unless ::Sass::Util.ruby1_8?
  248. opts.on('-E encoding', 'Specify the default encoding for Sass files.') do |encoding|
  249. Encoding.default_external = encoding
  250. end
  251. end
  252. end
  253. # Processes the options set by the command-line arguments,
  254. # and runs the Sass compiler appropriately.
  255. def process_result
  256. require 'sass'
  257. if !@options[:update] && !@options[:watch] &&
  258. @args.first && colon_path?(@args.first)
  259. if @args.size == 1
  260. @args = split_colon_path(@args.first)
  261. else
  262. @options[:update] = true
  263. end
  264. end
  265. load_compass if @options[:compass]
  266. return interactive if @options[:interactive]
  267. return watch_or_update if @options[:watch] || @options[:update]
  268. super
  269. @options[:for_engine][:filename] = @options[:filename]
  270. begin
  271. input = @options[:input]
  272. output = @options[:output]
  273. @options[:for_engine][:syntax] ||= :scss if input.is_a?(File) && input.path =~ /\.scss$/
  274. @options[:for_engine][:syntax] ||= @default_syntax
  275. engine =
  276. if input.is_a?(File) && !@options[:check_syntax]
  277. ::Sass::Engine.for_file(input.path, @options[:for_engine])
  278. else
  279. # We don't need to do any special handling of @options[:check_syntax] here,
  280. # because the Sass syntax checking happens alongside evaluation
  281. # and evaluation doesn't actually evaluate any code anyway.
  282. ::Sass::Engine.new(input.read(), @options[:for_engine])
  283. end
  284. input.close() if input.is_a?(File)
  285. output.write(engine.render)
  286. output.close() if output.is_a? File
  287. rescue ::Sass::SyntaxError => e
  288. raise e if @options[:trace]
  289. raise e.sass_backtrace_str("standard input")
  290. end
  291. end
  292. private
  293. def load_compass
  294. begin
  295. require 'compass'
  296. rescue LoadError
  297. require 'rubygems'
  298. begin
  299. require 'compass'
  300. rescue LoadError
  301. puts "ERROR: Cannot load compass."
  302. exit 1
  303. end
  304. end
  305. Compass.add_project_configuration
  306. Compass.configuration.project_path ||= Dir.pwd
  307. @options[:for_engine][:load_paths] += Compass.configuration.sass_load_paths
  308. end
  309. def interactive
  310. require 'sass/repl'
  311. ::Sass::Repl.new(@options).run
  312. end
  313. def watch_or_update
  314. require 'sass/plugin'
  315. ::Sass::Plugin.options.merge! @options[:for_engine]
  316. ::Sass::Plugin.options[:unix_newlines] = @options[:unix_newlines]
  317. if @options[:force]
  318. raise "The --force flag may only be used with --update." unless @options[:update]
  319. ::Sass::Plugin.options[:always_update] = true
  320. end
  321. raise <<MSG if @args.empty?
  322. What files should I watch? Did you mean something like:
  323. #{@default_syntax} --watch input.#{@default_syntax}:output.css
  324. #{@default_syntax} --watch input-dir:output-dir
  325. MSG
  326. if !colon_path?(@args[0]) && probably_dest_dir?(@args[1])
  327. flag = @options[:update] ? "--update" : "--watch"
  328. err =
  329. if !File.exist?(@args[1])
  330. "doesn't exist"
  331. elsif @args[1] =~ /\.css$/
  332. "is a CSS file"
  333. end
  334. raise <<MSG if err
  335. File #{@args[1]} #{err}.
  336. Did you mean: #{@default_syntax} #{flag} #{@args[0]}:#{@args[1]}
  337. MSG
  338. end
  339. dirs, files = @args.map {|name| split_colon_path(name)}.
  340. partition {|i, _| File.directory? i}
  341. files.map! {|from, to| [from, to || from.gsub(/\.[^.]*?$/, '.css')]}
  342. dirs.map! {|from, to| [from, to || from]}
  343. ::Sass::Plugin.options[:template_location] = dirs
  344. ::Sass::Plugin.on_updated_stylesheet do |_, css|
  345. if File.exists? css
  346. puts_action :overwrite, :yellow, css
  347. else
  348. puts_action :create, :green, css
  349. end
  350. end
  351. had_error = false
  352. ::Sass::Plugin.on_creating_directory {|dirname| puts_action :directory, :green, dirname}
  353. ::Sass::Plugin.on_deleting_css {|filename| puts_action :delete, :yellow, filename}
  354. ::Sass::Plugin.on_compilation_error do |error, _, _|
  355. raise error unless error.is_a?(::Sass::SyntaxError) && !@options[:stop_on_error]
  356. had_error = true
  357. puts_action :error, :red, "#{error.sass_filename} (Line #{error.sass_line}: #{error.message})"
  358. STDOUT.flush
  359. end
  360. if @options[:update]
  361. ::Sass::Plugin.update_stylesheets(files)
  362. exit 1 if had_error
  363. return
  364. end
  365. puts ">>> Sass is watching for changes. Press Ctrl-C to stop."
  366. ::Sass::Plugin.on_template_modified do |template|
  367. puts ">>> Change detected to: #{template}"
  368. STDOUT.flush
  369. end
  370. ::Sass::Plugin.on_template_created do |template|
  371. puts ">>> New template detected: #{template}"
  372. STDOUT.flush
  373. end
  374. ::Sass::Plugin.on_template_deleted do |template|
  375. puts ">>> Deleted template detected: #{template}"
  376. STDOUT.flush
  377. end
  378. ::Sass::Plugin.watch(files)
  379. end
  380. def colon_path?(path)
  381. !split_colon_path(path)[1].nil?
  382. end
  383. def split_colon_path(path)
  384. one, two = path.split(':', 2)
  385. if one && two && ::Sass::Util.windows? &&
  386. one =~ /\A[A-Za-z]\Z/ && two =~ /\A[\/\\]/
  387. # If we're on Windows and we were passed a drive letter path,
  388. # don't split on that colon.
  389. one2, two = two.split(':', 2)
  390. one = one + ':' + one2
  391. end
  392. return one, two
  393. end
  394. # Whether path is likely to be meant as the destination
  395. # in a source:dest pair.
  396. def probably_dest_dir?(path)
  397. return false unless path
  398. return false if colon_path?(path)
  399. return ::Sass::Util.glob(File.join(path, "*.s[ca]ss")).empty?
  400. end
  401. end
  402. class Scss < Sass
  403. # @param args [Array<String>] The command-line arguments
  404. def initialize(args)
  405. super
  406. @default_syntax = :scss
  407. end
  408. end
  409. # The `sass-convert` executable.
  410. class SassConvert < Generic
  411. # @param args [Array<String>] The command-line arguments
  412. def initialize(args)
  413. super
  414. require 'sass'
  415. @options[:for_tree] = {}
  416. @options[:for_engine] = {:cache => false, :read_cache => true}
  417. end
  418. # Tells optparse how to parse the arguments.
  419. #
  420. # @param opts [OptionParser]
  421. def set_opts(opts)
  422. opts.banner = <<END
  423. Usage: sass-convert [options] [INPUT] [OUTPUT]
  424. Description:
  425. Converts between CSS, Sass, and SCSS files.
  426. E.g. converts from SCSS to Sass,
  427. or converts from CSS to SCSS (adding appropriate nesting).
  428. Options:
  429. END
  430. opts.on('-F', '--from FORMAT',
  431. 'The format to convert from. Can be css, scss, sass, less.',
  432. 'By default, this is inferred from the input filename.',
  433. 'If there is none, defaults to css.') do |name|
  434. @options[:from] = name.downcase.to_sym
  435. unless [:css, :scss, :sass, :less].include?(@options[:from])
  436. raise "Unknown format for sass-convert --from: #{name}"
  437. end
  438. try_less_note if @options[:from] == :less
  439. end
  440. opts.on('-T', '--to FORMAT',
  441. 'The format to convert to. Can be scss or sass.',
  442. 'By default, this is inferred from the output filename.',
  443. 'If there is none, defaults to sass.') do |name|
  444. @options[:to] = name.downcase.to_sym
  445. unless [:scss, :sass].include?(@options[:to])
  446. raise "Unknown format for sass-convert --to: #{name}"
  447. end
  448. end
  449. opts.on('-R', '--recursive',
  450. 'Convert all the files in a directory. Requires --from and --to.') do
  451. @options[:recursive] = true
  452. end
  453. opts.on('-i', '--in-place',
  454. 'Convert a file to its own syntax.',
  455. 'This can be used to update some deprecated syntax.') do
  456. @options[:in_place] = true
  457. end
  458. opts.on('--dasherize', 'Convert underscores to dashes') do
  459. @options[:for_tree][:dasherize] = true
  460. end
  461. opts.on('--old', 'Output the old-style ":prop val" property syntax.',
  462. 'Only meaningful when generating Sass.') do
  463. @options[:for_tree][:old] = true
  464. end
  465. opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
  466. @options[:for_engine][:read_cache] = false
  467. end
  468. unless ::Sass::Util.ruby1_8?
  469. opts.on('-E encoding', 'Specify the default encoding for Sass and CSS files.') do |encoding|
  470. Encoding.default_external = encoding
  471. end
  472. end
  473. super
  474. end
  475. # Processes the options set by the command-line arguments,
  476. # and runs the CSS compiler appropriately.
  477. def process_result
  478. require 'sass'
  479. if @options[:recursive]
  480. process_directory
  481. return
  482. end
  483. super
  484. input = @options[:input]
  485. raise "Error: '#{input.path}' is a directory (did you mean to use --recursive?)" if File.directory?(input)
  486. output = @options[:output]
  487. output = input if @options[:in_place]
  488. process_file(input, output)
  489. end
  490. private
  491. def process_directory
  492. unless input = @options[:input] = @args.shift
  493. raise "Error: directory required when using --recursive."
  494. end
  495. output = @options[:output] = @args.shift
  496. raise "Error: --from required when using --recursive." unless @options[:from]
  497. raise "Error: --to required when using --recursive." unless @options[:to]
  498. raise "Error: '#{@options[:input]}' is not a directory" unless File.directory?(@options[:input])
  499. if @options[:output] && File.exists?(@options[:output]) && !File.directory?(@options[:output])
  500. raise "Error: '#{@options[:output]}' is not a directory"
  501. end
  502. @options[:output] ||= @options[:input]
  503. from = @options[:from]
  504. if @options[:to] == @options[:from] && !@options[:in_place]
  505. fmt = @options[:from]
  506. raise "Error: converting from #{fmt} to #{fmt} without --in-place"
  507. end
  508. ext = @options[:from]
  509. ::Sass::Util.glob("#{@options[:input]}/**/*.#{ext}") do |f|
  510. output =
  511. if @options[:in_place]
  512. f
  513. elsif @options[:output]
  514. output_name = f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
  515. output_name[0...@options[:input].size] = @options[:output]
  516. output_name
  517. else
  518. f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
  519. end
  520. unless File.directory?(File.dirname(output))
  521. puts_action :directory, :green, File.dirname(output)
  522. FileUtils.mkdir_p(File.dirname(output))
  523. end
  524. puts_action :convert, :green, f
  525. if File.exists?(output)
  526. puts_action :overwrite, :yellow, output
  527. else
  528. puts_action :create, :green, output
  529. end
  530. input = open_file(f)
  531. output = @options[:in_place] ? input : open_file(output, "w")
  532. process_file(input, output)
  533. end
  534. end
  535. def process_file(input, output)
  536. if input.is_a?(File)
  537. @options[:from] ||=
  538. case input.path
  539. when /\.scss$/; :scss
  540. when /\.sass$/; :sass
  541. when /\.less$/; :less
  542. when /\.css$/; :css
  543. end
  544. elsif @options[:in_place]
  545. raise "Error: the --in-place option requires a filename."
  546. end
  547. if output.is_a?(File)
  548. @options[:to] ||=
  549. case output.path
  550. when /\.scss$/; :scss
  551. when /\.sass$/; :sass
  552. end
  553. end
  554. @options[:from] ||= :css
  555. @options[:to] ||= :sass
  556. @options[:for_engine][:syntax] = @options[:from]
  557. out =
  558. ::Sass::Util.silence_sass_warnings do
  559. if @options[:from] == :css
  560. require 'sass/css'
  561. ::Sass::CSS.new(input.read, @options[:for_tree]).render(@options[:to])
  562. elsif @options[:from] == :less
  563. require 'sass/less'
  564. try_less_note
  565. input = input.read if input.is_a?(IO) && !input.is_a?(File) # Less is dumb
  566. Less::Engine.new(input).to_tree.to_sass_tree.send("to_#{@options[:to]}", @options[:for_tree])
  567. else
  568. if input.is_a?(File)
  569. ::Sass::Engine.for_file(input.path, @options[:for_engine])
  570. else
  571. ::Sass::Engine.new(input.read, @options[:for_engine])
  572. end.to_tree.send("to_#{@options[:to]}", @options[:for_tree])
  573. end
  574. end
  575. output = File.open(input.path, 'w') if @options[:in_place]
  576. output.write(out)
  577. rescue ::Sass::SyntaxError => e
  578. raise e if @options[:trace]
  579. file = " of #{e.sass_filename}" if e.sass_filename
  580. raise "Error on line #{e.sass_line}#{file}: #{e.message}\n Use --trace for backtrace"
  581. rescue LoadError => err
  582. handle_load_error(err)
  583. end
  584. @@less_note_printed = false
  585. def try_less_note
  586. return if @@less_note_printed
  587. @@less_note_printed = true
  588. warn <<NOTE
  589. * NOTE: Sass and Less are different languages, and they work differently.
  590. * I'll do my best to translate, but some features -- especially mixins --
  591. * should be checked by hand.
  592. NOTE
  593. end
  594. end
  595. end
  596. end