erb-formatter 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +120 -0
- data/Rakefile +12 -0
- data/erb-formatter.gemspec +32 -0
- data/exe/erb-format +65 -0
- data/lib/erb/formatter/ignore_list.rb +15 -0
- data/lib/erb/formatter/version.rb +3 -0
- data/lib/erb/formatter.rb +365 -0
- data/sig/erb/formatter.rbs +7 -0
- metadata +73 -0
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
data/Gemfile
ADDED
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,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,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
|
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: []
|