erbf 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/.rubocop.yml +40 -0
- data/.streerc +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +118 -0
- data/exe/erbf +7 -0
- data/lib/erbf/cli.rb +139 -0
- data/lib/erbf/config.rb +81 -0
- data/lib/erbf/formatter/embedded_language_formatter.rb +41 -0
- data/lib/erbf/formatter/html_helper.rb +110 -0
- data/lib/erbf/formatter/prettier_print_helper.rb +33 -0
- data/lib/erbf/formatter/ruby_formatter.rb +94 -0
- data/lib/erbf/formatter.rb +537 -0
- data/lib/erbf/version.rb +5 -0
- data/lib/erbf.rb +34 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fa38a8ddf489f16a90c48c63039d9509210f13764d1cbe7590f8b094de30d9c8
|
4
|
+
data.tar.gz: ec92348e114992ffef85120d0467cb53df0cd55fe7b55eff8e2fd9446e37abee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c14b1941af80e13905fea704dace9d92f633370ba5d18e8c3ab5a2159b8139120681aa9bc239018d7bb1b327c9cb2b686b367ac8716012963f41cdad79dadc6d
|
7
|
+
data.tar.gz: 061c828ee94a80c20ff91a2944924a4e73bb884490c1653f21a58727f7bb5bc0e08192c364b9fbbad324534d09e29a3e80b97a850f953a400ccbac7569c4f823
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
inherit_gem:
|
2
|
+
syntax_tree: config/rubocop.yml
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
DisplayCopNames: true
|
6
|
+
DisplayStyleGuide: true
|
7
|
+
NewCops: enable
|
8
|
+
SuggestExtensions: false
|
9
|
+
TargetRubyVersion: 3.0
|
10
|
+
|
11
|
+
Metrics:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Style/Documentation:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Naming/MethodName:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Naming/MethodParameterName:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Layout/LineLength:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Style/GuardClause:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Style/IfUnlessModifier:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
Style/DocumentDynamicEvalDefinition:
|
33
|
+
Exclude:
|
34
|
+
- test/fixtures/**/*.rb
|
35
|
+
|
36
|
+
Style/FormatString:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Style/ClassAndModuleChildren:
|
40
|
+
EnforcedStyle: compact
|
data/.streerc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--print-width=100
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Tomasz Szczęśniak-Szlagowski
|
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,118 @@
|
|
1
|
+
# erbf
|
2
|
+
|
3
|
+
A formatter for your `.html.erb` files.
|
4
|
+
|
5
|
+
> [!CAUTION]
|
6
|
+
> erbf is still in alpha stages of development. Use at your own risk.
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
#### Style similar to Prettier's
|
11
|
+
|
12
|
+
Many teams use prettier and it'd be strange if `.html.erb` files were formatted
|
13
|
+
differently than `.html` files in the same project.
|
14
|
+
|
15
|
+
If some HTML (without ERB tags) gets formatted differently than Prettier with
|
16
|
+
default settings would have done it, that's a bug.
|
17
|
+
|
18
|
+
Support for `--html-whitespace-sensitivity` may be added in the future.
|
19
|
+
|
20
|
+
#### Formats Ruby code
|
21
|
+
|
22
|
+
SyntaxTree will be used if it's available. If you don't want your Ruby code
|
23
|
+
reformatted, you can disable it in the config file.
|
24
|
+
|
25
|
+
#### Formats other embedded languages
|
26
|
+
|
27
|
+
The code within `<script>` and `<style>` tags will be formatted if `prettier`
|
28
|
+
is installed under the `node_modules` directory.
|
29
|
+
|
30
|
+
You can also specify any other formatter that has a CLI which can format STDIN.
|
31
|
+
|
32
|
+
#### CLI
|
33
|
+
|
34
|
+
Format/check/write specified files (or STDIN)
|
35
|
+
|
36
|
+
#### Planned
|
37
|
+
|
38
|
+
- Some kind of "no-format" comment support
|
39
|
+
- Support for any other Ruby formatter
|
40
|
+
- Ruby LSP Plugin
|
41
|
+
- SyntaxTree Plugin
|
42
|
+
- Rake tasks
|
43
|
+
- Website with examples of: formatting, configuration, integration with other tools and IDEs
|
44
|
+
|
45
|
+
## Installation
|
46
|
+
|
47
|
+
Install the gem, either as your project's dependency or globally:
|
48
|
+
|
49
|
+
```sh
|
50
|
+
bundle add erbf --group "development, test"
|
51
|
+
# or
|
52
|
+
gem install erbf
|
53
|
+
```
|
54
|
+
|
55
|
+
## Usage
|
56
|
+
|
57
|
+
```sh
|
58
|
+
# format all *.html.erb files in a directory
|
59
|
+
erbf directory
|
60
|
+
|
61
|
+
# check if all files are formatted
|
62
|
+
erbf -c directory
|
63
|
+
|
64
|
+
# auto-format all files
|
65
|
+
erbf -w directory
|
66
|
+
|
67
|
+
# auto-format files with a different extension
|
68
|
+
erbf 'directory/**/*.erb'
|
69
|
+
|
70
|
+
# format stdin
|
71
|
+
erbf < file.erb
|
72
|
+
```
|
73
|
+
|
74
|
+
You can configure it via a config file in your repo:
|
75
|
+
|
76
|
+
```yaml
|
77
|
+
# .erbf.yml or config/erbf.yml
|
78
|
+
line_length: 80
|
79
|
+
ruby:
|
80
|
+
formatter: syntax_tree
|
81
|
+
syntax_tree_plugins:
|
82
|
+
- plugin/single_quotes
|
83
|
+
embedded:
|
84
|
+
- types:
|
85
|
+
- text/javascript
|
86
|
+
- module
|
87
|
+
command: prettier --stdin-filepath file.js --print-width %<line_length>d
|
88
|
+
- types:
|
89
|
+
- text/css
|
90
|
+
command: prettier --stdin-filepath file.css --print-width %<line_length>d
|
91
|
+
```
|
92
|
+
|
93
|
+
## Development
|
94
|
+
|
95
|
+
```sh
|
96
|
+
# Install dependencies
|
97
|
+
bin/setup
|
98
|
+
|
99
|
+
# Run the linter, formatter and tests
|
100
|
+
bundle exec rake
|
101
|
+
|
102
|
+
# Run a REPL
|
103
|
+
bin/console
|
104
|
+
|
105
|
+
# Install the gem locally
|
106
|
+
bundle exec rake install
|
107
|
+
|
108
|
+
# Release a new version (after updating version.rb)
|
109
|
+
bundle exec rake release
|
110
|
+
```
|
111
|
+
|
112
|
+
## Contributing
|
113
|
+
|
114
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/spect88/erbf.
|
115
|
+
|
116
|
+
## License
|
117
|
+
|
118
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/erbf
ADDED
data/lib/erbf/cli.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
class Erbf::CLI
|
6
|
+
class Error < StandardError
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.call(...)
|
10
|
+
new.call(...)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@command = :format
|
15
|
+
@files = []
|
16
|
+
@input = nil
|
17
|
+
@config_path = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse!(argv, stdin)
|
21
|
+
argv_left = argv.dup
|
22
|
+
|
23
|
+
begin
|
24
|
+
options.parse!(argv_left)
|
25
|
+
rescue OptionParser::MissingArgument, OptionParser::InvalidOption => e
|
26
|
+
raise Error, e.message
|
27
|
+
end
|
28
|
+
|
29
|
+
if @command == :format
|
30
|
+
@input = stdin.read if (argv_left.empty? && !stdin.tty?) || argv_left.include?("-")
|
31
|
+
argv_left -= ["-"]
|
32
|
+
end
|
33
|
+
|
34
|
+
@files =
|
35
|
+
argv_left
|
36
|
+
.flat_map do |path|
|
37
|
+
paths = Dir[path]
|
38
|
+
raise Error, "invalid file/directory/glob: #{path}" if paths.empty?
|
39
|
+
|
40
|
+
dirs, files = paths.partition { |p| File.directory?(p) }
|
41
|
+
files + dirs.flat_map { |d| Dir["#{d}/**/*.html.erb"] }
|
42
|
+
end
|
43
|
+
.uniq
|
44
|
+
|
45
|
+
if @input.nil? && @files.empty? && %i[format check write].include?(@command)
|
46
|
+
raise Error, "no file/directory/glob specified"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def execute(stdout = $stdout, stderr = $stderr)
|
51
|
+
erbf = Erbf.new(config_file: @config_path)
|
52
|
+
case @command
|
53
|
+
when :help
|
54
|
+
stdout.puts options
|
55
|
+
true
|
56
|
+
when :version
|
57
|
+
stdout.puts Erbf::VERSION
|
58
|
+
true
|
59
|
+
when :format
|
60
|
+
@files.each do |path|
|
61
|
+
content = File.read(path)
|
62
|
+
stdout.puts erbf.format_code(content)
|
63
|
+
end
|
64
|
+
stdout.puts erbf.format_code(@input) if @input
|
65
|
+
true
|
66
|
+
when :check
|
67
|
+
success = true
|
68
|
+
@files.each do |path|
|
69
|
+
original = File.read(path)
|
70
|
+
formatted = erbf.format_code(original)
|
71
|
+
formatted = "#{formatted}\n"
|
72
|
+
next if original == formatted
|
73
|
+
|
74
|
+
stderr.puts "#{path} needs to be formatted"
|
75
|
+
success = false
|
76
|
+
end
|
77
|
+
success
|
78
|
+
when :write
|
79
|
+
@files.each do |path|
|
80
|
+
original = File.read(path)
|
81
|
+
formatted = erbf.format_code(original)
|
82
|
+
formatted = "#{formatted}\n"
|
83
|
+
next if original == formatted
|
84
|
+
|
85
|
+
File.write(path, formatted)
|
86
|
+
stdout.puts "Formatted: #{path}"
|
87
|
+
end
|
88
|
+
true
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def call(argv, stdin, stdout = $stdout, stderr = $stderr)
|
93
|
+
parse!(argv, stdin)
|
94
|
+
execute(stdout, stderr) ? 0 : 1
|
95
|
+
rescue Error => e
|
96
|
+
stderr.puts "#{e.message}\n\n"
|
97
|
+
stderr.puts options
|
98
|
+
1
|
99
|
+
end
|
100
|
+
|
101
|
+
def options
|
102
|
+
@options ||=
|
103
|
+
OptionParser.new do |opts|
|
104
|
+
opts.banner = <<~USAGE
|
105
|
+
Usage: erbf [options] [files/directories/glob]
|
106
|
+
|
107
|
+
By default the output is written to stdout
|
108
|
+
|
109
|
+
Output options:
|
110
|
+
|
111
|
+
USAGE
|
112
|
+
opts.on("-c", "--check", "Check if the files are formatted") do
|
113
|
+
raise Error, "incompatible options: --#{@command} and --check" if @command != :format
|
114
|
+
@command = :check
|
115
|
+
end
|
116
|
+
opts.on("-w", "--write", "Format the files in-place") do
|
117
|
+
raise Error, "incompatible options: --#{@command} and --write" if @command != :format
|
118
|
+
@command = :write
|
119
|
+
end
|
120
|
+
opts.separator <<~USAGE
|
121
|
+
|
122
|
+
Other options:
|
123
|
+
|
124
|
+
USAGE
|
125
|
+
opts.on("--config PATH", String, "Use a config file at a different location") do |path|
|
126
|
+
@config_path = path
|
127
|
+
end
|
128
|
+
opts.on("-h", "--help", "Show this help") do
|
129
|
+
raise Error, "incompatible options: --#{@command} and --help" if @command != :format
|
130
|
+
@command = :help
|
131
|
+
end
|
132
|
+
opts.on("-v", "--version", "Show erbf version") do
|
133
|
+
raise Error, "incompatible options: --#{@command} and --version" if @command != :format
|
134
|
+
@command = :version
|
135
|
+
end
|
136
|
+
opts.separator ""
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/erbf/config.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
class Erbf::Config
|
7
|
+
DEFAULT_FILEPATHS = %w[config/erbf.yml .erbf.yml].freeze
|
8
|
+
|
9
|
+
attr_reader :line_length, :logger, :embedded, :ruby
|
10
|
+
|
11
|
+
def initialize(line_length:, logger:, debug:, embedded:, ruby:)
|
12
|
+
@line_length = line_length
|
13
|
+
@logger = logger
|
14
|
+
@debug = debug
|
15
|
+
@embedded = embedded
|
16
|
+
@ruby = ruby
|
17
|
+
end
|
18
|
+
|
19
|
+
def debug? = @debug
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def load(config_file: nil, **options)
|
23
|
+
opts =
|
24
|
+
default_options.merge(
|
25
|
+
config_file.nil? ? options_from_default_file : options_from_file(config_file),
|
26
|
+
options
|
27
|
+
)
|
28
|
+
new(**opts)
|
29
|
+
end
|
30
|
+
|
31
|
+
def default_embedded
|
32
|
+
prettier = "node_modules/.bin/prettier"
|
33
|
+
if File.exist?(prettier)
|
34
|
+
[
|
35
|
+
{
|
36
|
+
types: %w[text/javascript module],
|
37
|
+
command: "#{prettier} --stdin-filepath file.js --print-width %<line_length>d"
|
38
|
+
},
|
39
|
+
{
|
40
|
+
types: ["text/css"],
|
41
|
+
command: "#{prettier} --stdin-filepath file.css --print-width %<line_length>d"
|
42
|
+
},
|
43
|
+
{
|
44
|
+
types: %w[importmap application/json application/ld+json],
|
45
|
+
command: "#{prettier} --stdin-filepath file.json --print-width %<line_length>d"
|
46
|
+
}
|
47
|
+
]
|
48
|
+
else
|
49
|
+
[]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def default_ruby
|
54
|
+
{ formatter: "syntax_tree", syntax_tree_plugins: [] }
|
55
|
+
end
|
56
|
+
|
57
|
+
def default_options
|
58
|
+
{
|
59
|
+
line_length: 80,
|
60
|
+
logger: Logger.new($stderr, level: Logger::WARN),
|
61
|
+
debug: false,
|
62
|
+
embedded: default_embedded,
|
63
|
+
ruby: default_ruby
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def options_from_file(path)
|
68
|
+
yaml = YAML.safe_load_file(path, symbolize_names: true)
|
69
|
+
{ line_length: yaml[:line_length], embedded: yaml[:embedded], ruby: yaml[:ruby] }.compact
|
70
|
+
end
|
71
|
+
|
72
|
+
def options_from_default_file
|
73
|
+
DEFAULT_FILEPATHS.each do |path|
|
74
|
+
next unless File.exist?(path)
|
75
|
+
|
76
|
+
return options_from_file(path)
|
77
|
+
end
|
78
|
+
{}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
class Erbf::Formatter::EmbeddedLanguageFormatter
|
6
|
+
def initialize(config)
|
7
|
+
@config = config
|
8
|
+
end
|
9
|
+
|
10
|
+
def supported?(type)
|
11
|
+
!find(type).nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def format(type, content, line_length)
|
15
|
+
formatter = find(type)
|
16
|
+
return content.chomp if formatter.nil?
|
17
|
+
|
18
|
+
command = sprintf(formatter[:command], line_length: line_length)
|
19
|
+
|
20
|
+
logger.debug(self.class.to_s) { "Formatting #{type}: #{command}" }
|
21
|
+
stdout, stderr, status = Open3.capture3({}, command, { stdin_data: content })
|
22
|
+
|
23
|
+
if status.exitstatus != 0
|
24
|
+
logger.error(self.class.to_s) { "[#{command}] exit status: #{status.exitstatus}" }
|
25
|
+
logger.warn(self.class.to_s) { "[#{command}] stderr output:\n#{stderr}" } unless stderr.empty?
|
26
|
+
logger.debug(self.class.to_s) { "[#{command}] input was:\n#{content}" }
|
27
|
+
end
|
28
|
+
|
29
|
+
status.exitstatus.zero? ? stdout.chomp : content.chomp
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def find(type)
|
35
|
+
@config.embedded.find { |e| e[:types].include?(type) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def logger
|
39
|
+
@config.logger
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Erbf::Formatter::HtmlHelper
|
4
|
+
CASE_INSENSITIVE_ATTRIBUTES = %w[id class].to_set.freeze
|
5
|
+
INLINE_TAGS = %w[
|
6
|
+
a
|
7
|
+
abbr
|
8
|
+
acronym
|
9
|
+
b
|
10
|
+
bdo
|
11
|
+
big
|
12
|
+
br
|
13
|
+
button
|
14
|
+
cite
|
15
|
+
code
|
16
|
+
dfn
|
17
|
+
em
|
18
|
+
i
|
19
|
+
img
|
20
|
+
input
|
21
|
+
kbd
|
22
|
+
label
|
23
|
+
map
|
24
|
+
object
|
25
|
+
output
|
26
|
+
q
|
27
|
+
samp
|
28
|
+
select
|
29
|
+
small
|
30
|
+
span
|
31
|
+
strong
|
32
|
+
sub
|
33
|
+
sup
|
34
|
+
textarea
|
35
|
+
time
|
36
|
+
tt
|
37
|
+
var
|
38
|
+
].to_set.freeze
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def inline?(node)
|
43
|
+
node.is_a?(Herb::AST::HTMLTextNode) || inline_tag?(node)
|
44
|
+
end
|
45
|
+
|
46
|
+
def inline_tag?(node)
|
47
|
+
node.is_a?(Herb::AST::HTMLElementNode) &&
|
48
|
+
INLINE_TAGS.include?(node.open_tag.tag_name.value.downcase)
|
49
|
+
end
|
50
|
+
|
51
|
+
def pre_tag?(node)
|
52
|
+
node.is_a?(Herb::AST::HTMLElementNode) && node.open_tag.tag_name.value.downcase == "pre"
|
53
|
+
end
|
54
|
+
|
55
|
+
def tag_attribute(node, name)
|
56
|
+
attribute =
|
57
|
+
node.open_tag.children.find do |child|
|
58
|
+
child.is_a?(Herb::AST::HTMLAttributeNode) && child.name.name.value.downcase == name.downcase
|
59
|
+
end
|
60
|
+
return nil if attribute.nil?
|
61
|
+
|
62
|
+
value_children = attribute.value.children
|
63
|
+
|
64
|
+
if value_children.size == 1 && value_children.first.is_a?(Herb::AST::LiteralNode)
|
65
|
+
value_children.first.content
|
66
|
+
else
|
67
|
+
:dynamic
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def case_insensitive_attribute_name?(node)
|
72
|
+
CASE_INSENSITIVE_ATTRIBUTES.include?(node.name.value.downcase)
|
73
|
+
end
|
74
|
+
|
75
|
+
def br_tag?(node)
|
76
|
+
node.is_a?(Herb::AST::HTMLElementNode) && node.open_tag.tag_name.value.downcase == "br"
|
77
|
+
end
|
78
|
+
|
79
|
+
def starts_with_whitespace?(node)
|
80
|
+
node.is_a?(Herb::AST::HTMLTextNode) && node.content =~ /\A\s/
|
81
|
+
end
|
82
|
+
|
83
|
+
def ends_with_whitespace?(node)
|
84
|
+
node.is_a?(Herb::AST::HTMLTextNode) && node.content =~ /\s\z/
|
85
|
+
end
|
86
|
+
|
87
|
+
def ends_with_double_newline?(node)
|
88
|
+
node.is_a?(Herb::AST::HTMLTextNode) && node.content =~ /\n\s*\n\z/
|
89
|
+
end
|
90
|
+
|
91
|
+
def ends_with_newline?(node)
|
92
|
+
node.is_a?(Herb::AST::HTMLTextNode) && node.content =~ /\n\z/
|
93
|
+
end
|
94
|
+
|
95
|
+
def begins_with_double_newline?(node)
|
96
|
+
node.is_a?(Herb::AST::HTMLTextNode) && node.content =~ /\A\n\s*\n/
|
97
|
+
end
|
98
|
+
|
99
|
+
def begins_with_newline?(node)
|
100
|
+
node.is_a?(Herb::AST::HTMLTextNode) && node.content =~ /\A\n/
|
101
|
+
end
|
102
|
+
|
103
|
+
def blank_node?(node)
|
104
|
+
node.is_a?(Herb::AST::HTMLTextNode) && node.content =~ /\A\s+\z/
|
105
|
+
end
|
106
|
+
|
107
|
+
def text_node?(node)
|
108
|
+
node.is_a?(Herb::AST::HTMLTextNode)
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Erbf::Formatter::PrettierPrintHelper
|
4
|
+
private
|
5
|
+
|
6
|
+
def current_indent_level
|
7
|
+
queue = [[0, q.groups.first]]
|
8
|
+
while (indent, node = queue.shift)
|
9
|
+
next_indent = indent
|
10
|
+
|
11
|
+
case node
|
12
|
+
when PrettierPrint::Indent
|
13
|
+
next_indent += 2
|
14
|
+
when PrettierPrint::Align
|
15
|
+
next_indent += node.indent
|
16
|
+
end
|
17
|
+
|
18
|
+
case node
|
19
|
+
when PrettierPrint::Indent, PrettierPrint::Align, PrettierPrint::Group,
|
20
|
+
PrettierPrint::LineSuffix
|
21
|
+
return indent if node.contents.equal?(q.target)
|
22
|
+
|
23
|
+
queue += node.contents.map { |child| [next_indent, child] }
|
24
|
+
when PrettierPrint::IfBreak
|
25
|
+
return indent if node.flat_contents.equal?(q.target) || node.break_contents.equal?(q.target)
|
26
|
+
|
27
|
+
queue += (node.flat_contents + node.break_contents).map { |child| [next_indent, child] }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Erbf::Formatter::RubyFormatter
|
4
|
+
def initialize(config)
|
5
|
+
@config = config
|
6
|
+
@loaded = false
|
7
|
+
end
|
8
|
+
|
9
|
+
def format(code, line_length)
|
10
|
+
formatter.call(code, line_length)
|
11
|
+
end
|
12
|
+
|
13
|
+
def format_incomplete(code, line_length)
|
14
|
+
code = code.strip
|
15
|
+
keyword = code[/\A[a-z]+/]
|
16
|
+
case keyword
|
17
|
+
when "if", "unless", "while", "until", "for", "begin"
|
18
|
+
format([code, "end"].join("\n"), line_length).delete_suffix("end").rstrip
|
19
|
+
when "elsif"
|
20
|
+
format(["if a", code, "end"].join("\n"), line_length)
|
21
|
+
.delete_prefix("if a")
|
22
|
+
.delete_suffix("end")
|
23
|
+
.strip
|
24
|
+
when "case"
|
25
|
+
format([code, "when true", "end"].join("\n"), line_length)
|
26
|
+
.delete_suffix("end")
|
27
|
+
.rstrip
|
28
|
+
.delete_suffix("when true")
|
29
|
+
.rstrip
|
30
|
+
when "when", "in"
|
31
|
+
format(["case a", code, "end"].join("\n"), line_length)
|
32
|
+
.delete_prefix("case a")
|
33
|
+
.delete_suffix("end")
|
34
|
+
.strip
|
35
|
+
when "rescue", "ensure"
|
36
|
+
format(["begin", code, "end"].join("\n"), line_length)
|
37
|
+
.delete_prefix("begin")
|
38
|
+
.delete_suffix("end")
|
39
|
+
.strip
|
40
|
+
else
|
41
|
+
if code =~ /(do|{)\s*(\|[\s,()\w-]+\|)?\z/
|
42
|
+
if Regexp.last_match(1) == "{"
|
43
|
+
format([code, "}"].join("\n"), line_length).delete_suffix("}").rstrip
|
44
|
+
else
|
45
|
+
# Formatter may convert short `do end` into `{}`, so let's add content
|
46
|
+
format([code, "a" * line_length, "end"].join("\n"), line_length)
|
47
|
+
.delete_suffix("end")
|
48
|
+
.rstrip
|
49
|
+
.delete_suffix("a" * line_length)
|
50
|
+
.rstrip
|
51
|
+
end
|
52
|
+
else
|
53
|
+
@config.logger.warn(self.class.to_s) { "Can't handle incomplete ruby: #{code}" }
|
54
|
+
code
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def formatter
|
62
|
+
@formatter ||=
|
63
|
+
case @config.ruby[:formatter]
|
64
|
+
when nil
|
65
|
+
@config.logger.debug(self.class.to_s) { "Using null formatter" }
|
66
|
+
method(:null_format)
|
67
|
+
when "syntax_tree"
|
68
|
+
begin
|
69
|
+
require "syntax_tree"
|
70
|
+
(@config.ruby[:syntax_tree_plugins] || []).each do |plugin|
|
71
|
+
require "syntax_tree/#{plugin}"
|
72
|
+
end
|
73
|
+
@config.logger.debug(self.class.to_s) { "Using syntax_tree formatter" }
|
74
|
+
method(:syntax_tree_format)
|
75
|
+
rescue LoadError => e
|
76
|
+
@config.logger.error(self.class.to_s) { e.to_s }
|
77
|
+
method(:null_format)
|
78
|
+
end
|
79
|
+
else
|
80
|
+
@config
|
81
|
+
.logger
|
82
|
+
.error(self.class.to_s) { "Unsupported Ruby formatter: #{@config.ruby[:formatter]}" }
|
83
|
+
method(:null_format)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def null_format(code, _line_length)
|
88
|
+
code.strip
|
89
|
+
end
|
90
|
+
|
91
|
+
def syntax_tree_format(code, line_length)
|
92
|
+
SyntaxTree.format(code, line_length).chomp
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,537 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Erbf::Formatter
|
4
|
+
autoload :HtmlHelper, "erbf/formatter/html_helper"
|
5
|
+
autoload :PrettierPrintHelper, "erbf/formatter/prettier_print_helper"
|
6
|
+
autoload :EmbeddedLanguageFormatter, "erbf/formatter/embedded_language_formatter"
|
7
|
+
autoload :RubyFormatter, "erbf/formatter/ruby_formatter"
|
8
|
+
|
9
|
+
include HtmlHelper
|
10
|
+
include PrettierPrintHelper
|
11
|
+
|
12
|
+
def initialize(q, config)
|
13
|
+
@q = q
|
14
|
+
@context = []
|
15
|
+
@config = config
|
16
|
+
@embedded_language = EmbeddedLanguageFormatter.new(config)
|
17
|
+
@ruby = RubyFormatter.new(config)
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit(node)
|
21
|
+
debug { "visiting #{node.class}" }
|
22
|
+
case node
|
23
|
+
when Herb::AST::DocumentNode
|
24
|
+
visit_document(node)
|
25
|
+
when Herb::AST::WhitespaceNode
|
26
|
+
visit_whitespace(node)
|
27
|
+
when Herb::AST::LiteralNode
|
28
|
+
visit_literal(node)
|
29
|
+
when Herb::AST::HTMLDoctypeNode
|
30
|
+
visit_html_doctype(node)
|
31
|
+
when Herb::AST::HTMLCommentNode
|
32
|
+
visit_html_comment(node)
|
33
|
+
when Herb::AST::HTMLElementNode
|
34
|
+
visit_html_element(node)
|
35
|
+
when Herb::AST::HTMLOpenTagNode
|
36
|
+
visit_html_open_tag(node)
|
37
|
+
when Herb::AST::HTMLCloseTagNode
|
38
|
+
visit_html_close_tag(node)
|
39
|
+
when Herb::AST::HTMLSelfCloseTagNode
|
40
|
+
visit_html_self_close_tag(node)
|
41
|
+
when Herb::AST::HTMLAttributeNode
|
42
|
+
visit_html_attribute(node)
|
43
|
+
when Herb::AST::HTMLAttributeNameNode
|
44
|
+
visit_html_attribute_name(node)
|
45
|
+
when Herb::AST::HTMLAttributeValueNode
|
46
|
+
visit_html_attribute_value(node)
|
47
|
+
when Herb::AST::HTMLTextNode
|
48
|
+
visit_html_text(node)
|
49
|
+
when Herb::AST::ERBIfNode
|
50
|
+
visit_erb_if(node)
|
51
|
+
when Herb::AST::ERBUnlessNode
|
52
|
+
visit_erb_unless(node)
|
53
|
+
when Herb::AST::ERBElseNode
|
54
|
+
visit_erb_else(node)
|
55
|
+
when Herb::AST::ERBCaseNode
|
56
|
+
visit_erb_case(node)
|
57
|
+
when Herb::AST::ERBWhenNode
|
58
|
+
visit_erb_when(node)
|
59
|
+
when Herb::AST::ERBWhileNode
|
60
|
+
visit_erb_while(node)
|
61
|
+
when Herb::AST::ERBUntilNode
|
62
|
+
visit_erb_until(node)
|
63
|
+
when Herb::AST::ERBBlockNode
|
64
|
+
visit_erb_block(node)
|
65
|
+
when Herb::AST::ERBForNode
|
66
|
+
visit_erb_for(node)
|
67
|
+
when Herb::AST::ERBRescueNode
|
68
|
+
visit_erb_rescue(node)
|
69
|
+
when Herb::AST::ERBBeginNode
|
70
|
+
visit_erb_begin(node)
|
71
|
+
when Herb::AST::ERBEnsureNode
|
72
|
+
visit_erb_ensure(node)
|
73
|
+
when Herb::AST::ERBContentNode
|
74
|
+
visit_erb_content(node)
|
75
|
+
when Herb::AST::ERBEndNode
|
76
|
+
visit_erb_end(node)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
attr_reader :q
|
83
|
+
|
84
|
+
def visit_document(node)
|
85
|
+
visit_elements(node.children)
|
86
|
+
end
|
87
|
+
|
88
|
+
def visit_html_doctype(node)
|
89
|
+
q.text(node.tag_opening.value.downcase)
|
90
|
+
node.children.each(&method(:visit))
|
91
|
+
q.text(node.tag_closing.value)
|
92
|
+
end
|
93
|
+
|
94
|
+
def visit_html_element(node)
|
95
|
+
if node.is_void
|
96
|
+
debug { "<#{node.open_tag.tag_name.value}> void" }
|
97
|
+
q.group do
|
98
|
+
visit(node.open_tag)
|
99
|
+
q.breakable(" ")
|
100
|
+
q.text("/>")
|
101
|
+
end
|
102
|
+
elsif inline_tag?(node)
|
103
|
+
debug { "<#{node.open_tag.tag_name.value}> inline" }
|
104
|
+
q.group do
|
105
|
+
visit(node.open_tag)
|
106
|
+
q.indent do
|
107
|
+
q.breakable("")
|
108
|
+
q.text(">")
|
109
|
+
visit_elements(node.body)
|
110
|
+
visit(node.close_tag) if node.close_tag
|
111
|
+
end
|
112
|
+
if node.close_tag
|
113
|
+
q.breakable("")
|
114
|
+
q.text(">")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
else
|
118
|
+
debug { "<#{node.open_tag.tag_name.value}> block" }
|
119
|
+
q.group do
|
120
|
+
q.group do
|
121
|
+
visit(node.open_tag)
|
122
|
+
q.breakable("") if node.body.any?
|
123
|
+
q.text(">")
|
124
|
+
end
|
125
|
+
if node.body.any?
|
126
|
+
if pre_tag?(node)
|
127
|
+
debug { "pre" }
|
128
|
+
q.breakable("", indent: false)
|
129
|
+
with_context(:preserve_whitespace) { node.body.each { |child| visit(child) } }
|
130
|
+
elsif (language = embedded_language(node))
|
131
|
+
debug { "embedded language: #{language}" }
|
132
|
+
if @embedded_language.supported?(language) && node.body.size == 1 &&
|
133
|
+
text_node?(node.body.first)
|
134
|
+
debug { "formatting" }
|
135
|
+
q.indent do
|
136
|
+
q.breakable("")
|
137
|
+
lines =
|
138
|
+
@embedded_language.format(
|
139
|
+
language,
|
140
|
+
node.body.first.content,
|
141
|
+
[@config.line_length - current_indent_level, 1].max
|
142
|
+
).lines
|
143
|
+
print_formatted_lines(lines)
|
144
|
+
end
|
145
|
+
else
|
146
|
+
debug { "unsupported or more than 1 child node" }
|
147
|
+
q.indent do
|
148
|
+
q.breakable("")
|
149
|
+
with_context(:preserve_whitespace) { node.body.each { |child| visit(child) } }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
else
|
153
|
+
debug { "normal block" }
|
154
|
+
q.indent do
|
155
|
+
q.breakable("")
|
156
|
+
visit_elements(node.body)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
if node.close_tag
|
161
|
+
q.breakable("") unless pre_tag?(node)
|
162
|
+
visit(node.close_tag)
|
163
|
+
q.text(">")
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def visit_html_open_tag(node)
|
170
|
+
q.group do
|
171
|
+
q.text("<")
|
172
|
+
q.text(node.tag_name.value.downcase)
|
173
|
+
if node.children.any?
|
174
|
+
q.indent do
|
175
|
+
q.breakable(" ")
|
176
|
+
q.seplist(node.children, -> { q.breakable(" ") }) { |child| visit(child) }
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
# The > is handled in #visit_html_element
|
181
|
+
end
|
182
|
+
|
183
|
+
def visit_html_close_tag(node)
|
184
|
+
q.text("</")
|
185
|
+
q.text(node.tag_name.value.downcase)
|
186
|
+
# The > is handled in #visit_html_element
|
187
|
+
end
|
188
|
+
|
189
|
+
def visit_html_attribute(node)
|
190
|
+
visit(node.name)
|
191
|
+
q.text("=") if node.value
|
192
|
+
visit(node.value)
|
193
|
+
end
|
194
|
+
|
195
|
+
def visit_html_attribute_name(node)
|
196
|
+
q.text(case_insensitive_attribute_name?(node) ? node.name.value.downcase : node.name.value)
|
197
|
+
end
|
198
|
+
|
199
|
+
def visit_html_attribute_value(node)
|
200
|
+
if node.children.size == 1 && node.children.first.is_a?(Herb::AST::LiteralNode)
|
201
|
+
# The value is a literal, so we can process it to find the optimal way to quote it
|
202
|
+
value = node.children.first.content
|
203
|
+
quotes_count = value.scan(/"|"|"/i).count
|
204
|
+
apostrophes_count = value.scan(/'|'|'/i).count
|
205
|
+
if quotes_count > apostrophes_count
|
206
|
+
q.text("'")
|
207
|
+
q.text(value.gsub(/"|"|"/i, '"').gsub(/'|'|'/i, "'"))
|
208
|
+
q.text("'")
|
209
|
+
else
|
210
|
+
q.text('"')
|
211
|
+
q.text(value.gsub(/"|"|"/i, """).gsub(/'|'|'/i, "'"))
|
212
|
+
q.text('"')
|
213
|
+
end
|
214
|
+
else
|
215
|
+
q.text(node.open_quote.value) if node.quoted
|
216
|
+
node.children.each(&method(:visit))
|
217
|
+
q.text(node.close_quote.value) if node.quoted
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def visit_literal(node)
|
222
|
+
q.text(node.content)
|
223
|
+
end
|
224
|
+
|
225
|
+
def visit_html_text(node)
|
226
|
+
if context?(:preserve_whitespace)
|
227
|
+
q.group { q.text(node.content) }
|
228
|
+
return
|
229
|
+
end
|
230
|
+
|
231
|
+
content = node.content.gsub(/\s+/, " ").strip
|
232
|
+
|
233
|
+
q.group do
|
234
|
+
content.split.each.with_index do |word, index|
|
235
|
+
q.fill_breakable(" ") if index.positive?
|
236
|
+
q.text(word)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def visit_html_comment(node)
|
242
|
+
q.text("<!--")
|
243
|
+
node.children.each(&method(:visit))
|
244
|
+
q.text("-->")
|
245
|
+
end
|
246
|
+
|
247
|
+
def visit_erb_content(node)
|
248
|
+
q.text(node.tag_opening.value)
|
249
|
+
|
250
|
+
# Don't format comments
|
251
|
+
if node.tag_opening.value == "<%#"
|
252
|
+
q.text(node.content.value)
|
253
|
+
q.text(node.tag_closing.value)
|
254
|
+
return
|
255
|
+
end
|
256
|
+
|
257
|
+
q.indent do
|
258
|
+
q.breakable(" ")
|
259
|
+
|
260
|
+
lines = format_ruby(node.content.value)
|
261
|
+
print_formatted_lines(lines)
|
262
|
+
end
|
263
|
+
|
264
|
+
q.breakable(" ")
|
265
|
+
q.text(node.tag_closing.value)
|
266
|
+
end
|
267
|
+
|
268
|
+
def visit_erb_if(node)
|
269
|
+
visit_erb_keyword(node) do
|
270
|
+
if node.subsequent
|
271
|
+
q.breakable("")
|
272
|
+
visit(node.subsequent)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def visit_erb_unless(node)
|
278
|
+
visit_erb_keyword(node) do
|
279
|
+
if node.else_clause
|
280
|
+
q.breakable("")
|
281
|
+
visit(node.else_clause)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def visit_erb_case(node)
|
287
|
+
visit_erb_keyword(node, can_have_statements: false) do
|
288
|
+
# "children" are the thing between "case condition" and "when value"
|
289
|
+
# Valid values are basically whitespace and ERB comments
|
290
|
+
if node.children.any?
|
291
|
+
q.breakable("")
|
292
|
+
visit_elements(node.children)
|
293
|
+
end
|
294
|
+
|
295
|
+
if node.conditions.any?
|
296
|
+
q.breakable("")
|
297
|
+
visit_elements(node.conditions)
|
298
|
+
end
|
299
|
+
|
300
|
+
if node.else_clause
|
301
|
+
q.breakable("")
|
302
|
+
visit(node.else_clause)
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def visit_erb_when(node)
|
308
|
+
visit_erb_keyword(node, can_have_end: false)
|
309
|
+
end
|
310
|
+
|
311
|
+
def visit_erb_else(node)
|
312
|
+
q.group do
|
313
|
+
q.text(node.tag_opening.value)
|
314
|
+
q.breakable(" ")
|
315
|
+
q.text(node.content.value.strip)
|
316
|
+
q.breakable(" ")
|
317
|
+
q.text(node.tag_closing.value)
|
318
|
+
end
|
319
|
+
|
320
|
+
if node.statements.any?
|
321
|
+
q.indent do
|
322
|
+
q.breakable("")
|
323
|
+
visit_elements(node.statements)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def visit_erb_while(node)
|
329
|
+
visit_erb_keyword(node)
|
330
|
+
end
|
331
|
+
|
332
|
+
def visit_erb_until(node)
|
333
|
+
visit_erb_keyword(node)
|
334
|
+
end
|
335
|
+
|
336
|
+
def visit_erb_for(node)
|
337
|
+
visit_erb_keyword(node)
|
338
|
+
end
|
339
|
+
|
340
|
+
def visit_erb_begin(node)
|
341
|
+
visit_erb_keyword(node) do
|
342
|
+
if node.rescue_clause
|
343
|
+
q.breakable("")
|
344
|
+
visit(node.rescue_clause)
|
345
|
+
end
|
346
|
+
|
347
|
+
if node.else_clause
|
348
|
+
q.breakable("")
|
349
|
+
visit(node.else_clause)
|
350
|
+
end
|
351
|
+
|
352
|
+
if node.ensure_clause
|
353
|
+
q.breakable("")
|
354
|
+
visit(node.ensure_clause)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
def visit_erb_rescue(node)
|
360
|
+
visit_erb_keyword(node, can_have_end: false) do
|
361
|
+
if node.subsequent
|
362
|
+
q.breakable("")
|
363
|
+
visit(node.subsequent)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def visit_erb_ensure(node)
|
369
|
+
visit_erb_keyword(node, can_have_end: false)
|
370
|
+
end
|
371
|
+
|
372
|
+
def visit_erb_block(node)
|
373
|
+
visit_erb_keyword(node, can_have_statements: false) do
|
374
|
+
if node.body.any?
|
375
|
+
q.indent do
|
376
|
+
q.breakable("")
|
377
|
+
visit_elements(node.body)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def visit_erb_end(node)
|
384
|
+
q.group do
|
385
|
+
q.text(node.tag_opening.value)
|
386
|
+
q.breakable(" ")
|
387
|
+
q.text(node.content.value.strip)
|
388
|
+
q.breakable(" ")
|
389
|
+
q.text(node.tag_closing.value)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def visit_erb_keyword(node, can_have_statements: true, can_have_end: true, &block)
|
394
|
+
q.group do
|
395
|
+
q.text(node.tag_opening.value)
|
396
|
+
|
397
|
+
q.indent do
|
398
|
+
q.breakable(" ")
|
399
|
+
|
400
|
+
lines = format_incomplete_ruby(node.content.value)
|
401
|
+
print_formatted_lines(lines)
|
402
|
+
end
|
403
|
+
|
404
|
+
q.breakable(" ")
|
405
|
+
q.text(node.tag_closing.value)
|
406
|
+
end
|
407
|
+
|
408
|
+
if can_have_statements && node.statements.any?
|
409
|
+
q.indent do
|
410
|
+
q.breakable("")
|
411
|
+
visit_elements(node.statements)
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
block.call if block_given?
|
416
|
+
|
417
|
+
if can_have_end && node.end_node
|
418
|
+
q.breakable("")
|
419
|
+
visit(node.end_node)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
def visit_elements(children)
|
424
|
+
if children.size == 1
|
425
|
+
visit(children.first)
|
426
|
+
return
|
427
|
+
end
|
428
|
+
|
429
|
+
groups =
|
430
|
+
children
|
431
|
+
.slice_when do |prev_child, next_child|
|
432
|
+
ends_with_double_newline?(prev_child) || begins_with_double_newline?(next_child)
|
433
|
+
end
|
434
|
+
.map do |group|
|
435
|
+
group.drop_while(&method(:blank_node?)).reverse.drop_while(&method(:blank_node?)).reverse
|
436
|
+
end
|
437
|
+
.reject(&:empty?)
|
438
|
+
|
439
|
+
break_next = false
|
440
|
+
|
441
|
+
(groups + [nil]).each_cons(2) do |group, next_group|
|
442
|
+
q.group do
|
443
|
+
(group + [nil]).each_cons(2) do |child, next_child|
|
444
|
+
next if blank_node?(child)
|
445
|
+
|
446
|
+
if break_next
|
447
|
+
q.group do
|
448
|
+
q.breakable(" ")
|
449
|
+
visit(child)
|
450
|
+
end
|
451
|
+
break_next = false
|
452
|
+
else
|
453
|
+
visit(child)
|
454
|
+
end
|
455
|
+
|
456
|
+
next if next_child.nil?
|
457
|
+
|
458
|
+
if br_tag?(child) && starts_with_whitespace?(next_child)
|
459
|
+
debug { "breakable(force) after <br> tag" }
|
460
|
+
q.breakable(force: true)
|
461
|
+
elsif inline?(child) && inline?(next_child)
|
462
|
+
if starts_with_whitespace?(next_child)
|
463
|
+
debug { "adding a break before whitespace" }
|
464
|
+
q.with_target(q.target.last.contents) { q.breakable(" ") }
|
465
|
+
elsif ends_with_whitespace?(child)
|
466
|
+
debug { "will add a break after whitespace" }
|
467
|
+
break_next = true
|
468
|
+
else
|
469
|
+
debug { "fill_breakable('') between inline/text without separating whitespace" }
|
470
|
+
q.fill_breakable("")
|
471
|
+
end
|
472
|
+
else
|
473
|
+
q.breakable(force: true)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
next if next_group.nil?
|
478
|
+
|
479
|
+
q.breakable(force: true)
|
480
|
+
q.breakable(force: true)
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
def with_context(context)
|
485
|
+
@context.push(context)
|
486
|
+
yield
|
487
|
+
ensure
|
488
|
+
@context.pop
|
489
|
+
end
|
490
|
+
|
491
|
+
def context?(value)
|
492
|
+
@context.last == value
|
493
|
+
end
|
494
|
+
|
495
|
+
def embedded_language(node)
|
496
|
+
return nil unless node.is_a?(Herb::AST::HTMLElementNode)
|
497
|
+
|
498
|
+
case node.open_tag.tag_name.value.downcase
|
499
|
+
when "script"
|
500
|
+
normalize_type(tag_attribute(node, "type"), "text/javascript")
|
501
|
+
when "style"
|
502
|
+
normalize_type(tag_attribute(node, "type"), "text/css")
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def normalize_type(value, default)
|
507
|
+
case value
|
508
|
+
when String
|
509
|
+
value.downcase
|
510
|
+
when :dynamic
|
511
|
+
"unknown"
|
512
|
+
else
|
513
|
+
default
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
def format_ruby(code)
|
518
|
+
@ruby.format(code, [@config.line_length - current_indent_level, 1].max).lines
|
519
|
+
end
|
520
|
+
|
521
|
+
def format_incomplete_ruby(code)
|
522
|
+
@ruby.format_incomplete(code, [@config.line_length - current_indent_level, 1].max).lines
|
523
|
+
end
|
524
|
+
|
525
|
+
def print_formatted_lines(lines)
|
526
|
+
(lines + [nil]).each_cons(2) do |line, next_line|
|
527
|
+
break if line.nil?
|
528
|
+
|
529
|
+
q.text(line.chomp)
|
530
|
+
q.breakable(force: true) unless next_line.nil?
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
def debug(&block)
|
535
|
+
@config.logger.debug(self.class.to_s, &block)
|
536
|
+
end
|
537
|
+
end
|
data/lib/erbf/version.rb
ADDED
data/lib/erbf.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "herb"
|
4
|
+
require "prettier_print"
|
5
|
+
require "pp"
|
6
|
+
|
7
|
+
class Erbf
|
8
|
+
autoload :Config, "erbf/config"
|
9
|
+
autoload :Formatter, "erbf/formatter"
|
10
|
+
autoload :CLI, "erbf/cli"
|
11
|
+
autoload :VERSION, "erbf/version"
|
12
|
+
|
13
|
+
def initialize(config_or_options = {})
|
14
|
+
@config =
|
15
|
+
(config_or_options.is_a?(Config) ? config_or_options : Config.load(**config_or_options))
|
16
|
+
@logger = @config.logger
|
17
|
+
@logger.debug! if @config.debug?
|
18
|
+
end
|
19
|
+
|
20
|
+
def format_code(input)
|
21
|
+
result = Herb.parse(input)
|
22
|
+
# TODO: Handle errors
|
23
|
+
format_ast(result.value)
|
24
|
+
end
|
25
|
+
|
26
|
+
def format_ast(ast_node)
|
27
|
+
PrettierPrint.format(+"", @config.line_length) do |q|
|
28
|
+
@logger.debug(to_s) { "AST:\n#{PP.pp(ast_node, +"", 80)}" }
|
29
|
+
# TODO: Use Herb::Visitor
|
30
|
+
Formatter.new(q, @config).visit(ast_node)
|
31
|
+
@logger.debug(to_s) { "Formatted:\n#{PP.pp(q.target, +"", 80)}" }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: erbf
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tomasz Szczęśniak-Szlagowski
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: herb
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.1'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.1'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: prettier_print
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.2'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.2'
|
40
|
+
description: It breaks longs lines and integrates with Ruby and JS/CSS formatters
|
41
|
+
email:
|
42
|
+
- spect88@gmail.com
|
43
|
+
executables:
|
44
|
+
- erbf
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".rubocop.yml"
|
49
|
+
- ".streerc"
|
50
|
+
- LICENSE.txt
|
51
|
+
- README.md
|
52
|
+
- exe/erbf
|
53
|
+
- lib/erbf.rb
|
54
|
+
- lib/erbf/cli.rb
|
55
|
+
- lib/erbf/config.rb
|
56
|
+
- lib/erbf/formatter.rb
|
57
|
+
- lib/erbf/formatter/embedded_language_formatter.rb
|
58
|
+
- lib/erbf/formatter/html_helper.rb
|
59
|
+
- lib/erbf/formatter/prettier_print_helper.rb
|
60
|
+
- lib/erbf/formatter/ruby_formatter.rb
|
61
|
+
- lib/erbf/version.rb
|
62
|
+
homepage: https://github.com/spect88/erbf
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
metadata:
|
66
|
+
homepage_uri: https://github.com/spect88/erbf
|
67
|
+
source_code_uri: https://github.com/spect88/erbf
|
68
|
+
rubygems_mfa_required: 'true'
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 3.0.0
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
requirements: []
|
83
|
+
rubygems_version: 3.6.7
|
84
|
+
specification_version: 4
|
85
|
+
summary: A formatter for .html.erb files
|
86
|
+
test_files: []
|