html2haml 1.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/.travis.yml +14 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +18 -0
- data/README.md +40 -0
- data/Rakefile +25 -0
- data/bin/html2haml +7 -0
- data/html2haml.gemspec +25 -0
- data/lib/html2haml.rb +8 -0
- data/lib/html2haml/exec.rb +260 -0
- data/lib/html2haml/html.rb +423 -0
- data/lib/html2haml/html/erb.rb +153 -0
- data/lib/html2haml/version.rb +3 -0
- data/test/erb_test.rb +477 -0
- data/test/html2haml_test.rb +323 -0
- data/test/test_helper.rb +22 -0
- metadata +180 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2006-2012 Hampton Catlin, Nathan Weizenbaum and Norman Clarke
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Html2haml
|
2
|
+
|
3
|
+
Converts HTML to Haml. This is in the process of being extracted from the Haml
|
4
|
+
gem.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'html2haml'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install html2haml
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
See `html2haml --help`:
|
23
|
+
|
24
|
+
Usage: html2haml [options] [INPUT] [OUTPUT]
|
25
|
+
|
26
|
+
Description: Transforms an HTML file into corresponding Haml code.
|
27
|
+
|
28
|
+
Options:
|
29
|
+
-e, --erb Parse ERb tags.
|
30
|
+
--no-erb Don't parse ERb tags.
|
31
|
+
-r, --rhtml Deprecated; same as --erb.
|
32
|
+
--no-rhtml Deprecated; same as --no-erb.
|
33
|
+
-x, --xhtml Parse the input using the more strict XHTML parser.
|
34
|
+
--html-attributes Use HTML style attributes instead of Ruby hash style.
|
35
|
+
-E ex[:in] Specify the default external and internal character encodings.
|
36
|
+
-s, --stdin Read input from standard input instead of an input file
|
37
|
+
--trace Show a full traceback on error
|
38
|
+
--unix-newlines Use Unix-style newlines in written files.
|
39
|
+
-?, -h, --help Show this message
|
40
|
+
-v, --version Print version
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "rake/clean"
|
2
|
+
require "rake/testtask"
|
3
|
+
require "rubygems/package_task"
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
CLEAN.replace %w(pkg doc coverage .yardoc)
|
8
|
+
|
9
|
+
Rake::TestTask.new do |t|
|
10
|
+
t.libs << 'lib' << 'test'
|
11
|
+
t.test_files = Dir["test/**/*_test.rb"]
|
12
|
+
t.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
task :set_coverage_env do
|
16
|
+
ENV["COVERAGE"] = "true"
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "Run Simplecov (only works on 1.9)"
|
20
|
+
task :coverage => [:set_coverage_env, :test]
|
21
|
+
|
22
|
+
gemspec = File.expand_path("../html2haml.gemspec", __FILE__)
|
23
|
+
if File.exist? gemspec
|
24
|
+
Gem::PackageTask.new(eval(File.read(gemspec))) { |pkg| }
|
25
|
+
end
|
data/bin/html2haml
ADDED
data/html2haml.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/html2haml/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Norman Clarke"]
|
6
|
+
gem.email = ["norman@njclarke.com"]
|
7
|
+
gem.description = %q{Converts HTML into Haml}
|
8
|
+
gem.summary = %q{Converts HTML into Haml}
|
9
|
+
gem.homepage = "http://haml.info"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "html2haml"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Html2haml::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'hpricot'
|
19
|
+
gem.add_dependency 'haml', '>= 3.2.0.beta.1'
|
20
|
+
gem.add_development_dependency 'erubis'
|
21
|
+
gem.add_development_dependency 'ruby_parser'
|
22
|
+
gem.add_development_dependency 'simplecov'
|
23
|
+
gem.add_development_dependency 'rake'
|
24
|
+
gem.add_development_dependency 'minitest'
|
25
|
+
end
|
data/lib/html2haml.rb
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'rbconfig'
|
4
|
+
|
5
|
+
module Html2haml
|
6
|
+
# This module handles the various Haml executables (`haml` and `haml-convert`).
|
7
|
+
module Exec
|
8
|
+
# An abstract class that encapsulates the executable code for all three executables.
|
9
|
+
class Generic
|
10
|
+
# @param args [Array<String>] The command-line arguments
|
11
|
+
def initialize(args)
|
12
|
+
@args = args
|
13
|
+
@options = {:for_engine => {}}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Parses the command-line arguments and runs the executable.
|
17
|
+
# Calls `Kernel#exit` at the end, so it never returns.
|
18
|
+
#
|
19
|
+
# @see #parse
|
20
|
+
def parse!
|
21
|
+
begin
|
22
|
+
parse
|
23
|
+
rescue Exception => e
|
24
|
+
raise e if @options[:trace] || e.is_a?(SystemExit)
|
25
|
+
|
26
|
+
$stderr.print "#{e.class}: " unless e.class == RuntimeError
|
27
|
+
$stderr.puts "#{e.message}"
|
28
|
+
$stderr.puts " Use --trace for backtrace."
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
exit 0
|
32
|
+
end
|
33
|
+
|
34
|
+
# Parses the command-line arguments and runs the executable.
|
35
|
+
# This does not handle exceptions or exit the program.
|
36
|
+
#
|
37
|
+
# @see #parse!
|
38
|
+
def parse
|
39
|
+
@opts = OptionParser.new(&method(:set_opts))
|
40
|
+
@opts.parse!(@args)
|
41
|
+
|
42
|
+
process_result
|
43
|
+
|
44
|
+
@options
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [String] A description of the executable
|
48
|
+
def to_s
|
49
|
+
@opts.to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
# Finds the line of the source template
|
55
|
+
# on which an exception was raised.
|
56
|
+
#
|
57
|
+
# @param exception [Exception] The exception
|
58
|
+
# @return [String] The line number
|
59
|
+
def get_line(exception)
|
60
|
+
# SyntaxErrors have weird line reporting
|
61
|
+
# when there's trailing whitespace,
|
62
|
+
# which there is for Haml documents.
|
63
|
+
return (exception.message.scan(/:(\d+)/).first || ["??"]).first if exception.is_a?(::SyntaxError)
|
64
|
+
(exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first
|
65
|
+
end
|
66
|
+
|
67
|
+
# Tells optparse how to parse the arguments
|
68
|
+
# available for all executables.
|
69
|
+
#
|
70
|
+
# This is meant to be overridden by subclasses
|
71
|
+
# so they can add their own options.
|
72
|
+
#
|
73
|
+
# @param opts [OptionParser]
|
74
|
+
def set_opts(opts)
|
75
|
+
opts.on('-s', '--stdin', :NONE, 'Read input from standard input instead of an input file') do
|
76
|
+
@options[:input] = $stdin
|
77
|
+
end
|
78
|
+
|
79
|
+
opts.on('--trace', :NONE, 'Show a full traceback on error') do
|
80
|
+
@options[:trace] = true
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on('--unix-newlines', 'Use Unix-style newlines in written files.') do
|
84
|
+
# Note that this is the preferred way to check for Windows, since
|
85
|
+
# JRuby and Rubinius also run there.
|
86
|
+
if RbConfig::CONFIG['host_os'] =~ /mswin|windows|mingw/i
|
87
|
+
@options[:unix_newlines] = true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
opts.on_tail("-?", "-h", "--help", "Show this message") do
|
92
|
+
puts opts
|
93
|
+
exit
|
94
|
+
end
|
95
|
+
|
96
|
+
opts.on_tail("-v", "--version", "Print version") do
|
97
|
+
puts("html2haml #{::Html2haml::VERSION}")
|
98
|
+
exit
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Processes the options set by the command-line arguments.
|
103
|
+
# In particular, sets `@options[:input]` and `@options[:output]`
|
104
|
+
# to appropriate IO streams.
|
105
|
+
#
|
106
|
+
# This is meant to be overridden by subclasses
|
107
|
+
# so they can run their respective programs.
|
108
|
+
def process_result
|
109
|
+
input, output = @options[:input], @options[:output]
|
110
|
+
args = @args.dup
|
111
|
+
input ||=
|
112
|
+
begin
|
113
|
+
filename = args.shift
|
114
|
+
@options[:filename] = filename
|
115
|
+
open_file(filename) || $stdin
|
116
|
+
end
|
117
|
+
output ||= open_file(args.shift, 'w') || $stdout
|
118
|
+
|
119
|
+
@options[:input], @options[:output] = input, output
|
120
|
+
end
|
121
|
+
|
122
|
+
COLORS = { :red => 31, :green => 32, :yellow => 33 }
|
123
|
+
|
124
|
+
# Prints a status message about performing the given action,
|
125
|
+
# colored using the given color (via terminal escapes) if possible.
|
126
|
+
#
|
127
|
+
# @param name [#to_s] A short name for the action being performed.
|
128
|
+
# Shouldn't be longer than 11 characters.
|
129
|
+
# @param color [Symbol] The name of the color to use for this action.
|
130
|
+
# Can be `:red`, `:green`, or `:yellow`.
|
131
|
+
def puts_action(name, color, arg)
|
132
|
+
return if @options[:for_engine][:quiet]
|
133
|
+
printf color(color, "%11s %s\n"), name, arg
|
134
|
+
end
|
135
|
+
|
136
|
+
# Same as `Kernel.puts`, but doesn't print anything if the `--quiet` option is set.
|
137
|
+
#
|
138
|
+
# @param args [Array] Passed on to `Kernel.puts`
|
139
|
+
def puts(*args)
|
140
|
+
return if @options[:for_engine][:quiet]
|
141
|
+
Kernel.puts(*args)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Wraps the given string in terminal escapes
|
145
|
+
# causing it to have the given color.
|
146
|
+
# If terminal esapes aren't supported on this platform,
|
147
|
+
# just returns the string instead.
|
148
|
+
#
|
149
|
+
# @param color [Symbol] The name of the color to use.
|
150
|
+
# Can be `:red`, `:green`, or `:yellow`.
|
151
|
+
# @param str [String] The string to wrap in the given color.
|
152
|
+
# @return [String] The wrapped string.
|
153
|
+
def color(color, str)
|
154
|
+
raise "[BUG] Unrecognized color #{color}" unless COLORS[color]
|
155
|
+
|
156
|
+
# Almost any real Unix terminal will support color,
|
157
|
+
# so we just filter for Windows terms (which don't set TERM)
|
158
|
+
# and not-real terminals, which aren't ttys.
|
159
|
+
return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty?
|
160
|
+
return "\e[#{COLORS[color]}m#{str}\e[0m"
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def open_file(filename, flag = 'r')
|
166
|
+
return if filename.nil?
|
167
|
+
flag = 'wb' if @options[:unix_newlines] && flag == 'w'
|
168
|
+
File.open(filename, flag)
|
169
|
+
end
|
170
|
+
|
171
|
+
def handle_load_error(err)
|
172
|
+
dep = err.message[/^no such file to load -- (.*)/, 1]
|
173
|
+
raise err if @options[:trace] || dep.nil? || dep.empty?
|
174
|
+
$stderr.puts <<MESSAGE
|
175
|
+
Required dependency #{dep} not found!
|
176
|
+
Run "gem install #{dep}" to get it.
|
177
|
+
Use --trace for backtrace.
|
178
|
+
MESSAGE
|
179
|
+
exit 1
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# The `html2haml` executable.
|
184
|
+
class HTML2Haml < Generic
|
185
|
+
# @param args [Array<String>] The command-line arguments
|
186
|
+
def initialize(args)
|
187
|
+
super
|
188
|
+
@module_opts = {}
|
189
|
+
end
|
190
|
+
|
191
|
+
# Tells optparse how to parse the arguments.
|
192
|
+
#
|
193
|
+
# @param opts [OptionParser]
|
194
|
+
def set_opts(opts)
|
195
|
+
opts.banner = <<END
|
196
|
+
Usage: html2haml [options] [INPUT] [OUTPUT]
|
197
|
+
|
198
|
+
Description: Transforms an HTML file into corresponding Haml code.
|
199
|
+
|
200
|
+
Options:
|
201
|
+
END
|
202
|
+
|
203
|
+
opts.on('-e', '--erb', 'Parse ERb tags.') do
|
204
|
+
@module_opts[:erb] = true
|
205
|
+
end
|
206
|
+
|
207
|
+
opts.on('--no-erb', "Don't parse ERb tags.") do
|
208
|
+
@options[:no_erb] = true
|
209
|
+
end
|
210
|
+
|
211
|
+
opts.on('-r', '--rhtml', 'Deprecated; same as --erb.') do
|
212
|
+
@module_opts[:erb] = true
|
213
|
+
end
|
214
|
+
|
215
|
+
opts.on('--no-rhtml', "Deprecated; same as --no-erb.") do
|
216
|
+
@options[:no_erb] = true
|
217
|
+
end
|
218
|
+
|
219
|
+
opts.on('-x', '--xhtml', 'Parse the input using the more strict XHTML parser.') do
|
220
|
+
@module_opts[:xhtml] = true
|
221
|
+
end
|
222
|
+
|
223
|
+
opts.on("--html-attributes", "Use HTML style attributes instead of Ruby hash style.") do
|
224
|
+
@module_opts[:html_style_attributes] = true
|
225
|
+
end
|
226
|
+
|
227
|
+
unless RUBY_VERSION < "1.9"
|
228
|
+
opts.on('-E ex[:in]', 'Specify the default external and internal character encodings.') do |encoding|
|
229
|
+
external, internal = encoding.split(':')
|
230
|
+
Encoding.default_external = external if external && !external.empty?
|
231
|
+
Encoding.default_internal = internal if internal && !internal.empty?
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
super
|
236
|
+
end
|
237
|
+
|
238
|
+
# Processes the options set by the command-line arguments,
|
239
|
+
# and runs the HTML compiler appropriately.
|
240
|
+
def process_result
|
241
|
+
super
|
242
|
+
|
243
|
+
require 'html2haml/html'
|
244
|
+
|
245
|
+
input = @options[:input]
|
246
|
+
output = @options[:output]
|
247
|
+
|
248
|
+
@module_opts[:erb] ||= input.respond_to?(:path) && input.path =~ /\.(rhtml|erb)$/
|
249
|
+
@module_opts[:erb] &&= @options[:no_erb] != false
|
250
|
+
|
251
|
+
output.write(::Haml::HTML.new(input, @module_opts).render)
|
252
|
+
rescue ::Haml::Error => e
|
253
|
+
raise "#{e.is_a?(::Haml::SyntaxError) ? "Syntax error" : "Error"} on line " +
|
254
|
+
"#{get_line e}: #{e.message}"
|
255
|
+
rescue LoadError => err
|
256
|
+
handle_load_error(err)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,423 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'cgi'
|
3
|
+
require 'hpricot'
|
4
|
+
require 'html2haml/html/erb'
|
5
|
+
|
6
|
+
# Haml monkeypatches various Hpricot classes
|
7
|
+
# to add methods for conversion to Haml.
|
8
|
+
# @private
|
9
|
+
module Hpricot
|
10
|
+
# @see Hpricot
|
11
|
+
module Node
|
12
|
+
# Whether this node has already been converted to Haml.
|
13
|
+
# Only used for text nodes and elements.
|
14
|
+
#
|
15
|
+
# @return [Boolean]
|
16
|
+
attr_accessor :converted_to_haml
|
17
|
+
|
18
|
+
# Returns the Haml representation of the given node.
|
19
|
+
#
|
20
|
+
# @param tabs [Fixnum] The indentation level of the resulting Haml.
|
21
|
+
# @option options (see Haml::HTML#initialize)
|
22
|
+
def to_haml(tabs, options)
|
23
|
+
return "" if converted_to_haml || to_s.strip.empty?
|
24
|
+
text = uninterp(self.to_s)
|
25
|
+
node = next_node
|
26
|
+
while node.is_a?(::Hpricot::Elem) && node.name == "haml:loud"
|
27
|
+
node.converted_to_haml = true
|
28
|
+
text << '#{' <<
|
29
|
+
CGI.unescapeHTML(node.inner_text).gsub(/\n\s*/, ' ').strip << '}'
|
30
|
+
|
31
|
+
if node.next_node.is_a?(::Hpricot::Text)
|
32
|
+
node = node.next_node
|
33
|
+
text << uninterp(node.to_s)
|
34
|
+
node.converted_to_haml = true
|
35
|
+
end
|
36
|
+
|
37
|
+
node = node.next_node
|
38
|
+
end
|
39
|
+
return parse_text_with_interpolation(text, tabs)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def erb_to_interpolation(text, options)
|
45
|
+
return text unless options[:erb]
|
46
|
+
text = CGI.escapeHTML(uninterp(text))
|
47
|
+
%w[<haml:loud> </haml:loud>].each {|str| text.gsub!(CGI.escapeHTML(str), str)}
|
48
|
+
::Hpricot::XML(text).children.inject("") do |str, elem|
|
49
|
+
if elem.is_a?(::Hpricot::Text)
|
50
|
+
str + CGI.unescapeHTML(elem.to_s)
|
51
|
+
else # <haml:loud> element
|
52
|
+
str + '#{' + CGI.unescapeHTML(elem.innerText.strip) + '}'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def tabulate(tabs)
|
58
|
+
' ' * tabs
|
59
|
+
end
|
60
|
+
|
61
|
+
def uninterp(text)
|
62
|
+
text.gsub('#{', '\#{') #'
|
63
|
+
end
|
64
|
+
|
65
|
+
def attr_hash
|
66
|
+
attributes.to_hash
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_text(text, tabs)
|
70
|
+
parse_text_with_interpolation(uninterp(text), tabs)
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_text_with_interpolation(text, tabs)
|
74
|
+
text.strip!
|
75
|
+
return "" if text.empty?
|
76
|
+
|
77
|
+
text.split("\n").map do |line|
|
78
|
+
line.strip!
|
79
|
+
"#{tabulate(tabs)}#{'\\' if Haml::Parser::SPECIAL_CHARACTERS.include?(line[0])}#{line}\n"
|
80
|
+
end.join
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @private
|
86
|
+
HAML_TAGS = %w[haml:block haml:loud haml:silent]
|
87
|
+
|
88
|
+
HAML_TAGS.each do |t|
|
89
|
+
Hpricot::ElementContent[t] = {}
|
90
|
+
Hpricot::ElementContent.keys.each do |key|
|
91
|
+
Hpricot::ElementContent[t][key.hash] = true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
Hpricot::ElementContent.keys.each do |k|
|
96
|
+
HAML_TAGS.each do |el|
|
97
|
+
val = Hpricot::ElementContent[k]
|
98
|
+
val[el.hash] = true if val.is_a?(Hash)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
module Haml
|
103
|
+
# Converts HTML documents into Haml templates.
|
104
|
+
# Depends on [Hpricot](http://github.com/whymirror/hpricot) for HTML parsing.
|
105
|
+
# If ERB conversion is being used, also depends on
|
106
|
+
# [Erubis](http://www.kuwata-lab.com/erubis) to parse the ERB
|
107
|
+
# and [ruby_parser](http://parsetree.rubyforge.org/) to parse the Ruby code.
|
108
|
+
#
|
109
|
+
# Example usage:
|
110
|
+
#
|
111
|
+
# Haml::HTML.new("<a href='http://google.com'>Blat</a>").render
|
112
|
+
# #=> "%a{:href => 'http://google.com'} Blat"
|
113
|
+
class HTML
|
114
|
+
# @param template [String, Hpricot::Node] The HTML template to convert
|
115
|
+
# @option options :erb [Boolean] (false) Whether or not to parse
|
116
|
+
# ERB's `<%= %>` and `<% %>` into Haml's `=` and `-`
|
117
|
+
# @option options :xhtml [Boolean] (false) Whether or not to parse
|
118
|
+
# the HTML strictly as XHTML
|
119
|
+
def initialize(template, options = {})
|
120
|
+
@options = options
|
121
|
+
|
122
|
+
if template.is_a? Hpricot::Node
|
123
|
+
@template = template
|
124
|
+
else
|
125
|
+
if template.is_a? IO
|
126
|
+
template = template.read
|
127
|
+
end
|
128
|
+
|
129
|
+
template = Haml::Util.check_encoding(template) {|msg, line| raise Haml::Error.new(msg, line)}
|
130
|
+
|
131
|
+
if @options[:erb]
|
132
|
+
require 'html2haml/html/erb'
|
133
|
+
template = ERB.compile(template)
|
134
|
+
end
|
135
|
+
|
136
|
+
method = @options[:xhtml] ? Hpricot.method(:XML) : method(:Hpricot)
|
137
|
+
@template = method.call(template.gsub('&', '&'))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Processes the document and returns the result as a string
|
142
|
+
# containing the Haml template.
|
143
|
+
def render
|
144
|
+
@template.to_haml(0, @options)
|
145
|
+
end
|
146
|
+
alias_method :to_haml, :render
|
147
|
+
|
148
|
+
TEXT_REGEXP = /^(\s*).*$/
|
149
|
+
|
150
|
+
# @see Hpricot
|
151
|
+
# @private
|
152
|
+
class ::Hpricot::Doc
|
153
|
+
# @see Haml::HTML::Node#to_haml
|
154
|
+
def to_haml(tabs, options)
|
155
|
+
(children || []).inject('') {|s, c| s << c.to_haml(0, options)}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# @see Hpricot
|
160
|
+
# @private
|
161
|
+
class ::Hpricot::XMLDecl
|
162
|
+
# @see Haml::HTML::Node#to_haml
|
163
|
+
def to_haml(tabs, options)
|
164
|
+
"#{tabulate(tabs)}!!! XML\n"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# @see Hpricot
|
169
|
+
# @private
|
170
|
+
class ::Hpricot::CData
|
171
|
+
# @see Haml::HTML::Node#to_haml
|
172
|
+
def to_haml(tabs, options)
|
173
|
+
content = parse_text_with_interpolation(
|
174
|
+
erb_to_interpolation(self.content, options), tabs + 1)
|
175
|
+
"#{tabulate(tabs)}:cdata\n#{content}"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# @see Hpricot
|
180
|
+
# @private
|
181
|
+
class ::Hpricot::DocType
|
182
|
+
# @see Haml::HTML::Node#to_haml
|
183
|
+
def to_haml(tabs, options)
|
184
|
+
attrs = public_id.nil? ? ["", "", ""] :
|
185
|
+
public_id.scan(/DTD\s+([^\s]+)\s*([^\s]*)\s*([^\s]*)\s*\/\//)[0]
|
186
|
+
raise Haml::SyntaxError.new("Invalid doctype") if attrs == nil
|
187
|
+
|
188
|
+
type, version, strictness = attrs.map { |a| a.downcase }
|
189
|
+
if type == "html"
|
190
|
+
version = ""
|
191
|
+
strictness = "strict" if strictness == ""
|
192
|
+
end
|
193
|
+
|
194
|
+
if version == "1.0" || version.empty?
|
195
|
+
version = nil
|
196
|
+
end
|
197
|
+
|
198
|
+
if strictness == 'transitional' || strictness.empty?
|
199
|
+
strictness = nil
|
200
|
+
end
|
201
|
+
|
202
|
+
version = " #{version.capitalize}" if version
|
203
|
+
strictness = " #{strictness.capitalize}" if strictness
|
204
|
+
|
205
|
+
"#{tabulate(tabs)}!!!#{version}#{strictness}\n"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# @see Hpricot
|
210
|
+
# @private
|
211
|
+
class ::Hpricot::Comment
|
212
|
+
# @see Haml::HTML::Node#to_haml
|
213
|
+
def to_haml(tabs, options)
|
214
|
+
content = self.content
|
215
|
+
if content =~ /\A(\[[^\]]+\])>(.*)<!\[endif\]\z/m
|
216
|
+
condition = $1
|
217
|
+
content = $2
|
218
|
+
end
|
219
|
+
|
220
|
+
if content.include?("\n")
|
221
|
+
"#{tabulate(tabs)}/#{condition}\n#{parse_text(content, tabs + 1)}"
|
222
|
+
else
|
223
|
+
"#{tabulate(tabs)}/#{condition} #{content.strip}\n"
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# @see Hpricot
|
229
|
+
# @private
|
230
|
+
class ::Hpricot::Elem
|
231
|
+
# @see Haml::HTML::Node#to_haml
|
232
|
+
def to_haml(tabs, options)
|
233
|
+
return "" if converted_to_haml
|
234
|
+
if name == "script" &&
|
235
|
+
(attr_hash['type'].nil? || attr_hash['type'] == "text/javascript") &&
|
236
|
+
(attr_hash.keys - ['type']).empty?
|
237
|
+
return to_haml_filter(:javascript, tabs, options)
|
238
|
+
elsif name == "style" &&
|
239
|
+
(attr_hash['type'].nil? || attr_hash['type'] == "text/css") &&
|
240
|
+
(attr_hash.keys - ['type']).empty?
|
241
|
+
return to_haml_filter(:css, tabs, options)
|
242
|
+
end
|
243
|
+
|
244
|
+
output = tabulate(tabs)
|
245
|
+
if options[:erb] && name[0...5] == 'haml:'
|
246
|
+
case name[5..-1]
|
247
|
+
when "loud"
|
248
|
+
lines = CGI.unescapeHTML(inner_text).split("\n").
|
249
|
+
map {|s| s.rstrip}.reject {|s| s.strip.empty?}
|
250
|
+
lines.first.gsub!(/^[ \t]*/, "= ")
|
251
|
+
|
252
|
+
if lines.size > 1 # Multiline script block
|
253
|
+
# Normalize the indentation so that the last line is the base
|
254
|
+
indent_str = lines.last[/^[ \t]*/]
|
255
|
+
indent_re = /^[ \t]{0,#{indent_str.count(" ") + 8 * indent_str.count("\t")}}/
|
256
|
+
lines.map! {|s| s.gsub!(indent_re, '')}
|
257
|
+
|
258
|
+
# Add an extra " " to make it indented relative to "= "
|
259
|
+
lines[1..-1].each {|s| s.gsub!(/^/, " ")}
|
260
|
+
|
261
|
+
# Add | at the end, properly aligned
|
262
|
+
length = lines.map {|s| s.size}.max + 1
|
263
|
+
lines.map! {|s| "%#{-length}s|" % s}
|
264
|
+
|
265
|
+
if next_sibling && next_sibling.is_a?(Hpricot::Elem) && next_sibling.name == "haml:loud" &&
|
266
|
+
next_sibling.inner_text.split("\n").reject {|s| s.strip.empty?}.size > 1
|
267
|
+
lines << "-#"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
return lines.map {|s| output + s + "\n"}.join
|
271
|
+
when "silent"
|
272
|
+
return CGI.unescapeHTML(inner_text).split("\n").map do |line|
|
273
|
+
next "" if line.strip.empty?
|
274
|
+
"#{output}- #{line.strip}\n"
|
275
|
+
end.join
|
276
|
+
when "block"
|
277
|
+
return render_children("", tabs, options)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
if self.next && self.next.text? && self.next.content =~ /\A[^\s]/
|
282
|
+
if self.previous.nil? || self.previous.text? &&
|
283
|
+
(self.previous.content =~ /[^\s]\Z/ ||
|
284
|
+
self.previous.content =~ /\A\s*\Z/ && self.previous.previous.nil?)
|
285
|
+
nuke_outer_whitespace = true
|
286
|
+
else
|
287
|
+
output << "= succeed #{self.next.content.slice!(/\A[^\s]+/).dump} do\n"
|
288
|
+
tabs += 1
|
289
|
+
output << tabulate(tabs)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
output << "%#{name}" unless name == 'div' &&
|
294
|
+
(static_id?(options) ||
|
295
|
+
static_classname?(options) &&
|
296
|
+
attr_hash['class'].split(' ').any?(&method(:haml_css_attr?)))
|
297
|
+
|
298
|
+
if attr_hash
|
299
|
+
if static_id?(options)
|
300
|
+
output << "##{attr_hash['id']}"
|
301
|
+
remove_attribute('id')
|
302
|
+
end
|
303
|
+
if static_classname?(options)
|
304
|
+
leftover = attr_hash['class'].split(' ').reject do |c|
|
305
|
+
next unless haml_css_attr?(c)
|
306
|
+
output << ".#{c}"
|
307
|
+
end
|
308
|
+
remove_attribute('class')
|
309
|
+
set_attribute('class', leftover.join(' ')) unless leftover.empty?
|
310
|
+
end
|
311
|
+
output << haml_attributes(options) if attr_hash.length > 0
|
312
|
+
end
|
313
|
+
|
314
|
+
output << ">" if nuke_outer_whitespace
|
315
|
+
output << "/" if empty? && !etag
|
316
|
+
|
317
|
+
if children && children.size == 1
|
318
|
+
child = children.first
|
319
|
+
if child.is_a?(::Hpricot::Text)
|
320
|
+
if !child.to_s.include?("\n")
|
321
|
+
text = child.to_haml(tabs + 1, options)
|
322
|
+
return output + " " + text.lstrip.gsub(/^\\/, '') unless text.chomp.include?("\n")
|
323
|
+
return output + "\n" + text
|
324
|
+
elsif ["pre", "textarea"].include?(name) ||
|
325
|
+
(name == "code" && parent.is_a?(::Hpricot::Elem) && parent.name == "pre")
|
326
|
+
return output + "\n#{tabulate(tabs + 1)}:preserve\n" +
|
327
|
+
innerText.gsub(/^/, tabulate(tabs + 2))
|
328
|
+
end
|
329
|
+
elsif child.is_a?(::Hpricot::Elem) && child.name == "haml:loud"
|
330
|
+
return output + child.to_haml(tabs + 1, options).lstrip
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
render_children(output + "\n", tabs, options)
|
335
|
+
end
|
336
|
+
|
337
|
+
private
|
338
|
+
|
339
|
+
def render_children(so_far, tabs, options)
|
340
|
+
(self.children || []).inject(so_far) do |output, child|
|
341
|
+
output + child.to_haml(tabs + 1, options)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
def dynamic_attributes
|
346
|
+
@dynamic_attributes ||= begin
|
347
|
+
Hash[attr_hash.map do |name, value|
|
348
|
+
next if value.empty?
|
349
|
+
full_match = nil
|
350
|
+
ruby_value = value.gsub(%r{<haml:loud>\s*(.+?)\s*</haml:loud>}) do
|
351
|
+
full_match = $`.empty? && $'.empty?
|
352
|
+
CGI.unescapeHTML(full_match ? $1: "\#{#{$1}}")
|
353
|
+
end
|
354
|
+
next if ruby_value == value
|
355
|
+
[name, full_match ? ruby_value : %("#{ruby_value}")]
|
356
|
+
end]
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def to_haml_filter(filter, tabs, options)
|
361
|
+
content =
|
362
|
+
if children.first.is_a?(::Hpricot::CData)
|
363
|
+
children.first.content
|
364
|
+
else
|
365
|
+
CGI.unescapeHTML(self.innerText)
|
366
|
+
end
|
367
|
+
|
368
|
+
content = erb_to_interpolation(content, options)
|
369
|
+
content.gsub!(/\A\s*\n(\s*)/, '\1')
|
370
|
+
original_indent = content[/\A(\s*)/, 1]
|
371
|
+
if content.split("\n").all? {|l| l.strip.empty? || l =~ /^#{original_indent}/}
|
372
|
+
content.gsub!(/^#{original_indent}/, tabulate(tabs + 1))
|
373
|
+
end
|
374
|
+
|
375
|
+
"#{tabulate(tabs)}:#{filter}\n#{content}"
|
376
|
+
end
|
377
|
+
|
378
|
+
def static_attribute?(name, options)
|
379
|
+
attr_hash[name] && !dynamic_attribute?(name, options)
|
380
|
+
end
|
381
|
+
|
382
|
+
def dynamic_attribute?(name, options)
|
383
|
+
options[:erb] and dynamic_attributes.key?(name)
|
384
|
+
end
|
385
|
+
|
386
|
+
def static_id?(options)
|
387
|
+
static_attribute?('id', options) && haml_css_attr?(attr_hash['id'])
|
388
|
+
end
|
389
|
+
|
390
|
+
def static_classname?(options)
|
391
|
+
static_attribute?('class', options)
|
392
|
+
end
|
393
|
+
|
394
|
+
def haml_css_attr?(attr)
|
395
|
+
attr =~ /^[-:\w]+$/
|
396
|
+
end
|
397
|
+
|
398
|
+
# Returns a string representation of an attributes hash
|
399
|
+
# that's prettier than that produced by Hash#inspect
|
400
|
+
def haml_attributes(options)
|
401
|
+
attrs = attr_hash.sort.map do |name, value|
|
402
|
+
haml_attribute_pair(name, value, options)
|
403
|
+
end
|
404
|
+
if options[:html_style_attributes]
|
405
|
+
"(#{attrs.join(' ')})"
|
406
|
+
else
|
407
|
+
"{#{attrs.join(', ')}}"
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
# Returns the string representation of a single attribute key value pair
|
412
|
+
def haml_attribute_pair(name, value, options)
|
413
|
+
value = dynamic_attribute?(name, options) ? dynamic_attributes[name] : value.inspect
|
414
|
+
if options[:html_style_attributes]
|
415
|
+
"#{name}=#{value}"
|
416
|
+
else
|
417
|
+
name = name.index(/\W/) ? name.inspect : ":#{name}"
|
418
|
+
"#{name} => #{value}"
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|