slimx 0.1.1 → 1.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 +4 -4
- data/CHANGELOG.md +30 -0
- data/LICENSE +1 -1
- data/README.md +180 -0
- data/lib/slimx/ast.rb +1 -1
- data/lib/slimx/cli.rb +5 -5
- data/lib/slimx/compiler.rb +14 -14
- data/lib/slimx/formatter.rb +1 -1
- data/lib/slimx/linter/rules.rb +62 -45
- data/lib/slimx/linter.rb +2 -2
- data/lib/slimx/parallel.rb +2 -2
- data/lib/slimx/parser.rb +12 -12
- data/lib/slimx/rails_handler.rb +2 -2
- data/lib/slimx/runtime.rb +7 -2
- data/lib/slimx/shortcuts.rb +5 -6
- data/lib/slimx/version.rb +1 -1
- data/lib/slimx.rb +2 -2
- metadata +26 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1226be0f289b66fcf4b695e789913b7a94df07468e7db6471f5e82cd5a58d05f
|
|
4
|
+
data.tar.gz: ceaca10aa563fcbf70bafa180f140495e640622f1809bb119027c35a868e9281
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d66c62e8a22d996b0a822f981a34cc8c2648d1cdf18cec491d5995c4c4acb96d8feeef85044313415888e0463c01dd58c3d6a4010de3b9455a12471dce13ea54
|
|
7
|
+
data.tar.gz: a5184ee72fcd2df6d2755b4baf35aca285bf2f05b7e2ab8a40928ee1ecd92bb902759ba9a8f906899a0576b424bb92eceafa793b2aae844403966ea7d522ec30
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [1.0.0] — 2026-11-04
|
|
6
|
+
|
|
7
|
+
First stable release: a Slim 5 engine written from scratch, without Temple. 0.1.0 and 0.1.1 were previews. It
|
|
8
|
+
renders what Slim 5 renders with `format: :html, sort_attrs: false`, byte for byte; XML formats (`.xml`, `.rss`,
|
|
9
|
+
`.atom`) render as XML.
|
|
10
|
+
|
|
11
|
+
**New**
|
|
12
|
+
- Rails through a railtie, Tilt through `require "slimx/tilt"`, and `SlimX.compile` for plain Ruby.
|
|
13
|
+
- Every `-` and `=` stays on its template line, so backtraces point at the right one.
|
|
14
|
+
- `slimx fmt`, which refuses any change to the compiled Ruby, and strict mode (`SlimX.strict`), which
|
|
15
|
+
refuses unformatted templates at compile time.
|
|
16
|
+
- `slimx lint` with 29 rules from slim-lint, haml-lint, erb_lint, Herb and eslint-plugin-jsx-a11y,
|
|
17
|
+
configured in `.slimx.yml`.
|
|
18
|
+
- `tailwind_classes`, off by default: class shortcuts read Tailwind class names.
|
|
19
|
+
|
|
20
|
+
**Left out**
|
|
21
|
+
- XHTML, `sort_attrs` and the other Temple options, the `coffee:`, `sass:`, `scss:`, `less:`, `erb:`,
|
|
22
|
+
`builder:` and wiki engines, conditional comments, the `logic_less`, `translator`, `include` and `smart`
|
|
23
|
+
plugins, and lambda shortcuts: each raises at compile time.
|
|
24
|
+
|
|
25
|
+
**Requires**
|
|
26
|
+
- Ruby 3.3 or later, every Ruby still maintained, Rails 8.0 or 8.1 for the railtie, and strscan 3.1.1 or later,
|
|
27
|
+
newer than the one Ruby 3.3 ships.
|
|
28
|
+
|
|
29
|
+
[Unreleased]: https://github.com/next-gen-gems/slimx/compare/v1.0.0...HEAD
|
|
30
|
+
[1.0.0]: https://github.com/next-gen-gems/slimx/releases/tag/v1.0.0
|
data/LICENSE
CHANGED
data/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# SlimX
|
|
2
|
+
|
|
3
|
+
A Slim template engine rewritten from scratch: no Temple, no filter pipeline, a single-pass compiler and a
|
|
4
|
+
render-time runtime of a few methods. Templates written for [Slim](https://github.com/slim-template/slim) 5
|
|
5
|
+
render byte for byte the same, faster, with fewer allocations.
|
|
6
|
+
|
|
7
|
+
https://next-gen-gems.github.io/slimx/
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem "slimx" # instead of slim-rails
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Rails picks it up through its railtie. Outside Rails, `require "slimx/tilt"` registers it with Tilt
|
|
19
|
+
(Sinatra, Hanami, Roda…), and `SlimX.compile(source)` returns the Ruby code.
|
|
20
|
+
|
|
21
|
+
## What SlimX renders
|
|
22
|
+
|
|
23
|
+
HTML5, with attributes in the order you write them — what Slim renders with
|
|
24
|
+
`format: :html, sort_attrs: false`. Templates whose format is XML (`.xml`, `.rss`, `.atom`) render as XML
|
|
25
|
+
(`<link />`, `doctype xml`) from their format alone.
|
|
26
|
+
|
|
27
|
+
The whole Slim 5 grammar is supported: tags and `#id`/`.class` shortcuts, custom shortcuts, attributes with
|
|
28
|
+
any delimiter, boolean attributes, `*splat`, `data`/`aria` hashes, `class` merging, `<`/`>` whitespace
|
|
29
|
+
markers, `|` and `'` text, inline HTML, `/` and `/!` comments, `-` control code, `=`/`==` output, `#{}` and
|
|
30
|
+
`#{{}}` interpolation, `doctype`, and the `ruby:`, `javascript:`, `css:` and `markdown:` (through
|
|
31
|
+
[Markly](https://github.com/ioquatix/markly)) embedded engines.
|
|
32
|
+
|
|
33
|
+
## Tailwind class names
|
|
34
|
+
|
|
35
|
+
Slim cuts `.rounded-b-[3px]` at the bracket, and refuses `.lg:[&:nth-child(3)]:hover:underline`. Turn on
|
|
36
|
+
`tailwind_classes` and the `.` shortcut takes any Tailwind class name:
|
|
37
|
+
|
|
38
|
+
```slim
|
|
39
|
+
.p-1.rounded-b-[3px].lg:[&:nth-child(3)]:hover:underline
|
|
40
|
+
.font-['Open_Sans'].bg-(--brand).mx-0.5.w-1/2.!font-bold.@lg:flex
|
|
41
|
+
ul.*:p-2
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
SlimX.tailwind_classes = true # config/initializers/slimx.rb, or tailwind_classes: true per compile
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
# .slimx.yml, for slimx lint and slimx fmt
|
|
50
|
+
tailwind_classes: true
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Attribute lists keep their meaning: Tailwind never puts a bracket right after a letter or a digit, so
|
|
54
|
+
`a.btn[href="/"]` is still a link with a class and an `href`. A colon followed by a space still nests a tag
|
|
55
|
+
(`li.item: a`), and a final `/` still closes one. What changes is what Slim mangles: it renders `.size-2.5` as
|
|
56
|
+
`class="size-2 5"`, the option as `class="size-2.5"`. On 7,505 templates from 31 apps, 16 compile differently
|
|
57
|
+
with the option, all of them Tailwind classes Slim breaks. It stays off by default all the same, so that
|
|
58
|
+
turning SlimX on changes nothing.
|
|
59
|
+
|
|
60
|
+
The option only changes how templates are read: compiling 1,408 templates takes the same time (+0.5%, within
|
|
61
|
+
noise), and the compiled Ruby, so the time it takes to render, is the same.
|
|
62
|
+
|
|
63
|
+
## What SlimX leaves out
|
|
64
|
+
|
|
65
|
+
Each fails at compile time with a clear error instead of being silently ignored:
|
|
66
|
+
|
|
67
|
+
- the XHTML format, `sort_attrs`, `attr_quote`, `pretty`, `js_wrapper` and every Temple option;
|
|
68
|
+
- the `coffee:`, `sass:`, `scss:`, `less:`, `erb:`, `builder:`, `textile:`, `rdoc:`, `asciidoc:` and wiki
|
|
69
|
+
engines, and `/[if IE]` conditional comments;
|
|
70
|
+
- the `logic_less`, `translator`, `include` and `smart` plugins, and lambda shortcuts.
|
|
71
|
+
|
|
72
|
+
They can come back if people ask, as long as they cost nothing at render time.
|
|
73
|
+
|
|
74
|
+
## Migrating from Slim
|
|
75
|
+
|
|
76
|
+
Slim's defaults are `format: :xhtml` and `sort_attrs: true`. If you never changed them, first switch Slim
|
|
77
|
+
to `format: :html, sort_attrs: false` and check your test suite: that is the output SlimX gives. Then swap
|
|
78
|
+
`slim-rails` for `slimx`, and drop any `Slim::Engine.set_options`/`with_options` call.
|
|
79
|
+
|
|
80
|
+
## Formatter, strict mode and linter
|
|
81
|
+
|
|
82
|
+
One parser serves the compiler, the formatter and the linter, so all three read templates the same way.
|
|
83
|
+
|
|
84
|
+
`slimx fmt [--check] [PATH…]` rewrites templates in their canonical layout: two spaces per level, `- code`,
|
|
85
|
+
`= output` and `p = output` spacing, no redundant `div`, no trailing whitespace, single blank lines, one final
|
|
86
|
+
newline. Text blocks move as a whole, so the whitespace they render stays put — and before writing anything,
|
|
87
|
+
the formatter compiles both versions and refuses if the Ruby differs. Formatting cannot change a page.
|
|
88
|
+
|
|
89
|
+
Strict mode (`SlimX.strict = true`, or `strict: true` per compile) refuses any template that `slimx fmt` would
|
|
90
|
+
change, pointing at the first line and what it should be: `gofmt` discipline at compile time.
|
|
91
|
+
|
|
92
|
+
`slimx lint [PATH…]` reads `.slimx.yml` and checks, in about half the time slim-lint takes:
|
|
93
|
+
|
|
94
|
+
| Rule | From | Default |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| Indentation, Tab, TrailingWhitespace, EmptyLines, TrailingBlankLines, ControlStatementSpacing, RedundantDiv | slim-lint, fixed by `slimx fmt` | on |
|
|
97
|
+
| LineLength (`max: 120`), CommentControlStatement, EmptyControlStatement, ConsecutiveControlStatements (`max: 2`), TagCase | slim-lint | on |
|
|
98
|
+
| InstanceVariables (in partials) | haml-lint, Rails conventions | on |
|
|
99
|
+
| StrictLocals (partials declare `/# locals:`) | Rails 7.1 strict locals | off |
|
|
100
|
+
| DuplicateId, ImageAlt, IframeTitle, AnchorHref, NestedLinks, PositiveTabindex, BooleanAttributeValue, AriaRole, AriaAttribute | Herb, eslint-plugin-jsx-a11y | on |
|
|
101
|
+
| ButtonType, InlineStyles | eslint-plugin-react, haml-lint | off |
|
|
102
|
+
| DeprecatedClasses (`classes: {old: new}`) | erb_lint | on, empty |
|
|
103
|
+
| UnnecessaryInterpolation, UnnecessaryStringOutput | haml-lint | on |
|
|
104
|
+
| Zwsp (invisible characters) | slim-lint | on |
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
# .slimx.yml
|
|
108
|
+
linters:
|
|
109
|
+
LineLength:
|
|
110
|
+
max: 150
|
|
111
|
+
ButtonType:
|
|
112
|
+
enabled: true
|
|
113
|
+
DeprecatedClasses:
|
|
114
|
+
classes:
|
|
115
|
+
text-muted: text-body-secondary
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## How it is tested
|
|
119
|
+
|
|
120
|
+
- Slim's own test suite and its literate specification (`TESTS.md`) run against SlimX: every template renders
|
|
121
|
+
through Slim and SlimX, and both outputs must match.
|
|
122
|
+
- A seeded fuzzer generates thousands of random templates from the grammar and compares both engines; CI adds
|
|
123
|
+
10,000 from a new seed on every run.
|
|
124
|
+
- Golden snapshots (`test/golden`) hold the rendered HTML, compiled Ruby, formatting and lint offenses of
|
|
125
|
+
templates covering the grammar, its errors and Tailwind class names: `UPDATE_GOLDEN=1` rewrites them.
|
|
126
|
+
- `bench/compiled_digests.rb` proves a refactoring leaves the compiled Ruby of a whole corpus unchanged.
|
|
127
|
+
|
|
128
|
+
Real templates come from 30 public apps pinned in `corpus/manifest.yml`: `rake corpus:fetch` downloads only
|
|
129
|
+
their `.slim` files into the ignored `corpus/cache`, `rake corpus:check` compiles, formats and lints each one
|
|
130
|
+
against Slim, and `rake corpus:diff BASE=rev` reports what changes between a revision and the working tree:
|
|
131
|
+
compiled Ruby, formatting, lint offenses. Downloaded templates are never rendered, as rendering runs their
|
|
132
|
+
Ruby; directories you trust, such as your own app, render too when passed in `SLIMX_CORPUS`.
|
|
133
|
+
|
|
134
|
+
## Benchmarks
|
|
135
|
+
|
|
136
|
+
`bench/compare.rb` renders the same blog page with SlimX, Slim, Haml, Erubi and Herb, checks that they all
|
|
137
|
+
produce the same HTML, and writes compile and render speed and allocations to `docs/benchmarks.json`, which
|
|
138
|
+
the website reads. Run it before each release, on a quiet machine:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
bundle exec rake bench # YJIT, 5 seconds per measurement
|
|
142
|
+
SLIMX_CORPUS=path/to/app/views bundle exec rake bench # adds the compile time of a whole app against Slim
|
|
143
|
+
BENCH_LOCK=path/to/.bench.lock bundle exec rake bench # waits for other benchmarks holding that lock
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
With `corpus/cache` fetched, it also times the open-source corpus, and `slimx lint` and `fmt` there against
|
|
147
|
+
slim-lint when it is installed. `bundle exec rake docs` rewrites the linter rules on the website
|
|
148
|
+
(`docs/tools.html`), after checking every example against the linter.
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
bundle install
|
|
154
|
+
bundle exec rake test # SlimX's tests, then Slim's own suites
|
|
155
|
+
BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile bundle exec rake test # the same on Rails 8.0
|
|
156
|
+
bundle exec rubocop
|
|
157
|
+
FUZZ_SEED=42 FUZZ_CASES=20000 bundle exec ruby -Ilib test/fuzz_test.rb
|
|
158
|
+
bundle exec ruby -Ilib bench/corpus.rb path/to/app # compile time and allocations against Slim
|
|
159
|
+
bundle exec ruby --yjit -Ilib bench/render.rb # render speed and allocations against Slim
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Releasing
|
|
163
|
+
|
|
164
|
+
Changes go under `## [Unreleased]` in `CHANGELOG.md` as they land. To release them, refresh the website's
|
|
165
|
+
numbers on a quiet machine (`SLIMX_CORPUS=path/to/app/views bundle exec rake bench`) and commit them, then,
|
|
166
|
+
from an up-to-date `main` whose CI is green:
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
bin/release minor --dry-run # the notes and the changes, nothing written
|
|
170
|
+
bin/release minor # or patch, major, or an exact version such as 1.2.0
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`bin/release` dates the Unreleased section, bumps the version, commits, and pushes the commit with a signed tag
|
|
174
|
+
`vX.Y.Z`. The tag starts the Release workflow, which publishes the gem to RubyGems through trusted publishing and
|
|
175
|
+
creates the GitHub release with the version's notes and the `.gem`.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT, by Level UP Solutions, part of [Next Gen Gems](https://github.com/next-gen-gems). The parser follows the
|
|
180
|
+
grammar and parsing algorithm of [Slim](https://github.com/slim-template/slim), by the Slim Team (MIT).
|
data/lib/slimx/ast.rb
CHANGED
data/lib/slimx/cli.rb
CHANGED
|
@@ -30,8 +30,8 @@ module SlimX
|
|
|
30
30
|
|
|
31
31
|
def lint(paths, configuration)
|
|
32
32
|
linter = Linter.new(configuration.fetch("linters", {}))
|
|
33
|
-
offenses = Parallel.map(templates(paths)) { linter.call(
|
|
34
|
-
offenses.each { puts "#{
|
|
33
|
+
offenses = Parallel.map(templates(paths)) { |path| linter.call(path, File.read(path)) }.flatten
|
|
34
|
+
offenses.each { |offense| puts "#{offense.path}:#{offense.lineno}: [#{offense.rule}] #{offense.message}" }
|
|
35
35
|
offenses.empty? ? 0 : 1
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -47,8 +47,8 @@ module SlimX
|
|
|
47
47
|
rescue Error => exception
|
|
48
48
|
[:refused, "#{path}: #{exception.message.lines.first.strip}"]
|
|
49
49
|
end
|
|
50
|
-
outcomes.compact.each { |outcome, text| outcome == :reformatted ? puts(text) : warn(text) }
|
|
51
|
-
outcomes.any? { |outcome,| outcome == :refused || (outcome == :reformatted && check) } ? 1 : 0
|
|
50
|
+
outcomes.compact.each { |outcome, text| (outcome == :reformatted) ? puts(text) : warn(text) }
|
|
51
|
+
(outcomes.any? { |outcome,| outcome == :refused || (outcome == :reformatted && check) }) ? 1 : 0
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def templates(paths)
|
|
@@ -56,7 +56,7 @@ module SlimX
|
|
|
56
56
|
paths.flat_map do |path|
|
|
57
57
|
next [path] unless File.directory?(path)
|
|
58
58
|
|
|
59
|
-
Dir.glob("**/*.slim", base: path).
|
|
59
|
+
Dir.glob("**/*.slim", base: path).filter_map { |template| File.join(path, template) unless template.match?(SKIPPED_DIRECTORIES) }
|
|
60
60
|
end.sort
|
|
61
61
|
end
|
|
62
62
|
end
|
data/lib/slimx/compiler.rb
CHANGED
|
@@ -4,7 +4,7 @@ module SlimX
|
|
|
4
4
|
class Compiler
|
|
5
5
|
include AST
|
|
6
6
|
|
|
7
|
-
VOID_ELEMENTS = %w[area base br col embed hr img input keygen link menuitem meta param source track wbr].to_h { [
|
|
7
|
+
VOID_ELEMENTS = %w[area base br col embed hr img input keygen link menuitem meta param source track wbr].to_h { [_1, true] }.freeze
|
|
8
8
|
HYPHENATED_ATTRIBUTES = %w[data aria].freeze
|
|
9
9
|
DOCTYPES = {
|
|
10
10
|
html: {
|
|
@@ -12,7 +12,7 @@ module SlimX
|
|
|
12
12
|
"html" => "<!DOCTYPE html>",
|
|
13
13
|
"strict" => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
|
|
14
14
|
"frameset" => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
|
|
15
|
-
"transitional" => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
|
|
15
|
+
"transitional" => '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
|
|
16
16
|
},
|
|
17
17
|
xml: {
|
|
18
18
|
"1.1" => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
|
|
@@ -23,8 +23,8 @@ module SlimX
|
|
|
23
23
|
"mobile" => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">',
|
|
24
24
|
"basic" => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
|
|
25
25
|
"transitional" => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
|
|
26
|
-
"svg" => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
|
|
27
|
-
}
|
|
26
|
+
"svg" => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
|
|
27
|
+
}
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
30
30
|
BLOCK_SELF_OPENING = /(\A(if|unless|else|elsif|when|in|begin|rescue|ensure|case)\b)|\bdo\s*(\|[^|]*\|\s*)?\z/
|
|
@@ -36,7 +36,7 @@ module SlimX
|
|
|
36
36
|
ESCAPED_INTERPOLATION = /\A\\#\{/
|
|
37
37
|
INTERPOLATION = /\A#\{((?>[^{}]|(\{(?>[^{}]|\g<1>)*\}))*)\}/
|
|
38
38
|
UNESCAPED_INTERPOLATION_CODE = /\A\{.*\}\Z/
|
|
39
|
-
INTERPOLATION_FREE_TEXT = /\A([#\\]?[^#\\]*([#\\][^\\#\{][^#\\]*)*)/
|
|
39
|
+
INTERPOLATION_FREE_TEXT = /\A([#\\]?[^#\\]*([#\\][^\\#\{][^#\\]*)*)/ # rubocop:disable Style/RedundantRegexpEscape -- `#{` would open an interpolation
|
|
40
40
|
|
|
41
41
|
def initialize(format: :html, rails: false, html_safe: String.method_defined?(:html_safe?), preamble: nil, postamble: nil)
|
|
42
42
|
raise ArgumentError, "Invalid format #{format.inspect}" unless DOCTYPES.key?(format)
|
|
@@ -130,7 +130,7 @@ module SlimX
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def with_do(code, children)
|
|
133
|
-
code.match?(BLOCK_SELF_OPENING) || AST.empty?(children) ? code : "#{code} do"
|
|
133
|
+
(code.match?(BLOCK_SELF_OPENING) || AST.empty?(children)) ? code : "#{code} do"
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
def compile_tag(tag)
|
|
@@ -139,7 +139,7 @@ module SlimX
|
|
|
139
139
|
closed = tag.content.nil? || (AST.renders_nothing?(tag.content) && (@xml || VOID_ELEMENTS.key?(tag.name)))
|
|
140
140
|
emit_static("<#{tag.name}")
|
|
141
141
|
compile_attributes(tag.attributes)
|
|
142
|
-
emit_static(closed && @xml ? " />" : ">")
|
|
142
|
+
emit_static((closed && @xml) ? " />" : ">")
|
|
143
143
|
return if closed && tag.content.nil?
|
|
144
144
|
|
|
145
145
|
compile_children(tag.content)
|
|
@@ -162,7 +162,7 @@ module SlimX
|
|
|
162
162
|
|
|
163
163
|
def compile_attributes(attributes)
|
|
164
164
|
return if attributes.empty?
|
|
165
|
-
return emit_output(compile_runtime_attributes(attributes), false) if attributes.any? { runtime_attribute?(
|
|
165
|
+
return emit_output(compile_runtime_attributes(attributes), false) if attributes.any? { runtime_attribute?(_1) }
|
|
166
166
|
|
|
167
167
|
if attributes.size > 1
|
|
168
168
|
seen_names = {}
|
|
@@ -180,8 +180,8 @@ module SlimX
|
|
|
180
180
|
next compile_attribute(attribute) unless attribute.name == "class"
|
|
181
181
|
next if class_attributes
|
|
182
182
|
|
|
183
|
-
class_attributes = attributes.select {
|
|
184
|
-
compile_class_attribute(class_attributes.map { attribute_parts(
|
|
183
|
+
class_attributes = attributes.select { _1.name == "class" }
|
|
184
|
+
compile_class_attribute(class_attributes.map { attribute_parts(_1) })
|
|
185
185
|
end
|
|
186
186
|
end
|
|
187
187
|
|
|
@@ -227,8 +227,8 @@ module SlimX
|
|
|
227
227
|
emit_static('"')
|
|
228
228
|
else
|
|
229
229
|
classes = new_variable
|
|
230
|
-
expressions = values.map { string_expression(
|
|
231
|
-
emit_code(values.size == 1 ? "#{classes} = #{expressions.first}" : "#{classes} = ::SlimX::Runtime.join_classes(#{expressions.join(", ")})")
|
|
230
|
+
expressions = values.map { string_expression(_1) }
|
|
231
|
+
emit_code((values.size == 1) ? "#{classes} = #{expressions.first}" : "#{classes} = ::SlimX::Runtime.join_classes(#{expressions.join(", ")})")
|
|
232
232
|
emit_code("unless #{classes}.empty?")
|
|
233
233
|
emit_attribute("class", [[:raw, classes]])
|
|
234
234
|
emit_code("end")
|
|
@@ -279,7 +279,7 @@ module SlimX
|
|
|
279
279
|
[[:raw, "::SlimX::Runtime.class_value(#{value}, #{attribute.escape}, #{@html_safe})"]]
|
|
280
280
|
when QuotedAttribute
|
|
281
281
|
interpolation_parts(attribute.text).map do |kind, value, escape|
|
|
282
|
-
kind == :static && attribute.escape ? [:static, ERB::Escape.html_escape(value)] : [kind, value, escape]
|
|
282
|
+
(kind == :static && attribute.escape) ? [:static, ERB::Escape.html_escape(value)] : [kind, value, escape]
|
|
283
283
|
end
|
|
284
284
|
end
|
|
285
285
|
end
|
|
@@ -373,7 +373,7 @@ module SlimX
|
|
|
373
373
|
|
|
374
374
|
placeholder = "%#{object_id.abs.to_s(36)}%"
|
|
375
375
|
dynamic_parts = []
|
|
376
|
-
markdown = children.grep(Text).flat_map { interpolation_parts(
|
|
376
|
+
markdown = children.grep(Text).flat_map { interpolation_parts(_1.text) }.map do |part|
|
|
377
377
|
next part[1] if part.first == :static
|
|
378
378
|
|
|
379
379
|
dynamic_parts << part
|
data/lib/slimx/formatter.rb
CHANGED
data/lib/slimx/linter/rules.rb
CHANGED
|
@@ -4,11 +4,14 @@ module SlimX
|
|
|
4
4
|
class Linter
|
|
5
5
|
module Rules
|
|
6
6
|
def self.all
|
|
7
|
-
constants.
|
|
7
|
+
constants.filter_map do |constant_name|
|
|
8
|
+
rule = const_get(constant_name)
|
|
9
|
+
rule if rule < Rule
|
|
10
|
+
end
|
|
8
11
|
end
|
|
9
12
|
|
|
10
13
|
class Rule
|
|
11
|
-
DEFAULTS = {
|
|
14
|
+
DEFAULTS = {enabled: true}.freeze
|
|
12
15
|
|
|
13
16
|
def self.rule_name
|
|
14
17
|
name.split("::").last
|
|
@@ -33,7 +36,7 @@ module SlimX
|
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def attribute_names(tag)
|
|
36
|
-
tag.attributes.filter_map {
|
|
39
|
+
tag.attributes.filter_map { |attribute| attribute.name unless attribute.is_a?(AST::Splat) }
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def each_line(document)
|
|
@@ -47,7 +50,7 @@ module SlimX
|
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
def splat?(tag)
|
|
50
|
-
tag.attributes.any? {
|
|
53
|
+
tag.attributes.any? { _1.is_a?(AST::Splat) }
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
# The value when it is written literally, nil when it is computed.
|
|
@@ -59,7 +62,7 @@ module SlimX
|
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
def literal_attributes(tag, name)
|
|
62
|
-
tag.attributes.select {
|
|
65
|
+
tag.attributes.select { |attribute| attribute.respond_to?(:name) && attribute.name == name && literal_value(attribute) }
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
|
|
@@ -158,7 +161,7 @@ module SlimX
|
|
|
158
161
|
# Slim-lint's rules on code and tags.
|
|
159
162
|
|
|
160
163
|
class LineLength < Rule
|
|
161
|
-
DEFAULTS = {
|
|
164
|
+
DEFAULTS = {enabled: true, max: 120}.freeze
|
|
162
165
|
|
|
163
166
|
def offenses(document, _source)
|
|
164
167
|
max = options[:max]
|
|
@@ -170,7 +173,7 @@ module SlimX
|
|
|
170
173
|
ALLOWED = /\A\s*#\s*(rubocop:|Template Dependency:)/
|
|
171
174
|
|
|
172
175
|
def offenses(document, _source)
|
|
173
|
-
document.
|
|
176
|
+
document.each_node.filter_map do |node,|
|
|
174
177
|
next unless node.is_a?(AST::Control) && node.code.match?(/\A\s*#/) && !node.code.match?(ALLOWED)
|
|
175
178
|
|
|
176
179
|
offense(document, document.line_of(node), "Write comments with `/`, not `- #`: they cost nothing at render time")
|
|
@@ -180,22 +183,25 @@ module SlimX
|
|
|
180
183
|
|
|
181
184
|
class EmptyControlStatement < Rule
|
|
182
185
|
def offenses(document, _source)
|
|
183
|
-
document.
|
|
186
|
+
document.each_node.filter_map do |node,|
|
|
184
187
|
offense(document, document.line_of(node), "Remove this empty `-`") if node.is_a?(AST::Control) && node.code.strip.empty?
|
|
185
188
|
end
|
|
186
189
|
end
|
|
187
190
|
end
|
|
188
191
|
|
|
189
192
|
class ConsecutiveControlStatements < Rule
|
|
190
|
-
DEFAULTS = {
|
|
193
|
+
DEFAULTS = {enabled: true, max: 2}.freeze
|
|
191
194
|
|
|
192
195
|
def offenses(document, _source)
|
|
193
196
|
sibling_lists = [document.nodes, *document.visits.filter_map { |node,| Document.children_of(node) }]
|
|
194
|
-
sibling_lists.select! { |siblings| siblings.count { flat_control?(
|
|
197
|
+
sibling_lists.select! { |siblings| siblings.count { flat_control?(_1) } > options[:max] }
|
|
195
198
|
sibling_lists.flat_map do |siblings|
|
|
196
199
|
siblings.chunk_while { |control, following| flat_control?(control) && flat_control?(following) }
|
|
197
|
-
.
|
|
198
|
-
|
|
200
|
+
.filter_map do |controls|
|
|
201
|
+
next unless controls.size > options[:max] && flat_control?(controls.first)
|
|
202
|
+
|
|
203
|
+
offense(document, document.line_of(controls.first), "Merge these #{controls.size} lines of Ruby into a `ruby:` block")
|
|
204
|
+
end
|
|
199
205
|
end
|
|
200
206
|
end
|
|
201
207
|
|
|
@@ -210,9 +216,9 @@ module SlimX
|
|
|
210
216
|
def offenses(document, _source)
|
|
211
217
|
return [] if document.xml?
|
|
212
218
|
|
|
213
|
-
document.
|
|
219
|
+
document.each_node.filter_map do |node, ancestors|
|
|
214
220
|
next unless node.is_a?(AST::Tag) && node.name.match?(/[A-Z]/)
|
|
215
|
-
next if ancestors.any? {
|
|
221
|
+
next if ancestors.any? { |ancestor| ancestor.is_a?(AST::Tag) && ancestor.name == "svg" }
|
|
216
222
|
|
|
217
223
|
offense(document, document.line_of(node), "Write `#{node.name.downcase}` in lowercase")
|
|
218
224
|
end
|
|
@@ -225,11 +231,11 @@ module SlimX
|
|
|
225
231
|
def offenses(document, _source)
|
|
226
232
|
return [] unless document.partial?
|
|
227
233
|
|
|
228
|
-
document.
|
|
234
|
+
document.each_node.filter_map do |node,|
|
|
229
235
|
code = case node
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
when AST::Control, AST::Output then node.code
|
|
237
|
+
when AST::Tag then node.attributes.filter_map { |attribute| attribute.code if attribute.respond_to?(:code) }.join(" ")
|
|
238
|
+
end
|
|
233
239
|
next unless code && (variable = code[INSTANCE_VARIABLE])
|
|
234
240
|
|
|
235
241
|
offense(document, document.line_of(node), "Pass `#{variable}` to the partial as a local")
|
|
@@ -238,10 +244,10 @@ module SlimX
|
|
|
238
244
|
end
|
|
239
245
|
|
|
240
246
|
class StrictLocals < Rule
|
|
241
|
-
DEFAULTS = {
|
|
247
|
+
DEFAULTS = {enabled: false}.freeze
|
|
242
248
|
|
|
243
249
|
def offenses(document, _source)
|
|
244
|
-
return [] if !document.partial? || document.lines.any? {
|
|
250
|
+
return [] if !document.partial? || document.lines.any? { _1.match?(%r{\A\s*/#\s*locals:}) }
|
|
245
251
|
|
|
246
252
|
[offense(document, 1, "Declare the partial's locals: `/# locals: (name:)`")]
|
|
247
253
|
end
|
|
@@ -253,7 +259,7 @@ module SlimX
|
|
|
253
259
|
def offenses(document, _source)
|
|
254
260
|
branch_by_id = {}
|
|
255
261
|
enum_for(:each_tag, document).filter_map do |tag, ancestors|
|
|
256
|
-
branch = ancestors.reverse.find {
|
|
262
|
+
branch = ancestors.reverse.find { _1.is_a?(AST::Control) }
|
|
257
263
|
literal_attributes(tag, "id").filter_map do |attribute|
|
|
258
264
|
id = literal_value(attribute)
|
|
259
265
|
if branch_by_id.key?(id) && branch_by_id[id].equal?(branch)
|
|
@@ -271,7 +277,6 @@ module SlimX
|
|
|
271
277
|
def offenses(document, _source)
|
|
272
278
|
missing_attribute(document, "img", "alt", 'Describe the image with `alt`, or mark it decorative with `alt=""`')
|
|
273
279
|
end
|
|
274
|
-
|
|
275
280
|
end
|
|
276
281
|
|
|
277
282
|
class IframeTitle < Rule
|
|
@@ -287,7 +292,7 @@ module SlimX
|
|
|
287
292
|
end
|
|
288
293
|
|
|
289
294
|
class ButtonType < Rule
|
|
290
|
-
DEFAULTS = {
|
|
295
|
+
DEFAULTS = {enabled: false}.freeze
|
|
291
296
|
|
|
292
297
|
def offenses(document, _source)
|
|
293
298
|
missing_attribute(document, "button", "type", 'Say what the button does with `type="button"` or `type="submit"`')
|
|
@@ -297,7 +302,7 @@ module SlimX
|
|
|
297
302
|
class NestedLinks < Rule
|
|
298
303
|
def offenses(document, _source)
|
|
299
304
|
enum_for(:each_tag, document, "a").filter_map do |tag, ancestors|
|
|
300
|
-
next unless ancestors.any? {
|
|
305
|
+
next unless ancestors.any? { |ancestor| ancestor.is_a?(AST::Tag) && ancestor.name == "a" }
|
|
301
306
|
|
|
302
307
|
offense(document, document.line_of(tag), "Links cannot contain links")
|
|
303
308
|
end
|
|
@@ -318,8 +323,8 @@ module SlimX
|
|
|
318
323
|
|
|
319
324
|
class BooleanAttributeValue < Rule
|
|
320
325
|
BOOLEAN_ATTRIBUTES = %w[allowfullscreen async autofocus autoplay checked controls default defer disabled formnovalidate
|
|
321
|
-
|
|
322
|
-
|
|
326
|
+
hidden inert ismap loop multiple muted nomodule novalidate open playsinline readonly required
|
|
327
|
+
reversed selected].freeze
|
|
323
328
|
|
|
324
329
|
def offenses(document, _source)
|
|
325
330
|
enum_for(:each_tag, document).flat_map do |tag,|
|
|
@@ -335,17 +340,19 @@ module SlimX
|
|
|
335
340
|
|
|
336
341
|
class AriaRole < Rule
|
|
337
342
|
ROLES = %w[alert alertdialog application article banner blockquote button caption cell checkbox code columnheader
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
combobox complementary contentinfo definition deletion dialog document emphasis feed figure form generic
|
|
344
|
+
grid gridcell group heading img insertion link list listbox listitem log main marquee math menu menubar
|
|
345
|
+
menuitem menuitemcheckbox menuitemradio meter navigation none note option paragraph presentation
|
|
346
|
+
progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider
|
|
347
|
+
spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer
|
|
348
|
+
toolbar tooltip tree treegrid treeitem].freeze
|
|
344
349
|
|
|
345
350
|
def offenses(document, _source)
|
|
346
351
|
enum_for(:each_tag, document).flat_map do |tag,|
|
|
347
352
|
literal_attributes(tag, "role").flat_map do |attribute|
|
|
348
|
-
literal_value(attribute).split.
|
|
353
|
+
literal_value(attribute).split.filter_map do |role|
|
|
354
|
+
next if ROLES.include?(role) || role.start_with?("doc-", "graphics-")
|
|
355
|
+
|
|
349
356
|
offense(document, document.line_of(tag), "`#{role}` is not an ARIA role")
|
|
350
357
|
end
|
|
351
358
|
end
|
|
@@ -355,11 +362,11 @@ module SlimX
|
|
|
355
362
|
|
|
356
363
|
class AriaAttribute < Rule
|
|
357
364
|
ATTRIBUTES = %w[activedescendant atomic autocomplete braillelabel brailleroledescription busy checked colcount
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
365
|
+
colindex colindextext colspan controls current describedby description details disabled
|
|
366
|
+
dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label
|
|
367
|
+
labelledby level live modal multiline multiselectable orientation owns placeholder posinset
|
|
368
|
+
pressed readonly relevant required roledescription rowcount rowindex rowindextext rowspan selected
|
|
369
|
+
setsize sort valuemax valuemin valuenow valuetext].map { "aria-#{_1}" }.freeze
|
|
363
370
|
|
|
364
371
|
def offenses(document, _source)
|
|
365
372
|
enum_for(:each_tag, document).flat_map do |tag,|
|
|
@@ -373,14 +380,14 @@ module SlimX
|
|
|
373
380
|
end
|
|
374
381
|
|
|
375
382
|
class DeprecatedClasses < Rule
|
|
376
|
-
DEFAULTS = {
|
|
383
|
+
DEFAULTS = {enabled: true, classes: {}}.freeze
|
|
377
384
|
|
|
378
385
|
def offenses(document, _source)
|
|
379
386
|
replacement_by_class = options[:classes]
|
|
380
387
|
return [] if replacement_by_class.empty?
|
|
381
388
|
|
|
382
389
|
enum_for(:each_tag, document).flat_map do |tag,|
|
|
383
|
-
literal_attributes(tag, "class").flat_map { literal_value(
|
|
390
|
+
literal_attributes(tag, "class").flat_map { literal_value(_1).split }.filter_map do |css_class|
|
|
384
391
|
next unless replacement_by_class.key?(css_class)
|
|
385
392
|
|
|
386
393
|
offense(document, document.line_of(tag), "`#{css_class}` is deprecated: use `#{replacement_by_class[css_class]}`")
|
|
@@ -390,7 +397,7 @@ module SlimX
|
|
|
390
397
|
end
|
|
391
398
|
|
|
392
399
|
class InlineStyles < Rule
|
|
393
|
-
DEFAULTS = {
|
|
400
|
+
DEFAULTS = {enabled: false}.freeze
|
|
394
401
|
|
|
395
402
|
def offenses(document, _source)
|
|
396
403
|
enum_for(:each_tag, document).filter_map do |tag,|
|
|
@@ -405,8 +412,9 @@ module SlimX
|
|
|
405
412
|
LONE_INTERPOLATION = /\A#\{([^{}]+)\}\z/
|
|
406
413
|
|
|
407
414
|
def offenses(document, _source)
|
|
408
|
-
document.
|
|
409
|
-
next unless node.is_a?(AST::TextBlock)
|
|
415
|
+
document.each_node.filter_map do |node, ancestors|
|
|
416
|
+
next unless node.is_a?(AST::TextBlock)
|
|
417
|
+
next unless (lineno = document.line_of(node) || document.line_of(ancestors.last))
|
|
410
418
|
|
|
411
419
|
texts = node.children.grep(AST::Text)
|
|
412
420
|
next unless texts.size == 1 && texts.first.text =~ LONE_INTERPOLATION
|
|
@@ -418,12 +426,21 @@ module SlimX
|
|
|
418
426
|
|
|
419
427
|
class UnnecessaryStringOutput < Rule
|
|
420
428
|
PLAIN_STRING = /\A(["'])([^"'\\#&<>\s](?:[^"'\\#&<>]*[^"'\\#&<>\s])?)\1\z/
|
|
429
|
+
# Text starting with one of these, written inline after a tag, reads as attributes or output: `span (note)`.
|
|
430
|
+
READ_AS_SYNTAX_AFTER_TAG = /\A[(\[{=*\/]/
|
|
421
431
|
|
|
422
432
|
def offenses(document, _source)
|
|
423
|
-
document.
|
|
433
|
+
document.each_node.filter_map do |node, ancestors|
|
|
424
434
|
next unless node.is_a?(AST::Output) && AST.empty?(node.children) && node.code =~ PLAIN_STRING
|
|
425
435
|
|
|
426
|
-
|
|
436
|
+
text = $2
|
|
437
|
+
lineno = document.line_of(node)
|
|
438
|
+
message = if text.match?(READ_AS_SYNTAX_AFTER_TAG) && ancestors.any? && document.line_of(ancestors.last) == lineno
|
|
439
|
+
"Write the text `| #{text}` on its own line, under the tag: inline, it would read as attributes"
|
|
440
|
+
else
|
|
441
|
+
"Write the text `| #{text}` instead of outputting a string"
|
|
442
|
+
end
|
|
443
|
+
offense(document, lineno, message)
|
|
427
444
|
end
|
|
428
445
|
end
|
|
429
446
|
end
|
data/lib/slimx/linter.rb
CHANGED
|
@@ -65,8 +65,8 @@ module SlimX
|
|
|
65
65
|
nodes = parser.call(source)
|
|
66
66
|
visits = Document.visits_of(nodes)
|
|
67
67
|
document = Document.new(path:, lines: source.split(/\r?\n/), nodes:, outline: parser.outline, line_by_node: parser.line_by_node,
|
|
68
|
-
|
|
69
|
-
offenses = @rules.flat_map {
|
|
68
|
+
visits:, tag_visits: visits.select { |node,| node.is_a?(AST::Tag) })
|
|
69
|
+
offenses = @rules.flat_map { _1.offenses(document, source) }
|
|
70
70
|
# sort_by is not stable: the index keeps offenses of one line in rule order.
|
|
71
71
|
offenses.each_with_index.sort_by { |offense, index| [offense.lineno, index] }.map(&:first)
|
|
72
72
|
rescue SyntaxError => exception
|
data/lib/slimx/parallel.rb
CHANGED
|
@@ -27,9 +27,9 @@ module SlimX
|
|
|
27
27
|
writer.close
|
|
28
28
|
reader
|
|
29
29
|
end
|
|
30
|
-
results_by_process = readers.map { Marshal.load(
|
|
30
|
+
results_by_process = readers.map { |reader| Marshal.load(reader.binmode.read) }
|
|
31
31
|
Process.waitall
|
|
32
|
-
results_by_process.each { raise
|
|
32
|
+
results_by_process.each { |results| raise results if results.is_a?(Exception) }
|
|
33
33
|
results_by_process.flatten(1)
|
|
34
34
|
end
|
|
35
35
|
end
|
data/lib/slimx/parser.rb
CHANGED
|
@@ -10,7 +10,7 @@ module SlimX
|
|
|
10
10
|
|
|
11
11
|
TAB_SIZE = 4
|
|
12
12
|
TAB = /\G((?: {#{TAB_SIZE}})*) {0,#{TAB_SIZE - 1}}\t/
|
|
13
|
-
DELIMITERS = {
|
|
13
|
+
DELIMITERS = {"(" => ")", "[" => "]", "{" => "}"}.freeze
|
|
14
14
|
SUPPORTED_ENGINES = %w[javascript css ruby markdown].freeze
|
|
15
15
|
# Engines Slim ships that SlimX deliberately refuses, so they fail loudly instead of parsing as a tag.
|
|
16
16
|
UNSUPPORTED_ENGINES = %w[asciidoc textile rdoc coffee sass scss less erb builder creole wiki org].freeze
|
|
@@ -39,15 +39,15 @@ module SlimX
|
|
|
39
39
|
CLOSED_TAG = /\s*\/\s*/
|
|
40
40
|
SHORTCUT_VALUE_TO_ESCAPE = /[&<>"']/
|
|
41
41
|
# Runs of characters the scanners would otherwise copy one by one: no delimiter, no line continuation.
|
|
42
|
-
CODE_CHUNK_BY_CLOSING = [nil, *DELIMITERS.values].to_h { [
|
|
42
|
+
CODE_CHUNK_BY_CLOSING = [nil, *DELIMITERS.values].to_h { |closing| [closing, /(?:[^\s(\[{,\\#{Regexp.escape(closing.to_s)}]|[,\\](?!\z))+/] }.freeze
|
|
43
43
|
NESTED_CODE_CHUNK_BY_OPENING = DELIMITERS.to_h { |opening, closing| [opening, /(?:[^,\\#{Regexp.escape(opening + closing)}]|[,\\](?!\z))+/] }.freeze
|
|
44
|
-
QUOTED_CHUNK_BY_QUOTE = %w[" '].to_h { [
|
|
44
|
+
QUOTED_CHUNK_BY_QUOTE = %w[" '].to_h { |quote| [quote, /(?:[^{}\\#{quote}]|\\(?!\z))+/] }.freeze
|
|
45
45
|
SLASH, PIPE, APOSTROPHE, LESS_THAN, GREATER_THAN, DASH, EQUALS = "/|'<>-=".bytes
|
|
46
46
|
OPENING_BRACE, CLOSING_BRACE = "{}".bytes
|
|
47
47
|
WHITESPACE_MARKER_BYTES = "<>'".bytes.freeze
|
|
48
|
-
BOOLEAN_ATTRIBUTE_BY_CLOSING = DELIMITERS.values.to_h { [
|
|
49
|
-
ATTRIBUTE_LIST_END_BY_CLOSING = DELIMITERS.values.to_h { [
|
|
50
|
-
CODE_END_BY_CLOSING = [nil, *DELIMITERS.values].to_h { [
|
|
48
|
+
BOOLEAN_ATTRIBUTE_BY_CLOSING = DELIMITERS.values.to_h { |closing| [closing, /#{ATTRIBUTE_NAME}(?=(\s|#{Regexp.escape(closing)}|\z))/] }.freeze
|
|
49
|
+
ATTRIBUTE_LIST_END_BY_CLOSING = DELIMITERS.values.to_h { |closing| [closing, /\s*#{Regexp.escape(closing)}/] }.freeze
|
|
50
|
+
CODE_END_BY_CLOSING = [nil, *DELIMITERS.values].to_h { |closing| [closing, /[\s#{Regexp.escape(closing.to_s)}]/] }.freeze
|
|
51
51
|
|
|
52
52
|
# What the formatter and the linter need to know about each source line, indexed by line number.
|
|
53
53
|
LineInfo = Struct.new(:kind, :depth, :owner, :text, :output_column)
|
|
@@ -297,7 +297,7 @@ module SlimX
|
|
|
297
297
|
end
|
|
298
298
|
|
|
299
299
|
if @outline
|
|
300
|
-
rendered_blank_linenos.each { @outline[
|
|
300
|
+
rendered_blank_linenos.each { @outline[_1].kind = :text_blank }
|
|
301
301
|
rendered_blank_linenos.clear
|
|
302
302
|
end
|
|
303
303
|
next_line
|
|
@@ -312,7 +312,7 @@ module SlimX
|
|
|
312
312
|
text_indent ||= indent
|
|
313
313
|
end
|
|
314
314
|
end
|
|
315
|
-
rendered_blank_linenos.each { @outline[
|
|
315
|
+
rendered_blank_linenos.each { @outline[_1].kind = :blank } if @outline
|
|
316
316
|
nodes
|
|
317
317
|
end
|
|
318
318
|
|
|
@@ -341,7 +341,7 @@ module SlimX
|
|
|
341
341
|
|
|
342
342
|
# Tailwind arbitrary values may hold quotes and ampersands, which Slim names never do.
|
|
343
343
|
value = ERB::Escape.html_escape(value) if value.match?(SHORTCUT_VALUE_TO_ESCAPE)
|
|
344
|
-
names.each { attributes << StaticAttribute.new(
|
|
344
|
+
names.each { |name| attributes << StaticAttribute.new(name, value) }
|
|
345
345
|
@shortcuts.additional_attributes_by_shortcut[shortcut]&.each { |name, value| attributes << StaticAttribute.new(name.to_s, value) }
|
|
346
346
|
@scanner.pos += shortcut_length
|
|
347
347
|
end
|
|
@@ -447,12 +447,12 @@ module SlimX
|
|
|
447
447
|
if @scanner.match?(LINE_CONTINUATION)
|
|
448
448
|
code << @scanner.getch << "\n"
|
|
449
449
|
expect_next_line
|
|
450
|
-
elsif (chunk = @scanner.scan(depth == 0 ? CODE_CHUNK_BY_CLOSING[outer_closing] : NESTED_CODE_CHUNK_BY_OPENING[opening]))
|
|
450
|
+
elsif (chunk = @scanner.scan((depth == 0) ? CODE_CHUNK_BY_CLOSING[outer_closing] : NESTED_CODE_CHUNK_BY_OPENING[opening]))
|
|
451
451
|
code << chunk
|
|
452
452
|
else
|
|
453
453
|
character = @scanner.getch
|
|
454
454
|
if depth > 0
|
|
455
|
-
depth += character == opening ? 1 : -1
|
|
455
|
+
depth += (character == opening) ? 1 : -1
|
|
456
456
|
else
|
|
457
457
|
depth = 1
|
|
458
458
|
opening = character
|
|
@@ -490,7 +490,7 @@ module SlimX
|
|
|
490
490
|
end
|
|
491
491
|
|
|
492
492
|
def syntax_error!(message)
|
|
493
|
-
error = SyntaxError.new(message, @file, @line, @lineno, @line && @scanner ? column : 0)
|
|
493
|
+
error = SyntaxError.new(message, @file, @line, @lineno, (@line && @scanner) ? column : 0)
|
|
494
494
|
error.set_backtrace(["#{@file}:#{@lineno}", *caller])
|
|
495
495
|
raise error
|
|
496
496
|
end
|
data/lib/slimx/rails_handler.rb
CHANGED
|
@@ -8,7 +8,7 @@ module SlimX
|
|
|
8
8
|
file: template.identifier,
|
|
9
9
|
format: xml?(template) ? :xml : :html,
|
|
10
10
|
rails: true,
|
|
11
|
-
**annotations(template)
|
|
11
|
+
**annotations(template)
|
|
12
12
|
)
|
|
13
13
|
end
|
|
14
14
|
|
|
@@ -25,7 +25,7 @@ module SlimX
|
|
|
25
25
|
def annotations(template)
|
|
26
26
|
return {} unless ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
|
|
27
27
|
|
|
28
|
-
{
|
|
28
|
+
{preamble: "<!-- BEGIN #{template.short_identifier} -->\n", postamble: "<!-- END #{template.short_identifier} -->\n"}
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
data/lib/slimx/runtime.rb
CHANGED
|
@@ -12,11 +12,16 @@ module SlimX
|
|
|
12
12
|
|
|
13
13
|
def escape_html_safe(value)
|
|
14
14
|
string = value.to_s
|
|
15
|
-
string.html_safe? || value.html_safe? ? string : ERB::Escape.html_escape(string)
|
|
15
|
+
(string.html_safe? || value.html_safe?) ? string : ERB::Escape.html_escape(string)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def class_value(value, escape, html_safe)
|
|
19
|
-
|
|
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
|
|
20
25
|
return value.to_s unless escape
|
|
21
26
|
|
|
22
27
|
html_safe ? escape_html_safe(value) : escape_html(value)
|
data/lib/slimx/shortcuts.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module SlimX
|
|
4
4
|
class Shortcuts
|
|
5
|
-
DEFAULT = {
|
|
5
|
+
DEFAULT = {"#" => {attr: "id"}, "." => {attr: "class"}}.freeze
|
|
6
6
|
DEFAULT_TAG = "div"
|
|
7
7
|
|
|
8
8
|
attr_reader :tag_by_shortcut, :attributes_by_shortcut, :additional_attributes_by_shortcut, :attribute_pattern, :tag_pattern
|
|
@@ -34,19 +34,18 @@ module SlimX
|
|
|
34
34
|
@additional_attributes_by_shortcut = {}
|
|
35
35
|
definitions.each { |shortcut, definition| register(shortcut, definition) }
|
|
36
36
|
|
|
37
|
-
attribute_keys = Regexp.union(@attributes_by_shortcut.keys.sort_by { -
|
|
37
|
+
attribute_keys = Regexp.union(@attributes_by_shortcut.keys.sort_by { -_1.size })
|
|
38
38
|
name_part = tailwind_classes ? TAILWIND_NAME_PART : SLIM_NAME_PART
|
|
39
39
|
@attribute_pattern = /\A(#{attribute_keys}+)((?:#{name_part})*)/
|
|
40
|
-
tag_keys = Regexp.union(@tag_by_shortcut.keys.sort_by { -
|
|
40
|
+
tag_keys = Regexp.union(@tag_by_shortcut.keys.sort_by { -_1.size })
|
|
41
41
|
@tag_pattern = /\A(?:#{tag_keys}|\*(?=[^\s]+)|(\p{Word}(?:\p{Word}|:|-)*\p{Word}|\p{Word}+))/
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
46
|
def register(shortcut, definition)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
end
|
|
47
|
+
valid = (definition[:attr] || definition[:tag]) && (definition.keys - %i[attr tag additional_attrs]).empty?
|
|
48
|
+
raise ArgumentError, "Shortcut requires :tag and/or :attr" unless valid
|
|
50
49
|
|
|
51
50
|
raise ArgumentError, "Lambda shortcuts are not supported" if definition[:attr].is_a?(Proc)
|
|
52
51
|
|
data/lib/slimx/version.rb
CHANGED
data/lib/slimx.rb
CHANGED
|
@@ -46,7 +46,7 @@ module SlimX
|
|
|
46
46
|
attr_accessor :tailwind_classes
|
|
47
47
|
|
|
48
48
|
def compile(source, file: nil, shortcuts: Shortcuts::DEFAULT, strict: self.strict, tailwind_classes: self.tailwind_classes,
|
|
49
|
-
|
|
49
|
+
**compiler_options)
|
|
50
50
|
ensure_formatted!(source, file, shortcuts, tailwind_classes) if strict
|
|
51
51
|
Compiler.new(**compiler_options).call(Parser.new(file:, shortcuts:, tailwind_classes:).call(source))
|
|
52
52
|
end
|
|
@@ -59,7 +59,7 @@ module SlimX
|
|
|
59
59
|
|
|
60
60
|
lines = source.split("\n", -1)
|
|
61
61
|
expected_lines = formatted.split("\n", -1)
|
|
62
|
-
index = lines.each_index.find { lines[
|
|
62
|
+
index = lines.each_index.find { |line_index| lines[line_index] != expected_lines[line_index] } || lines.size
|
|
63
63
|
raise SyntaxError.new("Not formatted, `slimx fmt` writes: #{expected_lines[index].inspect}", file, lines[index], index + 1, lines[index].to_s[/\A\s*/].size)
|
|
64
64
|
end
|
|
65
65
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slimx
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Level UP Solutions
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
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
|
|
12
26
|
description: 'A Slim template engine rewritten from scratch, without Temple: same
|
|
13
27
|
HTML as Slim 5, faster compilation and rendering with fewer allocations, a formatter
|
|
14
28
|
that cannot change a page, a strict mode, and a linter with HTML and accessibility
|
|
@@ -18,7 +32,9 @@ executables:
|
|
|
18
32
|
extensions: []
|
|
19
33
|
extra_rdoc_files: []
|
|
20
34
|
files:
|
|
35
|
+
- CHANGELOG.md
|
|
21
36
|
- LICENSE
|
|
37
|
+
- README.md
|
|
22
38
|
- exe/slimx
|
|
23
39
|
- lib/slimx.rb
|
|
24
40
|
- lib/slimx/ast.rb
|
|
@@ -35,9 +51,14 @@ files:
|
|
|
35
51
|
- lib/slimx/shortcuts.rb
|
|
36
52
|
- lib/slimx/tilt.rb
|
|
37
53
|
- lib/slimx/version.rb
|
|
54
|
+
homepage: https://github.com/next-gen-gems/slimx
|
|
38
55
|
licenses:
|
|
39
56
|
- MIT
|
|
40
57
|
metadata:
|
|
58
|
+
source_code_uri: https://github.com/next-gen-gems/slimx
|
|
59
|
+
changelog_uri: https://github.com/next-gen-gems/slimx/blob/main/CHANGELOG.md
|
|
60
|
+
documentation_uri: https://next-gen-gems.github.io/slimx/
|
|
61
|
+
bug_tracker_uri: https://github.com/next-gen-gems/slimx/issues
|
|
41
62
|
rubygems_mfa_required: 'true'
|
|
42
63
|
rdoc_options: []
|
|
43
64
|
require_paths:
|
|
@@ -46,14 +67,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
46
67
|
requirements:
|
|
47
68
|
- - ">="
|
|
48
69
|
- !ruby/object:Gem::Version
|
|
49
|
-
version: '3.
|
|
70
|
+
version: '3.3'
|
|
50
71
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
72
|
requirements:
|
|
52
73
|
- - ">="
|
|
53
74
|
- !ruby/object:Gem::Version
|
|
54
75
|
version: '0'
|
|
55
76
|
requirements: []
|
|
56
|
-
rubygems_version: 4.0.
|
|
77
|
+
rubygems_version: 4.0.20
|
|
57
78
|
specification_version: 4
|
|
58
79
|
summary: 'Slim templates, rewritten: faster to compile and render, with a formatter
|
|
59
80
|
and a linter'
|