") // 4 spaces instead of 2
+ })
+
+ it("should show --max-line-length option in help", async () => {
+ const result = await execBinary(["--help"])
+
+ expectExitCode(result, 0)
+ expect(result.stdout).toContain("herb-format --max-line-length")
+ expect(result.stdout).toContain("maximum line length before wrapping")
+ })
+
+ it("should accept valid --max-line-length", async () => {
+ const input = '
Short text that wraps
'
+ const result = await execBinary(["--max-line-length", "20"], input)
+
+ expectExitCode(result, 0)
+ expect(result.stdout).toContain("Short text that")
+ expect(result.stdout).toContain("wraps")
+ })
+
describe("Glob Pattern Support", () => {
beforeEach(async () => {
await mkdir("test-fixtures", { recursive: true })
diff --git a/javascript/packages/formatter/test/document-formatting.test.ts b/javascript/packages/formatter/test/document-formatting.test.ts
index 2b4a3f8cf..60a3e1e5f 100644
--- a/javascript/packages/formatter/test/document-formatting.test.ts
+++ b/javascript/packages/formatter/test/document-formatting.test.ts
@@ -785,4 +785,198 @@ describe("Document-level formatting", () => {
`)
})
+
+ test("preserves space between tag name and attributes (issue #477)", () => {
+ const source = dedent`
+
x
+ `
+ const result = formatter.format(source)
+ expect(result).toEqual(dedent`
+
x
+ `)
+ })
+
+ test("preserves space between tag name and attributes with multiple attributes", () => {
+ const source = dedent`
+ content
+ `
+ const result = formatter.format(source)
+ expect(result).toEqual(dedent`
+ content
+ `)
+ })
+
+ test("preserves space between tag name and attributes in pre tag with multiple attributes", () => {
+ const source = dedent`
+
content
+ `
+ const result = formatter.format(source)
+ expect(result).toEqual(dedent`
+
content
+ `)
+ })
+
+ test("preserves space between tag name and attributes in pre tag with mixed content", () => {
+ const source = dedent`
+
Some text code more text
+ `
+ const result = formatter.format(source)
+ expect(result).toEqual(dedent`
+
Some text code more text
+ `)
+ })
+
+ test("preserves space for nested HTML elements in other content-preserving tags", () => {
+ const source = dedent`
+
+ `
+ const result = formatter.format(source)
+ expect(result).toEqual(dedent`
+
+ `)
+ })
+
+ test("preserves multi-line content in pre tag with exact whitespace", () => {
+ const source = dedent`
+
%0A
+::error file=template.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required `alt` attribute on `` tag [html-img-require-alt]%0A%0A%0Atemplate.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A
[error] Missing required `alt` attribute on `` tag [html-img-require-alt]
@@ -199,7 +199,7 @@ template.html.erb:3:3
**Example: `--format=simple --github` (GitHub annotations + simple format)**
```
-::error file=template.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.0::Missing required `alt` attribute on `` tag [html-img-require-alt]%0A%0A%0Atemplate.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A
+::error file=template.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required `alt` attribute on `` tag [html-img-require-alt]%0A%0A%0Atemplate.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A
template.html.erb:
3:3 ✗ Missing required `alt` attribute on `` tag [html-img-require-alt]
diff --git a/javascript/packages/linter/docs/rules/README.md b/javascript/packages/linter/docs/rules/README.md
index b9ee35991..689d05bc4 100644
--- a/javascript/packages/linter/docs/rules/README.md
+++ b/javascript/packages/linter/docs/rules/README.md
@@ -31,7 +31,7 @@ This page contains documentation for all Herb Linter rules.
- [`html-no-empty-attributes`](./html-no-empty-attributes.md) - Attributes must not have empty values
- [`html-no-nested-links`](./html-no-nested-links.md) - Prevents nested anchor tags
- [`html-no-positive-tab-index`](./html-no-positive-tab-index.md) - Avoid positive `tabindex` values
-- [`html-no-self-closing`](./html-no-self-closing.md.md) - Disallow self closing tags
+- [`html-no-self-closing`](./html-no-self-closing.md) - Disallow self closing tags
- [`html-no-title-attribute`](./html-no-title-attribute.md) - Avoid using the `title` attribute
- [`html-tag-name-lowercase`](./html-tag-name-lowercase.md) - Enforces lowercase tag names in HTML
- [`html-no-underscores-in-attribute-names`](./html-no-underscores-in-attribute-names.md) - Disallow underscores in HTML attribute names
diff --git a/javascript/packages/linter/package.json b/javascript/packages/linter/package.json
index be14e28bf..aa3766a66 100644
--- a/javascript/packages/linter/package.json
+++ b/javascript/packages/linter/package.json
@@ -1,6 +1,6 @@
{
"name": "@herb-tools/linter",
- "version": "0.7.0",
+ "version": "0.7.4",
"description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
"license": "MIT",
"homepage": "https://herb-tools.dev",
@@ -39,10 +39,10 @@
}
},
"dependencies": {
- "@herb-tools/core": "0.7.0",
- "@herb-tools/highlighter": "0.7.0",
- "@herb-tools/node-wasm": "0.7.0",
- "@herb-tools/printer": "0.7.0",
+ "@herb-tools/core": "0.7.4",
+ "@herb-tools/highlighter": "0.7.4",
+ "@herb-tools/node-wasm": "0.7.4",
+ "@herb-tools/printer": "0.7.4",
"glob": "^11.0.3"
},
"files": [
diff --git a/javascript/packages/linter/src/rules/html-no-empty-attributes.ts b/javascript/packages/linter/src/rules/html-no-empty-attributes.ts
index 1d03df1f0..b0cc8eec6 100644
--- a/javascript/packages/linter/src/rules/html-no-empty-attributes.ts
+++ b/javascript/packages/linter/src/rules/html-no-empty-attributes.ts
@@ -1,8 +1,9 @@
import { ParserRule } from "../types.js"
import { AttributeVisitorMixin, StaticAttributeStaticValueParams, DynamicAttributeStaticValueParams } from "./rule-utils.js"
+import { IdentityPrinter } from "@herb-tools/printer"
import type { LintOffense, LintContext } from "../types.js"
-import type { ParseResult } from "@herb-tools/core"
+import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
// Attributes that must not have empty values
const RESTRICTED_ATTRIBUTES = new Set([
@@ -37,26 +38,41 @@ function isRestrictedAttribute(attributeName: string): boolean {
return false
}
+function isDataAttribute(attributeName: string): boolean {
+ return attributeName.startsWith('data-')
+}
+
class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
- if (!isRestrictedAttribute(attributeName)) return
- if (attributeValue.trim() !== "") return
-
- this.addOffense(
- `Attribute \`${attributeName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`,
- attributeNode.name!.location,
- "warning"
- )
+ this.checkEmptyAttribute(attributeName, attributeValue, attributeNode)
}
protected checkDynamicAttributeStaticValue({ combinedName, attributeValue, attributeNode }: DynamicAttributeStaticValueParams): void {
const name = (combinedName || "").toLowerCase()
- if (!isRestrictedAttribute(name)) return
+ this.checkEmptyAttribute(name, attributeValue, attributeNode)
+ }
+
+ private checkEmptyAttribute(attributeName: string, attributeValue: string, attributeNode: HTMLAttributeNode): void {
+ if (!isRestrictedAttribute(attributeName)) return
if (attributeValue.trim() !== "") return
+ const hasExplicitValue = attributeNode.value !== null
+
+ if (isDataAttribute(attributeName)) {
+ if (hasExplicitValue) {
+ this.addOffense(
+ `Data attribute \`${attributeName}\` should not have an empty value. Either provide a meaningful value or use \`${attributeName}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`,
+ attributeNode.location,
+ "warning"
+ )
+ }
+
+ return
+ }
+
this.addOffense(
- `Attribute \`${combinedName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`,
- attributeNode.name!.location,
+ `Attribute \`${attributeName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`,
+ attributeNode.location,
"warning"
)
}
diff --git a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap
index 1baad2c63..836e0922b 100644
--- a/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap
+++ b/javascript/packages/linter/test/__snapshots__/cli.test.ts.snap
@@ -1,11 +1,11 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`CLI Output Formatting > GitHub Actions format escapes special characters in messages 1`] = `
-"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.0::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A 5 │%0A
+"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A 5 │%0A
-::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.0::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
+::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
-::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.0::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
+::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
[error] Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. (html-img-require-alt)
@@ -55,7 +55,7 @@ test/fixtures/test-file-with-errors.html.erb:2:22
`;
exports[`CLI Output Formatting > GitHub Actions format includes rule codes 1`] = `
-"::error file=test/fixtures/no-trailing-newline.html.erb,line=1,col=29,title=erb-requires-trailing-newline • @herb-tools/linter@0.7.0::File must end with trailing newline [erb-requires-trailing-newline]%0A%0A%0Atest/fixtures/no-trailing-newline.html.erb:1:29%0A%0A → 1 │
No trailing newline
%0A │ ~%0A
+"::error file=test/fixtures/no-trailing-newline.html.erb,line=1,col=29,title=erb-requires-trailing-newline • @herb-tools/linter@0.7.4::File must end with trailing newline [erb-requires-trailing-newline]%0A%0A%0Atest/fixtures/no-trailing-newline.html.erb:1:29%0A%0A → 1 │
No trailing newline
%0A │ ~%0A
[error] File must end with trailing newline (erb-requires-trailing-newline)
@@ -193,7 +193,7 @@ test/fixtures/multiple-rule-offenses.html.erb:3:14
1 │
2 │
→ 3 │
7 │
@@ -389,9 +389,9 @@ test/fixtures/few-rule-offenses.html.erb:3:16
`;
exports[`CLI Output Formatting > formats GitHub Actions output correctly for bad file 1`] = `
-"::error file=test/fixtures/bad-file.html.erb,line=1,col=1,title=html-tag-name-lowercase • @herb-tools/linter@0.7.0::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/bad-file.html.erb:1:1%0A%0A → 1 │ Bad file%0A │ ~~~~%0A 2 │%0A
+"::error file=test/fixtures/bad-file.html.erb,line=1,col=1,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/bad-file.html.erb:1:1%0A%0A → 1 │ Bad file%0A │ ~~~~%0A 2 │%0A
-::error file=test/fixtures/bad-file.html.erb,line=1,col=16,title=html-tag-name-lowercase • @herb-tools/linter@0.7.0::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/bad-file.html.erb:1:16%0A%0A → 1 │ Bad file%0A │ ~~~~%0A 2 │%0A
+::error file=test/fixtures/bad-file.html.erb,line=1,col=16,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/bad-file.html.erb:1:16%0A%0A → 1 │ Bad file%0A │ ~~~~%0A 2 │%0A
[error] Opening tag name \`\` should be lowercase. Use \`\` instead. (html-tag-name-lowercase)
@@ -431,11 +431,11 @@ exports[`CLI Output Formatting > formats GitHub Actions output correctly for cle
`;
exports[`CLI Output Formatting > formats GitHub Actions output correctly for file with errors 1`] = `
-"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.0::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A 5 │%0A
+"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A 5 │%0A
-::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.0::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
+::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
-::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.0::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
+::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
[error] Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. (html-img-require-alt)
@@ -773,11 +773,11 @@ test/fixtures/bad-file.html.erb:1:16
`;
exports[`CLI Output Formatting > uses GitHub Actions format by default when GITHUB_ACTIONS is true 1`] = `
-"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.0::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A 5 │%0A
+"::error file=test/fixtures/test-file-with-errors.html.erb,line=3,col=3,title=html-img-require-alt • @herb-tools/linter@0.7.4::Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. [html-img-require-alt]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:3:3%0A%0A 1 │
%0A 2 │ Test content%0A → 3 │ %0A │ ~~~%0A 4 │
%0A 5 │%0A
-::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.0::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
+::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=3,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Opening tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:3%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
-::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.0::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
+::error file=test/fixtures/test-file-with-errors.html.erb,line=2,col=22,title=html-tag-name-lowercase • @herb-tools/linter@0.7.4::Closing tag name \`\` should be lowercase. Use \`\` instead. [html-tag-name-lowercase]%0A%0A%0Atest/fixtures/test-file-with-errors.html.erb:2:22%0A%0A 1 │
%0A → 2 │ Test content%0A │ ~~~~%0A 3 │ %0A 4 │
%0A
[error] Missing required \`alt\` attribute on \`\` tag. Add \`alt=""\` for decorative images or \`alt="description"\` for informative images. (html-img-require-alt)
diff --git a/javascript/packages/linter/test/rules/html-no-empty-attributes.test.ts b/javascript/packages/linter/test/rules/html-no-empty-attributes.test.ts
index bc875ff59..6a71543b4 100644
--- a/javascript/packages/linter/test/rules/html-no-empty-attributes.test.ts
+++ b/javascript/packages/linter/test/rules/html-no-empty-attributes.test.ts
@@ -132,9 +132,9 @@ describe("html-no-empty-attributes", () => {
expect(lintResult.errors).toBe(0)
expect(lintResult.warnings).toBe(2)
- expect(lintResult.offenses[0].message).toBe('Attribute `data-value` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expect(lintResult.offenses[0].message).toBe('Data attribute `data-value` should not have an empty value. Either provide a meaningful value or use `data-value` instead of `data-value=""`.')
expect(lintResult.offenses[0].severity).toBe("warning")
- expect(lintResult.offenses[1].message).toBe('Attribute `data-config` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expect(lintResult.offenses[1].message).toBe('Data attribute `data-config` should not have an empty value. Either provide a meaningful value or use `data-config` instead of `data-config=""`.')
expect(lintResult.offenses[1].severity).toBe("warning")
})
@@ -179,7 +179,7 @@ describe("html-no-empty-attributes", () => {
expect(lintResult.offenses[0].severity).toBe("warning")
expect(lintResult.offenses[1].message).toBe('Attribute `class` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
expect(lintResult.offenses[1].severity).toBe("warning")
- expect(lintResult.offenses[2].message).toBe('Attribute `data-test` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expect(lintResult.offenses[2].message).toBe('Data attribute `data-test` should not have an empty value. Either provide a meaningful value or use `data-test` instead of `data-test=""`.')
expect(lintResult.offenses[2].severity).toBe("warning")
})
@@ -226,7 +226,7 @@ describe("html-no-empty-attributes", () => {
expect(lintResult.errors).toBe(0)
expect(lintResult.warnings).toBe(1)
- expect(lintResult.offenses[0].message).toBe('Attribute `data-<%= key %>` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expect(lintResult.offenses[0].message).toBe('Data attribute `data-<%= key %>` should not have an empty value. Either provide a meaningful value or use `data-<%= key %>` instead of `data-<%= key %>=""`.')
expect(lintResult.offenses[0].severity).toBe("warning")
})
@@ -238,7 +238,7 @@ describe("html-no-empty-attributes", () => {
expect(lintResult.errors).toBe(0)
expect(lintResult.warnings).toBe(1)
- expect(lintResult.offenses[0].message).toBe('Attribute `data-<%= key %>-id` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expect(lintResult.offenses[0].message).toBe('Data attribute `data-<%= key %>-id` should not have an empty value. Either provide a meaningful value or use `data-<%= key %>-id` instead of `data-<%= key %>-id=""`.')
expect(lintResult.offenses[0].severity).toBe("warning")
})
@@ -250,7 +250,7 @@ describe("html-no-empty-attributes", () => {
expect(lintResult.errors).toBe(0)
expect(lintResult.warnings).toBe(1)
- expect(lintResult.offenses[0].message).toBe('Attribute `data-<%= key %>` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expect(lintResult.offenses[0].message).toBe('Data attribute `data-<%= key %>` should not have an empty value. Either provide a meaningful value or use `data-<%= key %>` instead of `data-<%= key %>=" "`.')
expect(lintResult.offenses[0].severity).toBe("warning")
})
@@ -296,4 +296,67 @@ describe("html-no-empty-attributes", () => {
expect(lintResult.errors).toBe(0)
expect(lintResult.warnings).toBe(0)
})
+
+ test("passes for data-* attributes without explicit values", () => {
+ const html = ''
+
+ const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
+ const lintResult = linter.lint(html)
+
+ expect(lintResult.errors).toBe(0)
+ expect(lintResult.warnings).toBe(0)
+ expect(lintResult.offenses).toHaveLength(0)
+ })
+
+ test("fails for data-* attributes with explicit empty string values", () => {
+ const html = ''
+
+ const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
+ const lintResult = linter.lint(html)
+
+ expect(lintResult.errors).toBe(0)
+ expect(lintResult.warnings).toBe(2)
+
+ expect(lintResult.offenses[0].message).toBe('Data attribute `data-test` should not have an empty value. Either provide a meaningful value or use `data-test` instead of `data-test=""`.')
+ expect(lintResult.offenses[0].severity).toBe("warning")
+ expect(lintResult.offenses[1].message).toBe('Data attribute `data-value` should not have an empty value. Either provide a meaningful value or use `data-value` instead of `data-value=""`.')
+ expect(lintResult.offenses[1].severity).toBe("warning")
+ })
+
+ test("mixed data attributes: passes for implicit values, fails for explicit empty values", () => {
+ const html = ''
+
+ const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
+ const lintResult = linter.lint(html)
+
+ expect(lintResult.errors).toBe(0)
+ expect(lintResult.warnings).toBe(1)
+
+ expect(lintResult.offenses[0].message).toBe('Data attribute `data-config` should not have an empty value. Either provide a meaningful value or use `data-config` instead of `data-config=""`.')
+ expect(lintResult.offenses[0].severity).toBe("warning")
+ })
+
+ test("passes for data-turbo-permanent without value", () => {
+ const html = '
Content
'
+
+ const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
+ const lintResult = linter.lint(html)
+
+ expect(lintResult.errors).toBe(0)
+ expect(lintResult.warnings).toBe(0)
+ expect(lintResult.offenses).toHaveLength(0)
+ })
+
+ test("fails for data-turbo-permanent with explicit empty value", () => {
+ const html = '
+ <% end %>
+ HTML
+ end
+
+ test "guard clause with break if modifier" do
+ assert_parsed_snapshot(<<~HTML)
+ <% loop do %>
+ <% break if condition %>
+
Loop content
+ <% end %>
+ HTML
+ end
end
end
diff --git a/test/analyze/unless_test.rb b/test/analyze/unless_test.rb
index 4a53eff97..8bc0628fb 100644
--- a/test/analyze/unless_test.rb
+++ b/test/analyze/unless_test.rb
@@ -65,5 +65,56 @@ class UnlessTest < Minitest::Spec
<% end %>
HTML
end
+
+ test "guard clause with unless modifier should not be parsed as ERBUnlessNode" do
+ assert_parsed_snapshot(<<~HTML)
+ <% items.each do |item| %>
+ <% next unless item.visible? %>
+
<%= item.name %>
+ <% end %>
+ HTML
+ end
+
+ test "guard clause with return unless modifier" do
+ assert_parsed_snapshot(<<~HTML)
+ <% def some_method %>
+ <% return unless condition %>
+
This will render
+ <% end %>
+ HTML
+ end
+
+ test "guard clause with break unless modifier" do
+ assert_parsed_snapshot(<<~HTML)
+ <% loop do %>
+ <% break unless continue? %>
+
Loop content
+ <% end %>
+ HTML
+ end
+
+ test "multiple unless guard clauses" do
+ assert_parsed_snapshot(<<~HTML)
+ <% items.each do |item| %>
+ <% next unless item %>
+ <% next unless item.active? %>
+ <% next unless item.published? %>
+
<%= item.title %>
+ <% end %>
+ HTML
+ end
+
+ test "distinguishes between block unless and modifier unless" do
+ assert_parsed_snapshot(<<~HTML)
+ <% items.each do |item| %>
+ <% next unless item.visible? %>
+ <% unless item.special? %>
+
<%= item.name %>
+ <% else %>
+
<%= item.name %>
+ <% end %>
+ <% end %>
+ HTML
+ end
end
end
diff --git a/test/c/test_herb.c b/test/c/test_herb.c
index 18a1111da..7eab745f0 100644
--- a/test/c/test_herb.c
+++ b/test/c/test_herb.c
@@ -2,7 +2,7 @@
#include "../../src/include/herb.h"
TEST(test_herb_version)
- ck_assert_str_eq(herb_version(), "0.7.0");
+ ck_assert_str_eq(herb_version(), "0.7.4");
END
TCase *herb_tests(void) {
diff --git a/test/engine/error_handling_test.rb b/test/engine/error_handling_test.rb
index 94fd8e77d..96398009c 100644
--- a/test/engine/error_handling_test.rb
+++ b/test/engine/error_handling_test.rb
@@ -312,5 +312,17 @@ class ErrorHandlingTest < Minitest::Spec
assert_instance_of Herb::Engine, engine
assert_instance_of String, engine.src
end
+
+ test "ruby comment at end of ERB content tag" do
+ template = <<~ERB
+ <% if true # some comment %> true <% else %> false <% end %>
+ ERB
+
+ begin
+ Herb::Engine.new(template)
+ rescue Herb::Engine::CompilationError => e
+ assert_includes e.message, "unexpected_token_close_context: unexpected end-of-input, assuming it is closing the parent top level context"
+ end
+ end
end
end
diff --git a/test/engine/evaluation_test.rb b/test/engine/evaluation_test.rb
index 712d7c29c..41a328211 100644
--- a/test/engine/evaluation_test.rb
+++ b/test/engine/evaluation_test.rb
@@ -377,5 +377,26 @@ class EvaluationTest < Minitest::Spec
posts: posts,
}, { escape: false })
end
+
+ test "utf8 handling" do
+ template = <<~ERB
+ Sitename • Title
+
+ <% @title = "Home" %>
+
+ <%= [@title, "Sitename"].compact.join(" • ") %>
+ ERB
+
+ assert_evaluated_snapshot(template, {}, { escape: false })
+ end
+
+ test "comment before content" do
+ template = <<~ERB
+ <% # This file contains a comment before other content %>
+
Hey there
+ ERB
+
+ assert_evaluated_snapshot(template, {}, { escape: false })
+ end
end
end
diff --git a/test/engine_visitors_test.rb b/test/engine_visitors_test.rb
new file mode 100644
index 000000000..9292dfbc8
--- /dev/null
+++ b/test/engine_visitors_test.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class EngineVisitorsTest < Minitest::Spec
+ test "engine works without any visitors" do
+ html = "
'.freeze;\n_buf.to_s\n"
+ assert_equal expected, engine.src
+ end
+
+ test "debug visitor can still be used explicitly" do
+ html = "
Debug test
"
+
+ debug_visitor = Herb::Engine::DebugVisitor.new(
+ file_path: "test.html.erb",
+ project_path: "/project"
+ )
+
+ visitors = [debug_visitor]
+
+ engine = Herb::Engine.new(html, visitors: visitors, debug: false)
+
+ refute_nil engine.src
+ end
+end
diff --git a/test/herb_test.rb b/test/herb_test.rb
new file mode 100644
index 000000000..866423e31
--- /dev/null
+++ b/test/herb_test.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+class HerbTest < Minitest::Spec
+ test "version" do
+ assert_equal "herb gem v0.7.4, libprism v1.5.1, libherb v0.7.4 (Ruby C native extension)", Herb.version
+ end
+end
diff --git a/test/parser/attributes_test.rb b/test/parser/attributes_test.rb
index acf9f1583..789d032a2 100644
--- a/test/parser/attributes_test.rb
+++ b/test/parser/attributes_test.rb
@@ -159,5 +159,41 @@ class AttributesTest < Minitest::Spec
test "attribute with backtick containing HTML (invalid)" do
assert_parsed_snapshot(%(
Hello`>
))
end
+
+ test "Vue-style directive attribute with value" do
+ assert_parsed_snapshot(%())
+ end
+
+ test "Vue-style directive attributes multiple" do
+ assert_parsed_snapshot(%())
+ end
+
+ test "Vue-style directive attribute without value" do
+ assert_parsed_snapshot(%())
+ end
+
+ test "Mixed Vue directives and regular attributes" do
+ assert_parsed_snapshot(%())
+ end
+
+ test "Standalone colon with space is invalid" do
+ assert_parsed_snapshot(%())
+ end
+
+ test "Colon immediately followed by attribute name is valid" do
+ assert_parsed_snapshot(%())
+ end
+
+ test "Double colon is invalid" do
+ assert_parsed_snapshot(%())
+ end
+
+ test "Vue directive with namespace-like syntax" do
+ assert_parsed_snapshot(%())
+ end
+
+ test "Empty attribute value with closing bracket immediatly following it" do
+ assert_parsed_snapshot(%(