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.
- 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
|