html2fortitude 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ require 'cgi'
2
+ require 'erubis'
3
+ require 'ruby_parser'
4
+
5
+ module Html2fortitude
6
+ class HTML
7
+ # A class for converting ERB code into a format that's easier
8
+ # for the {Html2fortitude::HTML} Nokogiri-based parser to understand.
9
+ #
10
+ # Uses [Erubis](http://www.kuwata-lab.com/erubis)'s extensible parsing powers
11
+ # to parse the ERB in a reliable way,
12
+ # and [ruby_parser](http://parsetree.rubyforge.org/)'s Ruby knowledge
13
+ # to figure out whether a given chunk of Ruby code starts a block or not.
14
+ #
15
+ # The ERB tags are converted to HTML tags in the following way.
16
+ # `<% ... %>` is converted into `<fortitude_silent> ... </fortitude_silent>`.
17
+ # `<%= ... %>` is converted into `<fortitude_loud> ... </fortitude_loud>`.
18
+ # `<%== ... %>` is converted into `<fortitude_loud raw=raw> ... </fortitude_loud>`.
19
+ # Finally, if either of these opens a Ruby block,
20
+ # `<fortitude_block> ... </fortitude_block>` will wrap the entire contents of the block -
21
+ # that is, everything that should be indented beneath the previous silent or loud tag.
22
+ class ERB < Erubis::Basic::Engine
23
+ # Compiles an ERB template into a HTML document containing `fortitude_*` tags.
24
+ #
25
+ # @param template [String] The ERB template
26
+ # @return [String] The output document
27
+ # @see Html2fortitude::HTML::ERB
28
+ def self.compile(template)
29
+ new(template).src
30
+ end
31
+
32
+ # The ERB-to-Fortitudeized-HTML conversion has no preamble.
33
+ def add_preamble(src); end
34
+
35
+ # The ERB-to-Fortitudeized-HTML conversion has no postamble.
36
+ def add_postamble(src); end
37
+
38
+ # Concatenates the text onto the source buffer.
39
+ #
40
+ # @param src [String] The source buffer
41
+ # @param text [String] The raw text to add to the buffer
42
+ def add_text(src, text)
43
+ src << text
44
+ end
45
+
46
+ # Concatenates a silent Ruby statement onto the source buffer.
47
+ # This uses the `<fortitude_silent>` tag,
48
+ # and may close and/or open a Ruby block with the `<fortitude_block>` tag.
49
+ #
50
+ # In particular, a block is closed if this statement is some form of `end`,
51
+ # opened if it's a block opener like `do`, `if`, or `begin`,
52
+ # and both closed and opened if it's a mid-block keyword
53
+ # like `else` or `when`.
54
+ #
55
+ # @param src [String] The source buffer
56
+ # @param code [String] The Ruby statement to add to the buffer
57
+ def add_stmt(src, code)
58
+ src << '</fortitude_block>' if block_closer?(code) || mid_block?(code)
59
+ src << '<fortitude_silent>' << h(code) << '</fortitude_silent>' unless code.strip == "end"
60
+ src << '<fortitude_block>' if block_opener?(code) || mid_block?(code)
61
+ end
62
+
63
+ # Concatenates a Ruby expression that's printed to the document
64
+ # onto the source buffer.
65
+ # This uses the `<fortitude:silent>` tag,
66
+ # and may open a Ruby block with the `<fortitude:block>` tag.
67
+ # An expression never closes a block.
68
+ #
69
+ # @param src [String] The source buffer
70
+ # @param code [String] The Ruby expression to add to the buffer
71
+ def add_expr_literal(src, code)
72
+ src << '<fortitude_loud>' << h(code) << '</fortitude_loud>'
73
+ src << '<fortitude_block>' if block_opener?(code)
74
+ end
75
+
76
+ def add_expr_escaped(src, code)
77
+ src << "<fortitude_loud raw=raw>" << h(code) << "</fortitude_loud>"
78
+ end
79
+
80
+ # `html2fortitude` doesn't support debugging expressions.
81
+ def add_expr_debug(src, code)
82
+ # TODO ageweke
83
+ # raise Haml::Error.new("html2haml doesn't support debugging expressions.")
84
+ end
85
+
86
+ private
87
+
88
+ # HTML-escaped some text (in practice, always Ruby code).
89
+ # A utility method.
90
+ #
91
+ # @param text [String] The text to escape
92
+ # @return [String] The escaped text
93
+ def h(text)
94
+ CGI.escapeHTML(text)
95
+ end
96
+
97
+ # Returns whether the code is valid Ruby code on its own.
98
+ #
99
+ # @param code [String] Ruby code to check
100
+ # @return [Boolean]
101
+ def valid_ruby?(code)
102
+ RubyParser.new.parse(code)
103
+ rescue Racc::ParseError, RubyParser::SyntaxError
104
+ false
105
+ end
106
+
107
+ # Returns whether the code has any content
108
+ # This is used to test whether lines have been removed by erubis, such as comments
109
+ #
110
+ # @param code [String] Ruby code to check
111
+ # @return [Boolean]
112
+ def has_code?(code)
113
+ return false if code == "\n"
114
+ return false if valid_ruby?(code) == nil
115
+ true
116
+ end
117
+
118
+ # Checks if a string of Ruby code opens a block.
119
+ # This could either be something like `foo do |a|`
120
+ # or a keyword that requires a matching `end`
121
+ # like `if`, `begin`, or `case`.
122
+ #
123
+ # @param code [String] Ruby code to check
124
+ # @return [Boolean]
125
+ def block_opener?(code)
126
+ return unless has_code?(code)
127
+ valid_ruby?(code + "\nend") ||
128
+ valid_ruby?(code + "\nwhen foo\nend")
129
+ end
130
+
131
+ # Checks if a string of Ruby code closes a block.
132
+ # This is always `end` followed optionally by some method calls.
133
+ #
134
+ # @param code [String] Ruby code to check
135
+ # @return [Boolean]
136
+ def block_closer?(code)
137
+ return unless has_code?(code)
138
+ valid_ruby?("begin\n" + code)
139
+ end
140
+
141
+ # Checks if a string of Ruby code comes in the middle of a block.
142
+ # This could be a keyword like `else`, `rescue`, or `when`,
143
+ # or even `end` with a method call that takes a block.
144
+ #
145
+ # @param code [String] Ruby code to check
146
+ # @return [Boolean]
147
+ def mid_block?(code)
148
+ return unless has_code?(code)
149
+ return if valid_ruby?(code)
150
+ valid_ruby?("if foo\n#{code}\nend") || # else, elsif
151
+ valid_ruby?("begin\n#{code}\nend") || # rescue, ensure
152
+ valid_ruby?("case foo\n#{code}\nend") # when
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,103 @@
1
+ require 'trollop'
2
+ require 'find'
3
+ require 'html2fortitude/source_template'
4
+
5
+ module Html2fortitude
6
+ class Run
7
+ def initialize(argv)
8
+ @argv = argv
9
+ end
10
+
11
+ def run!
12
+ parse_arguments!
13
+
14
+ for_each_input_file do |name_and_block|
15
+ name = name_and_block[:name]
16
+ block = name_and_block[:block]
17
+
18
+ contents = nil
19
+ block.call { |io| contents = io.read }
20
+
21
+ effective_options = trollop_options.select do |key, value|
22
+ %w{output class_name class_base superclass method assigns do_end new_style_hashes}.include?(key.to_s)
23
+ end
24
+
25
+ source_template = Html2fortitude::SourceTemplate.new(name, contents, effective_options)
26
+ source_template.write_transformed_content!
27
+
28
+ unless @argv.include?("-")
29
+ puts "#{source_template.filename} -> #{source_template.output_filename} (#{source_template.line_count} lines)"
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+ def for_each_input_file(&block)
36
+ @argv.each do |file_or_directory|
37
+ if file_or_directory == '-'
38
+ block.call({ :name => '-', :block => lambda { |&block| block.call($stdin) } })
39
+ next
40
+ end
41
+
42
+ file_or_directory = File.expand_path(file_or_directory)
43
+ raise Errno::ENOENT, "No such file or directory: #{file_or_directory}" unless File.exist?(file_or_directory)
44
+
45
+ files = [ ]
46
+ if File.directory?(file_or_directory)
47
+ Find.find(file_or_directory) do |f|
48
+ f = File.expand_path(f, file_or_directory)
49
+ files << f if File.file?(f) && f =~ /\.r?html/
50
+ end
51
+ else
52
+ files << file_or_directory
53
+ end
54
+
55
+ files.each do |file|
56
+ block.call(:name => file, :block => lambda { |&block| File.open(file, &block) })
57
+ end
58
+ end
59
+ end
60
+
61
+ def parse_arguments!
62
+ trollop_options
63
+ end
64
+
65
+ def trollop_options
66
+ @trollop_parser ||= Trollop::Parser.new do
67
+ version "html2fortitude version #{Html2fortitude::VERSION}"
68
+ banner <<-EOS
69
+ html2fortitude transforms HTML source files (with or without ERb embedded)
70
+ into Fortitude (https://github.com/ageweke/fortitude) source code.
71
+
72
+ Usage:
73
+ html2fortitude [options] file|directory [file|directory...]
74
+ where [options] are:
75
+ EOS
76
+
77
+ opt :output, "Output file or directory", :type => String
78
+
79
+ opt :class_name, "Class name for created Fortitude class", :type => String
80
+ opt :class_base, "Base directory for input files (e.g., my_rails_app/app) to use when --class-name is not specified", :type => String, :short => 'b'
81
+ opt :superclass, "Name of the class to inherit the output class from", :type => String, :default => 'Fortitude::Widget::Html5'
82
+
83
+ opt :method, "Name of method to write in widget (default 'content')", :type => String, :default => 'content'
84
+ opt :assigns, %{Method for using assigns passed to the widget:
85
+ needs_defaulted_to_nil: (default) standard Fortitude 'needs', but with a default of 'nil', so all needs are optional
86
+ required_needs: standard Fortitude 'needs', no default; widget will not render without all needs specified (dangerous)
87
+ instance_variables: Ruby instance variables; requires that a base class of the widget sets 'use_instance_variables_for_assigns true'
88
+ no_needs: Omit a 'needs' declaration entirely; requires that a base class sets 'extra_assigns :use'},
89
+ :type => String, :default => 'needs_defaulted_to_nil'
90
+
91
+ opt :do_end, "Use do ... end for blocks passed to tag methods, not { ... } (does not affect blocks from ERb)", :type => :boolean
92
+ opt :new_style_hashes, "Use hash_style: ruby19 instead of :hash_style => :ruby_18", :type => :boolean
93
+ end
94
+
95
+ @trollop_options ||= begin
96
+ Trollop::with_standard_exception_handling(@trollop_parser) do
97
+ raise Trollop::HelpNeeded if @argv.empty? # show help screen
98
+ @trollop_parser.parse @argv
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,93 @@
1
+ require 'html2fortitude/html'
2
+
3
+ module Html2fortitude
4
+ class SourceTemplate
5
+ attr_reader :filename, :line_count
6
+
7
+ def initialize(filename, contents, options)
8
+ options.assert_valid_keys(:output, :class_name, :class_base, :superclass, :method, :assigns,
9
+ :do_end, :new_style_hashes)
10
+
11
+ @filename = filename
12
+ @contents = contents
13
+ @options = options
14
+ @line_count = @contents.split(/(\r|\n|\r\n)/).length
15
+ end
16
+
17
+ def write_transformed_content!
18
+ html_options = {
19
+ :class_name => output_class_name,
20
+ :superclass => options[:superclass],
21
+ :method => options[:method],
22
+ :assigns => options[:assigns],
23
+ :do_end => options[:do_end],
24
+ :new_style_hashes => options[:new_style_hashes]
25
+ }
26
+
27
+ html = HTML.new(contents, html_options).render
28
+
29
+ if output_filename == '-'
30
+ puts html
31
+ else
32
+ FileUtils.mkdir_p(File.dirname(output_filename))
33
+ File.open(output_filename, 'w') { |f| f << html }
34
+ end
35
+ end
36
+
37
+ def output_filename
38
+ if (! options[:output]) && (filename == "-")
39
+ "-"
40
+ elsif (! options[:output])
41
+ if filename =~ /^(.*)\.html\.erb$/i || filename =~ /^(.*)\.rhtml$/i
42
+ "#{$1}.rb"
43
+ else
44
+ "#{filename}.rb"
45
+ end
46
+ elsif File.directory?(options[:output])
47
+ File.join(File.expand_path(options[:output]), "#{output_class_name.underscore}.rb")
48
+ else
49
+ File.expand_path(options[:output])
50
+ end
51
+ end
52
+
53
+ private
54
+ attr_reader :contents, :options
55
+
56
+ def output_class_name
57
+ if options[:class_name]
58
+ options[:class_name]
59
+ else
60
+ cb = class_base
61
+ if filename.start_with?("#{cb}/")
62
+ out = filename[(cb.length + 1)..-1].camelize
63
+ out = $1 if out =~ %r{^(.*?)\.[^/]+$}i
64
+ out
65
+ else
66
+ raise %{You specified a class base using the -b command-line option:
67
+ #{class_base}
68
+ but the file you asked to parse is not underneath that directory:
69
+ #{filename}}
70
+ end
71
+ end
72
+ end
73
+
74
+ def class_base
75
+ File.expand_path(options[:class_base] || infer_class_base)
76
+ end
77
+
78
+ def infer_class_base
79
+ if filename =~ %r{^(.*app)/views/.*$}
80
+ File.expand_path($1)
81
+ elsif filename == "-"
82
+ raise %{When converting standard input, you must specify a name for the output class
83
+ using the -c command-line option. (Otherwise, we have no way of knowing what to name this widget!)}
84
+ else
85
+ raise %{We can't figure out what the name of the widget class for this file should be:
86
+ #{filename}
87
+ You must either specify an explicit name for the class, using the -c command-line option, or
88
+ specify a base directory to infer the class name from, using the -b command-line option
89
+ (e.g., "-b my_rails_app/app").}
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,3 @@
1
+ module Html2fortitude
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'html2fortitude'
2
+ require 'helpers/standard_helper'
3
+
4
+ RSpec.configure do |c|
5
+ c.include StandardHelper
6
+ end
@@ -0,0 +1,71 @@
1
+ class Html2FortitudeResult
2
+ attr_reader :needs, :class_name, :superclass, :method_name
3
+
4
+ def initialize(text)
5
+ lines = text.split(/[\r\n]/)
6
+ if lines.shift =~ /^\s*class\s*(\S+)\s+<\s+(\S+)\s*$/i
7
+ @class_name = $1
8
+ @superclass = $2
9
+ else
10
+ raise "Can't find 'class' declaration in: #{text}"
11
+ end
12
+
13
+ @needs = { }
14
+
15
+ lines.shift while lines[0] =~ /^\s*$/i
16
+
17
+ while lines[0] =~ /^\s*needs\s*(.*?)\s*$/i
18
+ need_name = $1
19
+ need_value = nil
20
+
21
+ if need_name =~ /^(.*?)\s*=>\s*(.*?)\s*$/i
22
+ need_name = $1
23
+ need_value = $2
24
+ end
25
+
26
+ @needs[need_name] = need_value
27
+
28
+ lines.shift
29
+ end
30
+
31
+ lines.shift while lines[0] =~ /^\s*$/i
32
+
33
+ if lines[0] =~ /^\s+def\s+(\S+)\s*$/i
34
+ @method_name = $1
35
+ lines.shift
36
+ else
37
+ raise "Can't find 'def' in: #{text}"
38
+ end
39
+
40
+ lines.pop while lines[-1] =~ /^\s*$/i
41
+ if lines[-1] =~ /^\s*end\s*$/i
42
+ lines.pop
43
+ else
44
+ raise "Can't find last 'end' in: #{text}"
45
+ end
46
+
47
+ lines.pop while lines[-1] =~ /^\s*$/i
48
+ if lines[-1] =~ /^\s*end\s*$/i
49
+ lines.pop
50
+ else
51
+ raise "Can't find last 'end' in: #{text}"
52
+ end
53
+
54
+ @content_lines = lines
55
+ @content_lines = @content_lines.map do |content_line|
56
+ content_line = content_line.rstrip
57
+ if content_line[0..3] == ' '
58
+ content_line[4..-1]
59
+ else
60
+ content_line
61
+ end
62
+ end
63
+ end
64
+
65
+ def content_text
66
+ @content_lines.join("\n")
67
+ end
68
+
69
+ private
70
+ attr_reader :text
71
+ end
@@ -0,0 +1,70 @@
1
+ require 'helpers/html2fortitude_result'
2
+ require 'html2fortitude/html'
3
+
4
+ module StandardHelper
5
+ def default_html_options
6
+ {
7
+ :class_name => "SpecClass",
8
+ :superclass => "Fortitude::Widget::Html5",
9
+ :method => "content",
10
+ :assigns => :needs_defaulted_to_nil,
11
+ :do_end => false,
12
+ :new_style_hashes => false
13
+ }
14
+ end
15
+
16
+ def h2f(input, options = { })
17
+ Html2FortitudeResult.new(Html2fortitude::HTML.new(input, default_html_options.merge(options)).render)
18
+ end
19
+
20
+ def h2f_content(input, options = { })
21
+ h2f(input, options).content_text
22
+ end
23
+
24
+ def h2f_from(filename)
25
+ Html2FortitudeResult.new(get(filename))
26
+ end
27
+
28
+ def invoke(*args)
29
+ cmd = "#{binary_path} #{args.join(" ")} 2>&1"
30
+ output = `#{cmd}`
31
+ unless $?.success?
32
+ raise "Invocation failed: ran: #{cmd}\nin: #{Dir.pwd}\nand got: #{$?.inspect}\nwith output:\n#{output}"
33
+ end
34
+ output
35
+ end
36
+
37
+ def splat!(filename, data)
38
+ FileUtils.mkdir_p(File.dirname(filename))
39
+ File.open(filename, 'w') { |f| f << data }
40
+ end
41
+
42
+ def get(filename)
43
+ raise Errno::ENOENT, "No such file: #{filename.inspect}" unless File.file?(filename)
44
+ File.read(filename).strip
45
+ end
46
+
47
+ def with_temp_directory(name, &block)
48
+ directory = File.join(temp_directory_base, name)
49
+ FileUtils.rm_rf(directory) if File.exist?(directory)
50
+ FileUtils.mkdir_p(directory)
51
+ Dir.chdir(directory, &block)
52
+ end
53
+
54
+ private
55
+ def temp_directory_base
56
+ @temp_directory_base ||= begin
57
+ out = File.join(gem_root, 'tmp', 'specs')
58
+ FileUtils.mkdir_p(out)
59
+ out
60
+ end
61
+ end
62
+
63
+ def gem_root
64
+ @gem_root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
65
+ end
66
+
67
+ def binary_path
68
+ @binary_path ||= File.join(gem_root, 'bin', 'html2fortitude')
69
+ end
70
+ end