erb-formatter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0e92985fbdbf82a7b1fd9908e54a4363ef74458b354edd71f5c1d4bb89fb99c3
4
+ data.tar.gz: 549bb67e76b412780c7b5eed871d630b204659b0857d8393ba10ee5312603583
5
+ SHA512:
6
+ metadata.gz: 313b11be7a997560cb6e925b35b5ce9b574baaaba8d7d837e676a6d31737e1c2f672925f15210ac63d7aac7ae1c35fe77cc9007c833cfd2d889334a8b8782013
7
+ data.tar.gz: cd71d740571e141b4c748e13b28306b189b1a3833649f87cbc611c2e2837fb5993be9b357bc110bb840d25d58be9778c77f503e96e6df2b85dadd361bd4cae2f
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-03-11
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in erb-formatter.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,23 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ erb-formatter (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ minitest (5.15.0)
10
+ rake (13.0.6)
11
+ rufo (0.13.0)
12
+
13
+ PLATFORMS
14
+ arm64-darwin-21
15
+
16
+ DEPENDENCIES
17
+ erb-formatter!
18
+ minitest (~> 5.0)
19
+ rake (~> 13.0)
20
+ rufo
21
+
22
+ BUNDLED WITH
23
+ 2.3.7
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Elia Schito
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # ERB::Formatter 🪜
2
+
3
+ Format ERB files with speed and precision.
4
+
5
+ Features:
6
+
7
+ - very fast
8
+ - attempts to limit length (configurable)
9
+ - tries to have an ouput similar to prettier for HTML
10
+ - indents correctly ruby blocks (e.g. `if`/`elsif`/`do`/`end`)
11
+ - designed to be integrated into editors and commit hooks
12
+ - gives meaningful output in case of errors (most of the time)
13
+ - will use multiline values for `class` and `data-action` attributes
14
+
15
+ Roadmap:
16
+
17
+ - extensive unit testing
18
+ - more configuration options
19
+ - more ruby reformatting capabilities
20
+ - JavaScript and CSS formatting
21
+ - VSCode plugin
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ ```ruby
28
+ gem 'erb-formatter'
29
+ gem 'rufo' # for enabling minimal ruby re-formatting
30
+ ```
31
+
32
+ And then execute:
33
+
34
+ $ bundle install
35
+
36
+ Or install it yourself as:
37
+
38
+ $ gem install erb-formatter
39
+
40
+ ## Usage
41
+
42
+ ### From the command line
43
+
44
+ $ echo "<div > asdf <% if 123%> <%='foobar'%> <%end-%> </div>" | erb-format --stdin
45
+ <div>
46
+ asdf
47
+ <% if 123 %>
48
+ <%= 'foobar' %>
49
+ <% end -%>
50
+ </div>
51
+
52
+
53
+ Check out `erb-format --help` for more options.
54
+
55
+ ### From Ruby
56
+
57
+ ```ruby
58
+ require 'erb/formatter'
59
+
60
+ formatted = ERB::Formatter.format <<-ERB
61
+ <div >
62
+ asdf
63
+ <% if 123%>
64
+ <%='foobar'%> <%end-%>
65
+ </div>
66
+ ERB
67
+
68
+ # => "<div>\n asdf\n <% if 123 %>\n <%= 'foobar' %>\n <% end -%>\n</div>\n"
69
+ #
70
+ # Same as:
71
+ #
72
+ # <div>
73
+ # asdf
74
+ # <% if 123 %>
75
+ # <%= 'foobar' %>
76
+ # <% end -%>
77
+ # </div>
78
+ ```
79
+
80
+ ### With `lint-staged`
81
+
82
+ Add the gem to your gemfile and the following to your `package.json`:
83
+
84
+ ```js
85
+ "lint-staged": {
86
+ // …
87
+ "*.html.erb": "bundle exec erb-format --write"
88
+ }
89
+ ```
90
+
91
+ ### As a TextMate plugin
92
+
93
+ Create a command with the following settings:
94
+
95
+ - **Scope selector:** `text.html.erb`
96
+ - **Semantic class:** `callback.document.will-save`
97
+ - **Input:** `document` → `text`
98
+ - **Output:** `replace document` → `text`
99
+ - **Caret placement:** `line-interpolation`
100
+
101
+ ```bash
102
+ #!/usr/bin/env bash
103
+
104
+ cd "$TM_PROJECT_DIRECTORY"
105
+ bundle exec erb-format --stdin-filename "$TM_FILEPATH" < /dev/stdin 2> /dev/stdout
106
+ ```
107
+
108
+ ## Development
109
+
110
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
111
+
112
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
113
+
114
+ ## Contributing
115
+
116
+ Bug reports and pull requests are welcome on GitHub at https://github.com/elia/erb-formatter.
117
+
118
+ ## License
119
+
120
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/erb/formatter"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "erb-formatter"
7
+ spec.version = ERB::Formatter::VERSION
8
+ spec.authors = ["Elia Schito"]
9
+ spec.email = ["elia@schito.me"]
10
+
11
+ spec.summary = "Format ERB files with speed and precision."
12
+ spec.homepage = "https://github.com/nebulab/erb-formatter#readme"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/nebulab/erb-formatter"
18
+ spec.metadata["changelog_uri"] = "https://github.com/nebulab/erb-formatter/releases"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_development_dependency "rufo"
32
+ end
data/exe/erb-format ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+
5
+ write, filename, read_stdin, code = nil
6
+
7
+ OptionParser.new do |parser|
8
+ parser.banner = "Usage: #{$0} FILENAME... --write"
9
+
10
+ parser.on("-w", "--[no-]write", "Write file") do |value|
11
+ write = value
12
+ end
13
+
14
+ parser.on("--stdin-filename FILEPATH", "Set the stdin filename (implies --stdin)") do |value|
15
+ filename = value
16
+ read_stdin = true
17
+ end
18
+
19
+ parser.on("--[no-]stdin", "Read the file from stdin") do |value|
20
+ if read_stdin == true && value == false
21
+ abort "Can't set stdin filename and not use stdin at the same time"
22
+ end
23
+
24
+ read_stdin = value
25
+ filename ||= '-'
26
+ end
27
+
28
+ parser.on("-h", "--help", "Prints this help") do
29
+ puts parser
30
+ exit
31
+ end
32
+ end.parse!(ARGV)
33
+
34
+ abort "Can't read both stdin and a list of files" if read_stdin && !ARGV.empty?
35
+
36
+ # If multiple files are provided assume `--write` and
37
+ # execute on each of them.
38
+ if ARGV.size > 1
39
+ ARGV.each do
40
+ warn "==> Formatting #{_1}..."
41
+ system __FILE__, _1, *[
42
+ ('--write' if write)
43
+ ].compact or exit(1)
44
+ end
45
+ exit
46
+ end
47
+
48
+ require 'erb/formatter'
49
+
50
+ filename ||= ARGV.first
51
+ code = read_stdin ? $stdin.read : File.read(filename)
52
+ ignore = ERB::Formatter::IgnoreList.new
53
+
54
+ if ignore.should_ignore_file? filename
55
+ print code unless write
56
+ else
57
+ html = ERB::Formatter.format(code, filename: filename)
58
+
59
+ if write
60
+ File.write(filename, html)
61
+ else
62
+ puts html
63
+ end
64
+ end
65
+
@@ -0,0 +1,15 @@
1
+ class ERB::Formatter::IgnoreList
2
+ def initialize(contents: nil, base_dir: Dir.pwd)
3
+ ignore_list_path = "#{base_dir}/.format-erb-ignore"
4
+ @contents = contents || (File.exists?(ignore_list_path) ? File.read(ignore_list_path) : '')
5
+ @ignore_list = @contents.lines
6
+ end
7
+
8
+ def should_ignore_file?(path)
9
+ path = File.expand_path(path, @base_dir)
10
+ @ignore_list.any? do
11
+ File.fnmatch? File.expand_path(_1.chomp, @base_dir), path
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb/formatter'
@@ -0,0 +1,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ # $DEBUG = true
4
+ require "erb"
5
+ require "cgi"
6
+ require "ripper"
7
+ require 'securerandom'
8
+ require 'strscan'
9
+ require 'pp'
10
+ require 'stringio'
11
+
12
+ class ERB::Formatter
13
+ VERSION = "0.1.0"
14
+ autoload :IgnoreList, 'erb/formatter/ignore_list'
15
+
16
+ class Error < StandardError; end
17
+
18
+ # https://stackoverflow.com/a/317081
19
+ ATTR_NAME = %r{[^\r\n\t\f\v= '"<>]*[^\r\n\t\f\v= '"<>/]} # not ending with a slash
20
+ UNQUOTED_VALUE = ATTR_NAME
21
+ UNQUOTED_ATTR = %r{#{ATTR_NAME}=#{UNQUOTED_VALUE}}
22
+ SINGLE_QUOTE_ATTR = %r{(?:#{ATTR_NAME}='[^']*?')}m
23
+ DOUBLE_QUOTE_ATTR = %r{(?:#{ATTR_NAME}="[^"]*?")}m
24
+ BAD_ATTR = %r{#{ATTR_NAME}=\s+}
25
+ QUOTED_ATTR = Regexp.union(SINGLE_QUOTE_ATTR, DOUBLE_QUOTE_ATTR)
26
+ ATTR = Regexp.union(SINGLE_QUOTE_ATTR, DOUBLE_QUOTE_ATTR, UNQUOTED_ATTR, UNQUOTED_VALUE)
27
+ MULTILINE_ATTR_NAMES = %w[class data-action]
28
+
29
+ ERB_TAG = %r{(<%(?:==|=|-|))\s*(.*?)\s*(-?%>)}m
30
+ ERB_PLACEHOLDER = %r{erb[a-z0-9]+tag}
31
+ ERB_END = %r{(<%-?)\s*(end)\s*(-?%>)}
32
+ ERB_ELSE = %r{(<%-?)\s*(else|elsif\b.*)\s*(-?%>)}
33
+
34
+ HTML_ATTR = %r{\s+#{SINGLE_QUOTE_ATTR}|\s+#{DOUBLE_QUOTE_ATTR}|\s+#{UNQUOTED_ATTR}|\s+#{ATTR_NAME}}m
35
+ HTML_TAG_OPEN = %r{<(\w+)((?:#{HTML_ATTR})*)(\s*?)(/>|>)}m
36
+ HTML_TAG_CLOSE = %r{</\s*(\w+)\s*>}
37
+
38
+ SELF_CLOSING_TAG = /\A(area|base|br|col|command|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)\z/i
39
+
40
+ ERB_OPEN_BLOCK = ->(code) do
41
+ # is nil when the parsing is broken, meaning it's an open expression
42
+ Ripper.sexp(code).nil?
43
+ end.freeze
44
+
45
+ RUBOCOP_STDIN_MARKER = "===================="
46
+
47
+ # Override the max line length to account from already indented ERB
48
+ module RubocopForcedMaxLineLength
49
+ def max
50
+ Thread.current['RuboCop::Cop::Layout::LineLength#max'] || super
51
+ end
52
+ end
53
+
54
+ module DebugShovel
55
+ def <<(string)
56
+ puts "ADDING: #{string.inspect} FROM:\n #{caller(1, 5).join("\n ")}"
57
+ super
58
+ end
59
+ end
60
+
61
+ def self.format(source, filename: nil)
62
+ new(source, filename: filename).html
63
+ end
64
+
65
+ def initialize(source, line_width: 80, filename: nil)
66
+ @original_source = source
67
+ @filename = filename || '(erb)'
68
+ @line_width = line_width
69
+ @source = source.dup
70
+ @html = +""
71
+
72
+ html.extend DebugShovel if $DEBUG
73
+
74
+ @tag_stack = []
75
+ @pre_pos = 0
76
+
77
+ build_uid = -> { ['erb', SecureRandom.uuid, 'tag'].join.delete('-') }
78
+
79
+ @pre_placeholders = {}
80
+ @erb_tags = {}
81
+
82
+ @source.gsub!(ERB_PLACEHOLDER) { |tag| build_uid[].tap { |uid| pre_placeholders[uid] = tag } }
83
+ @source.gsub!(ERB_TAG) { |tag| build_uid[].tap { |uid| erb_tags[uid] = tag } }
84
+
85
+ @erb_tags_regexp = /(#{Regexp.union(erb_tags.keys)})/
86
+ @pre_placeholders_regexp = /(#{Regexp.union(pre_placeholders.keys)})/
87
+ @tags_regexp = Regexp.union(HTML_TAG_CLOSE, HTML_TAG_OPEN)
88
+
89
+ format
90
+ freeze
91
+ end
92
+
93
+ attr_accessor \
94
+ :source, :html, :tag_stack, :pre_pos, :pre_placeholders, :erb_tags, :erb_tags_regexp,
95
+ :pre_placeholders_regexp, :tags_regexp, :line_width
96
+
97
+ alias to_s html
98
+
99
+ def format_attributes(tag_name, attrs, tag_closing)
100
+ return "" if attrs.strip.empty?
101
+
102
+ plain_attrs = attrs.tr("\n", " ").squeeze(" ").gsub(erb_tags_regexp, erb_tags)
103
+ return " #{plain_attrs}" if "<#{tag_name} #{plain_attrs}#{tag_closing}".size <= line_width
104
+
105
+ attr_html = ""
106
+ tag_stack_push(['attr='], attrs)
107
+ attrs.scan(ATTR).flatten.each do |attr|
108
+ attr.strip!
109
+ full_attr = indented(attr)
110
+ name, value = attr.split('=', 2)
111
+
112
+ if full_attr.size > line_width && MULTILINE_ATTR_NAMES.include?(name) && attr.match?(QUOTED_ATTR)
113
+ attr_html << indented("#{name}=#{value[0]}")
114
+ tag_stack_push('attr"', value)
115
+ value[1...-1].strip.split(/\s+/).each do |value_part|
116
+ attr_html << indented(value_part)
117
+ end
118
+ tag_stack_pop('attr"', value)
119
+ attr_html << indented(value[-1])
120
+ else
121
+ attr_html << full_attr
122
+ end
123
+ end
124
+ tag_stack_pop(['attr='], attrs)
125
+ attr_html << indented("")
126
+ attr_html
127
+ end
128
+
129
+ def format_erb_attributes(string)
130
+ erb_scanner = StringScanner.new(string.to_s)
131
+ erb_pre_pos = 0
132
+ until erb_scanner.eos?
133
+ if erb_scanner.scan_until(erb_tags_regexp)
134
+ erb_pre_match = erb_scanner.pre_match
135
+ erb_pre_match = erb_pre_match[erb_pre_pos..]
136
+ erb_pre_pos = erb_scanner.pos
137
+
138
+ erb_code = erb_tags[erb_scanner.captures.first]
139
+
140
+ format_attributes(erb_pre_match)
141
+
142
+ erb_open, ruby_code, erb_close = ERB_TAG.match(erb_code).captures
143
+ full_erb_tag = "#{erb_open} #{ruby_code} #{erb_close}"
144
+
145
+ case ruby_code
146
+ when /\Aend\z/
147
+ tag_stack_pop('%erb%', ruby_code)
148
+ html << (erb_pre_match.match?(/\s+\z/) ? indented(full_erb_tag) : full_erb_tag)
149
+ when /\A(else|elsif\b(.*))\z/
150
+ tag_stack_pop('%erb%', ruby_code)
151
+ html << (erb_pre_match.match?(/\s+\z/) ? indented(full_erb_tag) : full_erb_tag)
152
+ tag_stack_push('%erb%', ruby_code)
153
+ when ERB_OPEN_BLOCK
154
+ ruby_code = format_ruby(ruby_code, autoclose: true)
155
+ html << (erb_pre_match.match?(/\s+\z/) ? indented(full_erb_tag) : full_erb_tag)
156
+ tag_stack_push('%erb%', ruby_code)
157
+ else
158
+ ruby_code = format_ruby(ruby_code, autoclose: false)
159
+ html << (erb_pre_match.match?(/\s+\z/) ? indented(full_erb_tag) : full_erb_tag)
160
+ end
161
+ else
162
+ rest = erb_scanner.rest.to_s
163
+ format_erb_attributes(rest)
164
+ erb_scanner.terminate
165
+ end
166
+ end
167
+ end
168
+
169
+ def tag_stack_push(tag_name, code)
170
+ tag_stack << [tag_name, code]
171
+ p PUSH: tag_stack if $DEBUG
172
+ end
173
+
174
+ def tag_stack_pop(tag_name, code)
175
+ if tag_name == tag_stack.last&.first
176
+ tag_stack.pop
177
+ p POP_: tag_stack if $DEBUG
178
+ else
179
+ raise "Unmatched close tag, tried with #{[tag_name, code]}, but #{tag_stack.last} was on the stack"
180
+ end
181
+ end
182
+
183
+ def raise(message)
184
+ line = @original_source[0..pre_pos].count("\n")
185
+ location = "#{@filename}:#{line}:in `#{tag_stack.last&.first}'"
186
+ error = RuntimeError.new([
187
+ nil,
188
+ "==> FORMATTED:",
189
+ html,
190
+ "==> STACK:",
191
+ tag_stack.pretty_inspect,
192
+ "==> ERROR: #{message}",
193
+ ].join("\n"))
194
+ error.set_backtrace caller.to_a + [location]
195
+ super error
196
+ end
197
+
198
+ def indented(string)
199
+ indent = " " * tag_stack.size
200
+ "\n#{indent}#{string.strip}"
201
+ end
202
+
203
+ def format_text(text)
204
+ starting_space = text.match?(/\A\s/)
205
+
206
+ final_newlines_count = text.match(/(\s*)\z/m).captures.last.count("\n")
207
+ html << "\n" if final_newlines_count > 1
208
+
209
+ return if text.match?(/\A\s*\z/m) # empty
210
+
211
+ text = text.gsub(/\s+/m, ' ').strip
212
+
213
+ offset = (starting_space ? indented("") : "").size
214
+ lines = []
215
+
216
+ until text.empty?
217
+ lines << text.slice!(0..text.rindex(' ', -(line_width - offset)))
218
+ offset = 0
219
+ end
220
+
221
+ html << lines.shift.strip unless starting_space
222
+ lines.each do |line|
223
+ html << indented(line)
224
+ end
225
+ end
226
+
227
+ def format_code_with_rubocop(code, line_width)
228
+ stdin, stdout = $stdin, $stdout
229
+ $stdin = StringIO.new(code)
230
+ $stdout = StringIO.new
231
+
232
+ Thread.current['RuboCop::Cop::Layout::LineLength#max'] = line_width
233
+
234
+ @rubocop_cli ||= begin
235
+ RuboCop::Cop::Layout::LineLength.prepend self
236
+ RuboCop::CLI.new
237
+ end
238
+
239
+ @rubocop_cli.run([
240
+ '--auto-correct',
241
+ '--stdin', @filename,
242
+ '-f', 'quiet',
243
+ ])
244
+
245
+ $stdout.string.split(RUBOCOP_STDIN_MARKER, 2).last
246
+ ensure
247
+ $stdin, $stdout = stdin, stdout
248
+ Thread.current['RuboCop::Cop::Layout::LineLength#max'] = nil
249
+ end
250
+
251
+ def format_ruby(code, autoclose: false)
252
+ if autoclose
253
+ code += "\nend" unless ERB_OPEN_BLOCK["#{code}\nend"]
254
+ code += "\n}" unless ERB_OPEN_BLOCK["#{code}\n}"]
255
+ end
256
+ p RUBY_IN_: code if $DEBUG
257
+
258
+ offset = tag_stack.size * 2
259
+ if defined? Rubocop
260
+ code = format_code_with_rubocop(code, line_width - offset) if (offset + code.size) > line_width
261
+ elsif defined?(Rufo)
262
+ code = Rufo.format(code) rescue code
263
+ end
264
+
265
+ lines = code.strip.lines
266
+ lines = lines[0...-1] if autoclose
267
+ code = lines.map { indented(_1) }.join.strip
268
+ p RUBY_OUT: code if $DEBUG
269
+ code
270
+ end
271
+
272
+ def format_erb_tags(string)
273
+ if %w[style script].include?(tag_stack.last&.first)
274
+ html << string.rstrip
275
+ return
276
+ end
277
+
278
+ erb_scanner = StringScanner.new(string.to_s)
279
+ erb_pre_pos = 0
280
+ until erb_scanner.eos?
281
+ if erb_scanner.scan_until(erb_tags_regexp)
282
+ erb_pre_match = erb_scanner.pre_match
283
+ erb_pre_match = erb_pre_match[erb_pre_pos..]
284
+ erb_pre_pos = erb_scanner.pos
285
+
286
+ erb_code = erb_tags[erb_scanner.captures.first]
287
+
288
+ format_text(erb_pre_match)
289
+
290
+ erb_open, ruby_code, erb_close = ERB_TAG.match(erb_code).captures
291
+ erb_open << ' ' unless ruby_code.start_with?('#')
292
+ full_erb_tag = "#{erb_open}#{ruby_code} #{erb_close}"
293
+
294
+ case ruby_code
295
+ when /\Aend\z/
296
+ tag_stack_pop('%erb%', ruby_code)
297
+ html << (erb_pre_match.match?(/\s+\z/) ? indented(full_erb_tag) : full_erb_tag)
298
+ when /\A(else|elsif\b(.*))\z/
299
+ tag_stack_pop('%erb%', ruby_code)
300
+ html << (erb_pre_match.match?(/\s+\z/) ? indented(full_erb_tag) : full_erb_tag)
301
+ tag_stack_push('%erb%', ruby_code)
302
+ when ERB_OPEN_BLOCK
303
+ ruby_code = format_ruby(ruby_code, autoclose: true)
304
+ html << (erb_pre_match.match?(/\s+\z/) ? indented(full_erb_tag) : full_erb_tag)
305
+ tag_stack_push('%erb%', ruby_code)
306
+ else
307
+ ruby_code = format_ruby(ruby_code, autoclose: false)
308
+ html << (erb_pre_match.match?(/\s+\z/) ? indented(full_erb_tag) : full_erb_tag)
309
+ end
310
+ else
311
+ rest = erb_scanner.rest.to_s
312
+ format_text(rest)
313
+ erb_scanner.terminate
314
+ end
315
+ end
316
+ end
317
+
318
+ def format
319
+ scanner = StringScanner.new(source)
320
+
321
+ until scanner.eos?
322
+
323
+ if matched = scanner.scan_until(tags_regexp)
324
+ pre_match = scanner.pre_match[pre_pos..]
325
+ self.pre_pos = scanner.pos
326
+
327
+ # Don't accept `name= "value"` attributes
328
+ raise "Bad attribute, please fix spaces after the equal sign." if BAD_ATTR.match? pre_match
329
+
330
+ format_erb_tags(pre_match) if pre_match
331
+
332
+ if matched.match?(HTML_TAG_CLOSE)
333
+ tag_name = scanner.captures.first
334
+
335
+ full_tag = "</#{tag_name}>"
336
+ tag_stack_pop(tag_name, full_tag)
337
+ html << (scanner.pre_match.match?(/\s+\z/) ? indented(full_tag) : full_tag)
338
+
339
+ elsif matched.match(HTML_TAG_OPEN)
340
+ _, tag_name, tag_attrs, _, tag_closing = *scanner.captures
341
+
342
+ raise "Unknown tag #{tag_name.inspect}" unless tag_name.match?(/\A[a-z0-9]+\z/)
343
+
344
+ tag_self_closing = tag_closing == '/>' || SELF_CLOSING_TAG.match?(tag_name)
345
+ tag_attrs.strip!
346
+ formatted_tag_name = format_attributes(tag_name, tag_attrs.strip, tag_closing).gsub(erb_tags_regexp, erb_tags)
347
+ full_tag = "<#{tag_name}#{formatted_tag_name}#{tag_closing}"
348
+ html << (scanner.pre_match.match?(/\s+\z/) ? indented(full_tag) : full_tag)
349
+
350
+ tag_stack_push(tag_name, full_tag) unless tag_self_closing
351
+ else
352
+ raise "Unrecognized content: #{matched.inspect}"
353
+ end
354
+ else
355
+ format_erb_tags(scanner.rest.to_s)
356
+ scanner.terminate
357
+ end
358
+ end
359
+
360
+ html.gsub!(erb_tags_regexp, erb_tags)
361
+ html.gsub!(pre_placeholders_regexp, pre_placeholders)
362
+ html.strip!
363
+ html << "\n"
364
+ end
365
+ end
@@ -0,0 +1,7 @@
1
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
2
+ class ERB::Formatter
3
+ VERSION: String
4
+
5
+ def initialize: (String, line_width: ?Integer, filename: ?String) -> void
6
+ def self.format(String, filename: ?String) -> String
7
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: erb-formatter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elia Schito
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-03-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rufo
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description:
28
+ email:
29
+ - elia@schito.me
30
+ executables:
31
+ - erb-format
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - CHANGELOG.md
36
+ - Gemfile
37
+ - Gemfile.lock
38
+ - LICENSE.txt
39
+ - README.md
40
+ - Rakefile
41
+ - erb-formatter.gemspec
42
+ - exe/erb-format
43
+ - lib/erb/formatter.rb
44
+ - lib/erb/formatter/ignore_list.rb
45
+ - lib/erb/formatter/version.rb
46
+ - sig/erb/formatter.rbs
47
+ homepage: https://github.com/nebulab/erb-formatter#readme
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ homepage_uri: https://github.com/nebulab/erb-formatter#readme
52
+ source_code_uri: https://github.com/nebulab/erb-formatter
53
+ changelog_uri: https://github.com/nebulab/erb-formatter/releases
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.6.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.3.7
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Format ERB files with speed and precision.
73
+ test_files: []