/scalate-jruby/src/main/resources/haml-3.0.25/lib/haml/exec.rb

http://github.com/scalate/scalate · Ruby · 871 lines · 689 code · 95 blank · 87 comment · 88 complexity · d5fb709456c0c9a5812b6705d10ec7a5 MD5 · raw file

  1. require 'optparse'
  2. require 'fileutils'
  3. module Haml
  4. # This module handles the various Haml executables (`haml`, `sass`, `sass-convert`, etc).
  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. # which there is for Haml documents.
  53. return (exception.message.scan(/:(\d+)/).first || ["??"]).first if exception.is_a?(::SyntaxError)
  54. (exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first
  55. end
  56. # Tells optparse how to parse the arguments
  57. # available for all executables.
  58. #
  59. # This is meant to be overridden by subclasses
  60. # so they can add their own options.
  61. #
  62. # @param opts [OptionParser]
  63. def set_opts(opts)
  64. opts.on('-s', '--stdin', :NONE, 'Read input from standard input instead of an input file') do
  65. @options[:input] = $stdin
  66. end
  67. opts.on('--trace', :NONE, 'Show a full traceback on error') do
  68. @options[:trace] = true
  69. end
  70. opts.on('--unix-newlines', 'Use Unix-style newlines in written files.') do
  71. @options[:unix_newlines] = true if ::Haml::Util.windows?
  72. end
  73. opts.on_tail("-?", "-h", "--help", "Show this message") do
  74. puts opts
  75. exit
  76. end
  77. opts.on_tail("-v", "--version", "Print version") do
  78. puts("Haml/Sass #{::Haml.version[:string]}")
  79. exit
  80. end
  81. end
  82. # Processes the options set by the command-line arguments.
  83. # In particular, sets `@options[:input]` and `@options[:output]`
  84. # to appropriate IO streams.
  85. #
  86. # This is meant to be overridden by subclasses
  87. # so they can run their respective programs.
  88. def process_result
  89. input, output = @options[:input], @options[:output]
  90. args = @args.dup
  91. input ||=
  92. begin
  93. filename = args.shift
  94. @options[:filename] = filename
  95. open_file(filename) || $stdin
  96. end
  97. output ||= open_file(args.shift, 'w') || $stdout
  98. @options[:input], @options[:output] = input, output
  99. end
  100. COLORS = { :red => 31, :green => 32, :yellow => 33 }
  101. # Prints a status message about performing the given action,
  102. # colored using the given color (via terminal escapes) if possible.
  103. #
  104. # @param name [#to_s] A short name for the action being performed.
  105. # Shouldn't be longer than 11 characters.
  106. # @param color [Symbol] The name of the color to use for this action.
  107. # Can be `:red`, `:green`, or `:yellow`.
  108. def puts_action(name, color, arg)
  109. return if @options[:for_engine][:quiet]
  110. printf color(color, "%11s %s\n"), name, arg
  111. end
  112. # Same as \{Kernel.puts}, but doesn't print anything if the `--quiet` option is set.
  113. #
  114. # @param args [Array] Passed on to \{Kernel.puts}
  115. def puts(*args)
  116. return if @options[:for_engine][:quiet]
  117. Kernel.puts(*args)
  118. end
  119. # Wraps the given string in terminal escapes
  120. # causing it to have the given color.
  121. # If terminal esapes aren't supported on this platform,
  122. # just returns the string instead.
  123. #
  124. # @param color [Symbol] The name of the color to use.
  125. # Can be `:red`, `:green`, or `:yellow`.
  126. # @param str [String] The string to wrap in the given color.
  127. # @return [String] The wrapped string.
  128. def color(color, str)
  129. raise "[BUG] Unrecognized color #{color}" unless COLORS[color]
  130. # Almost any real Unix terminal will support color,
  131. # so we just filter for Windows terms (which don't set TERM)
  132. # and not-real terminals, which aren't ttys.
  133. return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty?
  134. return "\e[#{COLORS[color]}m#{str}\e[0m"
  135. end
  136. private
  137. def open_file(filename, flag = 'r')
  138. return if filename.nil?
  139. flag = 'wb' if @options[:unix_newlines] && flag == 'w'
  140. File.open(filename, flag)
  141. end
  142. def handle_load_error(err)
  143. dep = err.message[/^no such file to load -- (.*)/, 1]
  144. raise err if @options[:trace] || dep.nil? || dep.empty?
  145. $stderr.puts <<MESSAGE
  146. Required dependency #{dep} not found!
  147. Run "gem install #{dep}" to get it.
  148. Use --trace for backtrace.
  149. MESSAGE
  150. exit 1
  151. end
  152. end
  153. # An abstrac class that encapsulates the code
  154. # specific to the `haml` and `sass` executables.
  155. class HamlSass < Generic
  156. # @param args [Array<String>] The command-line arguments
  157. def initialize(args)
  158. super
  159. @options[:for_engine] = {}
  160. end
  161. protected
  162. # Tells optparse how to parse the arguments
  163. # available for the `haml` and `sass` executables.
  164. #
  165. # This is meant to be overridden by subclasses
  166. # so they can add their own options.
  167. #
  168. # @param opts [OptionParser]
  169. def set_opts(opts)
  170. opts.banner = <<END
  171. Usage: #{@name.downcase} [options] [INPUT] [OUTPUT]
  172. Description:
  173. Uses the #{@name} engine to parse the specified template
  174. and outputs the result to the specified file.
  175. Options:
  176. END
  177. opts.on('--rails RAILS_DIR', "Install Haml and Sass from the Gem to a Rails project") do |dir|
  178. original_dir = dir
  179. env = File.join(dir, "config", "environment.rb")
  180. if File.exists?(File.join(dir, "Gemfile"))
  181. puts("haml --rails isn't needed for Rails 3 or greater.",
  182. "Add 'gem \"haml\"' to your Gemfile instead.", "",
  183. "haml --rails will no longer work in the next version of #{@name}.", "")
  184. elsif File.exists?(env) && File.open(env) {|env| env.grep(/config\.gem/)}
  185. puts("haml --rails isn't needed for Rails 2.1 or greater.",
  186. "Add 'config.gem \"haml\"' to config/environment.rb instead.", "",
  187. "haml --rails will no longer work in the next version of #{@name}.", "")
  188. end
  189. dir = File.join(dir, 'vendor', 'plugins')
  190. unless File.exists?(dir)
  191. puts "Directory #{dir} doesn't exist"
  192. exit 1
  193. end
  194. dir = File.join(dir, 'haml')
  195. if File.exists?(dir)
  196. print "Directory #{dir} already exists, overwrite [y/N]? "
  197. exit 2 if gets !~ /y/i
  198. FileUtils.rm_rf(dir)
  199. end
  200. begin
  201. Dir.mkdir(dir)
  202. rescue SystemCallError
  203. puts "Cannot create #{dir}"
  204. exit 1
  205. end
  206. File.open(File.join(dir, 'init.rb'), 'w') do |file|
  207. file << File.read(File.dirname(__FILE__) + "/../../init.rb")
  208. end
  209. puts "Haml plugin added to #{original_dir}"
  210. exit
  211. end
  212. opts.on('-c', '--check', "Just check syntax, don't evaluate.") do
  213. require 'stringio'
  214. @options[:check_syntax] = true
  215. @options[:output] = StringIO.new
  216. end
  217. super
  218. end
  219. # Processes the options set by the command-line arguments.
  220. # In particular, sets `@options[:for_engine][:filename]` to the input filename
  221. # and requires the appropriate file.
  222. #
  223. # This is meant to be overridden by subclasses
  224. # so they can run their respective programs.
  225. def process_result
  226. super
  227. @options[:for_engine][:filename] = @options[:filename] if @options[:filename]
  228. require File.dirname(__FILE__) + "/../#{@name.downcase}"
  229. end
  230. end
  231. # The `sass` executable.
  232. class Sass < HamlSass
  233. # @param args [Array<String>] The command-line arguments
  234. def initialize(args)
  235. super
  236. @name = "Sass"
  237. @options[:for_engine][:load_paths] = ['.'] + (ENV['SASSPATH'] || '').split(File::PATH_SEPARATOR)
  238. end
  239. protected
  240. # Tells optparse how to parse the arguments.
  241. #
  242. # @param opts [OptionParser]
  243. def set_opts(opts)
  244. super
  245. opts.on('--scss',
  246. 'Use the CSS-superset SCSS syntax.') do
  247. @options[:for_engine][:syntax] = :scss
  248. end
  249. opts.on('--watch', 'Watch files or directories for changes.',
  250. 'The location of the generated CSS can be set using a colon:',
  251. ' sass --watch input.sass:output.css',
  252. ' sass --watch input-dir:output-dir') do
  253. @options[:watch] = true
  254. end
  255. opts.on('--update', 'Compile files or directories to CSS.',
  256. 'Locations are set like --watch.') do
  257. @options[:update] = true
  258. end
  259. opts.on('--stop-on-error', 'If a file fails to compile, exit immediately.',
  260. 'Only meaningful for --watch and --update.') do
  261. @options[:stop_on_error] = true
  262. end
  263. opts.on('-t', '--style NAME',
  264. 'Output style. Can be nested (default), compact, compressed, or expanded.') do |name|
  265. @options[:for_engine][:style] = name.to_sym
  266. end
  267. opts.on('-q', '--quiet', 'Silence warnings and status messages during compilation.') do
  268. @options[:for_engine][:quiet] = true
  269. end
  270. opts.on('-g', '--debug-info',
  271. 'Emit extra information in the generated CSS that can be used by the FireSass Firebug plugin.') do
  272. @options[:for_engine][:debug_info] = true
  273. end
  274. opts.on('-l', '--line-numbers', '--line-comments',
  275. 'Emit comments in the generated CSS indicating the corresponding sass line.') do
  276. @options[:for_engine][:line_numbers] = true
  277. end
  278. opts.on('-i', '--interactive',
  279. 'Run an interactive SassScript shell.') do
  280. @options[:interactive] = true
  281. end
  282. opts.on('-I', '--load-path PATH', 'Add a sass import path.') do |path|
  283. @options[:for_engine][:load_paths] << path
  284. end
  285. opts.on('-r', '--require LIB', 'Require a Ruby library before running Sass.') do |lib|
  286. require lib
  287. end
  288. opts.on('--cache-location PATH', 'The path to put cached Sass files. Defaults to .sass-cache.') do |loc|
  289. @options[:for_engine][:cache_location] = loc
  290. end
  291. opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
  292. @options[:for_engine][:cache] = false
  293. end
  294. unless ::Haml::Util.ruby1_8?
  295. opts.on('-E encoding', 'Specify the default encoding for Sass files.') do |encoding|
  296. Encoding.default_external = encoding
  297. end
  298. end
  299. end
  300. # Processes the options set by the command-line arguments,
  301. # and runs the Sass compiler appropriately.
  302. def process_result
  303. if !@options[:update] && !@options[:watch] &&
  304. @args.first && colon_path?(@args.first)
  305. if @args.size == 1
  306. @args = split_colon_path(@args.first)
  307. else
  308. @options[:update] = true
  309. end
  310. end
  311. return interactive if @options[:interactive]
  312. return watch_or_update if @options[:watch] || @options[:update]
  313. super
  314. begin
  315. input = @options[:input]
  316. output = @options[:output]
  317. @options[:for_engine][:syntax] ||= :scss if input.is_a?(File) && input.path =~ /\.scss$/
  318. tree =
  319. if input.is_a?(File) && !@options[:check_syntax]
  320. ::Sass::Files.tree_for(input.path, @options[:for_engine])
  321. else
  322. # We don't need to do any special handling of @options[:check_syntax] here,
  323. # because the Sass syntax checking happens alongside evaluation
  324. # and evaluation doesn't actually evaluate any code anyway.
  325. ::Sass::Engine.new(input.read(), @options[:for_engine]).to_tree
  326. end
  327. input.close() if input.is_a?(File)
  328. output.write(tree.render)
  329. output.close() if output.is_a? File
  330. rescue ::Sass::SyntaxError => e
  331. raise e if @options[:trace]
  332. raise e.sass_backtrace_str("standard input")
  333. end
  334. end
  335. private
  336. def interactive
  337. require 'sass'
  338. require 'sass/repl'
  339. ::Sass::Repl.new(@options).run
  340. end
  341. def watch_or_update
  342. require 'sass'
  343. require 'sass/plugin'
  344. ::Sass::Plugin.options.merge! @options[:for_engine]
  345. ::Sass::Plugin.options[:unix_newlines] = @options[:unix_newlines]
  346. raise <<MSG if @args.empty?
  347. What files should I watch? Did you mean something like:
  348. sass --watch input.sass:output.css
  349. sass --watch input-dir:output-dir
  350. MSG
  351. if !colon_path?(@args[0]) && probably_dest_dir?(@args[1])
  352. flag = @options[:update] ? "--update" : "--watch"
  353. err =
  354. if !File.exist?(@args[1])
  355. "doesn't exist"
  356. elsif @args[1] =~ /\.css$/
  357. "is a CSS file"
  358. end
  359. raise <<MSG if err
  360. File #{@args[1]} #{err}.
  361. Did you mean: sass #{flag} #{@args[0]}:#{@args[1]}
  362. MSG
  363. end
  364. dirs, files = @args.map {|name| split_colon_path(name)}.
  365. partition {|i, _| File.directory? i}
  366. files.map! {|from, to| [from, to || from.gsub(/\..*?$/, '.css')]}
  367. dirs.map! {|from, to| [from, to || from]}
  368. ::Sass::Plugin.options[:template_location] = dirs
  369. ::Sass::Plugin.on_updating_stylesheet do |_, css|
  370. if File.exists? css
  371. puts_action :overwrite, :yellow, css
  372. else
  373. puts_action :create, :green, css
  374. end
  375. end
  376. had_error = false
  377. ::Sass::Plugin.on_creating_directory {|dirname| puts_action :directory, :green, dirname}
  378. ::Sass::Plugin.on_deleting_css {|filename| puts_action :delete, :yellow, filename}
  379. ::Sass::Plugin.on_compilation_error do |error, _, _|
  380. raise error unless error.is_a?(::Sass::SyntaxError) && !@options[:stop_on_error]
  381. had_error = true
  382. puts_action :error, :red, "#{error.sass_filename} (Line #{error.sass_line}: #{error.message})"
  383. end
  384. if @options[:update]
  385. ::Sass::Plugin.update_stylesheets(files)
  386. exit 1 if had_error
  387. return
  388. end
  389. puts ">>> Sass is watching for changes. Press Ctrl-C to stop."
  390. ::Sass::Plugin.on_template_modified {|template| puts ">>> Change detected to: #{template}"}
  391. ::Sass::Plugin.on_template_created {|template| puts ">>> New template detected: #{template}"}
  392. ::Sass::Plugin.on_template_deleted {|template| puts ">>> Deleted template detected: #{template}"}
  393. ::Sass::Plugin.watch(files)
  394. end
  395. def colon_path?(path)
  396. !split_colon_path(path)[1].nil?
  397. end
  398. def split_colon_path(path)
  399. one, two = path.split(':', 2)
  400. if one && two && ::Haml::Util.windows? &&
  401. one =~ /\A[A-Za-z]\Z/ && two =~ /\A[\/\\]/
  402. # If we're on Windows and we were passed a drive letter path,
  403. # don't split on that colon.
  404. one2, two = two.split(':', 2)
  405. one = one + ':' + one2
  406. end
  407. return one, two
  408. end
  409. # Whether path is likely to be meant as the destination
  410. # in a source:dest pair.
  411. def probably_dest_dir?(path)
  412. return false unless path
  413. return false if colon_path?(path)
  414. return Dir.glob(File.join(path, "*.s[ca]ss")).empty?
  415. end
  416. end
  417. # The `haml` executable.
  418. class Haml < HamlSass
  419. # @param args [Array<String>] The command-line arguments
  420. def initialize(args)
  421. super
  422. @name = "Haml"
  423. @options[:requires] = []
  424. @options[:load_paths] = []
  425. end
  426. # Tells optparse how to parse the arguments.
  427. #
  428. # @param opts [OptionParser]
  429. def set_opts(opts)
  430. super
  431. opts.on('-t', '--style NAME',
  432. 'Output style. Can be indented (default) or ugly.') do |name|
  433. @options[:for_engine][:ugly] = true if name.to_sym == :ugly
  434. end
  435. opts.on('-f', '--format NAME',
  436. 'Output format. Can be xhtml (default), html4, or html5.') do |name|
  437. @options[:for_engine][:format] = name.to_sym
  438. end
  439. opts.on('-e', '--escape-html',
  440. 'Escape HTML characters (like ampersands and angle brackets) by default.') do
  441. @options[:for_engine][:escape_html] = true
  442. end
  443. opts.on('-q', '--double-quote-attributes',
  444. 'Set attribute wrapper to double-quotes (default is single).') do
  445. @options[:for_engine][:attr_wrapper] = '"'
  446. end
  447. opts.on('-r', '--require FILE', "Same as 'ruby -r'.") do |file|
  448. @options[:requires] << file
  449. end
  450. opts.on('-I', '--load-path PATH', "Same as 'ruby -I'.") do |path|
  451. @options[:load_paths] << path
  452. end
  453. unless ::Haml::Util.ruby1_8?
  454. opts.on('-E ex[:in]', 'Specify the default external and internal character encodings.') do |encoding|
  455. external, internal = encoding.split(':')
  456. Encoding.default_external = external if external && !external.empty?
  457. Encoding.default_internal = internal if internal && !internal.empty?
  458. end
  459. end
  460. opts.on('--debug', "Print out the precompiled Ruby source.") do
  461. @options[:debug] = true
  462. end
  463. end
  464. # Processes the options set by the command-line arguments,
  465. # and runs the Haml compiler appropriately.
  466. def process_result
  467. super
  468. input = @options[:input]
  469. output = @options[:output]
  470. template = input.read()
  471. input.close() if input.is_a? File
  472. begin
  473. engine = ::Haml::Engine.new(template, @options[:for_engine])
  474. if @options[:check_syntax]
  475. puts "Syntax OK"
  476. return
  477. end
  478. @options[:load_paths].each {|p| $LOAD_PATH << p}
  479. @options[:requires].each {|f| require f}
  480. if @options[:debug]
  481. puts engine.precompiled
  482. puts '=' * 100
  483. end
  484. result = engine.to_html
  485. rescue Exception => e
  486. raise e if @options[:trace]
  487. case e
  488. when ::Haml::SyntaxError; raise "Syntax error on line #{get_line e}: #{e.message}"
  489. when ::Haml::Error; raise "Haml error on line #{get_line e}: #{e.message}"
  490. else raise "Exception on line #{get_line e}: #{e.message}\n Use --trace for backtrace."
  491. end
  492. end
  493. output.write(result)
  494. output.close() if output.is_a? File
  495. end
  496. end
  497. # The `html2haml` executable.
  498. class HTML2Haml < Generic
  499. # @param args [Array<String>] The command-line arguments
  500. def initialize(args)
  501. super
  502. @module_opts = {}
  503. end
  504. # Tells optparse how to parse the arguments.
  505. #
  506. # @param opts [OptionParser]
  507. def set_opts(opts)
  508. opts.banner = <<END
  509. Usage: html2haml [options] [INPUT] [OUTPUT]
  510. Description: Transforms an HTML file into corresponding Haml code.
  511. Options:
  512. END
  513. opts.on('-e', '--erb', 'Parse ERb tags.') do
  514. @module_opts[:erb] = true
  515. end
  516. opts.on('--no-erb', "Don't parse ERb tags.") do
  517. @options[:no_erb] = true
  518. end
  519. opts.on('-r', '--rhtml', 'Deprecated; same as --erb.') do
  520. @module_opts[:erb] = true
  521. end
  522. opts.on('--no-rhtml', "Deprecated; same as --no-erb.") do
  523. @options[:no_erb] = true
  524. end
  525. opts.on('-x', '--xhtml', 'Parse the input using the more strict XHTML parser.') do
  526. @module_opts[:xhtml] = true
  527. end
  528. super
  529. end
  530. # Processes the options set by the command-line arguments,
  531. # and runs the HTML compiler appropriately.
  532. def process_result
  533. super
  534. require 'haml/html'
  535. input = @options[:input]
  536. output = @options[:output]
  537. @module_opts[:erb] ||= input.respond_to?(:path) && input.path =~ /\.(rhtml|erb)$/
  538. @module_opts[:erb] &&= @options[:no_erb] != false
  539. output.write(::Haml::HTML.new(input, @module_opts).render)
  540. rescue ::Haml::Error => e
  541. raise "#{e.is_a?(::Haml::SyntaxError) ? "Syntax error" : "Error"} on line " +
  542. "#{get_line e}: #{e.message}"
  543. rescue LoadError => err
  544. handle_load_error(err)
  545. end
  546. end
  547. # The `sass-convert` executable.
  548. class SassConvert < Generic
  549. # @param args [Array<String>] The command-line arguments
  550. def initialize(args)
  551. super
  552. require 'sass'
  553. @options[:for_tree] = {}
  554. @options[:for_engine] = {:cache => false, :read_cache => true}
  555. end
  556. # Tells optparse how to parse the arguments.
  557. #
  558. # @param opts [OptionParser]
  559. def set_opts(opts)
  560. opts.banner = <<END
  561. Usage: sass-convert [options] [INPUT] [OUTPUT]
  562. Description:
  563. Converts between CSS, Sass, and SCSS files.
  564. E.g. converts from SCSS to Sass,
  565. or converts from CSS to SCSS (adding appropriate nesting).
  566. Options:
  567. END
  568. opts.on('-F', '--from FORMAT',
  569. 'The format to convert from. Can be css, scss, sass, less, or sass2.',
  570. 'sass2 is the same as sass, but updates more old syntax to new.',
  571. 'By default, this is inferred from the input filename.',
  572. 'If there is none, defaults to css.') do |name|
  573. @options[:from] = name.downcase.to_sym
  574. unless [:css, :scss, :sass, :less, :sass2].include?(@options[:from])
  575. raise "Unknown format for sass-convert --from: #{name}"
  576. end
  577. try_less_note if @options[:from] == :less
  578. end
  579. opts.on('-T', '--to FORMAT',
  580. 'The format to convert to. Can be scss or sass.',
  581. 'By default, this is inferred from the output filename.',
  582. 'If there is none, defaults to sass.') do |name|
  583. @options[:to] = name.downcase.to_sym
  584. unless [:scss, :sass].include?(@options[:to])
  585. raise "Unknown format for sass-convert --to: #{name}"
  586. end
  587. end
  588. opts.on('-R', '--recursive',
  589. 'Convert all the files in a directory. Requires --from and --to.') do
  590. @options[:recursive] = true
  591. end
  592. opts.on('-i', '--in-place',
  593. 'Convert a file to its own syntax.',
  594. 'This can be used to update some deprecated syntax.') do
  595. @options[:in_place] = true
  596. end
  597. opts.on('--dasherize', 'Convert underscores to dashes') do
  598. @options[:for_tree][:dasherize] = true
  599. end
  600. opts.on('--old', 'Output the old-style ":prop val" property syntax.',
  601. 'Only meaningful when generating Sass.') do
  602. @options[:for_tree][:old] = true
  603. end
  604. opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
  605. @options[:for_engine][:read_cache] = false
  606. end
  607. unless ::Haml::Util.ruby1_8?
  608. opts.on('-E encoding', 'Specify the default encoding for Sass and CSS files.') do |encoding|
  609. Encoding.default_external = encoding
  610. end
  611. end
  612. super
  613. end
  614. # Processes the options set by the command-line arguments,
  615. # and runs the CSS compiler appropriately.
  616. def process_result
  617. require 'sass'
  618. if @options[:recursive]
  619. process_directory
  620. return
  621. end
  622. super
  623. input = @options[:input]
  624. raise "Error: '#{input.path}' is a directory (did you mean to use --recursive?)" if File.directory?(input)
  625. output = @options[:output]
  626. output = input if @options[:in_place]
  627. process_file(input, output)
  628. end
  629. private
  630. def process_directory
  631. unless input = @options[:input] = @args.shift
  632. raise "Error: directory required when using --recursive."
  633. end
  634. output = @options[:output] = @args.shift
  635. raise "Error: --from required when using --recursive." unless @options[:from]
  636. raise "Error: --to required when using --recursive." unless @options[:to]
  637. raise "Error: '#{@options[:input]}' is not a directory" unless File.directory?(@options[:input])
  638. if @options[:output] && File.exists?(@options[:output]) && !File.directory?(@options[:output])
  639. raise "Error: '#{@options[:output]}' is not a directory"
  640. end
  641. @options[:output] ||= @options[:input]
  642. from = @options[:from]
  643. from = :sass if from == :sass2
  644. if @options[:to] == @options[:from] && !@options[:in_place]
  645. fmt = @options[:from]
  646. raise "Error: converting from #{fmt} to #{fmt} without --in-place"
  647. end
  648. ext = @options[:from]
  649. ext = :sass if ext == :sass2
  650. Dir.glob("#{@options[:input]}/**/*.#{ext}") do |f|
  651. output =
  652. if @options[:in_place]
  653. f
  654. elsif @options[:output]
  655. output_name = f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
  656. output_name[0...@options[:input].size] = @options[:output]
  657. output_name
  658. else
  659. f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
  660. end
  661. unless File.directory?(File.dirname(output))
  662. puts_action :directory, :green, File.dirname(output)
  663. FileUtils.mkdir_p(File.dirname(output))
  664. end
  665. puts_action :convert, :green, f
  666. if File.exists?(output)
  667. puts_action :overwrite, :yellow, output
  668. else
  669. puts_action :create, :green, output
  670. end
  671. input = open_file(f)
  672. output = @options[:in_place] ? input : open_file(output, "w")
  673. process_file(input, output)
  674. end
  675. end
  676. def process_file(input, output)
  677. if input.is_a?(File)
  678. @options[:from] ||=
  679. case input.path
  680. when /\.scss$/; :scss
  681. when /\.sass$/; :sass
  682. when /\.less$/; :less
  683. when /\.css$/; :css
  684. end
  685. elsif @options[:in_place]
  686. raise "Error: the --in-place option requires a filename."
  687. end
  688. if output.is_a?(File)
  689. @options[:to] ||=
  690. case output.path
  691. when /\.scss$/; :scss
  692. when /\.sass$/; :sass
  693. end
  694. end
  695. if @options[:from] == :sass2
  696. @options[:from] = :sass
  697. @options[:for_engine][:sass2] = true
  698. end
  699. @options[:from] ||= :css
  700. @options[:to] ||= :sass
  701. @options[:for_engine][:syntax] = @options[:from]
  702. out =
  703. ::Haml::Util.silence_haml_warnings do
  704. if @options[:from] == :css
  705. require 'sass/css'
  706. ::Sass::CSS.new(input.read, @options[:for_tree]).render(@options[:to])
  707. elsif @options[:from] == :less
  708. require 'sass/less'
  709. try_less_note
  710. input = input.read if input.is_a?(IO) && !input.is_a?(File) # Less is dumb
  711. Less::Engine.new(input).to_tree.to_sass_tree.send("to_#{@options[:to]}", @options[:for_tree])
  712. else
  713. if input.is_a?(File)
  714. ::Sass::Files.tree_for(input.path, @options[:for_engine])
  715. else
  716. ::Sass::Engine.new(input.read, @options[:for_engine]).to_tree
  717. end.send("to_#{@options[:to]}", @options[:for_tree])
  718. end
  719. end
  720. output = File.open(input.path, 'w') if @options[:in_place]
  721. output.write(out)
  722. rescue ::Sass::SyntaxError => e
  723. raise e if @options[:trace]
  724. file = " of #{e.sass_filename}" if e.sass_filename
  725. raise "Error on line #{e.sass_line}#{file}: #{e.message}\n Use --trace for backtrace"
  726. rescue LoadError => err
  727. handle_load_error(err)
  728. end
  729. @@less_note_printed = false
  730. def try_less_note
  731. return if @@less_note_printed
  732. @@less_note_printed = true
  733. warn <<NOTE
  734. * NOTE: Sass and Less are different languages, and they work differently.
  735. * I'll do my best to translate, but some features -- especially mixins --
  736. * should be checked by hand.
  737. NOTE
  738. end
  739. end
  740. end
  741. end