Formatter & linter
One parser serves the compiler, the formatter and the linter, so all three read a template the same way. Both commands split the templates between every core.
Formatter
super-slim format [PATH…] # rewrite templates in place
super-slim format --check [PATH…] # list the ones it would rewrite, exit 1 if any
One layout: two spaces per level, - code and = output spacing, p = output after a tag, no redundant div, no trailing whitespace, single blank lines, one final newline. Text blocks move as a whole, so the whitespace they render stays put.
Before writing a file, the formatter compiles the template before and after, and refuses if the Ruby differs: formatting cannot change a page. A refused file is reported and left untouched, and the command exits 1.
With format: max_line_length in .super-slim.yml, longer lines are wrapped: a tag’s attributes one per line inside attribute_delimiters (() by default, or [], {}), then the arguments of its Ruby code after each comma, which Slim reads as a line that goes on. When the line opens a block, its arguments go four spaces deeper, apart from the block’s children. Lines already spread over several lines stay as they are, and a wrap is only kept when Prism reads the same compiled Ruby.
a.btn(
href=edit_post_path(post)
data-turbo-frame="modal"
) = t ".edit",
title: post.title
Paths default to the current directory; .git, node_modules, tmp and vendor are skipped.
Linter
$ super-slim lint [PATH…]
app/views/posts/_card.html.slim:3: [ImageAlt] Describe the image with `alt`, or mark it decorative with `alt=""`
33 rules taken from slim-lint, haml-lint, erb_lint, Herb and eslint-plugin-jsx-a11y: layout, template hygiene, Ruby in templates, Rails conventions, HTML and accessibility. It exits 1 when it reports an offense.
Ruby in templates
The Ruby inside templates is RuboCop’s job. SuperSlim ships a RuboCop plugin that hands it each - and = line, code attribute, splat, #{} interpolation and ruby: block, read by SuperSlim’s own parser:
# .rubocop-slim.yml
inherit_from: .rubocop.yml
plugins:
- super-slim
AllCops:
Include:
- "**/*.slim"
$ bundle exec rubocop -c .rubocop-slim.yml app
app/views/posts/show.html.slim:4:20: C: [Correctable] Style/StringLiterals: Prefer single-quoted strings…
Offenses point at their line and column in the .slim file, and --autocorrect writes back into it. The cops that cannot hold in a template, such as indentation across lines or frozen string comments, are off for .slim files. It replaces rubocop-slim, and with it slimi and Temple.
Configuration
Both commands read .super-slim.yml from the current directory. Every key is optional:
tailwind_classes: true # read templates the way the app compiles them
format:
max_line_length: 120 # wrap longer lines, default off
attribute_delimiters: "{}" # around wrapped attributes: "()", "[]" or "{}", default "()"
linters:
LineLength:
max: 150 # default 120
ConsecutiveControlStatements:
max: 3 # lines of `-` before asking for a ruby: block, default 2
ButtonType:
enabled: true # off by default: ButtonType, EmbeddedEngines, HardcodedText, InlineStyles, StrictLocals, UnescapedOutput
DeprecatedClasses:
preset: bootstrap5 # Bootstrap 4 classes that Bootstrap 5.3 renamed or dropped
classes:
media: ~ # drops an entry of the preset
legacy-*: modern-* # a prefix: legacy-card becomes modern-card
DeprecatedAttributes:
preset: bootstrap5 # data-toggle becomes data-bs-toggle, and so on
EmbeddedEngines:
enabled: true
forbidden_engines: [javascript, css]
HardcodedText:
enabled: true
include: ["app/views/shop/**/*.slim"] # every rule takes include: and exclude: globs
InlineStyles:
enabled: true
exclude: ["app/views/*_mailer/**/*"] # relative to .super-slim.yml
InstanceVariables:
enabled: false
In CI
# .github/workflows/lint.yml
- run: bundle exec super-slim format --check app
- run: bundle exec super-slim lint app
- run: bundle exec rubocop -c .rubocop-slim.yml app
Both print one line per file or offense, path:line: [Rule] message for the linter, and exit 0 when there is nothing to report, 1 otherwise, 64 on a usage error.
Rules
Each example below is run through the linter when the page is generated: the flagged one must be flagged, the clean one must pass.
Layout — fixed by super-slim format
Indentation
Flagged
ul
li Home
Clean
ul
li Home
Tab
Flagged
ul
→ li Home
Clean
ul
li Home
TrailingWhitespace
Flagged
p Hello·
Clean
p Hello
EmptyLines
Flagged
h1 Title
p Text
Clean
h1 Title
p Text
TrailingBlankLines
Flagged
p Text
Clean
p Text
ControlStatementSpacing
Flagged
-if admin?
p Admin
Clean
- if admin?
p Admin
RedundantDiv
Flagged
div.card
p Text
Clean
.card
p Text
Template hygiene — from slim-lint
LineLength
Flagged
p word word word word word word word word word word word word word word word word word word word word word word word word word
Clean
p
| word word word word word word word word word word
CommentControlStatement
Flagged
- # TODO
p Text
Clean
/ TODO
p Text
EmptyControlStatement
Flagged
-
p Text
Clean
p Text
ConsecutiveControlStatements
Flagged
- a = 1
- b = 2
- c = 3
p = a + b + c
Clean
ruby:
a = 1
b = 2
c = 3
p = a + b + c
TagCase
Flagged
P Text
Clean
p Text
Zwsp
Flagged
p Hello\u200Bworld
Clean
p Hello world
Ruby in templates — from haml-lint and erb_lint
UnnecessaryInterpolation
Flagged
p #{post.title}
Clean
p = post.title
UnnecessaryStringOutput
Flagged
p
= "Hello"
Clean
p
| Hello
DeprecatedClasses
Flagged
p.text-muted Text
Clean
p.text-body-secondary Text
DeprecatedAttributes
Flagged
button data-toggle="modal" type="button" Open
Clean
button data-bs-toggle="modal" type="button" Open
InlineStylesoff by default
Flagged
p style="color: red" Text
Clean
p.text-danger Text
UnescapedOutputoff by default
Flagged
p == post.intro
Clean
p = t(".intro_html")
EmbeddedEnginesoff by default
Flagged
javascript:
track("view")
Clean
div data-controller="tracking"
HardcodedTextoff by default
Flagged
h1 Welcome
Clean
h1 = t(".welcome")
Rails conventions
InstanceVariables
Flagged
p = @post.title
Clean
/# locals: (post:)
p = post.title
StrictLocalsoff by default
Flagged
p = post.title
Clean
/# locals: (post:)
p = post.title
HTML and accessibility — from Herb and jsx-a11y
DuplicateId
Flagged
#menu
#menu
Clean
#menu
#footer-menu
ImageAlt
Flagged
img src=post.image_url
Clean
img src=post.image_url alt=post.title
IframeTitle
Flagged
iframe src=video_url
Clean
iframe src=video_url title="Intro video"
AnchorHref
Flagged
a Read more
Clean
a href=post_path(post) Read more
NestedLinks
Flagged
a href="/a"
a href="/b" B
Clean
a href="/a" A
a href="/b" B
PositiveTabindex
Flagged
button tabindex="3" type="button" Save
Clean
button tabindex="0" type="button" Save
BooleanAttributeValue
Flagged
input disabled="disabled"
Clean
input disabled=true
AriaRole
Flagged
div role="buton"
Clean
div role="button"
AriaAttribute
Flagged
button aria-lable="Close" type="button"
Clean
button aria-label="Close" type="button"
ButtonTypeoff by default
Flagged
button Save
Clean
button type="submit" Save