html2fortitude 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fc1089b4f31923e3b4b0168354cd980e2d42873c
4
+ data.tar.gz: 468732b99cc8f73a0ea8e85214c46fbf9455d539
5
+ SHA512:
6
+ metadata.gz: e028d3c04146e3f1e5fee458b53a4851ea6528ea884a4b607d96b4f96c66ab3875dbf1a20041dd77e746250b3ca3bae7891a4a23f3707c1753428cc241da79c9
7
+ data.tar.gz: eabead76d22e3b8c5bd2a49b9684dfcaae0d39a27570af09e377b1d298f36c2f1d6dbbe8a7daaa8c6b93bc0c5051ddeb46d6784fd1f64ca239ba78dcffca48db
@@ -0,0 +1,10 @@
1
+ /.yardoc
2
+ /coverage
3
+ /doc
4
+ /pkg
5
+ *.rbc
6
+ .rbenv-version
7
+ Gemfile.lock
8
+ .rvmrc
9
+ .rbx
10
+ tmp
@@ -0,0 +1,4 @@
1
+ --format documentation
2
+ --color
3
+ --order random
4
+ -r helpers/global_helper
@@ -0,0 +1,16 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.0
4
+ - 2.0.0
5
+ - 1.9.3
6
+ - rbx-2
7
+ - jruby-19mode
8
+
9
+ gemfile:
10
+ - Gemfile
11
+
12
+ branches:
13
+ only:
14
+ - master
15
+
16
+ script: "bundle exec rake test"
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'fortitude', :path => '../fortitude'
@@ -0,0 +1,38 @@
1
+ Copyright (c) 2014 Andrew Geweke
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.
19
+
20
+
21
+ Copyright (c) 2006-2013 Hampton Catlin, Nathan Weizenbaum and Norman Clarke
22
+
23
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
24
+ this software and associated documentation files (the "Software"), to deal in
25
+ the Software without restriction, including without limitation the rights to
26
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
27
+ the Software, and to permit persons to whom the Software is furnished to do so,
28
+ subject to the following conditions:
29
+
30
+ The above copyright notice and this permission notice shall be included in all
31
+ copies or substantial portions of the Software.
32
+
33
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
35
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
36
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
37
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
38
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,97 @@
1
+ # `html2fortitude`
2
+
3
+ `html2fortitude` converts HTML to [Fortitude](http://github.com/ageweke/fortitude). It works on HTML with
4
+ embedded ERB tags as well as plain old HTML.
5
+
6
+ `html2fortitude` is a heavily-modified fork of [`html2haml`](https://github.com/haml/html2haml), version 2.0.0beta1.
7
+ Many, many thanks to [Stefan Natchev](https://github.com/snatchev), [Norman Clarke](https://github.com/norman),
8
+ Hampton Catlin, and Nathan Weizenbaum for all their work on `html2haml`, and for the excellent idea of transforming
9
+ ERb into valid HTML with ERb pseudo-tags, then parsing it with Nokogiri. `html2fortitude` could not have been created
10
+ without all their hard work.
11
+
12
+ ## Usage
13
+
14
+ % gem install html2fortitude
15
+ % html2fortitude <input-file>
16
+
17
+ For more information:
18
+
19
+ % html2fortitude --help
20
+
21
+ Because Fortitude views have a class name, while nearly no other templating languages do, `html2fortitude` needs to
22
+ determine a class name for the generated class. If you're converting files that lie underneath a Rails repository,
23
+ `html2fortitude` will automatically detect this and give them the correct name depending on their hierarchy underneath
24
+ `app/views`. If you're converting files that aren't underneath a Rails repository, you'll have to either specify the
25
+ root directory from which class names should be computed using the `--class-base`/`-b` option, or give each file an
26
+ explicit class name, using `--class-name`/`-c`.
27
+
28
+ You can convert an entire directory full of files by passing a directory on the command line. By default, translated
29
+ files will be written right along side the original files, but you can create a separate hierarchy under a new
30
+ directory by passing the new directory as `--output`/`-o`. For a single file, `-o` specifies the filename of the
31
+ translated file to be written to.
32
+
33
+ Other useful options:
34
+
35
+ * You can set the superclass of the generated widget using `--superclass`/`-s`.
36
+ * You can set the name of the content method using `--method`/`-m`.
37
+ * You can set the style of assigns to use using `--assigns`/`-a`, which can be one of:
38
+ * `needs_defaulted_to_nil`, the default; this is standard Fortitude syntax, but with all `need`ed variables
39
+ defaulted to `nil` (_e.g._, `needs :foo => nil, :bar => nil`). This is the default because it is impossible to
40
+ know whether callers of this template always pass in a value for each variable or not, so this is the safest
41
+ option.
42
+ * `required_needs`; this has all `need`ed variables required. If a caller of this template doesn't pass in a value
43
+ for all `need`ed variables, the widget will raise an exception. This produces cleaner code, but may force you to
44
+ manually go add `nil` defaults for any `need`s that are actually optional.
45
+ * `instance_variables`; this uses Ruby instance variables for `need`ed variables (`@foo` instead of `foo`). This is
46
+ not preferred Fortitude style, and requires that you've set `use_instance_variables_for_assigns true` on your
47
+ widget class or superclass in your code, but can be useful if you prefer this style or want to maintain more
48
+ compatibility with Erector, which always uses this style.
49
+ * `no_needs`; this declares no `needs` at all, which means you'll either have to go fill them in yourself, or
50
+ set `extra_assigns :use` in order for things to work.
51
+ * You can set the style of blocks passed to HTML elements using `--do-end`. By default, code is generated that
52
+ looks like `p { ... }` even on multi-line blocks (which is preferred Fortitude style, as it helps you differentiate
53
+ between blocks resulting from control flow vs. blocks resulting from HTML), but `html2fortitude` will generate
54
+ `p do ... end` if you pass this option.
55
+ * You can use new-style hashes (`class: 'foo'`) rather than old-style (`:class => 'foo'`) by passing
56
+ `--new-style-hashes`.
57
+
58
+ ## License
59
+
60
+ Copyright (c) 2014 Andrew Geweke
61
+
62
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
63
+ this software and associated documentation files (the "Software"), to deal in
64
+ the Software without restriction, including without limitation the rights to
65
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
66
+ the Software, and to permit persons to whom the Software is furnished to do so,
67
+ subject to the following conditions:
68
+
69
+ The above copyright notice and this permission notice shall be included in all
70
+ copies or substantial portions of the Software.
71
+
72
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
73
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
74
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
75
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
76
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
77
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
78
+
79
+
80
+ Copyright (c) 2006-2013 Hampton Catlin, Nathan Weizenbaum and Norman Clarke
81
+
82
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
83
+ this software and associated documentation files (the "Software"), to deal in
84
+ the Software without restriction, including without limitation the rights to
85
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
86
+ the Software, and to permit persons to whom the Software is furnished to do so,
87
+ subject to the following conditions:
88
+
89
+ The above copyright notice and this permission notice shall be included in all
90
+ copies or substantial portions of the Software.
91
+
92
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
93
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
94
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
95
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
96
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
97
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
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
+ if RUBY_PLATFORM == 'java'
12
+ t.test_files = FileList["test/**/*_test.rb"]
13
+ else
14
+ t.test_files = FileList["test/**/*_test.rb"].exclude(/jruby/)
15
+ end
16
+ t.verbose = true
17
+ end
18
+
19
+ task :set_coverage_env do
20
+ ENV["COVERAGE"] = "true"
21
+ end
22
+
23
+ desc "Run Simplecov"
24
+ task :coverage => [:set_coverage_env, :test]
25
+
26
+ gemspec = File.expand_path("../html2fortitude.gemspec", __FILE__)
27
+ if File.exist? gemspec
28
+ Gem::PackageTask.new(eval(File.read(gemspec))) { |pkg| }
29
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'html2fortitude'
4
+ require 'html2fortitude/run'
5
+
6
+ runner = Html2fortitude::Run.new(ARGV)
7
+ runner.run!
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/html2fortitude/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Norman Clarke", "Stefan Natchev", "Andrew Geweke"]
6
+ gem.email = ["norman@njclarke.com", "stefan.natchev@gmail.com", "andrew@geweke.org"]
7
+ gem.description = %q{Converts HTML into the Fortitude Ruby HTML-generation DSL}
8
+ gem.summary = %q{Converts HTML into the Fortitude Ruby HTML-generation DSL}
9
+ gem.homepage = "http://github.com/ageweke/html2fortitude"
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 = "html2fortitude"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Html2fortitude::VERSION
17
+
18
+ gem.required_ruby_version = '>= 1.9.2'
19
+
20
+ gem.add_dependency 'activesupport', '>= 3.0.0'
21
+ gem.add_dependency 'nokogiri', '~> 1.6.0'
22
+ gem.add_dependency 'erubis', '~> 2.7.0'
23
+ gem.add_dependency 'ruby_parser', '~> 3.4.1'
24
+ gem.add_dependency 'trollop', '~> 2.0.0'
25
+ # TODO ageweke: eliminate
26
+ gem.add_dependency 'haml', '~> 4.0.0'
27
+ gem.add_development_dependency 'simplecov', '~> 0.7.1'
28
+ gem.add_development_dependency 'minitest', '~> 4.4.0'
29
+ gem.add_development_dependency 'rake'
30
+ gem.add_development_dependency 'rspec', '~> 2.14'
31
+ end
@@ -0,0 +1,6 @@
1
+ require "rubygems"
2
+ require File.expand_path("../html2fortitude/version", __FILE__)
3
+ require "html2fortitude/html"
4
+
5
+ module Html2fortitude
6
+ end
@@ -0,0 +1,755 @@
1
+ require 'cgi'
2
+ require 'nokogiri'
3
+ require 'html2fortitude/html/erb'
4
+ require 'active_support/core_ext/object'
5
+ require 'fortitude/rails/helpers'
6
+
7
+ # Html2fortitude monkeypatches various Nokogiri classes
8
+ # to add methods for conversion to Fortitude.
9
+ # @private
10
+ module Nokogiri
11
+
12
+ module XML
13
+ # @see Nokogiri
14
+ class Node
15
+ # Whether this node has already been converted to Fortitude.
16
+ # Only used for text nodes and elements.
17
+ #
18
+ # @return [Boolean]
19
+ attr_accessor :converted_to_fortitude
20
+
21
+ # Returns the Fortitude representation of the given node.
22
+ #
23
+ # @param tabs [Fixnum] The indentation level of the resulting Fortitude.
24
+ # @option options (see Html2fortitude::HTML#initialize)
25
+ def to_fortitude(tabs, options)
26
+ return "" if converted_to_fortitude
27
+
28
+ # Eliminate whitespace that has no newlines
29
+ as_string = self.to_s
30
+ return "" if as_string.strip.empty? && as_string !~ /[\r\n]/mi
31
+
32
+ if as_string.strip.empty?
33
+ # If we get here, it's whitespace, but containing newlines; eliminate trailing indentation
34
+ as_string = $1 if as_string =~ /^(.*?[\r\n])[ \t]+$/mi
35
+ return as_string
36
+ end
37
+
38
+ # We have actual content if we get here; deal with leading and trailing newline/whitespace combinations properly
39
+ text = uninterp(as_string)
40
+ if text =~ /\A((?:\s*[\r\n])*)(.*?)((?:\s*[\r\n])*)\Z/mi
41
+ prefix, middle, suffix = $1, $2, $3
42
+ middle = parse_text_with_interpolation(middle, tabs)
43
+ return prefix + middle + suffix
44
+ else
45
+ return parse_text_with_interpolation(text, tabs)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # Converts a string that may contain ERb interpolation into valid Fortitude code.
52
+ #
53
+ # This is actually NOT called in nearly all the cases you might imagine. Generally speaking, our strategy is to
54
+ # convert ERb interpolation into faux-HTML tags (<fortitude_loud>, <fortitude_silent>, and <fortitude_block>),
55
+ # and use Nokogiri to parse the resulting "HTML"; we then transform elements properly, converting most into
56
+ # Fortitude tags (e.g., 'p', 'div', etc.) and converting, _e.g._, <fortitude_loud>...</fortitude_loud> into
57
+ # 'text(...)', <fortitude_silent>...</fortitude_silent> into just '...', and so on.
58
+ #
59
+ # However, in certain cases -- like the content of <script> and <style> tags -- we need to convert an entire
60
+ # string, at once, to Fortitude, because the content of those tags is special; it's not parsed as HTML.
61
+ # This method does exactly that.
62
+ #
63
+ # There is, however, one case we cannot trivially convert. If you do something like this with ERb:
64
+ #
65
+ # <script>
66
+ # var message = "You are ";
67
+ # <% if @current_user.admin? %>
68
+ # message = message + "an admin";
69
+ # <% else %>
70
+ # message = message + "a user";
71
+ # <% end %>
72
+ # ...
73
+ # </script>
74
+ #
75
+ # Then there actually _is_ no valid Fortitude transformation of this block -- because here you're using ERb as
76
+ # a Javascript text-substitution preprocessor, not an HTML-generation engine. Short of actually having
77
+ # Fortitude invoke ERb at runtime, there's no simple answer here.
78
+ #
79
+ # Instead, we choose to emit this with a big FIXME comment around it, saying that you need to fix it yourself;
80
+ # most cases actually don't seem to be very hard to fix, as long as you know about it.
81
+ def erb_to_interpolation(text, options)
82
+ # Escape the text...
83
+ text = CGI.escapeHTML(uninterp(text))
84
+ # Unescape our <fortitude_loud> tags.
85
+ %w[<fortitude_loud> </fortitude_loud>].each do |str|
86
+ text.gsub!(CGI.escapeHTML(str), str)
87
+ end
88
+
89
+ # Find any instances of the escaped form of tags we're not compatible with, and put in the FIXME comments.
90
+ %w[fortitude_silent fortitude_block].each do |fake_tag_name|
91
+ while text =~ %r{^(.*?)&lt;#{fake_tag_name}&gt;(.*?)&lt;/#{fake_tag_name}&gt;(.*)$}mi
92
+ before, middle, after = $1, $2, $3
93
+ text = before +
94
+ %{
95
+ # HTML2FORTITUDE_FIXME_BEGIN: The following code was interpolated into this block using ERb;
96
+ # Fortitude isn't a simple string-manipulation engine, so you will have to find another
97
+ # way of accomplishing the same result here:
98
+ # &lt;%
99
+ } +
100
+ middle.split(/\n/).map { |l| "# #{l}" }.join("\n") +
101
+ %{
102
+ # %&gt;
103
+ } +
104
+ after
105
+ end
106
+ end
107
+
108
+ ::Nokogiri::XML.fragment(text).children.inject("") do |str, elem|
109
+ if elem.is_a?(::Nokogiri::XML::Text)
110
+ str + CGI.unescapeHTML(elem.to_s)
111
+ else # <fortitude_loud> element
112
+ data = extract_needs_from!(elem.inner_text.strip, options)
113
+ str + '#{' + CGI.unescapeHTML(data) + '}'
114
+ end
115
+ end
116
+ end
117
+
118
+ # Given a string of text, extracts the 'needs' declarations we'll, ahem, need from it in order to render it --
119
+ # in short, just the instance variables we see in it -- and adds them to options[:needs]. Returns a version of
120
+ # +text+ with instance variable references replaced with +needs+ references; this typically just means converting,
121
+ # _e.g._, +@foo+ to +foo+, although we leave it alone if you've told us that you're going to use Fortitude in
122
+ # that mode.
123
+ def extract_needs_from!(text, options)
124
+ text.gsub(/@[A-Za-z0-9_]+/) do |variable_name|
125
+ without_at = variable_name[1..-1]
126
+ options[:needs] << without_at
127
+
128
+ if options[:assign_reference] == :instance_variable
129
+ variable_name
130
+ else
131
+ without_at
132
+ end
133
+ end
134
+ end
135
+
136
+ TAB_SIZE = 2
137
+
138
+ # Returns a number of spaces equivalent to that many tabs.
139
+ def tabulate(tabs)
140
+ ' ' * TAB_SIZE * tabs
141
+ end
142
+
143
+ # Replaces actual "#{" strings with the escaped version thereof.
144
+ def uninterp(text)
145
+ text.gsub('#{', '\#{') #'
146
+ end
147
+
148
+ # Returns a Hash of the attributes for this node. This just transforms the internal Nokogiri attribute list
149
+ # (which is an Array) into a Hash.
150
+ def attr_hash
151
+ Hash[attributes.map {|k, v| [k.to_s, v.to_s]}]
152
+ end
153
+
154
+ # Turns a String into a Fortitude 'text' command.
155
+ def parse_text(text, tabs)
156
+ parse_text_with_interpolation(uninterp(text), tabs)
157
+ end
158
+
159
+ # Escapes single-line text properly.
160
+ def escape_single_line_text(text)
161
+ text.gsub(/"/) { |m| "\\" + m }
162
+ end
163
+
164
+ # Escapes multi-line text properly.
165
+ def escape_multiline_text(text)
166
+ text.gsub(/\}/, '\\}')
167
+ end
168
+
169
+ # Given another Node (which can be nil), tells us whether we can elide any whitespace present between this node
170
+ # and that node. This is true only if the next-or-previous node is an Element and not one of our special
171
+ # <fortitude...> elements.
172
+ def can_elide_whitespace_against?(previous_or_next)
173
+ (! previous_or_next) ||
174
+ (previous_or_next.is_a?(::Nokogiri::XML::Element) && (! FORTITUDE_TAGS.include?(previous_or_next.name)))
175
+ end
176
+
177
+ # Given text, produces a valid Fortitude command to output that text.
178
+ def parse_text_with_interpolation(text, tabs)
179
+ return "" if text.empty?
180
+
181
+ text = text.lstrip if can_elide_whitespace_against?(previous)
182
+ text = text.rstrip if can_elide_whitespace_against?(self.next)
183
+
184
+ "#{tabulate(tabs)}text #{quoted_string_for_text(text)}\n"
185
+ end
186
+
187
+ # Quotes text properly; this deals with figuring out whether it's a single line of text or multiline text.
188
+ def quoted_string_for_text(text)
189
+ if text =~ /[\r\n]/
190
+ text = "%{#{escape_multiline_text(text)}}"
191
+ else
192
+ text = "\"#{escape_single_line_text(text)}\""
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ # @private
200
+ FORTITUDE_TAGS = %w[fortitude_block fortitude_loud fortitude_silent fortitude_spacer]
201
+
202
+ module Html2fortitude
203
+ # Converts HTML documents into Fortitude templates.
204
+ # Depends on [Nokogiri](http://nokogiri.org/) for HTML parsing.
205
+ # If ERB conversion is being used, also depends on
206
+ # [Erubis](http://www.kuwata-lab.com/erubis) to parse the ERB
207
+ # and [ruby_parser](http://parsetree.rubyforge.org/) to parse the Ruby code.
208
+ #
209
+ # Example usage:
210
+ #
211
+ # HTML.new("<a href='http://google.com'>Blat</a>").render
212
+ # #=> "%a{:href => 'http://google.com'} Blat"
213
+ class HTML
214
+ # @param template [String, Nokogiri::Node] The HTML template to convert
215
+ # @option options :class_name [String] (required) The name of the class to generate
216
+ # @option options :superclass [String] (required) The name of the superclass for this widget
217
+ # @option options :method [String] (required) The name of the method to generate (usually 'content')
218
+ # @option options :assigns [Symbol] (required) Can be one of +:needs_defaulted_to_nil+ (generate +needs+
219
+ # declarations with defaults of +nil+), +:required_needs+ (generate
220
+ # +needs+ declarations with no defaults), +:instance_variables+
221
+ # (generate +needs+ declarations with defaults of +nil+, but reference
222
+ # them using instance variables, not methods), or +:no_needs+ (omit
223
+ # any +needs+ declarations entirely -- requires that you have
224
+ # +extra_assigns :use+ set on your widget, or it won't work)
225
+ # @option options :do_end [Boolean] (false) Use 'do ... end' rather than '{ ... }' for tag content
226
+ # @option options :new_style_hashes [Boolean] (false) Use Ruby 1.9-style Hashes
227
+ def initialize(template, options = {})
228
+ options.assert_valid_keys(:class_name, :superclass, :method, :assigns, :do_end, :new_style_hashes)
229
+
230
+ if template.is_a? Nokogiri::XML::Node
231
+ @template = template
232
+ else
233
+ require 'html2fortitude/html/erb'
234
+ template = ERB.compile(template)
235
+
236
+ @class_name = options[:class_name] || raise(ArgumentError, "You must specify a class name")
237
+ @superclass = options[:superclass] || raise(ArgumentError, "You must specify a superclass")
238
+ @method = options[:method] || raise(ArgumentError, "You must specify a method name")
239
+ @assigns = (options[:assigns] || raise(ArgumentError, "You must specify :assigns")).to_sym
240
+
241
+ @do_end = options[:do_end]
242
+ @new_style_hashes = options[:new_style_hashes]
243
+
244
+ template = add_spacers_to(template)
245
+
246
+ if template =~ /^\s*<!DOCTYPE|<html/i
247
+ return @template = Nokogiri.HTML(template)
248
+ end
249
+
250
+ @template = Nokogiri::HTML.fragment(template)
251
+
252
+ #detect missplaced head or body tag
253
+ #XML_HTML_STRUCURE_ERROR : 800
254
+ if @template.errors.any? { |e| e.code == 800 }
255
+ return @template = Nokogiri.HTML(template).at('/html').children
256
+ end
257
+
258
+ #in order to support CDATA in HTML (which is invalid) try using the XML parser
259
+ # we can detect this when libxml returns error code XML_ERR_NAME_REQUIRED : 68
260
+ if @template.errors.any? { |e| e.code == 68 } || template =~ /CDATA/
261
+ return @template = Nokogiri::XML.fragment(template)
262
+ end
263
+ end
264
+ end
265
+
266
+ # So: ERb that looks like this: <%= @first_name %> <%= @last_name %>
267
+ # is presumably intended to produce "John Doe". It gets run through our ERb filter, and comes out as this HTML:
268
+ # <fortitude_loud> @first_name </fortitude_loud> <fortitude_loud> @last_name </fortitude_loud>
269
+ #
270
+ # This seems OK, except that when Nokogiri parses it, it throws away the space between the end of the first
271
+ # </fortitude_loud> and the second <fortitude_loud>, because it "knows" that whitespace between tags in HTML isn't
272
+ # significant. Which, you know, is true for HTML in general, but is NOT true here; we need to keep that whitespace,
273
+ # or we'll end up with "JohnDoe".
274
+ #
275
+ # As a result, we look for this pattern, and insert a <fortitude_spacer/> element there, which we explicitly just
276
+ # output as 'text " "'. This brings back our spacer.
277
+ def add_spacers_to(template)
278
+ template.gsub(%r{</fortitude_[a-z]+>\s+<fortitude_}) do |match|
279
+ if match =~ %r{(</fortitude_[a-z]+>)\s+(<fortitude_)}
280
+ "#{$1}<fortitude_spacer/>#{$2}"
281
+ else
282
+ raise "Should always match!"
283
+ end
284
+ end
285
+ end
286
+
287
+ # Processes the document and returns the result as a String containing the Fortitude template, including the
288
+ # class declaration, needs text, method declaration, content, and ends.
289
+ def render
290
+ to_fortitude_options = {
291
+ :needs => [ ],
292
+ :assign_reference => (@assigns == :instance_variables ? :instance_variable : :method),
293
+ :do_end => @do_end,
294
+ :new_style_hashes => @new_style_hashes
295
+ }
296
+
297
+ content_text = @template.to_fortitude(2, to_fortitude_options)
298
+
299
+ out = "class #{@class_name} < #{@superclass}\n"
300
+ needs_text = needs_declarations(to_fortitude_options[:needs])
301
+ out << "#{needs_text}\n \n" if needs_text
302
+
303
+ out << " def #{@method}\n"
304
+ out << "#{content_text.rstrip}\n"
305
+ out << " end\n"
306
+ out << "end\n"
307
+
308
+ out
309
+ end
310
+
311
+ private
312
+ # Returns the 'needs' line appropriate for this class; this can be nil if they've set +:no_needs+.
313
+ def needs_declarations(needs)
314
+ return nil if @assigns == :no_needs
315
+
316
+ needs = needs.map { |n| n.to_s.strip.downcase }.uniq.compact.sort
317
+ return nil if needs.empty?
318
+
319
+ out = ""
320
+ out << needs.map do |need|
321
+ if [ :needs_defaulted_to_nil, :instance_variables ].include?(@assigns)
322
+ " needs :#{need} => nil"
323
+ else
324
+ " needs :#{need}"
325
+ end
326
+ end.join("\n")
327
+ out
328
+ end
329
+
330
+ alias_method :to_fortitude, :render
331
+
332
+ # @see Nokogiri
333
+ # @private
334
+ class ::Nokogiri::XML::Document
335
+ # @see Html2fortitude::HTML::Node#to_fortitude
336
+ def to_fortitude(tabs, options)
337
+ (children || []).inject('') {|s, c| s << c.to_fortitude(tabs, options)}
338
+ end
339
+ end
340
+
341
+ class ::Nokogiri::XML::DocumentFragment
342
+ # @see Html2fortitude::HTML::Node#to_fortitude
343
+ def to_fortitude(tabs, options)
344
+ (children || []).inject('') {|s, c| s << c.to_fortitude(tabs, options)}
345
+ end
346
+ end
347
+
348
+ class ::Nokogiri::XML::NodeSet
349
+ # @see Html2fortitude::HTML::Node#to_fortitude
350
+ def to_fortitude(tabs, options)
351
+ self.inject('') {|s, c| s << c.to_fortitude(tabs, options)}
352
+ end
353
+ end
354
+
355
+ # @see Nokogiri
356
+ # @private
357
+ class ::Nokogiri::XML::ProcessingInstruction
358
+ # @see Html2fortitude::HTML::Node#to_fortitude
359
+ def to_fortitude(tabs, options)
360
+ # "#{tabulate(tabs)}!!! XML\n"
361
+ "#{tabulate(tabs)}rawtext(\"#{self.to_s}\")\n"
362
+ end
363
+ end
364
+
365
+ # @see Nokogiri
366
+ # @private
367
+ class ::Nokogiri::XML::CDATA
368
+ # @see Html2fortitude::HTML::Node#to_fortitude
369
+ def to_fortitude(tabs, options)
370
+ content = erb_to_interpolation(self.content, options).strip
371
+ # content = parse_text_with_interpolation(
372
+ # erb_to_interpolation(self.content, options), tabs + 1)
373
+ "#{tabulate(tabs)}cdata <<-END_OF_CDATA_CONTENT\n#{content}\nEND_OF_CDATA_CONTENT"
374
+ end
375
+
376
+ # removes the start and stop markers for cdata
377
+ def content_without_cdata_tokens
378
+ content.
379
+ gsub(/^\s*<!\[CDATA\[\n/,"").
380
+ gsub(/^\s*\]\]>\n/, "")
381
+ end
382
+ end
383
+
384
+ # @see Nokogiri
385
+ # @private
386
+ class ::Nokogiri::XML::DTD
387
+ # @see Html2fortitude::HTML::Node#to_fortitude
388
+
389
+ # We just emit 'doctype!' here, because the base widget knows its doctype.
390
+ def to_fortitude(tabs, options)
391
+ "#{tabulate(tabs)}doctype!\n"
392
+ end
393
+ end
394
+
395
+ # @see Nokogiri
396
+ # @private
397
+ class ::Nokogiri::XML::Comment
398
+ # @see Html2fortitude::HTML::Node#to_fortitude
399
+ def to_fortitude(tabs, options)
400
+ return "#{tabulate(tabs)}comment #{quoted_string_for_text(self.content.strip)}\n"
401
+ end
402
+ end
403
+
404
+ # @see Nokogiri
405
+ # @private
406
+ class ::Nokogiri::XML::Element
407
+ BUILT_IN_RENDERING_HELPERS = %w{render}
408
+
409
+ # Given a section of code that we're going to output, can we skip putting 'text' or 'rawtext' in front of it?
410
+ # We can do this under the following scenarios:
411
+ #
412
+ # * The code is a single line, and contains no semicolons; and
413
+ # * The method it's calling is either 'render' (which Fortitude implements internally) or a helper method that
414
+ # Fortitude automatically outputs the return value from.
415
+ def can_skip_text_or_rawtext_prefix?(code)
416
+ return false if code =~ /[\r\n]/mi
417
+ return false if code =~ /;/mi
418
+ method = $1 if code =~ /^\s*([A-Za-z_][A-Za-z0-9_]*[\!\?\=]?)[\s\(]/
419
+ method ||= $1 if code =~ /^\s*([A-Za-z_][A-Za-z0-9_]*[\!\?\=]?)$/
420
+ options = Fortitude::Rails::Helpers.helper_options(method.strip.downcase) if method
421
+ (options && options[:transform] == :output_return_value) ||
422
+ (method && BUILT_IN_RENDERING_HELPERS.include?(method.strip.downcase))
423
+ end
424
+
425
+ # Given a section of code that we're going to output, can we use it as a method argument directly? Or do we need
426
+ # to nest it inside a block to the tag?
427
+ #
428
+ # In other words, can we say just:
429
+ #
430
+ # p(...code...)
431
+ #
432
+ # ...or do we need to say:
433
+ #
434
+ # p {
435
+ # text(...code...)
436
+ # }
437
+ def code_can_be_used_as_a_method_argument?(code)
438
+ code !~ /[\r\n;]/ && (! can_skip_text_or_rawtext_prefix?(code))
439
+ end
440
+
441
+ # Kinda just like what it says ;)
442
+ def is_text_element_starting_with_newline?(node)
443
+ node && node.is_a?(::Nokogiri::XML::Text) && node.to_s =~ /^\s*[\r\n]/
444
+ end
445
+
446
+ # This is used to support blocks like form_for -- this tells the next element that it needs to put a close
447
+ # parenthesis on the end, since we end up outputting something like:
448
+ #
449
+ # text(form_for do |f|
450
+ # text(f.text_field :name)
451
+ # end)
452
+ def is_loud_block!
453
+ @is_loud_block = true
454
+ end
455
+
456
+ VALID_JAVASCRIPT_SCRIPT_TYPES = [ 'text/javascript', 'text/ecmascript', 'application/javascript', 'application/ecmascript']
457
+ VALID_JAVASCRIPT_LANGUAGE_TYPES = [ 'javascript', 'ecmascript' ]
458
+
459
+ VALID_CSS_TYPES = [ 'text/css' ]
460
+
461
+
462
+ # If this is a <script> or <style> block, return the correct syntax for it; we use the #javascript convenience
463
+ # method if possible. If this is not either of those, returns nil.
464
+ def contents_for_direct_input(tabs, options)
465
+ if name == "script"
466
+ if VALID_JAVASCRIPT_SCRIPT_TYPES.include?((attr_hash['type'] || VALID_JAVASCRIPT_SCRIPT_TYPES.first).strip.downcase) &&
467
+ VALID_JAVASCRIPT_LANGUAGE_TYPES.include?((attr_hash['language'] || VALID_JAVASCRIPT_LANGUAGE_TYPES.first).strip.downcase)
468
+ new_attrs = Hash[attr_hash.reject { |k,v| %w{type language}.include?(k.to_s.strip.downcase) }]
469
+ contents_as_direct_input_to_tag(:javascript, tabs, options, new_attrs)
470
+ else
471
+ contents_as_direct_input_to_tag(:script, tabs, options, attr_hash)
472
+ end
473
+ elsif name == "style"
474
+ contents_as_direct_input_to_tag(:style, tabs, options, attr_hash)
475
+ else
476
+ nil
477
+ end
478
+ end
479
+
480
+ # Some HTML tags like <script> and <style> have content that isn't parsed at all; Fortitude handles this by
481
+ # simply supplying it as direct content to the tag, typically as an <<-EOS string:
482
+ #
483
+ # script <<-END_OF_SCRIPT_CONTENT
484
+ # var foo = 1;
485
+ # ...
486
+ # END_OF_SCRIPT_CONTENT
487
+ #
488
+ # This method creates exactly that form.
489
+ def contents_as_direct_input_to_tag(tag_name, tabs, options, attributes_hash)
490
+ tag_name = tag_name.to_s.strip.downcase
491
+
492
+ content =
493
+ # We want to remove any CDATA present if it's Javascript; the Fortitude #javascript method takes care of
494
+ # adding CDATA if needed (_i.e._, for XHTML doctypes only).
495
+ if children.first && children.first.cdata? && tag_name == 'javascript'
496
+ decode_entities(children.first.content_without_cdata_tokens)
497
+ else
498
+ decode_entities(self.inner_text)
499
+ end
500
+
501
+ content = erb_to_interpolation(content, options)
502
+ content.strip!
503
+ content << "\n"
504
+
505
+ first_line = "#{tabulate(tabs)}#{tag_name} <<-END_OF_#{tag_name.upcase}_CONTENT"
506
+ first_line += ", #{fortitude_attributes({ }, attributes_hash)}" unless attributes_hash.empty?
507
+ middle = content.rstrip
508
+ last_line = "#{tabulate(tabs)}END_OF_#{tag_name.upcase}_CONTENT"
509
+
510
+ first_line + "\n" + middle + "\n" + last_line
511
+ end
512
+
513
+ # If this is a <fortitude_loud>, <fortitude_silent>, or <fortitude_block> element, return the Fortitude code
514
+ # for it. Otherwise, returns nil.
515
+ def result_for_fortitude_tag(tabs, options, output)
516
+ # Here's where the real heart of a lot of our ERb processing happens. We process the special tags:
517
+ #
518
+ # * +<fortitude_loud>+ -- equivalent to ERb's +<%= %>+;
519
+ # * +<fortitude_silent>+ -- equivalent to ERb's +<% %>+;
520
+ # * +<fortitude_block>+ -- used when a +<%=+ or +<%+ starts a block of Ruby code; encloses the whole thing
521
+ if FORTITUDE_TAGS.include?(name)
522
+ case name
523
+ # This is ERb +<%= %>+ -- i.e., code we need to output the return value of
524
+ when "fortitude_loud"
525
+ # Extract any instance variables, add 'needs' for them, and turn them into method calls
526
+ t = extract_needs_from!(CGI.unescapeHTML(inner_text), options)
527
+ lines = t.split("\n").map { |s| s.strip }
528
+
529
+ outputting_method = if attribute("raw") then "rawtext" else "text" end
530
+
531
+ # Handle this case:
532
+ # <%= form_for(@user) do |f| %>
533
+ # <%= f.whatever %>
534
+ # <% end %>
535
+ if self.next && self.next.is_a?(::Nokogiri::XML::Element) && self.next.name == 'fortitude_block'
536
+ self.next.is_loud_block!
537
+ # What gets output is whatever the code returns, so we put the method on the last line of the block
538
+ lines[-1] = "#{outputting_method}(" + lines[-1]
539
+ elsif lines.length == 1 && can_skip_text_or_rawtext_prefix?(lines.first)
540
+ # OK, we're good -- this means there's only a single line, and we're calling a Rails helper method
541
+ # that automatically outputs, so we don't actually have to use 'text' or 'rawtext'
542
+ else
543
+ # What gets output is whatever the code returns, so we put the method on the last line of the block
544
+ lines[-1] = "#{outputting_method}(" + lines[-1] + ")"
545
+ end
546
+
547
+ return lines.map {|s| output + s + "\n"}.join
548
+ # This is ERb +<% %>+ -- i.e., code we just need to run
549
+ when "fortitude_silent"
550
+ # Extract any instance variables, add 'needs' for them, and turn them into method calls
551
+ t = extract_needs_from!(CGI.unescapeHTML(inner_text), options)
552
+ return t.split("\n").map do |line|
553
+ next "" if line.strip.empty?
554
+ "#{output}#{line.strip}\n"
555
+ end.join
556
+ # This is ERb +<%+ or +<%=+ that starts a Ruby block
557
+ when "fortitude_block"
558
+ needs_coda = true unless self.next && self.next.is_a?(::Nokogiri::XML::Element) &&
559
+ self.next.name == 'fortitude_silent' && self.next.inner_text =~ /^\s*els(e|if)\s*$/i
560
+ coda = if needs_coda then "\n#{tabulate(tabs)}end" else "" end
561
+ coda << ")" if @is_loud_block
562
+ coda << "\n"
563
+ children_text = render_children("", tabs, options).rstrip
564
+ return children_text + coda
565
+ when "fortitude_spacer"
566
+ return %{text " "\n}
567
+ else
568
+ raise "Unknown special tag: #{name.inspect}"
569
+ end
570
+ end
571
+ end
572
+
573
+ # @see Html2fortitude::HTML::Node#to_fortitude
574
+ def to_fortitude(tabs, options)
575
+ return "" if converted_to_fortitude
576
+
577
+ # Get <script> and <style> elements out of the way.
578
+ direct_input = contents_for_direct_input(tabs, options)
579
+ return direct_input if direct_input
580
+
581
+ output = tabulate(tabs)
582
+
583
+ # Get <fortitude_loud>, <fortitude_silent>, and <fortitude_block> elements out of the way.
584
+ fortitude_result = result_for_fortitude_tag(tabs, options, output)
585
+ return fortitude_result if fortitude_result
586
+
587
+ output << "#{name}"
588
+
589
+ attributes_text = fortitude_attributes(options) if attr_hash && attr_hash.length > 0
590
+ direct_content = nil
591
+ render_children = true
592
+
593
+ # If the element only has a single run of text as its content, try just passing it as a direct argument to
594
+ # our tag method, rather than starting a block
595
+ if children.try(:size) == 1 && children.first.is_a?(::Nokogiri::XML::Text)
596
+ direct_content = quoted_string_for_text(child.to_s.strip)
597
+ render_children = false
598
+ # If the element only has one thing as its content, and that's an ERb +<%= %>+ block, try just passing that
599
+ # code directly as a method argument, if we can do that
600
+ elsif children.try(:size) == 1 && children.first.is_a?(::Nokogiri::XML::Element) &&
601
+ children.first.name == "fortitude_loud" &&
602
+ code_can_be_used_as_a_method_argument?(child.inner_text)
603
+
604
+ it = extract_needs_from!(child.inner_text, options)
605
+
606
+ direct_content = "#{it.strip}"
607
+ # Put parentheses around it if we have attributes, and it's a method call without parentheses
608
+ direct_content = "(#{direct_content})" if attributes_text && direct_content =~ /^\s*[A-Za-z_][A-Za-z0-9_]*[\!\?\=]?\s+\S/
609
+ render_children = false
610
+ end
611
+
612
+ # Produce the arguments to our tag method...
613
+ if attributes_text && direct_content
614
+ output << "(#{direct_content}, #{attributes_text})"
615
+ elsif direct_content
616
+ output << "(#{direct_content})"
617
+ elsif attributes_text
618
+ output << "(#{attributes_text})"
619
+ end
620
+
621
+ # Render the children, if we need to.
622
+ if render_children && children && children.size >= 1
623
+ children_output = render_children("", tabs, options).strip
624
+ output << " #{element_block_start(options)}\n"
625
+ output << tabulate(tabs + 1)
626
+ output << children_output
627
+ output << "\n#{tabulate(tabs)}#{element_block_end(options)}\n"
628
+ else
629
+ output << "\n" unless is_text_element_starting_with_newline?(self.next)
630
+ end
631
+
632
+ output
633
+ end
634
+
635
+ private
636
+
637
+ # Just string together the children, calling #to_fortitude on each of them.
638
+ def render_children(so_far, tabs, options)
639
+ (self.children || []).inject(so_far) do |output, child|
640
+ output + child.to_fortitude(tabs + 1, options)
641
+ end
642
+ end
643
+
644
+ # Take the attributes for this node (from +attr_hash+) and return from it a Hash. This Hash will have entries
645
+ # for any attributes that have substitutions (_i.e._, ERb tags) in their values, mapping the name of each
646
+ # attribute to the text we should use for it -- that is, pure Ruby code where possible, Ruby String interpolations
647
+ # where not.
648
+ def dynamic_attributes(options)
649
+ return @dynamic_attributes if @dynamic_attributes
650
+
651
+ # reject any attrs without <fortitude>
652
+ @dynamic_attributes = attr_hash.select {|name, value| value =~ %r{<fortitude.*</fortitude} }
653
+ @dynamic_attributes.each do |name, value|
654
+ fragment = Nokogiri::XML.fragment(CGI.unescapeHTML(value))
655
+
656
+ # unwrap interpolation if we can:
657
+ if fragment.children.size == 1 && fragment.child.name == 'fortitude_loud'
658
+ t = extract_needs_from!(fragment.text, options)
659
+ if attribute_value_can_be_bare_ruby?(t)
660
+ value.replace(t.strip)
661
+ next
662
+ end
663
+ end
664
+
665
+ # turn erb into interpolations
666
+ fragment.css('fortitude_loud').each do |el|
667
+ inner_text = el.text.strip
668
+ next if inner_text == ""
669
+ inner_text = extract_needs_from!(inner_text, options)
670
+ el.replace('#{' + inner_text + '}')
671
+ end
672
+
673
+ # put the resulting text in a string
674
+ value.replace('"' + fragment.text.strip + '"')
675
+ end
676
+ end
677
+
678
+ # Given an attribute value, can we simply use bare Ruby code for it, or do we need to use string interpolation?
679
+ def attribute_value_can_be_bare_ruby?(value)
680
+ begin
681
+ ruby = RubyParser.new.parse(value)
682
+ rescue Racc::ParseError, RubyParser::SyntaxError
683
+ return false
684
+ end
685
+
686
+ return false if ruby.nil?
687
+ return true if ruby.sexp_type == :str #regular string
688
+ return true if ruby.sexp_type == :dstr #string with interpolation
689
+ return true if ruby.sexp_type == :lit #symbol
690
+ return true if ruby.sexp_type == :call #local var or method
691
+
692
+ false
693
+ end
694
+
695
+ # Returns the string we want to use to start a block -- either '{' (by default) or 'do' (if asked)
696
+ def element_block_start(options)
697
+ if options[:do_end]
698
+ "do"
699
+ else
700
+ "{"
701
+ end
702
+ end
703
+
704
+ # Returns the string we want to use to end a block -- either '}' (by default) or 'end' (if asked)
705
+ def element_block_end(options)
706
+ if options[:do_end]
707
+ "end"
708
+ else
709
+ "}"
710
+ end
711
+ end
712
+
713
+ def decode_entities(str)
714
+ return str
715
+ str.gsub(/&[\S]+;/) do |entity|
716
+ begin
717
+ [Nokogiri::HTML::NamedCharacters[entity[1..-2]]].pack("C")
718
+ rescue TypeError
719
+ entity
720
+ end
721
+ end
722
+ end
723
+
724
+ # Does the attribute with the given name include any ERb in its value?
725
+ def dynamic_attribute?(name, options)
726
+ dynamic_attributes(options).key?(name)
727
+ end
728
+
729
+ # Returns a string representation of an attributes hash
730
+ # that's prettier than that produced by Hash#inspect
731
+ def fortitude_attributes(options, override_attr_hash = nil)
732
+ attrs = override_attr_hash || attr_hash
733
+ attrs = attrs.sort.map do |name, value|
734
+ fortitude_attribute_pair(name, value.to_s, options)
735
+ end
736
+ "#{attrs.join(', ')}"
737
+ end
738
+
739
+ # Returns the string representation of a single attribute key value pair
740
+ def fortitude_attribute_pair(name, value, options)
741
+ value = dynamic_attribute?(name, options) ? dynamic_attributes(options)[name] : value.inspect
742
+
743
+ if name.index(/\W/)
744
+ "#{name.inspect} => #{value}"
745
+ else
746
+ if options[:new_style_hashes]
747
+ "#{name}: #{value}"
748
+ else
749
+ ":#{name} => #{value}"
750
+ end
751
+ end
752
+ end
753
+ end
754
+ end
755
+ end