Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions javascript/packages/formatter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,34 @@ herb-format --force app/views/excluded-file.html.erb

When using `--force` on an excluded file, the formatter will show a warning but proceed with formatting.

### Disabling Formatting for Entire Files <Badge type="info" text="v0.8.2+" />

You can disable formatting for an entire file by adding the `ignore` directive anywhere in your template:

```erb
<%# herb:formatter ignore %>
```

**Example:**

:::code-group
```erb [ignored.html.erb]
<%# herb:formatter ignore %>

<div><div>This entire file will not be formatted</div></div>
```

```erb [formatted.html.erb]
<div>
<div>This file will be formatted</div>
</div>
```
:::

::: warning Important
The `<%# herb:formatter ignore %>` directive must be an exact match. Extra text or spacing will prevent it from working.
:::

## Rewriters

The formatter supports **rewriters** that allow you to transform templates before and after formatting.
Expand Down
51 changes: 51 additions & 0 deletions javascript/packages/formatter/src/format-ignore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { isERBCommentNode } from "@herb-tools/core"
import { Visitor } from "@herb-tools/core"

import type { Node, ERBContentNode } from "@herb-tools/core"

const HERB_FORMATTER_PREFIX = "herb:formatter"
const HERB_FORMATTER_IGNORE_PREFIX = `${HERB_FORMATTER_PREFIX} ignore`

/**
* Check if an ERB content node is a herb:formatter ignore comment
*/
export function isHerbFormatterIgnoreComment(node: Node): boolean {
if (!isERBCommentNode(node)) return false

const content = node?.content?.value || ""

return content.trim() === HERB_FORMATTER_IGNORE_PREFIX
}

/**
* Check if the document contains a herb:formatter ignore directive anywhere.
*/
export function hasFormatterIgnoreDirective(node: Node): boolean {
const detector = new FormatterIgnoreDetector()
detector.visit(node)
return detector.hasIgnoreDirective
}

/**
* Visitor that detects if the AST contains a herb:formatter ignore directive.
*/
class FormatterIgnoreDetector extends Visitor {
public hasIgnoreDirective = false

visitNode(node: Node): void {
if (this.hasIgnoreDirective) return

this.visitChildNodes(node)
}

visitERBContentNode(node: ERBContentNode): void {
if (isHerbFormatterIgnoreComment(node)) {
this.hasIgnoreDirective = true
return
}

if (this.hasIgnoreDirective) return

this.visitChildNodes(node)
}
}
3 changes: 2 additions & 1 deletion javascript/packages/formatter/src/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { FormatPrinter } from "./format-printer.js"

import { isScaffoldTemplate } from "./scaffold-template-detector.js"
import { resolveFormatOptions } from "./options.js"
import { hasFormatterIgnoreDirective } from "./format-ignore.js"

import type { Config } from "@herb-tools/config"
import type { RewriteContext } from "@herb-tools/rewriter"
import type { HerbBackend, ParseResult } from "@herb-tools/core"
import type { FormatOptions } from "./options.js"


/**
* Formatter uses a Herb Backend to parse the source and then
* formats the resulting AST into a well-indented, wrapped string.
Expand Down Expand Up @@ -61,6 +61,7 @@ export class Formatter {

if (result.failed) return source
if (isScaffoldTemplate(result)) return source
if (hasFormatterIgnoreDirective(result.value)) return source

const resolvedOptions = resolveFormatOptions({ ...this.options, ...options })

Expand Down
120 changes: 120 additions & 0 deletions javascript/packages/formatter/test/herb-formatter-ignore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, test, expect, beforeAll } from "vitest"
import { Herb } from "@herb-tools/node-wasm"
import { Formatter } from "../src"
import dedent from "dedent"

let formatter: Formatter

describe("herb:formatter ignore directive", () => {
beforeAll(async () => {
await Herb.load()
formatter = new Formatter(Herb, {
indentWidth: 2,
maxLineLength: 80,
})
})

test("should ignore formatting when directive is at top of file", () => {
const source = dedent`
<%# herb:formatter ignore %>
<DIV>
<SPAN> Badly formatted content </SPAN>
</DIV>
`

const result = formatter.format(source)
expect(result).toBe(source)
})

test("should ignore formatting when directive is in middle of file", () => {
const source = dedent`
<div>
<%# herb:formatter ignore %>
<SPAN> Badly formatted content </SPAN>
</div>
`

const result = formatter.format(source)
expect(result).toBe(source)
})

test("should work with frontmatter before directive", () => {
const source = dedent`
---
title: Test
---
<%# herb:formatter ignore %>
<DIV>
<SPAN>content</SPAN>
</DIV>
`

const result = formatter.format(source)
expect(result).toBe(source)
})

test("should work with whitespace before directive", () => {
const source = `

<%# herb:formatter ignore %>
<DIV>
<SPAN>content</SPAN>
</DIV>
`

const result = formatter.format(source)
expect(result).toBe(source)
})

test("should not match herb:formatter ignore with extra text", () => {
const source = dedent`
<%# herb:formatter ignore some-rule %>
<div>
<span>content</span>
</div>
`

const result = formatter.format(source)
expect(result).not.toBe(source)
})

test("should not match herb:disable all", () => {
const source = dedent`
<%# herb:disable all %>
<DIV>
<SPAN>content</SPAN>
</DIV>
`

const result = formatter.format(source)
expect(result).not.toBe(source)
})

test("should ignore formatting when directive is at end of file", () => {
const source = dedent`
<DIV>
<SPAN>content</SPAN>
</DIV>
<%# herb:formatter ignore %>
`

const result = formatter.format(source)
expect(result).toBe(source)
})

test("should ignore formatting when directive is nested deep in document", () => {
const source = dedent`
<DIV>
<SECTION>
<ARTICLE>
<%# herb:formatter ignore %>
<SPAN> Badly formatted </SPAN>
</ARTICLE>
</SECTION>
</DIV>
`

const result = formatter.format(source)
expect(result).toBe(source)
})
})
30 changes: 30 additions & 0 deletions javascript/packages/linter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,36 @@ To disable all linting rules for a specific line, use `all`:
Inline disable comments only affect the line they appear on. Each line that needs linting disabled must have its own disable comment.
:::

### Disabling Linting for Entire Files <Badge type="info" text="v0.8.2+" />

You can disable linting for an entire file by adding the `ignore` directive anywhere in your template:

```erb
<%# herb:linter ignore %>
```

**Example:**

:::code-group
```erb [ignored.html.erb]
<%# herb:linter ignore %>

<DIV>
<SPAN>This entire file will not be linted</SPAN>
</DIV>
```

```erb [regular.html.erb]
<DIV>
<SPAN>This entire file will be linted</SPAN>
</DIV>
```
:::

::: warning Important
The `<%# herb:linter ignore %>` directive must be an exact match. Extra text or spacing will prevent it from working.
:::

## Configuration

Create a `.herb.yml` file in your project root to configure the linter:
Expand Down
3 changes: 3 additions & 0 deletions javascript/packages/linter/src/herb-disable-comment-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
* Utilities for parsing herb:disable comments
*/

import { isERBCommentNode } from "@herb-tools/core"
import type { Node } from "@herb-tools/core"

/**
* Information about a single rule name in a herb:disable comment
*/
Expand Down
56 changes: 56 additions & 0 deletions javascript/packages/linter/src/linter-ignore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { isERBCommentNode } from "@herb-tools/core"
import { Visitor, ERBContentNode } from "@herb-tools/core"

import type { Node, ParseResult } from "@herb-tools/core"

const HERB_LINTER_PREFIX = "herb:linter"
const HERB_LINTER_IGNORE_PREFIX = `${HERB_LINTER_PREFIX} ignore`

/**
* Check if an ERB content node is a herb:linter ignore comment.
*
* @param node - The ERB content node to check
* @returns true if this is a linter ignore directive
*/
export function isHerbLinterIgnoreComment(node: Node): boolean {
if (!isERBCommentNode(node)) return false

const content = node?.content?.value || ""

return content.trim() === HERB_LINTER_IGNORE_PREFIX
}

/**
* Check if the document contains a herb:linter ignore directive anywhere.
*/
export function hasLinterIgnoreDirective(parseResult: ParseResult): boolean {
if (parseResult.failed) return false

const detector = new LinterIgnoreDetector()
detector.visit(parseResult.value)
return detector.hasIgnoreDirective
}

/**
* Visitor that detects if the AST contains a herb:linter ignore directive.
*/
class LinterIgnoreDetector extends Visitor {
public hasIgnoreDirective = false

visitNode(node: Node): void {
if (this.hasIgnoreDirective) return

this.visitChildNodes(node)
}

visitERBContentNode(node: ERBContentNode): void {
if (isHerbLinterIgnoreComment(node)) {
this.hasIgnoreDirective = true
return
}

if (this.hasIgnoreDirective) return

this.visitChildNodes(node)
}
}
14 changes: 14 additions & 0 deletions javascript/packages/linter/src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { minimatch } from "minimatch"
import { rules } from "./rules.js"
import { findNodeByLocation } from "./rules/rule-utils.js"
import { parseHerbDisableLine } from "./herb-disable-comment-utils.js"
import { hasLinterIgnoreDirective } from "./linter-ignore.js"

import { ParserNoErrorsRule } from "./rules/parser-no-errors.js"
import { DEFAULT_RULE_CONFIG } from "./types.js"
Expand Down Expand Up @@ -303,6 +304,18 @@ export class Linter {
let wouldBeIgnoredCount = 0

const parseResult = this.herb.parse(source, { track_whitespace: true })

// Check for file-level ignore directive using visitor
if (hasLinterIgnoreDirective(parseResult)) {
return {
offenses: [],
errors: 0,
warnings: 0,
info: 0,
hints: 0,
ignored: 0
}
}
const lexResult = this.herb.lex(source)
const hasParserErrors = parseResult.recursiveErrors().length > 0
const sourceLines = source.split("\n")
Expand Down Expand Up @@ -573,3 +586,4 @@ export class Linter {
}
}
}

Loading
Loading