html2fortitude 0.0.1

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