html2fortitude 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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