htmlformatter 1.5.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a5ebc277c83a5ccab865848d6aeb362fc75d2f79e873521840c11bb279462a9e
4
+ data.tar.gz: 5068a689fedc3844836affa11eb2fa2afb0018073be7802f22f7fa5b1068f4bf
5
+ SHA512:
6
+ metadata.gz: 578e632d90a7c13840b0f195e0eba4f983f9f4ddd4d3ce7c63e6f1197d5cb88765a8496af8057e33c69037806a45d0b57cd54cf765ed78c504b61bb241d9c8ab
7
+ data.tar.gz: 4a88997ed83e8086e696ccd053c9af830bca8ac8f6d0377ab612aa5a6d9812df2b0951312e74caf8849d1478c16e9d0604d5bc6952f1433b2518e1830590f360
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # HTML Formatter
2
+
3
+ A normaliser/formatter for HTML that also understands embedded Ruby and Elixir.
4
+ Ideal for tidying up Rails and Phoenix templates.
5
+
6
+ ## What it does
7
+
8
+ * Normalises hard tabs to spaces (or vice versa)
9
+ * Removes trailing spaces
10
+ * Indents after opening HTML elements
11
+ * Outdents before closing elements
12
+ * Collapses multiple whitespace
13
+ * Indents after block-opening embedded Ruby or Elixir (if, do etc.)
14
+ * Outdents before closing Ruby or Elixir blocks
15
+ * Outdents elsif and then indents again
16
+ * Indents the left-hand margin of JavaScript and CSS blocks to match the
17
+ indentation level of the code
18
+
19
+ ## Usage
20
+
21
+ ### From the command line
22
+
23
+ To update files in-place:
24
+
25
+ ``` sh
26
+ $ htmlformatter file1 [file2 ...]
27
+ ```
28
+
29
+ or to operate on standard input and output:
30
+
31
+ ``` sh
32
+ $ htmlformatter < input > output
33
+ ```
34
+
35
+ ### In your code
36
+
37
+ ```ruby
38
+ require 'htmlformatter'
39
+
40
+ formatted = HtmlFormatter.format(messy)
41
+ ```
42
+
43
+ You can also specify how to indent (the default is two spaces):
44
+
45
+ ```ruby
46
+ formatted = HtmlFormatter.format(messy, indent: "\t")
47
+ ```
48
+
49
+ ## Installation
50
+
51
+ This is a Ruby gem.
52
+ To install the command-line tool (you may need `sudo`):
53
+
54
+ ```sh
55
+ $ gem install htmlformatter
56
+ ```
57
+
58
+ To use the gem with Bundler, add to your `Gemfile`:
59
+
60
+ ```ruby
61
+ gem 'htmlformatter'
62
+ ```
63
+
64
+ ## Contributing
65
+
66
+ 1. Follow [these guidelines][git-commit] when writing commit messages (briefly,
67
+ the first line should begin with a capital letter, use the imperative mood,
68
+ be no more than 50 characters, and not end with a period).
69
+ 2. Include tests.
70
+
71
+ [git-commit]:http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require "rspec/core/rake_task"
2
+
3
+ desc "Run the specs."
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.pattern = "spec/**/*_spec.rb"
6
+ t.verbose = false
7
+ end
8
+
9
+ task :default => [:spec]
10
+
11
+ if Gem.loaded_specs.key?('rubocop')
12
+ require 'rubocop/rake_task'
13
+ RuboCop::RakeTask.new
14
+
15
+ task(:default).prerequisites << task(:rubocop)
16
+ end
data/bin/htmlformatter ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+ require "htmlformatter"
3
+ require "optparse"
4
+ require "fileutils"
5
+
6
+ def format(name, input, output, options)
7
+ output.puts HtmlFormatter.format(input, options)
8
+ rescue => e
9
+ raise "Error parsing #{name}: #{e}"
10
+ end
11
+
12
+ executable = File.basename(__FILE__)
13
+
14
+ options = { indent: " " }
15
+ parser = OptionParser.new do |opts|
16
+ opts.banner = "Usage: #{executable} [options] [file ...]"
17
+ opts.separator <<END
18
+
19
+ #{executable} has two modes of operation:
20
+
21
+ 1. If no files are listed, it will read from standard input and write to
22
+ standard output.
23
+ 2. If files are listed, it will modify each file in place, overwriting it
24
+ with the formatted output.
25
+
26
+ The following options are available:
27
+
28
+ END
29
+ opts.on(
30
+ "-t", "--tab-stops NUMBER", Integer,
31
+ "Set number of spaces per indent (default #{options[:tab_stops]})"
32
+ ) do |num|
33
+ options[:indent] = " " * num
34
+ end
35
+ opts.on(
36
+ "-T", "--tab",
37
+ "Indent using tabs"
38
+ ) do
39
+ options[:indent] = "\t"
40
+ end
41
+ opts.on(
42
+ "-i", "--indent-by NUMBER", Integer,
43
+ "Indent the output by NUMBER steps (default 0)."
44
+ ) do |num|
45
+ options[:initial_level] = num
46
+ end
47
+ opts.on(
48
+ "-e", "--stop-on-errors",
49
+ "Stop when invalid nesting is encountered in the input"
50
+ ) do |num|
51
+ options[:stop_on_errors] = num
52
+ end
53
+ opts.on(
54
+ "-b", "--keep-blank-lines NUMBER", Integer,
55
+ "Set number of consecutive blank lines"
56
+ ) do |num|
57
+ options[:keep_blank_lines] = num
58
+ end
59
+ opts.on(
60
+ "-n", "--engine STRING", String,
61
+ "Use engine STRING (default erb, allowed: eex)."
62
+ ) do |eng|
63
+ options[:engine] = eng
64
+ end
65
+ opts.on(
66
+ "-h", "--help",
67
+ "Display this help message and exit"
68
+ ) do
69
+ puts opts
70
+ exit
71
+ end
72
+ end
73
+ parser.parse!
74
+
75
+ if ARGV.any?
76
+ ARGV.each do |path|
77
+ input = File.read(path)
78
+ temppath = path + ".tmp"
79
+ File.open(temppath, "w") do |output|
80
+ format path, input, output, options
81
+ end
82
+ FileUtils.mv temppath, path
83
+ end
84
+ else
85
+ format "standard input", $stdin.read, $stdout, options
86
+ end
@@ -0,0 +1,29 @@
1
+ require "htmlformatter/builder"
2
+ require "htmlformatter/html_parser"
3
+ require "htmlformatter/version"
4
+
5
+ module HtmlFormatter
6
+ #
7
+ # Returns a formatted HTML/HTML+EEX document as a String.
8
+ # html must be an object that responds to +#to_s+.
9
+ #
10
+ # Available options are:
11
+ # tab_stops - an integer for the number of spaces to indent, default 2.
12
+ # Deprecated: see indent.
13
+ # indent - what to indent with (" ", "\t" etc.), default " "
14
+ # stop_on_errors - raise an exception on a badly-formed document. Default
15
+ # is false, i.e. continue to process the rest of the document.
16
+ # initial_level - The entire output will be indented by this number of steps.
17
+ # Default is 0.
18
+ # keep_blank_lines - an integer for the number of consecutive empty lines
19
+ # to keep in output.
20
+ #
21
+ def self.format(html, options = {})
22
+ if options[:tab_stops]
23
+ options[:indent] = " " * options[:tab_stops]
24
+ end
25
+ "".tap { |output|
26
+ HtmlParser.new.scan html.to_s, Builder.new(output, options)
27
+ }
28
+ end
29
+ end
@@ -0,0 +1,147 @@
1
+ require "htmlformatter/parser"
2
+ require "htmlformatter/ruby_indenter"
3
+ require "htmlformatter/elixir_indenter"
4
+
5
+ module HtmlFormatter
6
+ class Builder
7
+ DEFAULT_OPTIONS = {
8
+ indent: " ",
9
+ initial_level: 0,
10
+ stop_on_errors: false,
11
+ keep_blank_lines: 0
12
+ }
13
+
14
+ def initialize(output, options = {})
15
+ options = DEFAULT_OPTIONS.merge(options)
16
+ @tab = options[:indent]
17
+ @stop_on_errors = options[:stop_on_errors]
18
+ @level = options[:initial_level]
19
+ @keep_blank_lines = options[:keep_blank_lines]
20
+ @new_line = false
21
+ @empty = true
22
+ @ie_cc_levels = []
23
+ @output = output
24
+
25
+ if options[:engine] == "eex"
26
+ @embedded_indenter = ElixirIndenter.new
27
+ else
28
+ @embedded_indenter = RubyIndenter.new
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def error(text)
35
+ return unless @stop_on_errors
36
+ raise text
37
+ end
38
+
39
+ def indent
40
+ @level += 1
41
+ end
42
+
43
+ def outdent
44
+ error "Extraneous closing tag" if @level == 0
45
+ @level = [@level - 1, 0].max
46
+ end
47
+
48
+ def emit(*strings)
49
+ @output << "\n" if @new_line && !@empty
50
+ @output << (@tab * @level) if @new_line
51
+ @output << strings.join("")
52
+ @new_line = false
53
+ @empty = false
54
+ end
55
+
56
+ def new_line
57
+ @new_line = true
58
+ end
59
+
60
+ def embed(opening, code, closing)
61
+ lines = code.split(%r{\n}).map(&:strip)
62
+ outdent if @embedded_indenter.outdent?(lines)
63
+ emit opening, code, closing
64
+ indent if @embedded_indenter.indent?(lines)
65
+ end
66
+
67
+ def foreign_block(opening, code, closing)
68
+ emit opening
69
+ emit_reindented_block_content code unless code.strip.empty?
70
+ emit closing
71
+ end
72
+
73
+ def emit_reindented_block_content(code)
74
+ lines = code.strip.split(%r{\n})
75
+ indentation = foreign_block_indentation(code)
76
+
77
+ indent
78
+ new_line
79
+ lines.each do |line|
80
+ emit line.rstrip.sub(%r{^#{indentation}}, "")
81
+ new_line
82
+ end
83
+ outdent
84
+ end
85
+
86
+ def foreign_block_indentation(code)
87
+ code.split(%r{\n}).find { |ln| !ln.strip.empty? }[%r{^\s+}]
88
+ end
89
+
90
+ def preformatted_block(opening, content, closing)
91
+ new_line
92
+ emit opening, content, closing
93
+ new_line
94
+ end
95
+
96
+ def standalone_element(e)
97
+ emit e
98
+ new_line if e =~ %r{^<br[^\w]}
99
+ end
100
+
101
+ def close_element(e)
102
+ outdent
103
+ emit e
104
+ end
105
+
106
+ def close_block_element(e)
107
+ close_element e
108
+ new_line
109
+ end
110
+
111
+ def open_element(e)
112
+ emit e
113
+ indent
114
+ end
115
+
116
+ def open_block_element(e)
117
+ new_line
118
+ open_element e
119
+ end
120
+
121
+ def close_ie_cc(e)
122
+ if @ie_cc_levels.empty?
123
+ error "Unclosed conditional comment"
124
+ else
125
+ @level = @ie_cc_levels.pop
126
+ end
127
+ emit e
128
+ end
129
+
130
+ def open_ie_cc(e)
131
+ emit e
132
+ @ie_cc_levels.push @level
133
+ indent
134
+ end
135
+
136
+ def new_lines(*content)
137
+ blank_lines = content.first.scan(%r{\n}).count - 1
138
+ blank_lines = [blank_lines, @keep_blank_lines].min
139
+ @output << "\n" * blank_lines
140
+ new_line
141
+ end
142
+
143
+ def text(t)
144
+ emit t
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,19 @@
1
+ module HtmlFormatter
2
+ class ElixirIndenter
3
+ INDENT_KEYWORDS = %w[ else ]
4
+ OUTDENT_KEYWORDS = %w[ else end ]
5
+ ELIXIR_INDENT = %r{
6
+ ^ ( #{INDENT_KEYWORDS.join("|")} )\b
7
+ | ( -\> | do ) $
8
+ }xo
9
+ ELIXIR_OUTDENT = %r{ ^ ( #{OUTDENT_KEYWORDS.join("|")} | \} ) \b }xo
10
+
11
+ def outdent?(lines)
12
+ lines.first =~ ELIXIR_OUTDENT
13
+ end
14
+
15
+ def indent?(lines)
16
+ lines.last =~ ELIXIR_INDENT
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,61 @@
1
+ require "htmlformatter/parser"
2
+
3
+ module HtmlFormatter
4
+ class HtmlParser < Parser
5
+ ELEMENT_CONTENT = %r{ (?:<%.*?%>|[^>])* }mx
6
+ HTML_VOID_ELEMENTS = %r{(?:
7
+ area | base | br | col | command | embed | hr | img | input | keygen |
8
+ link | meta | param | source | track | wbr
9
+ )}mix
10
+ HTML_BLOCK_ELEMENTS = %r{(?:
11
+ address | article | aside | audio | blockquote | canvas | dd | dir | div |
12
+ dl | dt | fieldset | figcaption | figure | footer | form | h1 | h2 | h3 |
13
+ h4 | h5 | h6 | header | hr | li | menu | noframes | noscript | ol | p |
14
+ pre | section | table | tbody | td | tfoot | th | thead | tr | ul | video
15
+ )}mix
16
+
17
+ MAPPINGS = [
18
+ [%r{(<%-?=?)(.*?)(-?%>)}om,
19
+ :embed],
20
+ [%r{<!--\[.*?\]>}om,
21
+ :open_ie_cc],
22
+ [%r{<!\[.*?\]-->}om,
23
+ :close_ie_cc],
24
+ [%r{<!--.*?-->}om,
25
+ :standalone_element],
26
+ [%r{<!.*?>}om,
27
+ :standalone_element],
28
+ [%r{(<script#{ELEMENT_CONTENT}>)(.*?)(</script>)}omi,
29
+ :foreign_block],
30
+ [%r{(<style#{ELEMENT_CONTENT}>)(.*?)(</style>)}omi,
31
+ :foreign_block],
32
+ [%r{(<pre#{ELEMENT_CONTENT}>)(.*?)(</pre>)}omi,
33
+ :preformatted_block],
34
+ [%r{(<textarea#{ELEMENT_CONTENT}>)(.*?)(</textarea>)}omi,
35
+ :preformatted_block],
36
+ [%r{<#{HTML_VOID_ELEMENTS}(?: #{ELEMENT_CONTENT})?/?>}om,
37
+ :standalone_element],
38
+ [%r{<\w+(?: #{ELEMENT_CONTENT})?/>}om,
39
+ :standalone_element],
40
+ [%r{</#{HTML_BLOCK_ELEMENTS}>}om,
41
+ :close_block_element],
42
+ [%r{<#{HTML_BLOCK_ELEMENTS}(?: #{ELEMENT_CONTENT})?>}om,
43
+ :open_block_element],
44
+ [%r{</#{ELEMENT_CONTENT}>}om,
45
+ :close_element],
46
+ [%r{<#{ELEMENT_CONTENT}>}om,
47
+ :open_element],
48
+ [%r{(\s*\r?\n\s*)+}om,
49
+ :new_lines],
50
+ [%r{[^<\n]+},
51
+ :text]]
52
+
53
+ def initialize
54
+ super do |p|
55
+ MAPPINGS.each do |regexp, method|
56
+ p.map regexp, method
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,48 @@
1
+ require "strscan"
2
+
3
+ module HtmlFormatter
4
+ class Parser
5
+ def initialize
6
+ @maps = []
7
+ yield self if block_given?
8
+ end
9
+
10
+ def map(pattern, method)
11
+ @maps << [pattern, method]
12
+ end
13
+
14
+ def scan(subject, receiver)
15
+ @scanner = StringScanner.new(subject)
16
+ dispatch(receiver) until @scanner.eos?
17
+ end
18
+
19
+ def source_so_far
20
+ @scanner.string[0...@scanner.pos]
21
+ end
22
+
23
+ def source_line_number
24
+ [source_so_far.chomp.split(%r{\n}).count, 1].max
25
+ end
26
+
27
+ private
28
+
29
+ def dispatch(receiver)
30
+ _, method = @maps.find { |pattern, _| @scanner.scan(pattern) }
31
+ raise "Unmatched sequence" unless method
32
+ receiver.__send__(method, *extract_params(@scanner))
33
+ rescue => ex
34
+ raise "#{ex.message} on line #{source_line_number}"
35
+ end
36
+
37
+ def extract_params(scanner)
38
+ return [scanner[0]] unless scanner[1]
39
+ params = []
40
+ i = 1
41
+ while scanner[i]
42
+ params << scanner[i]
43
+ i += 1
44
+ end
45
+ params
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ module HtmlFormatter
2
+ class RubyIndenter
3
+ INDENT_KEYWORDS = %w[ if elsif else unless while until begin for ]
4
+ OUTDENT_KEYWORDS = %w[ elsif else end ]
5
+ RUBY_INDENT = %r{
6
+ ^ ( #{INDENT_KEYWORDS.join("|")} )\b
7
+ | \b ( do | \{ ) ( \s* \| [^\|]+ \| )? $
8
+ }xo
9
+ RUBY_OUTDENT = %r{ ^ ( #{OUTDENT_KEYWORDS.join("|")} | \} ) \b }xo
10
+
11
+ def outdent?(lines)
12
+ lines.first =~ RUBY_OUTDENT
13
+ end
14
+
15
+ def indent?(lines)
16
+ lines.last =~ RUBY_INDENT
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module HtmlFormatter #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 1
4
+ MINOR = 5
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join(".")
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: htmlformatter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Battley
8
+ - Bartosz Kalinowski
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2021-06-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '3'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '3'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rubocop
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: 0.30.0
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: 0.30.0
56
+ description: A normaliser/formatter for HTML that also understands embedded Ruby and
57
+ Elixir.
58
+ email: kelostrada@gmail.com
59
+ executables:
60
+ - htmlformatter
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - README.md
65
+ - Rakefile
66
+ - bin/htmlformatter
67
+ - lib/htmlformatter.rb
68
+ - lib/htmlformatter/builder.rb
69
+ - lib/htmlformatter/elixir_indenter.rb
70
+ - lib/htmlformatter/html_parser.rb
71
+ - lib/htmlformatter/parser.rb
72
+ - lib/htmlformatter/ruby_indenter.rb
73
+ - lib/htmlformatter/version.rb
74
+ homepage: http://github.com/kelostrada/htmlformatter
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 1.9.2
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 3.0.3
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: HTML/ERB/EEX formatter
97
+ test_files: []