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

slimx fmt [PATH…]           # rewrite templates in place
slimx fmt --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.

Paths default to the current directory; .git, node_modules, tmp and vendor are skipped.

Strict mode

# config/initializers/slimx.rb
SlimX.strict = Rails.env.local?
Not formatted, `slimx fmt` writes: "  li Home"
  app/views/home/show.html.slim, Line 2, Column 3
    li Home
    ^

A template slimx fmt would change fails to compile, pointing at the first line and what it should be. Templates stay formatted without a CI step catching them late. Per template: strict: true.

Linter

$ slimx lint [PATH…]
app/views/posts/_card.html.slim:3: [ImageAlt] Describe the image with `alt`, or mark it decorative with `alt=""`

29 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.

Configuration

Both commands read .slimx.yml from the current directory. Every key is optional:

tailwind_classes: true     # read templates the way the app compiles them

linters:
  LineLength:
    max: 150                 # default 120
  ConsecutiveControlStatements:
    max: 3                   # lines of `-` before asking for a ruby: block, default 2
  ButtonType:
    enabled: true            # rules off by default: ButtonType, InlineStyles, StrictLocals
  DeprecatedClasses:
    classes:
      text-muted: text-body-secondary
  InstanceVariables:
    enabled: false

In CI

# .github/workflows/lint.yml
- run: bundle exec slimx fmt --check app
- run: bundle exec slimx lint 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 slimx fmt

IndentationIndent with 2 spaces, not 3

Flagged

ul
   li Home

Clean

ul
  li Home
TabIndent with spaces, not tabs

Flagged

ul
→   li Home

Clean

ul
  li Home
TrailingWhitespaceTrailing whitespace renders: write it with ' or > instead

Flagged

p Hello·

Clean

p Hello
EmptyLinesRemove this extra empty line

Flagged

h1 Title


p Text

Clean

h1 Title

p Text
TrailingBlankLinesRemove the empty lines at the end of the file

Flagged

p Text

Clean

p Text
ControlStatementSpacingPut a space after -

Flagged

-if admin?
  p Admin

Clean

- if admin?
  p Admin
RedundantDivdiv is implied by the .class or #id shortcut

Flagged

div.card
  p Text

Clean

.card
  p Text

Template hygiene — from slim-lint

LineLengthLine is too long [127/120]

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
CommentControlStatementWrite comments with /, not - #: they cost nothing at render time

Flagged

- # TODO
p Text

Clean

/ TODO
p Text
EmptyControlStatementRemove this empty -

Flagged

-
p Text

Clean

p Text
ConsecutiveControlStatementsMerge these 3 lines of Ruby into a ruby: block

Flagged

- a = 1
- b = 2
- c = 3
p = a + b + c

Clean

ruby:
  a = 1
  b = 2
  c = 3
p = a + b + c
TagCaseWrite p in lowercase

Flagged

P Text

Clean

p Text
ZwspRemove the invisible zero-width character

Flagged

p Hello\u200Bworld

Clean

p Hello world

Ruby in templates — from haml-lint and erb_lint

UnnecessaryInterpolationWrite = post.title instead of interpolating it alone

Flagged

p #{post.title}

Clean

p = post.title
UnnecessaryStringOutputWrite the text | Hello instead of outputting a string

Flagged

p
  = "Hello"

Clean

p
  | Hello
DeprecatedClassestext-muted is deprecated: use text-body-secondary

Flagged

p.text-muted Text

Clean

p.text-body-secondary Text
InlineStylesoff by defaultMove inline styles to a stylesheet

Flagged

p style="color: red" Text

Clean

p.text-danger Text

Rails conventions

InstanceVariablesPass @post to the partial as a local

Flagged

p = @post.title

Clean

/# locals: (post:)
p = post.title
StrictLocalsoff by defaultDeclare the partial's locals: /# locals: (name:)

Flagged

p = post.title

Clean

/# locals: (post:)
p = post.title

HTML and accessibility — from Herb and jsx-a11y

DuplicateIdThe id menu is already used in this template

Flagged

#menu
#menu

Clean

#menu
#footer-menu
ImageAltDescribe the image with alt, or mark it decorative with alt=""

Flagged

img src=post.image_url

Clean

img src=post.image_url alt=post.title
IframeTitleName the frame with title

Flagged

iframe src=video_url

Clean

iframe src=video_url title="Intro video"
AnchorHrefA link without href is not a link: use a button for actions

Flagged

a Read more

Clean

a href=post_path(post) Read more
NestedLinksLinks cannot contain links

Flagged

a href="/a"
  a href="/b" B

Clean

a href="/a" A
a href="/b" B
PositiveTabindexA positive tabindex breaks the reading order: use 0 or -1

Flagged

button tabindex="3" type="button" Save

Clean

button tabindex="0" type="button" Save
BooleanAttributeValueWrite disabled=true, or disabled alone inside an attribute list

Flagged

input disabled="disabled"

Clean

input disabled=true
AriaRolebuton is not an ARIA role

Flagged

div role="buton"

Clean

div role="button"
AriaAttributearia-lable is not an ARIA attribute

Flagged

button aria-lable="Close" type="button"

Clean

button aria-label="Close" type="button"
ButtonTypeoff by defaultSay what the button does with type="button" or type="submit"

Flagged

button Save

Clean

button type="submit" Save