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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee525fd8483ce0a207faa8ea7a61dc5f17a13720b130e6b819420afe8a6452c2
4
- data.tar.gz: 0c902f495a605426f96ec1da183c48d3d7b316e6992eb9dd8f095c3bfbb8502d
3
+ metadata.gz: 1226be0f289b66fcf4b695e789913b7a94df07468e7db6471f5e82cd5a58d05f
4
+ data.tar.gz: ceaca10aa563fcbf70bafa180f140495e640622f1809bb119027c35a868e9281
5
5
  SHA512:
6
- metadata.gz: 05ab88358c968355515af67ffe3a5cb87359bb7dfa88232f4f553809c4fa6268e522a228fff8930e3c2c390adfffbf13f151842429462606e0b9f84e1da1ee66
7
- data.tar.gz: 409b2ddcc6fb27168758f05e8908cada7a8c72f28870484683a893b30fe522bf8c1197735e541c11459a88107a905f03b4aef666816d55771cd3e11fdccdf7dd
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
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright (c) 2026 Clément Joubert
3
+ Copyright (c) 2026 Level UP Solutions
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
@@ -23,7 +23,7 @@ module SlimX
23
23
  Splat = Struct.new(:code)
24
24
 
25
25
  def self.empty?(nodes)
26
- nodes.all? { |node| node == NEWLINE }
26
+ nodes.all?(NEWLINE)
27
27
  end
28
28
 
29
29
  # Once compiled, a text block with no text leaves nothing behind: Slim then closes a void tag holding one.
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(it, File.read(it)) }.flatten
34
- offenses.each { puts "#{it.path}:#{it.lineno}: [#{it.rule}] #{it.message}" }
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).reject { it.match?(SKIPPED_DIRECTORIES) }.map { File.join(path, it) }
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
@@ -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 { [it, true] }.freeze
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?(it) }
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 { it.name == "class" }
184
- compile_class_attribute(class_attributes.map { attribute_parts(it) })
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(it) }
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(it.text) }.map do |part|
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
@@ -87,7 +87,7 @@ module SlimX
87
87
  end
88
88
 
89
89
  def expand_tabs(line)
90
- line.sub(/\A[ \t]*/) { it.gsub(Parser::TAB, "\\1#{" " * Parser::TAB_SIZE}") }
90
+ line.sub(/\A[ \t]*/) { _1.gsub(Parser::TAB, "\\1#{" " * Parser::TAB_SIZE}") }
91
91
  end
92
92
 
93
93
  def compiled(source)
@@ -4,11 +4,14 @@ module SlimX
4
4
  class Linter
5
5
  module Rules
6
6
  def self.all
7
- constants.map { const_get(it) }.select { it < Rule }
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 = { enabled: true }.freeze
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 { it.name unless it.is_a?(AST::Splat) }
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? { it.is_a?(AST::Splat) }
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 { it.respond_to?(:name) && it.name == name && literal_value(it) }
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 = { enabled: true, max: 120 }.freeze
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.enum_for(:each_node).filter_map do |node,|
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.enum_for(:each_node).filter_map do |node,|
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 = { enabled: true, max: 2 }.freeze
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?(it) } > options[:max] }
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
- .select { it.size > options[:max] && flat_control?(it.first) }
198
- .map { offense(document, document.line_of(it.first), "Merge these #{it.size} lines of Ruby into a `ruby:` block") }
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.enum_for(:each_node).filter_map do |node, ancestors|
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? { it.is_a?(AST::Tag) && it.name == "svg" }
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.enum_for(:each_node).filter_map do |node,|
234
+ document.each_node.filter_map do |node,|
229
235
  code = case node
230
- when AST::Control, AST::Output then node.code
231
- when AST::Tag then node.attributes.filter_map { it.code if it.respond_to?(:code) }.join(" ")
232
- end
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 = { enabled: false }.freeze
247
+ DEFAULTS = {enabled: false}.freeze
242
248
 
243
249
  def offenses(document, _source)
244
- return [] if !document.partial? || document.lines.any? { it.match?(%r{\A\s*/#\s*locals:}) }
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 { it.is_a?(AST::Control) }
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 = { enabled: false }.freeze
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? { it.is_a?(AST::Tag) && it.name == "a" }
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
- hidden inert ismap loop multiple muted nomodule novalidate open playsinline readonly required
322
- reversed selected].freeze
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
- combobox complementary contentinfo definition deletion dialog document emphasis feed figure form generic
339
- grid gridcell group heading img insertion link list listbox listitem log main marquee math menu menubar
340
- menuitem menuitemcheckbox menuitemradio meter navigation none note option paragraph presentation
341
- progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider
342
- spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer
343
- toolbar tooltip tree treegrid treeitem].freeze
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.reject { ROLES.include?(it) || it.start_with?("doc-", "graphics-") }.map do |role|
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
- colindex colindextext colspan controls current describedby description details disabled
359
- dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label
360
- labelledby level live modal multiline multiselectable orientation owns placeholder posinset
361
- pressed readonly relevant required roledescription rowcount rowindex rowindextext rowspan selected
362
- setsize sort valuemax valuemin valuenow valuetext].map { "aria-#{it}" }.freeze
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 = { enabled: true, classes: {} }.freeze
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(it).split }.filter_map do |css_class|
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 = { enabled: false }.freeze
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.enum_for(:each_node).filter_map do |node, ancestors|
409
- next unless node.is_a?(AST::TextBlock) && (lineno = document.line_of(node) || document.line_of(ancestors.last))
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.enum_for(:each_node).filter_map do |node,|
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
- offense(document, document.line_of(node), "Write the text `| #{$2}` instead of outputting a string")
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
- visits:, tag_visits: visits.select { |node,| node.is_a?(AST::Tag) })
69
- offenses = @rules.flat_map { it.offenses(document, source) }
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
@@ -27,9 +27,9 @@ module SlimX
27
27
  writer.close
28
28
  reader
29
29
  end
30
- results_by_process = readers.map { Marshal.load(it.binmode.read) }
30
+ results_by_process = readers.map { |reader| Marshal.load(reader.binmode.read) }
31
31
  Process.waitall
32
- results_by_process.each { raise it if it.is_a?(Exception) }
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 = { "(" => ")", "[" => "]", "{" => "}" }.freeze
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 { [it, /(?:[^\s(\[{,\\#{Regexp.escape(it.to_s)}]|[,\\](?!\z))+/] }.freeze
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 { [it, /(?:[^{}\\#{it}]|\\(?!\z))+/] }.freeze
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 { [it, /#{ATTRIBUTE_NAME}(?=(\s|#{Regexp.escape(it)}|\z))/] }.freeze
49
- ATTRIBUTE_LIST_END_BY_CLOSING = DELIMITERS.values.to_h { [it, /\s*#{Regexp.escape(it)}/] }.freeze
50
- CODE_END_BY_CLOSING = [nil, *DELIMITERS.values].to_h { [it, /[\s#{Regexp.escape(it.to_s)}]/] }.freeze
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[it].kind = :text_blank }
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[it].kind = :blank } if @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(it, value) }
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
@@ -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
- { preamble: "<!-- BEGIN #{template.short_identifier} -->\n", postamble: "<!-- END #{template.short_identifier} -->\n" }
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
- value = value.flatten.map(&:to_s).reject(&:empty?).join(" ") if value.is_a?(Array)
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)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module SlimX
4
4
  class Shortcuts
5
- DEFAULT = { "#" => { attr: "id" }, "." => { attr: "class" } }.freeze
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 { -it.size })
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 { -it.size })
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
- unless (definition[:attr] || definition[:tag]) && (definition.keys - %i[attr tag additional_attrs]).empty?
48
- raise ArgumentError, "Shortcut requires :tag and/or :attr"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SlimX
4
- VERSION = "0.1.1"
4
+ VERSION = "1.0.0"
5
5
  end
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
- **compiler_options)
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[it] != expected_lines[it] } || lines.size
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.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
- - Clément Joubert
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.4'
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.23
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'