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,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lint_roller"
|
|
4
|
+
require "strscan"
|
|
5
|
+
require_relative "../super_slim"
|
|
6
|
+
require_relative "linter"
|
|
7
|
+
|
|
8
|
+
module SuperSlim
|
|
9
|
+
# Loaded by RuboCop from `plugins: [super_slim]`: lints the Ruby of each template, offenses at their place in the `.slim` file.
|
|
10
|
+
module RuboCop
|
|
11
|
+
class Plugin < LintRoller::Plugin
|
|
12
|
+
def about
|
|
13
|
+
LintRoller::About.new(name: "super_slim", version: VERSION, homepage: "https://github.com/levups/super-slim",
|
|
14
|
+
description: "Lints the Ruby of Slim templates, read by SuperSlim's parser")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def supported?(context)
|
|
18
|
+
context.engine == :rubocop
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def rules(_context)
|
|
22
|
+
::RuboCop::Runner.ruby_extractors.unshift(RubyExtractor)
|
|
23
|
+
LintRoller::Rules.new(type: :path, config_format: :rubocop, value: File.expand_path("../../config/rubocop.yml", __dir__))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Hands RuboCop each piece of Ruby with its offset: a piece is a slice of the template, so autocorrection
|
|
28
|
+
# writes back into the template.
|
|
29
|
+
class RubyExtractor
|
|
30
|
+
Fragment = Struct.new(:code, :offset)
|
|
31
|
+
|
|
32
|
+
# Slim reads a control line as the head of a block: its keyword and `do` belong to the indented lines.
|
|
33
|
+
LEADING_KEYWORD = /\A(?:begin|case|else|elsif|ensure|if|rescue|unless|until|when|while|for[ \t]+\w+[ \t]+in)\b[ \t]*/
|
|
34
|
+
TRAILING_DO = /(?:\b[ \t]*|[ \t])do[ \t]*(\|[^|]*\|)?[ \t]*(#.*)?\z/
|
|
35
|
+
WHEN = /\A\s*when[ \t]/
|
|
36
|
+
THEN = /[ \t]then(?:[ \t].*)?\z/m
|
|
37
|
+
INTERPOLATION = /#\{((?>[^{}]|(\{(?>[^{}]|\g<1>)*\}))*)\}/
|
|
38
|
+
ESCAPED_INTERPOLATION = /\\#\{/
|
|
39
|
+
UNESCAPED_INTERPOLATION_CODE = /\A\{.*\}\z/m
|
|
40
|
+
TEXT_WITHOUT_INTERPOLATION = /[^#\\]+|./m
|
|
41
|
+
|
|
42
|
+
def self.call(processed_source)
|
|
43
|
+
new(processed_source).call if processed_source.path&.end_with?(".slim")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def initialize(processed_source)
|
|
47
|
+
@processed_source = processed_source
|
|
48
|
+
@source = processed_source.raw_source
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def call
|
|
52
|
+
fragments.filter_map do |fragment|
|
|
53
|
+
next if fragment.code.strip.empty?
|
|
54
|
+
|
|
55
|
+
{offset: fragment.offset, processed_source: processed_source_of(fragment.code)}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def fragments
|
|
62
|
+
parser = Parser.new(file: @processed_source.path, outline: true, tailwind_classes: tailwind_classes?)
|
|
63
|
+
parser.call(@source)
|
|
64
|
+
parser.ruby_ranges.flat_map do |kind, first, last|
|
|
65
|
+
# From the start of its first line, so that the indentation of a `ruby:` block is the same on every line.
|
|
66
|
+
first = (@source.rindex("\n", first - 1) || -1) + 1 if kind == :embedded_ruby
|
|
67
|
+
fragment = Fragment.new(@source[first...last], first)
|
|
68
|
+
case kind
|
|
69
|
+
when :text then interpolations(fragment)
|
|
70
|
+
when :code then without_block_syntax(fragment)
|
|
71
|
+
when :embedded_ruby then [fragment]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
rescue SyntaxError
|
|
75
|
+
[]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def tailwind_classes?
|
|
79
|
+
configuration = Linter.configuration
|
|
80
|
+
configuration.key?("tailwind_classes") ? configuration["tailwind_classes"] : SuperSlim.tailwind_classes
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def interpolations(text)
|
|
84
|
+
scanner = StringScanner.new(text.code)
|
|
85
|
+
fragments = []
|
|
86
|
+
until scanner.eos?
|
|
87
|
+
next if scanner.skip(ESCAPED_INTERPOLATION)
|
|
88
|
+
|
|
89
|
+
if scanner.scan(INTERPOLATION)
|
|
90
|
+
code = scanner[1]
|
|
91
|
+
offset = text.offset + scanner.charpos - code.size - 1
|
|
92
|
+
fragments << (code.match?(UNESCAPED_INTERPOLATION_CODE) ? Fragment.new(code[1..-2], offset + 1) : Fragment.new(code, offset))
|
|
93
|
+
else
|
|
94
|
+
scanner.skip(TEXT_WITHOUT_INTERPOLATION)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
fragments
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def without_block_syntax(fragment)
|
|
101
|
+
whens(fragment).map do |clause|
|
|
102
|
+
keyword = clause.code[LEADING_KEYWORD].to_s
|
|
103
|
+
Fragment.new(clause.code[keyword.size..].sub(TRAILING_DO, ""), clause.offset + keyword.size)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `when a, b` holds two expressions, which RuboCop reads as an array: each one becomes a fragment.
|
|
108
|
+
def whens(fragment)
|
|
109
|
+
keyword = fragment.code[WHEN] or return [fragment]
|
|
110
|
+
conditions = fragment.code[keyword.size..].sub(THEN, "")
|
|
111
|
+
array = processed_source_of("[#{conditions}]").ast
|
|
112
|
+
return [fragment] unless array&.array_type?
|
|
113
|
+
|
|
114
|
+
array.children.map do |condition|
|
|
115
|
+
Fragment.new(condition.source, fragment.offset + keyword.size + condition.source_range.begin_pos - 1)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def processed_source_of(code)
|
|
120
|
+
processed_source = ::RuboCop::ProcessedSource.new(code, @processed_source.ruby_version, @processed_source.path,
|
|
121
|
+
parser_engine: @processed_source.parser_engine)
|
|
122
|
+
processed_source.config = @processed_source.config
|
|
123
|
+
processed_source.registry = @processed_source.registry
|
|
124
|
+
processed_source
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb/escape"
|
|
4
|
+
|
|
5
|
+
module SuperSlim
|
|
6
|
+
module Runtime
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def escape_html(value)
|
|
10
|
+
ERB::Escape.html_escape(value.to_s)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def escape_html_safe(value)
|
|
14
|
+
string = value.to_s
|
|
15
|
+
(string.html_safe? || value.html_safe?) ? string : ERB::Escape.html_escape(string)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def class_value(value, escape, html_safe)
|
|
19
|
+
if value.is_a?(Array)
|
|
20
|
+
value = value.flatten.filter_map do |element|
|
|
21
|
+
string = element.to_s
|
|
22
|
+
string unless string.empty?
|
|
23
|
+
end.join(" ")
|
|
24
|
+
end
|
|
25
|
+
return value.to_s unless escape
|
|
26
|
+
|
|
27
|
+
html_safe ? escape_html_safe(value) : escape_html(value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def join_classes(*values)
|
|
31
|
+
values.reject(&:empty?).join(" ")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Attributes only known at render time: splats (`*hash`), `data=`/`aria=` hashes and dynamic tags (`*{tag: "a"}`).
|
|
35
|
+
class Attributes
|
|
36
|
+
INVALID_NAME = /[ \0"'>\/=]/
|
|
37
|
+
HYPHENATED = %w[data aria].freeze
|
|
38
|
+
|
|
39
|
+
def initialize(html_safe)
|
|
40
|
+
@html_safe = html_safe
|
|
41
|
+
@value_by_name = {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def code_attr(name, escape, value)
|
|
45
|
+
if name == "class"
|
|
46
|
+
value = value.is_a?(Array) ? value.join(" ") : value.to_s
|
|
47
|
+
attr(name, escape(escape, value)) unless value.empty?
|
|
48
|
+
elsif HYPHENATED.include?(name) && value.is_a?(Hash)
|
|
49
|
+
hyphenated_attr(name, escape, value)
|
|
50
|
+
elsif value != false && !value.nil?
|
|
51
|
+
attr(name, escape(escape, value))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def splat_attrs(splat)
|
|
56
|
+
splat.each { |name, value| code_attr(name.is_a?(Symbol) ? name.name : name.to_s, true, value) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def attr(name, value)
|
|
60
|
+
raise InvalidAttributeNameError, "Invalid attribute name '#{name}' was rendered" if name.match?(INVALID_NAME)
|
|
61
|
+
|
|
62
|
+
if !@value_by_name.key?(name)
|
|
63
|
+
@value_by_name[name] = value
|
|
64
|
+
elsif name == "class"
|
|
65
|
+
@value_by_name[name] = "#{@value_by_name[name]} #{value}"
|
|
66
|
+
else
|
|
67
|
+
raise "Multiple #{name} attributes specified"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_tag(content = nil)
|
|
72
|
+
tag = @value_by_name.delete("tag").to_s
|
|
73
|
+
tag = "div" if tag.empty?
|
|
74
|
+
content ? "<#{tag}#{self}>#{content}</#{tag}>" : "<#{tag}#{self} />"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def to_s
|
|
78
|
+
html = +""
|
|
79
|
+
@value_by_name.each do |name, value|
|
|
80
|
+
html << " " << name
|
|
81
|
+
html << %(=") << value.to_s << %(") unless value == true
|
|
82
|
+
end
|
|
83
|
+
html
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def hyphenated_attr(name, escape, value)
|
|
89
|
+
if value.is_a?(Hash)
|
|
90
|
+
value.each { |key, nested_value| hyphenated_attr("#{name}-#{key}", escape, nested_value) }
|
|
91
|
+
else
|
|
92
|
+
attr(name, escape(escape, value))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def escape(escape, value)
|
|
97
|
+
return value if !escape || value == true
|
|
98
|
+
|
|
99
|
+
@html_safe ? Runtime.escape_html_safe(value) : Runtime.escape_html(value)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SuperSlim
|
|
4
|
+
# `#name` sets the id and `.name` a class, on a div unless a tag name comes first.
|
|
5
|
+
module Shortcuts
|
|
6
|
+
ATTRIBUTE_BY_SHORTCUT = {"#" => "id", "." => "class"}.freeze
|
|
7
|
+
DEFAULT_TAG = "div"
|
|
8
|
+
|
|
9
|
+
SLIM_NAME_PART = /\p{Word}|-|\/\d+|:(?:\w|-)+/
|
|
10
|
+
# Tailwind class names: arbitrary values in brackets (one level of nesting, no whitespace), CSS variables in
|
|
11
|
+
# parentheses after a dash, variants joined by `:` (`: ` with a space still nests a tag), `!` and `@`,
|
|
12
|
+
# `*:` child variants, fractions and opacities after `/` (a final `/` still closes the tag), and decimals
|
|
13
|
+
# such as `py-1.5`, where a dot between two digits does not start a new class.
|
|
14
|
+
# Tailwind never puts a bracket right after a letter or a digit: `.btn[href="/"]` stays an attribute list.
|
|
15
|
+
TAILWIND_NAME_PART = %r{
|
|
16
|
+
\p{Word} | - | ! | @ | \*(?=:)
|
|
17
|
+
| :(?=[\p{Word}\[(!@*-])
|
|
18
|
+
| /(?=[\p{Word}\[(])
|
|
19
|
+
| (?<=\d)\.(?=\d)
|
|
20
|
+
| (?<![\p{Word}\])]) \[ (?:[^\[\]\s] | \[[^\[\]\s]*\])+ \]
|
|
21
|
+
| (?<=-)\( (?:[^()\s] | \([^()\s]*\))+ \)
|
|
22
|
+
}x
|
|
23
|
+
|
|
24
|
+
ATTRIBUTE_PATTERN = /\A([#.]+)((?:#{SLIM_NAME_PART})*)/
|
|
25
|
+
TAILWIND_ATTRIBUTE_PATTERN = /\A([#.]+)((?:#{TAILWIND_NAME_PART})*)/
|
|
26
|
+
TAG_PATTERN = /\A(?:[#.]|\*(?=[^\s]+)|(\p{Word}(?:\p{Word}|:|-)*\p{Word}|\p{Word}+))/
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/super_slim.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "super_slim/version"
|
|
4
|
+
require_relative "super_slim/ast"
|
|
5
|
+
require_relative "super_slim/shortcuts"
|
|
6
|
+
require_relative "super_slim/parser"
|
|
7
|
+
require_relative "super_slim/compiler"
|
|
8
|
+
require_relative "super_slim/runtime"
|
|
9
|
+
|
|
10
|
+
module SuperSlim
|
|
11
|
+
autoload :Formatter, "super_slim/formatter"
|
|
12
|
+
autoload :RuboCop, "super_slim/rubocop"
|
|
13
|
+
|
|
14
|
+
class Error < StandardError; end
|
|
15
|
+
class InvalidAttributeNameError < Error; end
|
|
16
|
+
class UnsafeFormattingError < Error; end
|
|
17
|
+
|
|
18
|
+
class SyntaxError < Error
|
|
19
|
+
attr_reader :error, :file, :line, :lineno, :column
|
|
20
|
+
|
|
21
|
+
def initialize(error, file, line, lineno, column)
|
|
22
|
+
@error = error
|
|
23
|
+
@file = file || "(__TEMPLATE__)"
|
|
24
|
+
@line = line.to_s
|
|
25
|
+
@lineno = lineno
|
|
26
|
+
@column = column
|
|
27
|
+
super(to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
line = @line.lstrip
|
|
32
|
+
column = @column + line.size - @line.size
|
|
33
|
+
<<~MESSAGE
|
|
34
|
+
#{error}
|
|
35
|
+
#{file}, Line #{lineno}, Column #{@column}
|
|
36
|
+
#{line}
|
|
37
|
+
#{" " * column}^
|
|
38
|
+
MESSAGE
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
# Shortcuts also read Tailwind class names, such as `.rounded-b-[3px]` or `.lg:hover:underline`. Off by
|
|
44
|
+
# default so that switching from Slim changes no template: Slim renders `.size-2.5` as two classes.
|
|
45
|
+
attr_accessor :tailwind_classes
|
|
46
|
+
|
|
47
|
+
def compile(source, file: nil, tailwind_classes: self.tailwind_classes, **compiler_options)
|
|
48
|
+
Compiler.new(**compiler_options).call(Parser.new(file:, tailwind_classes:).call(source))
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
require_relative "super_slim/railtie" if defined?(Rails::Railtie)
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: super-slim
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 3.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Level UP Solutions
|
|
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: strscan
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 3.1.1
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: 3.1.1
|
|
26
|
+
description: 'A Slim template engine rewritten from scratch, without Temple: same
|
|
27
|
+
HTML as Slim 5, faster compilation and rendering with fewer allocations, a formatter
|
|
28
|
+
that cannot change a page, a linter with HTML and accessibility rules, and a RuboCop
|
|
29
|
+
plugin for the Ruby inside templates.'
|
|
30
|
+
executables:
|
|
31
|
+
- super-slim
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE
|
|
37
|
+
- LICENSE-slim
|
|
38
|
+
- README.md
|
|
39
|
+
- config/rubocop.yml
|
|
40
|
+
- exe/super-slim
|
|
41
|
+
- lib/super-slim.rb
|
|
42
|
+
- lib/super_slim.rb
|
|
43
|
+
- lib/super_slim/ast.rb
|
|
44
|
+
- lib/super_slim/cli.rb
|
|
45
|
+
- lib/super_slim/compiler.rb
|
|
46
|
+
- lib/super_slim/formatter.rb
|
|
47
|
+
- lib/super_slim/formatter/line_wrapper.rb
|
|
48
|
+
- lib/super_slim/linter.rb
|
|
49
|
+
- lib/super_slim/linter/rules.rb
|
|
50
|
+
- lib/super_slim/parallel.rb
|
|
51
|
+
- lib/super_slim/parser.rb
|
|
52
|
+
- lib/super_slim/rails_handler.rb
|
|
53
|
+
- lib/super_slim/railtie.rb
|
|
54
|
+
- lib/super_slim/rubocop.rb
|
|
55
|
+
- lib/super_slim/runtime.rb
|
|
56
|
+
- lib/super_slim/shortcuts.rb
|
|
57
|
+
- lib/super_slim/version.rb
|
|
58
|
+
homepage: https://github.com/levups/super-slim
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
source_code_uri: https://github.com/levups/super-slim
|
|
63
|
+
changelog_uri: https://github.com/levups/super-slim/blob/main/CHANGELOG.md
|
|
64
|
+
documentation_uri: https://levups.github.io/super-slim/
|
|
65
|
+
bug_tracker_uri: https://github.com/levups/super-slim/issues
|
|
66
|
+
rubygems_mfa_required: 'true'
|
|
67
|
+
default_lint_roller_plugin: SuperSlim::RuboCop::Plugin
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.3'
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 4.0.24
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: 'Slim templates, rewritten: faster to compile and render, with a formatter
|
|
85
|
+
and a linter'
|
|
86
|
+
test_files: []
|