html2fortitude 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec-local +4 -0
- data/.travis.yml +16 -0
- data/.yardopts +1 -0
- data/Gemfile +5 -0
- data/MIT-LICENSE +38 -0
- data/README.md +97 -0
- data/Rakefile +29 -0
- data/bin/html2fortitude +7 -0
- data/html2fortitude.gemspec +31 -0
- data/lib/html2fortitude.rb +6 -0
- data/lib/html2fortitude/html.rb +755 -0
- data/lib/html2fortitude/html/erb.rb +156 -0
- data/lib/html2fortitude/run.rb +103 -0
- data/lib/html2fortitude/source_template.rb +93 -0
- data/lib/html2fortitude/version.rb +3 -0
- data/spec/helpers/global_helper.rb +6 -0
- data/spec/helpers/html2fortitude_result.rb +71 -0
- data/spec/helpers/standard_helper.rb +70 -0
- data/spec/system/basic_system_spec.rb +13 -0
- data/spec/system/command_line_spec.rb +411 -0
- data/spec/system/erb_system_spec.rb +208 -0
- data/spec/system/needs_system_spec.rb +73 -0
- data/spec/system/options_system_spec.rb +43 -0
- data/spec/system/other_stuff_system_spec.rb +39 -0
- data/spec/system/rails_system_spec.rb +9 -0
- data/spec/system/tags_system_spec.rb +252 -0
- data/spec/system/text_system_spec.rb +106 -0
- data/test/erb_test.rb +546 -0
- data/test/html2fortitude_test.rb +424 -0
- data/test/jruby/erb_test.rb +39 -0
- data/test/jruby/html2fortitude_test.rb +72 -0
- data/test/test_helper.rb +22 -0
- metadata +238 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec-local
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/html2fortitude
ADDED
@@ -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,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{^(.*?)<#{fake_tag_name}>(.*?)</#{fake_tag_name}>(.*)$}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
|
+
# <%
|
99
|
+
} +
|
100
|
+
middle.split(/\n/).map { |l| "# #{l}" }.join("\n") +
|
101
|
+
%{
|
102
|
+
# %>
|
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
|