super-slim 3.0.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 +118 -0
- data/LICENSE +21 -0
- data/LICENSE-slim +22 -0
- data/README.md +217 -0
- data/config/rubocop.yml +212 -0
- data/exe/super-slim +6 -0
- data/lib/super-slim.rb +4 -0
- data/lib/super_slim/ast.rb +34 -0
- data/lib/super_slim/cli.rb +71 -0
- data/lib/super_slim/compiler.rb +467 -0
- data/lib/super_slim/formatter/line_wrapper.rb +131 -0
- data/lib/super_slim/formatter.rb +125 -0
- data/lib/super_slim/linter/rules.rb +585 -0
- data/lib/super_slim/linter.rb +81 -0
- data/lib/super_slim/parallel.rb +36 -0
- data/lib/super_slim/parser.rb +556 -0
- data/lib/super_slim/rails_handler.rb +31 -0
- data/lib/super_slim/railtie.rb +12 -0
- data/lib/super_slim/rubocop.rb +128 -0
- data/lib/super_slim/runtime.rb +103 -0
- data/lib/super_slim/shortcuts.rb +28 -0
- data/lib/super_slim/version.rb +5 -0
- data/lib/super_slim.rb +53 -0
- metadata +86 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "formatter/line_wrapper"
|
|
4
|
+
|
|
5
|
+
module SuperSlim
|
|
6
|
+
# Rewrites a template in its canonical layout. Text blocks shift as a whole, so the whitespace they render is
|
|
7
|
+
# kept; and the formatted template must compile to the same Ruby as the original, or nothing is rewritten.
|
|
8
|
+
# With max_line_length, longer lines are then wrapped, but only if the Ruby differs by nothing but whitespace.
|
|
9
|
+
class Formatter
|
|
10
|
+
INDENT_WIDTH = 2
|
|
11
|
+
CONTROL = /\A-\s*/
|
|
12
|
+
OUTPUT = /\A(==?)([<>]*)\s*/
|
|
13
|
+
TAG_OUTPUT = /\A\s*(==?)(['<>]*)\s*/
|
|
14
|
+
REDUNDANT_DIV = /\Adiv(?=[.#][\p{Word}-])/
|
|
15
|
+
|
|
16
|
+
def self.call(source, **)
|
|
17
|
+
new(**).call(source)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(tailwind_classes: SuperSlim.tailwind_classes, max_line_length: nil, attribute_delimiters: "()")
|
|
21
|
+
@tailwind_classes = tailwind_classes
|
|
22
|
+
@line_wrapper = LineWrapper.new(max_line_length, tailwind_classes:, attribute_delimiters:) if max_line_length
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call(source)
|
|
26
|
+
formatted = layout(source)
|
|
27
|
+
unless formatted == source || compiled(formatted) == compiled(source)
|
|
28
|
+
raise UnsafeFormattingError, "Formatting would change how this template renders"
|
|
29
|
+
end
|
|
30
|
+
return formatted unless @line_wrapper
|
|
31
|
+
|
|
32
|
+
wrapped = @line_wrapper.call(formatted)
|
|
33
|
+
return formatted if wrapped == formatted
|
|
34
|
+
|
|
35
|
+
wrapped_tokens = ruby_tokens(wrapped)
|
|
36
|
+
(wrapped_tokens && wrapped_tokens == ruby_tokens(formatted)) ? wrapped : formatted
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def layout(source)
|
|
42
|
+
parser = Parser.new(outline: true, tailwind_classes: @tailwind_classes)
|
|
43
|
+
parser.call(source)
|
|
44
|
+
lines = source.delete_prefix("").split(/\r?\n/)
|
|
45
|
+
|
|
46
|
+
formatted = []
|
|
47
|
+
indent_shift_by_lineno = {}
|
|
48
|
+
lines.each.with_index(1) do |line, lineno|
|
|
49
|
+
info = parser.outline[lineno]
|
|
50
|
+
case info.kind
|
|
51
|
+
when :blank
|
|
52
|
+
formatted << "" unless formatted.empty? || formatted.last.empty?
|
|
53
|
+
when :text_blank
|
|
54
|
+
formatted << ""
|
|
55
|
+
when :structure
|
|
56
|
+
indent = info.depth * INDENT_WIDTH
|
|
57
|
+
indent_shift_by_lineno[lineno] = indent - indent_of(line)
|
|
58
|
+
formatted << (" " * indent) + structure(line, info)
|
|
59
|
+
else
|
|
60
|
+
formatted << continuation(line, info, indent_shift_by_lineno.fetch(info.owner, 0))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
formatted.pop while formatted.last&.empty?
|
|
64
|
+
formatted.empty? ? "" : "#{formatted.join("\n")}\n"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def structure(line, info)
|
|
68
|
+
content = with_output_spacing(line, info).lstrip
|
|
69
|
+
content = content.rstrip unless info.text
|
|
70
|
+
case content
|
|
71
|
+
when CONTROL then $'.empty? ? "-" : "- #{$'}"
|
|
72
|
+
when OUTPUT then "#{$1}#{$2} #{$'}"
|
|
73
|
+
when REDUNDANT_DIV then $'
|
|
74
|
+
else content
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def continuation(line, info, indent_shift)
|
|
79
|
+
line = with_output_spacing(expand_tabs(line), info)
|
|
80
|
+
indent = [line[/\A */].size + indent_shift, 0].max
|
|
81
|
+
content = line.lstrip
|
|
82
|
+
content = content.rstrip unless info.kind == :text || info.text
|
|
83
|
+
(" " * indent) + content
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def with_output_spacing(line, info)
|
|
87
|
+
return line unless info.output_column
|
|
88
|
+
|
|
89
|
+
before = line[0...info.output_column].rstrip
|
|
90
|
+
line[info.output_column..] =~ TAG_OUTPUT
|
|
91
|
+
$'.empty? ? "#{before} #{$1}#{$2}" : "#{before} #{$1}#{$2} #{$'}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def indent_of(line)
|
|
95
|
+
expand_tabs(line)[/\A */].size
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def expand_tabs(line)
|
|
99
|
+
line.sub(/\A[ \t]*/) { _1.gsub(Parser::TAB, "\\1#{" " * Parser::TAB_SIZE}") }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def compiled(source)
|
|
103
|
+
SuperSlim.compile(source, tailwind_classes: @tailwind_classes, format: :xml).gsub(/(?:\s*[;\n]\s*)+/, ";")
|
|
104
|
+
rescue Error => exception
|
|
105
|
+
exception.message
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Whitespace aside, wrapping only turns spaces into newlines that Ruby ignores after a comma, and newlines
|
|
109
|
+
# that end statements into semicolons.
|
|
110
|
+
def ruby_tokens(source)
|
|
111
|
+
code = SuperSlim.compile(source, tailwind_classes: @tailwind_classes, format: :xml)
|
|
112
|
+
tokens = Ripper.lex(code).flat_map do |_, type, token|
|
|
113
|
+
case type
|
|
114
|
+
when :on_sp, :on_ignored_nl then []
|
|
115
|
+
when :on_nl, :on_semicolon then [";"]
|
|
116
|
+
when :on_comment then [token.strip, ";"]
|
|
117
|
+
else [token]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
tokens.chunk_while { |previous, token| previous == ";" && token == ";" }.map(&:first)
|
|
121
|
+
rescue Error
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|