'
-
- const linter = new Linter(Herb, [HTMLNoDuplicateAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`
'
-
- const linter = new Linter(Herb, [HTMLNoDuplicateAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].message).toBe('Duplicate attribute `src` found on tag. Remove the duplicate occurrence.')
+ expectError('Duplicate attribute `src` found on tag. Remove the duplicate occurrence.')
+ assertOffenses(`
`)
})
test("handles ERB templates with attributes", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoDuplicateAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`
`)
})
test("ignores closing tags", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoDuplicateAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`
`)
})
})
diff --git a/javascript/packages/linter/test/rules/html-no-duplicate-ids.test.ts b/javascript/packages/linter/test/rules/html-no-duplicate-ids.test.ts
index ca25f3b61..573b89f45 100644
--- a/javascript/packages/linter/test/rules/html-no-duplicate-ids.test.ts
+++ b/javascript/packages/linter/test/rules/html-no-duplicate-ids.test.ts
@@ -1,127 +1,66 @@
import dedent from "dedent"
-import { Herb } from "@herb-tools/node-wasm";
-import { beforeAll, describe, expect, test } from "vitest";
-import { Linter } from "../../src/linter.js";
-import { HTMLNoDuplicateIdsRule } from "../../src/rules/html-no-duplicate-ids.js";
+import { describe, test } from "vitest"
+import { HTMLNoDuplicateIdsRule } from "../../src/rules/html-no-duplicate-ids.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
-describe("html-no-duplicate-ids", () => {
- beforeAll(async () => {
- await Herb.load();
- })
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLNoDuplicateIdsRule)
+describe("html-no-duplicate-ids", () => {
test("passes for unique IDs", () => {
- const html = '
';
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ expectNoOffenses(`
`)
})
test("fails for duplicate IDs", () => {
- const html = '
';
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].rule).toBe("html-no-duplicate-ids");
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `duplicate` found. IDs must be unique within a document.');
- expect(lintResult.offenses[0].severity).toBe("error");
+ expectError('Duplicate ID `duplicate` found. IDs must be unique within a document.')
+ assertOffenses(`
`)
})
test("passes for missing IDs", () => {
- const html = '
';
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ expectNoOffenses(`
`)
})
test("passes for IDs without value", () => {
- const html = '
';
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ expectNoOffenses(`
`)
})
test("passes for other attributes with equal value", () => {
- const html = '
';
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ expectNoOffenses(`
`)
})
test("passes when using ERB in ID", () => {
- const html = '
';
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ expectNoOffenses(`
`)
})
// TODO: this should also warn if it's in the same "context"
test.todo("fails for multiple duplicate IDs in ERB in the same context", () => {
- const html = '
';
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `<%= user.id %>` found. IDs must be unique within a document.');
+ expectError('Duplicate ID `<%= user.id %>` found. IDs must be unique within a document.')
+ assertOffenses(`
`)
})
test("passes for IDs in mutually exclusive if/else branches", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% if some_condition? %>
content1
<% else %>
content2
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("passes for IDs in mutually exclusive unless/else branches", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% unless some_condition? %>
content1
<% else %>
content2
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("fails for IDs in mutually exclusive unless/else branches and global", () => {
- const html = dedent`
+ expectError('Duplicate ID `my-id` found. IDs must be unique within a document.')
+ expectError('Duplicate ID `my-id` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
content
<% unless some_condition? %>
@@ -129,21 +68,11 @@ describe("html-no-duplicate-ids", () => {
<% else %>
content2
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(2);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(2);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `my-id` found. IDs must be unique within a document.');
- expect(lintResult.offenses[1].message).toBe('Duplicate ID `my-id` found. IDs must be unique within a document.');
+ `)
})
test("passes for IDs in mutually exclusive case/when branches", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% case status %>
<% when 'active' %>
Active
@@ -152,18 +81,14 @@ describe("html-no-duplicate-ids", () => {
<% else %>
Unknown
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("fails for IDs in mutually exclusive case/when branches and global", () => {
- const html = dedent`
+ expectError('Duplicate ID `status-indicator` found. IDs must be unique within a document.')
+ expectError('Duplicate ID `status-indicator` found. IDs must be unique within a document.')
+ expectError('Duplicate ID `status-indicator` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
Active
<% case status %>
@@ -174,40 +99,22 @@ describe("html-no-duplicate-ids", () => {
<% else %>
Unknown
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(3);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(3);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `status-indicator` found. IDs must be unique within a document.');
- expect(lintResult.offenses[1].message).toBe('Duplicate ID `status-indicator` found. IDs must be unique within a document.');
- expect(lintResult.offenses[2].message).toBe('Duplicate ID `status-indicator` found. IDs must be unique within a document.');
+ `)
})
test("fails for duplicate IDs within same control flow branch", () => {
- const html = dedent`
+ expectError('Duplicate ID `duplicate-in-branch` found within the same control flow branch. IDs must be unique within the same control flow branch.')
+ assertOffenses(dedent`
<% if some_condition? %>
content1
content2
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `duplicate-in-branch` found within the same control flow branch. IDs must be unique within the same control flow branch.');
+ `)
})
test("fails for IDs duplicated outside of control flow", () => {
- const html = dedent`
+ expectError('Duplicate ID `global-duplicate` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
outside
<% if some_condition? %>
@@ -215,58 +122,34 @@ describe("html-no-duplicate-ids", () => {
<% end %>
outside again
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `global-duplicate` found. IDs must be unique within a document.');
+ `)
})
test("fails for IDs duplicated outside before control flow", () => {
- const html = dedent`
+ expectError('Duplicate ID `global-duplicate` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
outside
<% if some_condition? %>
inside branch
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `global-duplicate` found. IDs must be unique within a document.');
+ `)
})
test("fails for IDs duplicated outside after control flow", () => {
- const html = dedent`
+ expectError('Duplicate ID `global-duplicate` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
<% if some_condition? %>
inside branch
<% end %>
outside
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `global-duplicate` found. IDs must be unique within a document.');
+ `)
})
test("fails for IDs duplicated outside after control flow", () => {
- const html = dedent`
+ expectError('Duplicate ID `global-duplicate` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
<% if some_condition? %>
<% elsif another_condition? %>
@@ -276,20 +159,12 @@ describe("html-no-duplicate-ids", () => {
<% end %>
outside
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `global-duplicate` found. IDs must be unique within a document.');
+ `)
})
test("fails for IDs duplicated outside after control flow", () => {
- const html = dedent`
+ expectError('Duplicate ID `global-duplicate` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
<% if some_condition? %>
<% elsif other_condition? %>
@@ -297,20 +172,11 @@ describe("html-no-duplicate-ids", () => {
<% end %>
outside
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `global-duplicate` found. IDs must be unique within a document.');
+ `)
})
test("passes for IDs duplicated in elsif and else", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% if some_condition? %>
<% elsif other_condition? %>
@@ -318,18 +184,11 @@ describe("html-no-duplicate-ids", () => {
<% else other_condition? %>
inside branch
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("handles nested control flow properly", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% if outer_condition? %>
<% if inner_condition? %>
inner true
@@ -339,33 +198,20 @@ describe("html-no-duplicate-ids", () => {
<% else %>
outer false
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("passes for ID tag.div (ERBBlockNode)", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% tag.div do %>
User
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("passes for output ERB IDs in loops (unique per iteration)", () => {
- const html = dedent`
+ expectError('Duplicate ID `user-<%= user.id %>` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
<% users.each do |user| %>
User
<% end %>
@@ -373,38 +219,21 @@ describe("html-no-duplicate-ids", () => {
<% users.each do |user| %>
User again
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `user-<%= user.id %>` found. IDs must be unique within a document.');
+ `)
})
test("fails for non-output ERB IDs in loops (same value repeated)", () => {
- const html = dedent`
+ expectError('Duplicate ID `user-` found within the same control flow branch. IDs must be unique within the same control flow branch.')
+ assertOffenses(dedent`
<% users.each do |user| %>
User
Duplicate
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `user-` found within the same control flow branch. IDs must be unique within the same control flow branch.');
+ `)
})
test("passes for output ERB IDs in while loops", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% counter = 0 %>
<% count = 0 %>
@@ -414,156 +243,82 @@ describe("html-no-duplicate-ids", () => {
Post
<% counter += 1 %>
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("fails for static ID in while loops", () => {
- const html = dedent`
+ expectError('Duplicate ID `static-id` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
<% while condition %>
Item
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `static-id` found. IDs must be unique within a document.');
+ `)
})
test("fails for non-dynamic ID in until loops", () => {
- const html = dedent`
+ expectError('Duplicate ID `static-id` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
<% until condition %>
Item
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `static-id` found. IDs must be unique within a document.');
+ `)
})
test("handles output ERB IDs in conditional flow normally", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% if condition %>
User A
<% else %>
User B
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("fails for duplicate output ERB IDs within same conditional branch", () => {
- const html = dedent`
+ expectError('Duplicate ID `user-<%= user.id %>` found within the same control flow branch. IDs must be unique within the same control flow branch.')
+ assertOffenses(dedent`
<% if condition %>
User A
User A duplicate
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `user-<%= user.id %>` found within the same control flow branch. IDs must be unique within the same control flow branch.');
+ `)
})
test("passes for static ID conflicting with dynamic ID prefix", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
Static
Dynamic
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("passes for dynamic ID conflicting with existing static ID", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
Dynamic
Static
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test("passes for non-conflicting static and dynamic IDs", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
Static
Dynamic
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
test.todo("fails for static attribute in a loop context", () => {
- const html = dedent`
+ expectError('Duplicate ID `user` found. IDs must be unique within a document.')
+ assertOffenses(dedent`
<% @users.each do |user| %>
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(1);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(1);
-
- expect(lintResult.offenses[0].message).toBe('Duplicate ID `user` found. IDs must be unique within a document.');
+ `)
})
test("passes for dynamic attribute in a ERBBlockNode each context", () => {
- const html = dedent`
+ expectNoOffenses(dedent`
<% @users.each do |user| %>
<% end %>
- `
-
- const linter = new Linter(Herb, [HTMLNoDuplicateIdsRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.warnings).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
+ `)
})
})
diff --git a/javascript/packages/linter/test/rules/html-no-duplicate-meta-names.test.ts b/javascript/packages/linter/test/rules/html-no-duplicate-meta-names.test.ts
new file mode 100644
index 000000000..14e5c2b41
--- /dev/null
+++ b/javascript/packages/linter/test/rules/html-no-duplicate-meta-names.test.ts
@@ -0,0 +1,552 @@
+import dedent from "dedent"
+
+import { describe, test } from "vitest"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
+
+import { HTMLNoDuplicateMetaNamesRule } from "../../src/rules/html-no-duplicate-meta-names.js"
+
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLNoDuplicateMetaNamesRule)
+
+describe("html-no-duplicate-meta-names", () => {
+ test("passes when meta names are unique", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("passes when http-equiv values are unique", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("passes when mixing name and http-equiv attributes", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("fails when meta names are duplicated", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("fails when http-equiv values are duplicated", () => {
+ expectError('Duplicate `
` tag with `http-equiv="X-UA-Compatible"`. `http-equiv` values should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("handles case insensitive duplicates", () => {
+ expectError('Duplicate `
` tag with `name="description"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("fails with multiple duplicates", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+ expectError('Duplicate `
` tag with `name="description"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("ignores meta tags without name or http-equiv attributes", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("only checks meta tags inside head", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("works with ERB templates", () => {
+ expectError('Duplicate `
` tag with `name="description"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("handles self-closing meta tags", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("handles mixed name and http-equiv duplicates", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+ expectError('Duplicate `
` tag with `http-equiv="refresh"`. `http-equiv` values should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+
+
+
+
Welcome
+
+
+ `)
+ })
+
+ test("handles erb conditionals", () => {
+ expectNoOffenses(dedent`
+
+ <% if mobile? %>
+
+ <% elsif hotwire_native_app? %>
+
+ <% else %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("detects duplicates when meta tags are outside and inside erb conditionals", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+ <% if mobile? %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("detects duplicates between global meta tag and erb else branch", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+ <% if mobile? %>
+
+ <% else %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("detects duplicates when meta tag is outside erb conditional block", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+ <% if mobile? %>
+
+ <% else %>
+
+ <% end %>
+
+
+
+ `)
+ })
+
+ test("handles nested erb conditionals", () => {
+ expectNoOffenses(dedent`
+
+ <% if mobile? %>
+ <% if ios? %>
+
+ <% else %>
+
+ <% end %>
+ <% end %>
+
+ `)
+ })
+
+ test.todo("detects duplicates in nested conditionals within same execution path", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the same control flow branch.')
+
+ assertOffenses(dedent`
+
+ <% if mobile? %>
+
+ <% if ios? %>
+
+ <% end %>
+ <% end %>
+
+ `)
+ })
+
+ test("detects static duplicate meta tags in loops", () => {
+ expectError('Duplicate `
` tag with `name="viewport"` within the same control flow branch. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+ <% items.each do |item| %>
+
+
+ <% end %>
+
+ `)
+ })
+
+ test("allows dynamic meta tags in loops", () => {
+ expectNoOffenses(dedent`
+
+ <% keywords.each do |keyword| %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("detects duplicate in loop and outside loop", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+ <% items.each do |item| %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("resets checking for each head tag", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+
+
First document
+
+
+
+
+
+
+
+
Second document
+
+
+ `)
+ })
+
+ test("detects duplicates within each head tag separately", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+ expectError('Duplicate `
` tag with `name="description"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+
+
+
+
+
+
+
+ `)
+ })
+
+ test("handles meta tags with whitespace in attribute values", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+ `)
+ })
+
+ test("treats empty name attributes as different from missing", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+
+
+ `)
+ })
+
+ test("detects duplicates in elsif branches", () => {
+ expectError('Duplicate `
` tag with `name="viewport"` within the same control flow branch. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+ <% if mobile? %>
+
+ <% elsif tablet? %>
+
+
+ <% else %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("allows same meta across if and elsif branches", () => {
+ expectNoOffenses(dedent`
+
+ <% if mobile? %>
+
+ <% elsif tablet? %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("handles conditional inside loop", () => {
+ expectNoOffenses(dedent`
+
+ <% items.each do |item| %>
+ <% if item.mobile? %>
+
+ <% end %>
+ <% end %>
+
+ `)
+ })
+
+ test.todo("detects static duplicate in conditional inside loop", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the same loop iteration.')
+
+ assertOffenses(dedent`
+
+ <% items.each do |item| %>
+ <% if item.mobile? %>
+
+ <% else %>
+
+ <% end %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("handles http-equiv case insensitivity", () => {
+ expectError('Duplicate `
` tag with `http-equiv="x-ua-compatible"`. `http-equiv` values should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+ `)
+ })
+
+ test("detects all duplicate occurrences when there are three or more", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+
+ `)
+ })
+
+ test("handles ERB output in name attribute", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+ `)
+ })
+
+ test.todo("detects duplicates with partial ERB in name", () => {
+ expectError('Duplicate `
` tag with `name="prefix-viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+ `)
+ })
+
+ test("handles unless conditionals", () => {
+ expectNoOffenses(dedent`
+
+ <% unless mobile? %>
+
+ <% else %>
+
+ <% end %>
+
+ `)
+ })
+
+ test("detects duplicates in unless branch", () => {
+ expectError('Duplicate `
` tag with `name="viewport"` within the same control flow branch. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+ <% unless mobile? %>
+
+
+ <% end %>
+
+ `)
+ })
+
+ test("handles document without head tag", () => {
+ expectNoOffenses(dedent`
+
+
+
+
+
+
+ `)
+ })
+
+ test("detects duplicate when meta has both name and http-equiv (edge case)", () => {
+ expectError('Duplicate `
` tag with `name="viewport"`. Meta names should be unique within the `` section.')
+
+ assertOffenses(dedent`
+
+
+
+
+ `)
+ })
+
+ test("allows same meta in different loop iterations with conditionals", () => {
+ expectNoOffenses(dedent`
+
+ <% items.each do |item| %>
+ <% if item.active? %>
+
+ <% end %>
+ <% end %>
+
+ `)
+ })
+})
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 6a71543b4..7a4d17d21 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
@@ -1,362 +1,146 @@
-import { describe, test, expect, beforeAll } from "vitest"
-import { Herb } from "@herb-tools/node-wasm"
-import { Linter } from "../../src/linter.js"
+import { describe, test } from "vitest"
import { HTMLNoEmptyAttributesRule } from "../../src/rules/html-no-empty-attributes.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
-describe("html-no-empty-attributes", () => {
- beforeAll(async () => {
- await Herb.load()
- })
+const { expectNoOffenses, expectWarning, assertOffenses } = createLinterTest(HTMLNoEmptyAttributesRule)
+describe("html-no-empty-attributes", () => {
test("passes for attributes with meaningful 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)
+ expectNoOffenses(``)
})
test("fails for empty id attribute", () => {
- 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('Attribute `id` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `id` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
`)
})
test("fails for empty class attribute", () => {
- 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('Attribute `class` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `class` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
`)
})
test("fails for empty name attribute", () => {
- 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('Attribute `name` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `name` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
`)
})
test("fails for empty for attribute", () => {
- const html = '
Label '
-
- 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('Attribute `for` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `for` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
Label `)
})
test("fails for empty src attribute", () => {
- 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('Attribute `src` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `src` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
`)
})
test("fails for empty href attribute", () => {
- const html = '
Link '
-
- 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('Attribute `href` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `href` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
Link `)
})
test("fails for empty title attribute", () => {
- const html = '
Link '
-
- 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('Attribute `title` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `title` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
Link `)
})
test("fails for empty data attribute", () => {
- 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('Attribute `data` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `data` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
`)
})
test("fails for data-* attributes with empty 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-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('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")
+ expectWarning('Data attribute `data-value` should not have an empty value. Either provide a meaningful value or use `data-value` instead of `data-value=""`.')
+ expectWarning('Data attribute `data-config` should not have an empty value. Either provide a meaningful value or use `data-config` instead of `data-config=""`.')
+ assertOffenses(`
`)
})
test("fails for aria-* attributes with empty values", () => {
- const html = '
Button '
-
- 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('Attribute `aria-label` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
- expect(lintResult.offenses[1].message).toBe('Attribute `aria-describedby` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[1].severity).toBe("warning")
+ expectWarning('Attribute `aria-label` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expectWarning('Attribute `aria-describedby` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
Button `)
})
test("fails for role attribute with empty value", () => {
- 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('Attribute `role` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `role` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
`)
})
test("fails for multiple empty attributes", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(3)
-
- expect(lintResult.offenses[0].message).toBe('Attribute `id` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- 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('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")
+ expectWarning('Attribute `id` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expectWarning('Attribute `class` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expectWarning('Data attribute `data-test` should not have an empty value. Either provide a meaningful value or use `data-test` instead of `data-test=""`.')
+ assertOffenses(`
`)
})
test("fails for whitespace-only 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('Attribute `id` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- 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")
+ expectWarning('Attribute `id` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expectWarning('Attribute `class` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
`)
})
test("passes for attributes with ERB output", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`
`)
})
test("passes for mixed static and ERB content", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`
`)
})
test("fails for dynamic data-* attribute name with empty value", () => {
- 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-<%= 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")
+ expectWarning('Data attribute `data-<%= key %>` should not have an empty value. Either provide a meaningful value or use `data-<%= key %>` instead of `data-<%= key %>=""`.')
+ assertOffenses(`
="">
`)
})
test("fails for dynamic data-* composite name with empty value", () => {
- const html = '
-id="">
'
-
- 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-<%= 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")
+ expectWarning('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=""`.')
+ assertOffenses(`
-id="">
`)
})
test("fails for dynamic data-* attribute name with whitespace-only value", () => {
- 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-<%= 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")
+ expectWarning('Data attribute `data-<%= key %>` should not have an empty value. Either provide a meaningful value or use `data-<%= key %>` instead of `data-<%= key %>=" "`.')
+ assertOffenses(`
=" ">
`)
})
test("fails for dynamic aria-* attribute name with empty value", () => {
- 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('Attribute `aria-<%= prop %>` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Attribute `aria-<%= prop %>` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
=""> `)
})
test("passes for dynamic attribute name with ERB value", () => {
- const html = '
="<%= value %>">
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`
="<%= value %>">
`)
})
test("fails for attribute with ERB that doesn't output anything", () => {
- 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('Attribute `class` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ expectWarning('Attribute `class` must not be empty. Either provide a meaningful value or remove the attribute entirely.')
+ assertOffenses(`
`)
})
test("passes for attribute with static value and ERB that doesn't output anything", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyAttributesRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`
`)
})
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)
+ expectNoOffenses(`
`)
})
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")
+ expectWarning('Data attribute `data-test` should not have an empty value. Either provide a meaningful value or use `data-test` instead of `data-test=""`.')
+ expectWarning('Data attribute `data-value` should not have an empty value. Either provide a meaningful value or use `data-value` instead of `data-value=""`.')
+ assertOffenses(`
`)
})
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")
+ expectWarning('Data attribute `data-config` should not have an empty value. Either provide a meaningful value or use `data-config` instead of `data-config=""`.')
+ assertOffenses(`
`)
})
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)
+ expectNoOffenses(`
Content
`)
})
test("fails for data-turbo-permanent with explicit empty value", () => {
- const html = '
Content
'
-
- 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-turbo-permanent` should not have an empty value. Either provide a meaningful value or use `data-turbo-permanent` instead of `data-turbo-permanent=""`.')
- expect(lintResult.offenses[0].severity).toBe("warning")
+ expectWarning('Data attribute `data-turbo-permanent` should not have an empty value. Either provide a meaningful value or use `data-turbo-permanent` instead of `data-turbo-permanent=""`.')
+ assertOffenses(`
Content
`)
})
})
diff --git a/javascript/packages/linter/test/rules/html-no-empty-headings.test.ts b/javascript/packages/linter/test/rules/html-no-empty-headings.test.ts
index a27e3e4ed..b6aeca11a 100644
--- a/javascript/packages/linter/test/rules/html-no-empty-headings.test.ts
+++ b/javascript/packages/linter/test/rules/html-no-empty-headings.test.ts
@@ -1,299 +1,138 @@
-import { describe, test, expect, beforeAll } from "vitest"
-import { Herb } from "@herb-tools/node-wasm"
-import { Linter } from "../../src/linter.js"
+import { describe, test } from "vitest"
import { HTMLNoEmptyHeadingsRule } from "../../src/rules/html-no-empty-headings.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
-describe("html-no-empty-headings", () => {
- beforeAll(async () => {
- await Herb.load()
- })
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLNoEmptyHeadingsRule)
+describe("html-no-empty-headings", () => {
test("passes for heading with text content", () => {
- const html = '
Heading Content '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('
Heading Content ')
})
test("passes for heading with nested elements", () => {
- const html = '
Text '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('
Text ')
})
test("passes for heading with ERB content", () => {
- const html = '
<%= title %> '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('
<%= title %> ')
})
test("fails for empty heading", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
-
- expect(lintResult.offenses[0].rule).toBe("html-no-empty-headings")
- expect(lintResult.offenses[0].message).toBe("Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.")
- expect(lintResult.offenses[0].severity).toBe("error")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+
+ assertOffenses(' ')
})
test("fails for heading with only whitespace", () => {
- const html = ' \n\t '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
- expect(lintResult.offenses[0].rule).toBe("html-no-empty-headings")
- expect(lintResult.offenses[0].message).toBe("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ assertOffenses(' \n\t ')
})
test("fails for self-closing heading", () => {
- const html = ' '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
-
- expect(lintResult.offenses[0].rule).toBe("html-no-empty-headings")
- expect(lintResult.offenses[0].message).toBe("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ assertOffenses(' ')
})
test("handles all heading levels h1-h6", () => {
- const html = ' '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(6)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(6)
-
- expect(lintResult.offenses[0].message).toBe("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
- expect(lintResult.offenses[1].message).toBe("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
- expect(lintResult.offenses[5].message).toBe("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+
+ assertOffenses(' ')
})
test("handles mixed case heading tags", () => {
- const html = ' '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].message).toBe("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+ assertOffenses(' ')
})
test("ignores non-heading tags", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses('
')
})
test("passes for headings with mixed content", () => {
- const html = '
Welcome <%= user.name %>! '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses('
Welcome <%= user.name %>! ')
})
test("passes for heading with only ERB", () => {
- const html = '
<%= page.title %> '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses('
<%= page.title %> ')
})
test("handles multiple empty headings", () => {
- const html = '
Valid '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(2)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(2)
+ expectError("Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+
+ assertOffenses('Valid ')
})
test("passes for div with role='heading' and content", () => {
- const html = ' Heading Content
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('Heading Content
')
})
test("fails for empty div with role='heading'", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
-
- expect(lintResult.offenses[0].rule).toBe("html-no-empty-headings")
- expect(lintResult.offenses[0].message).toBe('Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.')
- expect(lintResult.offenses[0].severity).toBe("error")
+ expectError('Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.')
+
+ assertOffenses('
')
})
test("fails for div with role='heading' containing only whitespace", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
+ expectError('Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.')
- expect(lintResult.offenses[0].message).toBe('Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.')
+ assertOffenses('
')
})
test("fails for self-closing div with role='heading'", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
+ expectError('Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.')
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
-
- expect(lintResult.offenses[0].message).toBe('Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.')
+ assertOffenses('
')
})
test("ignores div without role='heading'", () => {
- const html = '
Button
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('
Button
')
})
test("handles mixed standard headings and ARIA headings", () => {
- const html = '
Valid
Valid
'
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(2)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(2)
+ expectError("Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.")
+ expectError('Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.')
- expect(lintResult.offenses[0].message).toBe("Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.")
- expect(lintResult.offenses[1].message).toBe('Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.')
+ assertOffenses('
Valid
Valid
')
})
test("fails for heading with only aria-hidden content", () => {
- const html = '
Inaccessible text '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
-
- expect(lintResult.offenses[0].rule).toBe("html-no-empty-headings")
- expect(lintResult.offenses[0].message).toBe("Heading element `
` must not be empty. Provide accessible text content for screen readers and SEO.")
- expect(lintResult.offenses[0].severity).toBe("error")
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+
+ assertOffenses('Inaccessible text ')
})
test("fails for heading with mixed accessible and inaccessible content", () => {
- const html = 'Hidden Also hidden '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
+ expectError("Heading element `` must not be empty. Provide accessible text content for screen readers and SEO.")
+
+ assertOffenses('Hidden Also hidden ')
})
test("passes for heading with mix of accessible and inaccessible content", () => {
- const html = 'Visible textHidden text '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('Visible textHidden text ')
})
test("passes for heading itself with aria-hidden='true' but has content", () => {
- const html = 'Heading Content '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('Heading Content ')
})
test("passes for heading itself with hidden attribute but has content", () => {
- const html = 'Heading Content '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('Heading Content ')
+ })
+
+ test("passes for heading with nested span containing text", () => {
+ expectNoOffenses('Text ')
})
test("passes for heading with nested span containing text", () => {
- const html = 'Text '
-
- const linter = new Linter(Herb, [HTMLNoEmptyHeadingsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('<%= content %> ')
})
})
diff --git a/javascript/packages/linter/test/rules/html-no-nested-links.test.ts b/javascript/packages/linter/test/rules/html-no-nested-links.test.ts
index 0af945fe6..ce8c051be 100644
--- a/javascript/packages/linter/test/rules/html-no-nested-links.test.ts
+++ b/javascript/packages/linter/test/rules/html-no-nested-links.test.ts
@@ -1,108 +1,48 @@
-import { describe, test, expect, beforeAll } from "vitest"
-import { Herb } from "@herb-tools/node-wasm"
-import { Linter } from "../../src/linter.js"
+import { describe, test } from "vitest"
import { HTMLNoNestedLinksRule } from "../../src/rules/html-no-nested-links.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
-describe("html-no-nested-links", () => {
- beforeAll(async () => {
- await Herb.load()
- })
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLNoNestedLinksRule)
+describe("html-no-nested-links", () => {
test("passes for separate links", () => {
- const html = 'View products About us '
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses(`View products About us `)
})
test("fails for directly nested links", () => {
- const html = 'View special offer '
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(1)
-
- expect(lintResult.offenses[0].rule).toBe("html-no-nested-links")
- expect(lintResult.offenses[0].message).toBe("Nested `` elements are not allowed. Links cannot contain other links.")
- expect(lintResult.offenses[0].severity).toBe("error")
+ expectError("Nested ` ` elements are not allowed. Links cannot contain other links.")
+ assertOffenses(` View special offer `)
})
test("fails for indirectly nested links", () => {
- const html = ' '
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].message).toBe("Nested `` elements are not allowed. Links cannot contain other links.")
+ expectError("Nested ` ` elements are not allowed. Links cannot contain other links.")
+ assertOffenses(` `)
})
test("passes for links in different containers", () => {
- const html = ''
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(``)
})
test("fails for multiple levels of nesting", () => {
- const html = 'Deep nesting '
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(2) // Second and third links are nested
- expect(lintResult.warnings).toBe(0)
+ expectError("Nested `` elements are not allowed. Links cannot contain other links.")
+ expectError("Nested ` ` elements are not allowed. Links cannot contain other links.")
+ assertOffenses(` Deep nesting `)
})
test("handles mixed case anchor tags", () => {
- const html = 'Nested '
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].message).toBe("Nested `` elements are not allowed. Links cannot contain other links.")
+ expectError("Nested ` ` elements are not allowed. Links cannot contain other links.")
+ assertOffenses(` Nested `)
})
test("passes for links with complex content", () => {
- const html = 'User Name '
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`User Name `)
})
test("handles ERB templates with links", () => {
- const html = ''
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(``)
})
test("fails for nested links in ERB", () => {
- const html = '<%= link_to "Products", products_path do %><%= link_to "Special offer", offer_path %><% end %>'
-
- const linter = new Linter(Herb, [HTMLNoNestedLinksRule])
- const lintResult = linter.lint(html)
-
- // This test might not catch ERB helpers since they're not parsed as HTML elements
- // but we're testing the structure we can detect
- expect(lintResult.errors).toBe(0) // ERB helper calls are not HTML elements
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`<%= link_to "Products", products_path do %><%= link_to "Special offer", offer_path %><% end %>`)
})
})
diff --git a/javascript/packages/linter/test/rules/html-no-positive-tab-index.test.ts b/javascript/packages/linter/test/rules/html-no-positive-tab-index.test.ts
index 3af933582..bd0c56256 100644
--- a/javascript/packages/linter/test/rules/html-no-positive-tab-index.test.ts
+++ b/javascript/packages/linter/test/rules/html-no-positive-tab-index.test.ts
@@ -1,148 +1,71 @@
-import { describe, test, expect, beforeAll } from "vitest"
-import { Herb } from "@herb-tools/node-wasm"
-import { Linter } from "../../src/linter.js"
+import { describe, test } from "vitest"
import { HTMLNoPositiveTabIndexRule } from "../../src/rules/html-no-positive-tab-index.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
-describe("html-no-positive-tab-index", () => {
- beforeAll(async () => {
- await Herb.load()
- })
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLNoPositiveTabIndexRule)
+describe("html-no-positive-tab-index", () => {
test("passes for element without tabindex", () => {
- const html = 'Click me '
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses(`Click me `)
})
test("passes for tabindex=\"0\"", () => {
- const html = ' Focusable div
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`Focusable div
`)
})
test("passes for tabindex=\"-1\"", () => {
- const html = 'Programmatically focusable div
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`Programmatically focusable div
`)
})
test("passes for negative tabindex values", () => {
- const html = 'Skip in tab order
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`Skip in tab order
`)
})
test("fails for tabindex=\"1\"", () => {
- const html = 'Bad tabindex
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].rule).toBe("html-no-positive-tab-index")
- expect(lintResult.offenses[0].message).toBe("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
+ expectError("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
+ assertOffenses(`Bad tabindex
`)
})
test("fails for tabindex=\"2\"", () => {
- const html = 'Bad button '
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].rule).toBe("html-no-positive-tab-index")
+ expectError("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
+ assertOffenses(`Bad button `)
})
test("fails for high positive tabindex", () => {
- const html = ' '
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].rule).toBe("html-no-positive-tab-index")
+ expectError("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
+ assertOffenses(` `)
})
test("handles multiple elements with mixed tabindex values", () => {
- const html = `
+ expectError("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
+ expectError("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
+ assertOffenses(`
OK
Bad
Skip
Also bad
- `
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(2)
- expect(lintResult.offenses).toHaveLength(2)
+ `)
})
test("passes for non-numeric tabindex values", () => {
- const html = 'Should be handled by other rules
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`Should be handled by other rules
`)
})
test("passes for empty tabindex", () => {
- const html = 'Empty value
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`Empty value
`)
})
test("passes for tabindex with ERB content", () => {
- const html = 'Dynamic tabindex
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses(`Dynamic tabindex
`)
})
test("handles mixed case attribute names", () => {
- const html = 'Mixed case
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].message).toBe("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
- expect(lintResult.offenses[0].rule).toBe("html-no-positive-tab-index")
+ expectError("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
+ assertOffenses(`Mixed case
`)
})
test("handles tabindex with leading/trailing spaces", () => {
- const html = 'With spaces
'
-
- const linter = new Linter(Herb, [HTMLNoPositiveTabIndexRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses[0].rule).toBe("html-no-positive-tab-index")
+ expectError("Do not use positive `tabindex` values as they are error prone and can severely disrupt navigation experience for keyboard users. Use `tabindex=\"0\"` to make an element focusable or `tabindex=\"-1\"` to remove it from the tab sequence.")
+ assertOffenses(`With spaces
`)
})
})
diff --git a/javascript/packages/linter/test/rules/html-no-self-closing.test.ts b/javascript/packages/linter/test/rules/html-no-self-closing.test.ts
index c0d648cb5..0f7ce3523 100644
--- a/javascript/packages/linter/test/rules/html-no-self-closing.test.ts
+++ b/javascript/packages/linter/test/rules/html-no-self-closing.test.ts
@@ -1,15 +1,12 @@
-import { Herb } from "@herb-tools/node-wasm";
-import { beforeAll, describe, expect, test } from "vitest";
-import { Linter } from "../../src/linter.js";
-import { HTMLNoSelfClosingRule } from "../../src/rules/html-no-self-closing.js";
+import { describe, test } from "vitest"
+import { HTMLNoSelfClosingRule } from "../../src/rules/html-no-self-closing.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
-describe("html-no-self-closing", () => {
- beforeAll(async () => {
- await Herb.load();
- });
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLNoSelfClosingRule)
+describe("html-no-self-closing", () => {
test("passes for standard HTML tags", () => {
- const html = `
+ expectNoOffenses(`
@@ -18,151 +15,95 @@ describe("html-no-self-closing", () => {
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
- });
+ `)
+ })
test("fails for self-closing non-void elements", () => {
- const html = `
+ expectError('Use `
` instead of self-closing `
` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ expectError('Use `` instead of self-closing `` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ assertOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(5);
- expect(lintResult.offenses).toHaveLength(5);
-
- expect(lintResult.offenses[0].rule).toBe("html-no-self-closing");
- expect(lintResult.offenses[0].severity).toBe("error");
-
- expect(lintResult.offenses[0].message).toBe('Use `
` instead of self-closing `
` for HTML compatibility.')
- expect(lintResult.offenses[1].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- expect(lintResult.offenses[2].message).toBe('Use `` instead of self-closing `` for HTML compatibility.')
- expect(lintResult.offenses[3].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- expect(lintResult.offenses[4].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- });
+ `)
+ })
test("fails for self-closing void elements", () => {
- const html = `
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ assertOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(4);
- expect(lintResult.offenses).toHaveLength(4);
-
- expect(lintResult.offenses[0].rule).toBe("html-no-self-closing");
- expect(lintResult.offenses[0].severity).toBe("error");
-
- expect(lintResult.offenses[0].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- expect(lintResult.offenses[1].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- expect(lintResult.offenses[2].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- expect(lintResult.offenses[3].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- });
+ `)
+ })
test("passes for mixed correct and incorrect tags", () => {
- const html = `
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ assertOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(2);
- expect(lintResult.offenses).toHaveLength(2);
-
- expect(lintResult.offenses[0].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- expect(lintResult.offenses[1].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- });
+ `)
+ })
test("passes for nested non-self-closing tags", () => {
- const html = `
+ expectNoOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
- });
+ `)
+ })
test("fails for nested self-closing tags", () => {
- const html = `
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ expectError('Use `` instead of self-closing `` for HTML compatibility.')
+ assertOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(2);
- expect(lintResult.offenses).toHaveLength(2);
-
- expect(lintResult.offenses[0].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- expect(lintResult.offenses[1].message).toBe('Use `` instead of self-closing `` for HTML compatibility.')
- });
+ `)
+ })
test("passes for custom elements without self-closing", () => {
- const html = `
+ expectNoOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
- });
+ `)
+ })
test("fails for custom elements with self-closing", () => {
- const html = `
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ assertOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(2);
- expect(lintResult.offenses).toHaveLength(2);
-
- expect(lintResult.offenses[0].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- expect(lintResult.offenses[1].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.')
- });
+ `)
+ })
test("passes for void elements without self-closing", () => {
- const html = `
+ expectNoOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
- });
+ `)
+ })
test("passes for self-closing elements inside SVG", () => {
- const html = `
+ expectNoOffenses(`
@@ -170,30 +111,19 @@ describe("html-no-self-closing", () => {
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(0);
- expect(lintResult.offenses).toHaveLength(0);
- });
+ `)
+ })
test("fails for self-closing elements outside SVG but passes inside SVG", () => {
- const html = `
+ expectError('Use `
` instead of self-closing `
` for HTML compatibility.')
+ expectError('Use ` ` instead of self-closing ` ` for HTML compatibility.')
+ assertOffenses(`
- `;
- const linter = new Linter(Herb, [HTMLNoSelfClosingRule]);
- const lintResult = linter.lint(html);
-
- expect(lintResult.errors).toBe(2);
- expect(lintResult.offenses).toHaveLength(2);
-
- expect(lintResult.offenses[0].message).toBe('Use `
` instead of self-closing `
` for HTML compatibility.');
- expect(lintResult.offenses[1].message).toBe('Use ` ` instead of self-closing ` ` for HTML compatibility.');
- });
-});
+ `)
+ })
+})
diff --git a/javascript/packages/linter/test/rules/html-no-space-in-tag.test.ts b/javascript/packages/linter/test/rules/html-no-space-in-tag.test.ts
new file mode 100644
index 000000000..38ade222f
--- /dev/null
+++ b/javascript/packages/linter/test/rules/html-no-space-in-tag.test.ts
@@ -0,0 +1,209 @@
+import dedent from "dedent"
+import { describe, test } from "vitest"
+import { HTMLNoSpaceInTagRule } from "../../src/rules/html-no-space-in-tag.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
+
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLNoSpaceInTagRule)
+
+describe("HTMLNoSpaceInTagRule", () => {
+ describe("when space is correct", () => {
+ test("plain opening tag", () => {
+ expectNoOffenses(``, { allowInvalidSyntax: true })
+ })
+
+ test("closing tag", () => {
+ expectNoOffenses(`
`, { allowInvalidSyntax: true })
+ })
+
+ test("tag with no name", () => {
+ expectNoOffenses(`>`, { allowInvalidSyntax: true })
+ })
+
+ test("empty tag", () => {
+ expectNoOffenses(`<>`, { allowInvalidSyntax: true })
+ })
+
+ test("void tag", () => {
+ expectNoOffenses(` `)
+ })
+
+ test("plain tag with attribute", () => {
+ expectNoOffenses(`
`)
+ })
+
+ test("between attributes", () => {
+ expectNoOffenses(` `)
+ })
+
+ test("multi line tag", () => {
+ expectNoOffenses(dedent`
+
+ `)
+ })
+
+ test("tag with erb", () => {
+ expectNoOffenses(` >`)
+ })
+
+ test("multi line tag with erb", () => {
+ expectNoOffenses(dedent`
+
+ class="foo"
+ >
+ `)
+ })
+
+ test("multi line tag with erb nested", () => {
+ expectNoOffenses(dedent`
+
+
+ class="foo"
+ >
+
+ `)
+ })
+ })
+
+ describe("when no space should be present", () => {
+ test("after name", () => {
+ expectError("Extra space detected where there should be no space.")
+ assertOffenses(`
`)
+ })
+
+ test("before name", () => {
+ expectNoOffenses(`< div> `, { allowInvalidSyntax: true })
+ })
+
+ test("before start solidus", () => {
+ expectNoOffenses(`< /div>`, { allowInvalidSyntax: true })
+ })
+
+ test("after start solidus", () => {
+ expectError("Extra space detected where there should be no space.")
+ assertOffenses(`
div>`)
+ })
+
+ test("after end solidus", () => {
+ expectNoOffenses(`
`, { allowInvalidSyntax: true })
+ })
+ })
+
+ describe("when space is missing", () => {
+ test("between attributes", () => {
+ expectNoOffenses(`
`, { allowInvalidSyntax: true })
+ })
+
+ test("between last attribute and solidus", () => {
+ expectError("No space detected where there should be a single space.")
+ assertOffenses(`
`)
+ })
+
+ test("between name and solidus", () => {
+ expectError("No space detected where there should be a single space.")
+ assertOffenses(`
`)
+ })
+ })
+
+ describe("when extra space is present", () => {
+ test("between name and end of tag", () => {
+ expectError("Extra space detected where there should be no space.")
+ assertOffenses(`
`)
+ })
+
+ test("between name and first attribute", () => {
+ expectError("Extra space detected where there should be a single space.")
+ assertOffenses(`
`)
+ })
+
+ test("between name and end solidus", () => {
+ expectError("Extra space detected where there should be no space.")
+ assertOffenses(`
`)
+ })
+
+ test("between last attribute and solidus", () => {
+ expectError("Extra space detected where there should be no space.")
+ assertOffenses(`
`)
+ })
+
+ test("between last attribute and end of tag", () => {
+ expectError("Extra space detected where there should be no space.")
+ assertOffenses(`
`)
+ })
+
+ test("between attributes", () => {
+ expectError("Extra space detected where there should be a single space.")
+ assertOffenses(`
`)
+ })
+
+ test("extra newline between name and first attribute", () => {
+ expectError("Extra space detected where there should be a single space or a single line break.")
+ expectError("Extra space detected where there should be no space.")
+
+ assertOffenses(dedent`
+
+ `)
+ })
+
+ test("extra newline between name and end of tag", () => {
+ expectError("Extra space detected where there should be a single space or a single line break.")
+ expectError("Extra space detected where there should be no space.")
+
+ assertOffenses(dedent`
+
+ `)
+ })
+
+ test("extra newline between attributes", () => {
+ expectError("Extra space detected where there should be a single space or a single line break.")
+ expectError("Extra space detected where there should be no space.")
+
+ assertOffenses(dedent`
+
+ `)
+ })
+
+ test("end solidus is on newline", () => {
+ expectError("Extra space detected where there should be no space.")
+
+ assertOffenses(dedent`
+
+ `)
+ })
+
+ test("end of tag is on newline", () => {
+ expectError("Extra space detected where there should be no space.")
+
+ assertOffenses(dedent`
+
+ `)
+ })
+
+ test("non-space detected between name and attribute", () => {
+ expectNoOffenses(`
`, { allowInvalidSyntax: true })
+ })
+
+ test("non-space detected between attributes", () => {
+ expectNoOffenses(`
`, { allowInvalidSyntax: true })
+ })
+ })
+})
diff --git a/javascript/packages/linter/test/rules/html-no-title-attribute.test.ts b/javascript/packages/linter/test/rules/html-no-title-attribute.test.ts
index 29d3f36e6..304cc0db2 100644
--- a/javascript/packages/linter/test/rules/html-no-title-attribute.test.ts
+++ b/javascript/packages/linter/test/rules/html-no-title-attribute.test.ts
@@ -1,188 +1,112 @@
-import { describe, test, expect, beforeAll } from "vitest"
-import { Herb } from "@herb-tools/node-wasm"
-import { Linter } from "../../src/linter.js"
+import { describe, test } from "vitest"
import { HTMLNoTitleAttributeRule } from "../../src/rules/html-no-title-attribute.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
-describe("html-no-title-attribute", () => {
- beforeAll(async () => {
- await Herb.load()
- })
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(HTMLNoTitleAttributeRule)
+describe("html-no-title-attribute", () => {
test("passes for elements without title attribute", () => {
- const html = '
Click me '
-
- const linter = new Linter(Herb, [HTMLNoTitleAttributeRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ expectNoOffenses('
Click me ')
})
test("passes for iframe with title attribute", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoTitleAttributeRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses('
')
})
test("passes for link with title attribute", () => {
- const html = '
'
-
- const linter = new Linter(Herb, [HTMLNoTitleAttributeRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ expectNoOffenses('
')
})
test("fails for button with title attribute", () => {
- const html = '
Submit '
+ expectError("The `title` attribute should never be used as it is inaccessible for several groups of users. Use `aria-label` or `aria-describedby` instead. Exceptions are provided for `
` at (2:2) without a matching opening tag. (`MISSING_OPENING_TAG_ERROR`)")
+ `)
})
test("should report errors for mismatched quotes in attributes", () => {
- const html = `
{
- const html = `<%= 1 + %>`
- const linter = new Linter(Herb, [ParserNoErrorsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(1)
- expect(lintResult.offenses).toHaveLength(1)
-
- expect(lintResult.offenses[0].rule).toBe("parser-no-errors")
- expect(lintResult.offenses[0].severity).toBe("error")
- expect(lintResult.offenses[0].message).toBe("expect_expression_after_operator: unexpected ';'; expected an expression after the operator (`RUBY_PARSE_ERROR`)")
+ expectError("expect_expression_after_operator: unexpected ';'; expected an expression after the operator (`RUBY_PARSE_ERROR`)")
+ assertOffenses(`<%= 1 + %>`)
})
test("should report multiple parser errors", () => {
- const html = dedent`
+ expectError("Tag `
` opened at (2:3) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ expectError("Tag `
` opened at (1:1) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ expectError("Opening tag ` ` at (2:3) closed with `
` at (4:2). (`TAG_NAMES_MISMATCH_ERROR`)")
+ expectError("Opening tag `
` at (2:3) closed with `` at (3:2). (`TAG_NAMES_MISMATCH_ERROR`)")
+ assertOffenses(dedent`
Unclosed paragraph
- `
- const linter = new Linter(Herb, [ParserNoErrorsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(4)
- expect(lintResult.offenses).toHaveLength(4)
-
- expect(lintResult.offenses[0].rule).toBe("parser-no-errors")
- expect(lintResult.offenses[0].severity).toBe("error")
- expect(lintResult.offenses[0].message).toBe("Tag `
` opened at (2:3) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
-
- expect(lintResult.offenses[1].message).toBe("Tag `
` opened at (1:1) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
- expect(lintResult.offenses[2].message).toBe("Opening tag ` ` at (2:3) closed with `
` at (4:2). (`TAG_NAMES_MISMATCH_ERROR`)")
- expect(lintResult.offenses[3].message).toBe("Opening tag `
` at (2:3) closed with `` at (3:2). (`TAG_NAMES_MISMATCH_ERROR`)")
+ `)
})
test("should work alongside other linting rules", () => {
- const html = dedent`
+ expectError("Tag `
` opened at (1:1) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ expectError("Opening tag `` at (1:1) closed with `` at (3:2). (`TAG_NAMES_MISMATCH_ERROR`)")
+ assertOffenses(dedent`
<% %>
- `
- const linter = new Linter(Herb)
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(3)
- expect(lintResult.offenses).toHaveLength(3)
-
- const parserErrors = lintResult.offenses.filter(offense => offense.rule === "parser-no-errors")
- const erbErrors = lintResult.offenses.filter(offense => offense.rule === "erb-no-empty-tags")
-
- expect(parserErrors).toHaveLength(2)
- expect(erbErrors).toHaveLength(1)
-
- expect(parserErrors[0].message).toBe("Tag `` opened at (1:1) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
- expect(parserErrors[1].message).toBe("Opening tag `` at (1:1) closed with `` at (3:2). (`TAG_NAMES_MISMATCH_ERROR`)")
- expect(erbErrors[0].message).toBe("ERB tag should not be empty. Remove empty ERB tags or add content.")
+ `)
})
test("should include error location information", () => {
- const html = dedent`
+ expectError("Tag `` opened at (2:3) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ expectError("Tag ` ` opened at (1:1) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ expectError("Opening tag `
` at (2:3) closed with ` ` at (3:2). (`TAG_NAMES_MISMATCH_ERROR`)")
+ expectError("Opening tag `` at (2:3) closed with `` at (2:15). (`TAG_NAMES_MISMATCH_ERROR`)")
+ assertOffenses(dedent`
Content
- `
- const linter = new Linter(Herb, [ParserNoErrorsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(4)
- expect(lintResult.offenses).toHaveLength(4)
-
- const offense = lintResult.offenses[0]
- expect(offense.rule).toBe("parser-no-errors")
- expect(offense.severity).toBe("error")
- expect(offense.message).toBe("Tag `` opened at (2:3) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
-
- expect(offense.location).toBeDefined()
- expect(offense.location.start).toBeDefined()
- expect(offense.location.end).toBeDefined()
- expect(offense.location.start.line).toBe(3)
- expect(offense.location.start.column).toBe(6)
-
- expect(lintResult.offenses[1].message).toBe("Tag `` opened at (1:1) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ `)
})
test("should handle the specific case from issue #359", () => {
- const html = dedent`
+ expectError("Tag `
` opened at (1:25) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ expectError("Tag `` opened at (1:1) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ expectError("Opening tag `` at (1:1) doesn't have a matching closing tag ` `. (`MISSING_CLOSING_TAG_ERROR`)")
+ expectError("Opening tag `` at (1:25) doesn't have a matching closing tag ` `. (`MISSING_CLOSING_TAG_ERROR`)")
+ assertOffenses(dedent`
Some heading content
- `
- const linter = new Linter(Herb, [ParserNoErrorsRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(4)
- expect(lintResult.offenses).toHaveLength(4)
-
- expect(lintResult.offenses[0].rule).toBe("parser-no-errors")
- expect(lintResult.offenses[0].severity).toBe("error")
- expect(lintResult.offenses[0].message).toBe("Tag `` opened at (1:25) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
+ `)
+ })
- expect(lintResult.offenses[1].message).toBe("Tag `` opened at (1:1) was never closed before the end of document. (`UNCLOSED_ELEMENT_ERROR`)")
- expect(lintResult.offenses[2].message).toBe("Opening tag `` at (1:1) doesn't have a matching closing tag ` `. (`MISSING_CLOSING_TAG_ERROR`)")
- expect(lintResult.offenses[3].message).toBe("Opening tag `` at (1:25) doesn't have a matching closing tag ` `. (`MISSING_CLOSING_TAG_ERROR`)")
+ test("html element ending with boolean attribute followed by ERB tag", () => {
+ expectNoOffenses(dedent`
+
+ <%= hello %>
+ `)
})
})
diff --git a/javascript/packages/linter/test/rules/svg-tag-name-capitalization.test.ts b/javascript/packages/linter/test/rules/svg-tag-name-capitalization.test.ts
index f28bef453..1d93705cc 100644
--- a/javascript/packages/linter/test/rules/svg-tag-name-capitalization.test.ts
+++ b/javascript/packages/linter/test/rules/svg-tag-name-capitalization.test.ts
@@ -1,15 +1,12 @@
-import { describe, test, expect, beforeAll } from "vitest"
-import { Herb } from "@herb-tools/node-wasm"
-import { Linter } from "../../src/linter.js"
+import { describe, test } from "vitest"
import { SVGTagNameCapitalizationRule } from "../../src/rules/svg-tag-name-capitalization.js"
+import { createLinterTest } from "../helpers/linter-test-helper.js"
-describe("svg-tag-name-capitalization", () => {
- beforeAll(async () => {
- await Herb.load()
- })
+const { expectNoOffenses, expectError, assertOffenses } = createLinterTest(SVGTagNameCapitalizationRule)
+describe("svg-tag-name-capitalization", () => {
test("passes for correctly cased SVG elements", () => {
- const html = `
+ expectNoOffenses(`
@@ -29,18 +26,11 @@ describe("svg-tag-name-capitalization", () => {
Text along a path
- `
-
- const linter = new Linter(Herb, [SVGTagNameCapitalizationRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ `)
})
test("passes for SVG filter elements with correct camelCase", () => {
- const html = `
+ expectNoOffenses(`
@@ -52,17 +42,18 @@ describe("svg-tag-name-capitalization", () => {
- `
-
- const linter = new Linter(Herb, [SVGTagNameCapitalizationRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ `)
})
test("fails for incorrectly cased SVG elements", () => {
- const html = `
+ expectError('Opening SVG tag name `LINEARGRADIENT` should use proper capitalization. Use `linearGradient` instead.')
+ expectError('Closing SVG tag name `LINEARGRADIENT` should use proper capitalization. Use `linearGradient` instead.')
+ expectError('Opening SVG tag name `lineargradient` should use proper capitalization. Use `linearGradient` instead.')
+ expectError('Closing SVG tag name `lineargradient` should use proper capitalization. Use `linearGradient` instead.')
+ expectError('Opening SVG tag name `CLIPPATH` should use proper capitalization. Use `clipPath` instead.')
+ expectError('Closing SVG tag name `CLIPPATH` should use proper capitalization. Use `clipPath` instead.')
+
+ assertOffenses(`
@@ -74,22 +65,11 @@ describe("svg-tag-name-capitalization", () => {
- `
-
- const linter = new Linter(Herb, [SVGTagNameCapitalizationRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(6) // 3 opening + 3 closing tags
- expect(lintResult.offenses[0].message).toBe('Opening SVG tag name `LINEARGRADIENT` should use proper capitalization. Use `linearGradient` instead.')
- expect(lintResult.offenses[1].message).toBe('Closing SVG tag name `LINEARGRADIENT` should use proper capitalization. Use `linearGradient` instead.')
- expect(lintResult.offenses[2].message).toBe('Opening SVG tag name `lineargradient` should use proper capitalization. Use `linearGradient` instead.')
- expect(lintResult.offenses[3].message).toBe('Closing SVG tag name `lineargradient` should use proper capitalization. Use `linearGradient` instead.')
- expect(lintResult.offenses[4].message).toBe('Opening SVG tag name `CLIPPATH` should use proper capitalization. Use `clipPath` instead.')
- expect(lintResult.offenses[5].message).toBe('Closing SVG tag name `CLIPPATH` should use proper capitalization. Use `clipPath` instead.')
+ `)
})
test("passes for animateMotion and animateTransform", () => {
- const html = `
+ expectNoOffenses(`
@@ -102,17 +82,11 @@ describe("svg-tag-name-capitalization", () => {
dur="10s"
repeatCount="indefinite" />
- `
-
- const linter = new Linter(Herb, [SVGTagNameCapitalizationRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
+ `)
})
test("ignores non-SVG elements", () => {
- const html = `
+ expectNoOffenses(`
@@ -121,18 +95,14 @@ describe("svg-tag-name-capitalization", () => {
- `
-
- const linter = new Linter(Herb, [SVGTagNameCapitalizationRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(0)
- expect(lintResult.warnings).toBe(0)
- expect(lintResult.offenses).toHaveLength(0)
+ `)
})
test("only checks elements within SVG context", () => {
- const html = `
+ expectError('Opening SVG tag name `LINEARGRADIENT` should use proper capitalization. Use `linearGradient` instead.')
+ expectError('Closing SVG tag name `LINEARGRADIENT` should use proper capitalization. Use `linearGradient` instead.')
+
+ assertOffenses(`
Outside SVG
@@ -141,12 +111,6 @@ describe("svg-tag-name-capitalization", () => {
Outside SVG again
- `
-
- const linter = new Linter(Herb, [SVGTagNameCapitalizationRule])
- const lintResult = linter.lint(html)
-
- expect(lintResult.errors).toBe(2) // 1 opening + 1 closing tag
- expect(lintResult.offenses[0].message).toBe('Opening SVG tag name `LINEARGRADIENT` should use proper capitalization. Use `linearGradient` instead.')
+ `)
})
})
diff --git a/javascript/packages/linter/unformatted.html.erb b/javascript/packages/linter/unformatted.html.erb
new file mode 100644
index 000000000..581b4a43e
--- /dev/null
+++ b/javascript/packages/linter/unformatted.html.erb
@@ -0,0 +1,3 @@
+<% if true %>
+
+<% end %>
diff --git a/javascript/packages/node-wasm/package.json b/javascript/packages/node-wasm/package.json
index ae23a1a7e..da589ec3c 100644
--- a/javascript/packages/node-wasm/package.json
+++ b/javascript/packages/node-wasm/package.json
@@ -1,6 +1,6 @@
{
"name": "@herb-tools/node-wasm",
- "version": "0.7.4",
+ "version": "0.7.5",
"description": "WebAssembly-based HTML-aware ERB parser for Node.js.",
"type": "module",
"license": "MIT",
@@ -36,7 +36,7 @@
}
},
"dependencies": {
- "@herb-tools/core": "0.7.4"
+ "@herb-tools/core": "0.7.5"
},
"files": [
"package.json",
diff --git a/javascript/packages/node-wasm/test/node-wasm.test.ts b/javascript/packages/node-wasm/test/node-wasm.test.ts
index 7533c7b1a..cb4d19bfd 100644
--- a/javascript/packages/node-wasm/test/node-wasm.test.ts
+++ b/javascript/packages/node-wasm/test/node-wasm.test.ts
@@ -17,7 +17,7 @@ describe("@herb-tools/node-wasm", () => {
test("version() returns a string", async () => {
const version = Herb.version
expect(typeof version).toBe("string")
- expect(version).toBe("@herb-tools/node-wasm@0.7.4, @herb-tools/core@0.7.4, libprism@1.5.1, libherb@0.7.4 (WebAssembly)")
+ expect(version).toBe("@herb-tools/node-wasm@0.7.5, @herb-tools/core@0.7.5, libprism@1.6.0, libherb@0.7.5 (WebAssembly)")
})
test("parse() can process a simple template", async () => {
diff --git a/javascript/packages/node/binding.gyp b/javascript/packages/node/binding.gyp
index 95096ee66..f01664fa8 100644
--- a/javascript/packages/node/binding.gyp
+++ b/javascript/packages/node/binding.gyp
@@ -13,25 +13,20 @@
"./extension/libherb/analyze_helpers.c",
"./extension/libherb/analyze.c",
"./extension/libherb/analyzed_ruby.c",
- "./extension/libherb/array.c",
"./extension/libherb/ast_node.c",
"./extension/libherb/ast_nodes.c",
"./extension/libherb/ast_pretty_print.c",
- "./extension/libherb/buffer.c",
"./extension/libherb/element_source.c",
"./extension/libherb/errors.c",
"./extension/libherb/extract.c",
"./extension/libherb/herb.c",
"./extension/libherb/html_util.c",
"./extension/libherb/io.c",
- "./extension/libherb/json.c",
"./extension/libherb/lexer_peek_helpers.c",
"./extension/libherb/lexer.c",
"./extension/libherb/location.c",
- "./extension/libherb/memory.c",
"./extension/libherb/parser_helpers.c",
"./extension/libherb/parser.c",
- "./extension/libherb/position.c",
"./extension/libherb/pretty_print.c",
"./extension/libherb/prism_helpers.c",
"./extension/libherb/range.c",
@@ -39,6 +34,10 @@
"./extension/libherb/token.c",
"./extension/libherb/utf8.c",
"./extension/libherb/util.c",
+ "./extension/libherb/util/hb_arena.c",
+ "./extension/libherb/util/hb_array.c",
+ "./extension/libherb/util/hb_buffer.c",
+ "./extension/libherb/util/hb_string.c",
"./extension/libherb/visitor.c",
# Prism main source files
diff --git a/javascript/packages/node/extension/extension_helpers.cpp b/javascript/packages/node/extension/extension_helpers.cpp
index e64c18dc1..9ca985ef0 100644
--- a/javascript/packages/node/extension/extension_helpers.cpp
+++ b/javascript/packages/node/extension/extension_helpers.cpp
@@ -4,15 +4,15 @@
#include
extern "C" {
-#include "../extension/libherb/include/array.h"
#include "../extension/libherb/include/ast_nodes.h"
-#include "../extension/libherb/include/buffer.h"
#include "../extension/libherb/include/herb.h"
#include "../extension/libherb/include/io.h"
#include "../extension/libherb/include/location.h"
#include "../extension/libherb/include/position.h"
#include "../extension/libherb/include/range.h"
#include "../extension/libherb/include/token.h"
+#include "../extension/libherb/include/util/hb_array.h"
+#include "../extension/libherb/include/util/hb_buffer.h"
}
#include "error_helpers.h"
@@ -46,19 +46,13 @@ napi_value CreateString(napi_env env, const char* str) {
return result;
}
-napi_value CreatePosition(napi_env env, position_T* position) {
- if (!position) {
- napi_value null_value;
- napi_get_null(env, &null_value);
- return null_value;
- }
-
+napi_value CreatePosition(napi_env env, position_T position) {
napi_value result;
napi_create_object(env, &result);
napi_value line, column;
- napi_create_uint32(env, (uint32_t)position->line, &line);
- napi_create_uint32(env, (uint32_t)position->column, &column);
+ napi_create_uint32(env, (uint32_t)position.line, &line);
+ napi_create_uint32(env, (uint32_t)position.column, &column);
napi_set_named_property(env, result, "line", line);
napi_set_named_property(env, result, "column", column);
@@ -66,18 +60,12 @@ napi_value CreatePosition(napi_env env, position_T* position) {
return result;
}
-napi_value CreateLocation(napi_env env, location_T* location) {
- if (!location) {
- napi_value null_value;
- napi_get_null(env, &null_value);
- return null_value;
- }
-
+napi_value CreateLocation(napi_env env, location_T location) {
napi_value result;
napi_create_object(env, &result);
- napi_value start = CreatePosition(env, location->start);
- napi_value end = CreatePosition(env, location->end);
+ napi_value start = CreatePosition(env, location.start);
+ napi_value end = CreatePosition(env, location.end);
napi_set_named_property(env, result, "start", start);
napi_set_named_property(env, result, "end", end);
@@ -85,19 +73,13 @@ napi_value CreateLocation(napi_env env, location_T* location) {
return result;
}
-napi_value CreateRange(napi_env env, range_T* range) {
- if (!range) {
- napi_value null_value;
- napi_get_null(env, &null_value);
- return null_value;
- }
-
+napi_value CreateRange(napi_env env, range_T range) {
napi_value result;
napi_create_array(env, &result);
napi_value from, to;
- napi_create_uint32(env, (uint32_t)range->from, &from);
- napi_create_uint32(env, (uint32_t)range->to, &to);
+ napi_create_uint32(env, (uint32_t)range.from, &from);
+ napi_create_uint32(env, (uint32_t)range.to, &to);
napi_set_element(env, result, 0, from);
napi_set_element(env, result, 1, to);
@@ -154,7 +136,7 @@ napi_value ReadFileToString(napi_env env, const char* file_path) {
return result;
}
-napi_value CreateLexResult(napi_env env, array_T* tokens, napi_value source) {
+napi_value CreateLexResult(napi_env env, hb_array_T* tokens, napi_value source) {
napi_value result, tokens_array, errors_array, warnings_array;
napi_create_object(env, &result);
@@ -164,8 +146,8 @@ napi_value CreateLexResult(napi_env env, array_T* tokens, napi_value source) {
// Add tokens to array
if (tokens) {
- for (size_t i = 0; i < array_size(tokens); i++) {
- token_T* token = (token_T*)array_get(tokens, i);
+ for (size_t i = 0; i < hb_array_size(tokens); i++) {
+ token_T* token = (token_T*)hb_array_get(tokens, i);
if (token) {
napi_value token_obj = CreateToken(env, token);
napi_set_element(env, tokens_array, i, token_obj);
diff --git a/javascript/packages/node/extension/extension_helpers.h b/javascript/packages/node/extension/extension_helpers.h
index 7387ea9d8..2e3530b0c 100644
--- a/javascript/packages/node/extension/extension_helpers.h
+++ b/javascript/packages/node/extension/extension_helpers.h
@@ -4,19 +4,19 @@
#include
extern "C" {
-#include "../extension/libherb/include/array.h"
#include "../extension/libherb/include/ast_nodes.h"
+#include "../extension/libherb/include/util/hb_array.h"
}
char* CheckString(napi_env env, napi_value value);
napi_value CreateString(napi_env env, const char* str);
napi_value ReadFileToString(napi_env env, const char* file_path);
-napi_value CreateLexResult(napi_env env, array_T* tokens, napi_value source);
+napi_value CreateLexResult(napi_env env, hb_array_T* tokens, napi_value source);
napi_value CreateParseResult(napi_env env, AST_DOCUMENT_NODE_T* root, napi_value source);
-napi_value CreateLocation(napi_env env, location_T* location);
+napi_value CreateLocation(napi_env env, location_T location);
napi_value CreateToken(napi_env env, token_T* token);
-napi_value CreatePosition(napi_env env, position_T* position);
-napi_value CreateRange(napi_env env, range_T* range);
+napi_value CreatePosition(napi_env env, position_T position);
+napi_value CreateRange(napi_env env, range_T range);
#endif
diff --git a/javascript/packages/node/extension/herb.cpp b/javascript/packages/node/extension/herb.cpp
index 3b8ba8ebb..ec29d11c0 100644
--- a/javascript/packages/node/extension/herb.cpp
+++ b/javascript/packages/node/extension/herb.cpp
@@ -1,12 +1,12 @@
extern "C" {
#include "../extension/libherb/include/analyze.h"
-#include "../extension/libherb/include/array.h"
#include "../extension/libherb/include/ast_nodes.h"
-#include "../extension/libherb/include/buffer.h"
#include "../extension/libherb/include/herb.h"
#include "../extension/libherb/include/location.h"
#include "../extension/libherb/include/range.h"
#include "../extension/libherb/include/token.h"
+#include "../extension/libherb/include/util/hb_array.h"
+#include "../extension/libherb/include/util/hb_buffer.h"
}
#include "error_helpers.h"
@@ -31,7 +31,7 @@ napi_value Herb_lex(napi_env env, napi_callback_info info) {
char* string = CheckString(env, args[0]);
if (!string) { return nullptr; }
- array_T* tokens = herb_lex(string);
+ hb_array_T* tokens = herb_lex(string);
napi_value result = CreateLexResult(env, tokens, args[0]);
herb_free_tokens(&tokens);
@@ -53,7 +53,7 @@ napi_value Herb_lex_file(napi_env env, napi_callback_info info) {
char* file_path = CheckString(env, args[0]);
if (!file_path) { return nullptr; }
- array_T* tokens = herb_lex_file(file_path);
+ hb_array_T* tokens = herb_lex_file(file_path);
napi_value source_value = ReadFileToString(env, file_path);
napi_value result = CreateLexResult(env, tokens, source_value);
@@ -142,37 +142,6 @@ napi_value Herb_parse_file(napi_env env, napi_callback_info info) {
return result;
}
-napi_value Herb_lex_to_json(napi_env env, napi_callback_info info) {
- size_t argc = 1;
- napi_value args[1];
- napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
-
- if (argc < 1) {
- napi_throw_error(env, nullptr, "Wrong number of arguments");
- return nullptr;
- }
-
- char* string = CheckString(env, args[0]);
- if (!string) { return nullptr; }
-
- buffer_T output;
- if (!buffer_init(&output)) {
- free(string);
- napi_throw_error(env, nullptr, "Failed to initialize buffer");
- return nullptr;
- }
-
- herb_lex_json_to_buffer(string, &output);
-
- napi_value result;
- napi_create_string_utf8(env, output.value, output.length, &result);
-
- buffer_free(&output);
- free(string);
-
- return result;
-}
-
napi_value Herb_extract_ruby(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
@@ -186,8 +155,8 @@ napi_value Herb_extract_ruby(napi_env env, napi_callback_info info) {
char* string = CheckString(env, args[0]);
if (!string) { return nullptr; }
- buffer_T output;
- if (!buffer_init(&output)) {
+ hb_buffer_T output;
+ if (!hb_buffer_init(&output, strlen(string))) {
free(string);
napi_throw_error(env, nullptr, "Failed to initialize buffer");
return nullptr;
@@ -198,7 +167,7 @@ napi_value Herb_extract_ruby(napi_env env, napi_callback_info info) {
napi_value result;
napi_create_string_utf8(env, output.value, NAPI_AUTO_LENGTH, &result);
- buffer_free(&output);
+ free(output.value);
free(string);
return result;
}
@@ -216,8 +185,8 @@ napi_value Herb_extract_html(napi_env env, napi_callback_info info) {
char* string = CheckString(env, args[0]);
if (!string) { return nullptr; }
- buffer_T output;
- if (!buffer_init(&output)) {
+ hb_buffer_T output;
+ if (!hb_buffer_init(&output, strlen(string))) {
free(string);
napi_throw_error(env, nullptr, "Failed to initialize buffer");
return nullptr;
@@ -228,7 +197,7 @@ napi_value Herb_extract_html(napi_env env, napi_callback_info info) {
napi_value result;
napi_create_string_utf8(env, output.value, NAPI_AUTO_LENGTH, &result);
- buffer_free(&output);
+ free(output.value);
free(string);
return result;
}
@@ -252,7 +221,6 @@ napi_value Init(napi_env env, napi_value exports) {
{ "lex", nullptr, Herb_lex, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "parseFile", nullptr, Herb_parse_file, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "lexFile", nullptr, Herb_lex_file, nullptr, nullptr, nullptr, napi_default, nullptr },
- { "lexToJson", nullptr, Herb_lex_to_json, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "extractRuby", nullptr, Herb_extract_ruby, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "extractHTML", nullptr, Herb_extract_html, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "version", nullptr, Herb_version, nullptr, nullptr, nullptr, napi_default, nullptr },
diff --git a/javascript/packages/node/package.json b/javascript/packages/node/package.json
index f5f9e9b95..b31eae271 100644
--- a/javascript/packages/node/package.json
+++ b/javascript/packages/node/package.json
@@ -1,6 +1,6 @@
{
"name": "@herb-tools/node",
- "version": "0.7.4",
+ "version": "0.7.5",
"description": "Native Node.js addon for HTML-aware ERB parsing using Herb.",
"type": "module",
"license": "MIT",
@@ -48,7 +48,7 @@
"host": "https://github.com/marcoroth/herb/releases/download/"
},
"dependencies": {
- "@herb-tools/core": "0.7.4",
+ "@herb-tools/core": "0.7.5",
"@mapbox/node-pre-gyp": "^2.0.0",
"node-addon-api": "^5.1.0"
},
diff --git a/javascript/packages/node/test/node.test.ts b/javascript/packages/node/test/node.test.ts
index 032656916..892167245 100644
--- a/javascript/packages/node/test/node.test.ts
+++ b/javascript/packages/node/test/node.test.ts
@@ -17,7 +17,7 @@ describe("@herb-tools/node", () => {
test("version() returns a string", async () => {
const version = Herb.version
expect(typeof version).toBe("string")
- expect(version).toBe("@herb-tools/node@0.7.4, @herb-tools/core@0.7.4, libprism@1.5.1, libherb@0.7.4 (Node.js C++ native extension)")
+ expect(version).toBe("@herb-tools/node@0.7.5, @herb-tools/core@0.7.5, libprism@1.6.0, libherb@0.7.5 (Node.js C++ native extension)")
})
test("parse() can process a simple template", async () => {
diff --git a/javascript/packages/printer/package.json b/javascript/packages/printer/package.json
index 16928c514..08de93261 100644
--- a/javascript/packages/printer/package.json
+++ b/javascript/packages/printer/package.json
@@ -1,6 +1,6 @@
{
"name": "@herb-tools/printer",
- "version": "0.7.4",
+ "version": "0.7.5",
"description": "AST printer infrastructure and lossless reconstruction tool for HTML+ERB templates",
"license": "MIT",
"homepage": "https://herb-tools.dev",
@@ -37,7 +37,7 @@
"prepublishOnly": "yarn clean && yarn build && yarn test"
},
"dependencies": {
- "@herb-tools/core": "0.7.4",
+ "@herb-tools/core": "0.7.5",
"glob": "^10.3.10"
},
"files": [
diff --git a/javascript/packages/printer/src/cli.ts b/javascript/packages/printer/src/cli.ts
index e54e00e6c..6cd8cde9d 100644
--- a/javascript/packages/printer/src/cli.ts
+++ b/javascript/packages/printer/src/cli.ts
@@ -7,6 +7,8 @@ import { resolve } from "path"
import { glob } from "glob"
import { Herb } from "@herb-tools/node-wasm"
+import { HERB_FILES_GLOB } from "@herb-tools/core"
+
import { IdentityPrinter } from "./index.js"
interface CLIOptions {
@@ -74,7 +76,7 @@ export class CLI {
-o, --output Output file path (defaults to stdout)
--verify Verify that output matches input exactly
--stats Show parsing and printing statistics
- --glob Treat input as glob pattern (default: **/*.html.erb)
+ --glob Treat input as glob pattern (default: ${HERB_FILES_GLOB})
-h, --help Show this help message
Examples:
@@ -84,10 +86,10 @@ export class CLI {
herb-print input.html.erb --stats
# Glob patterns (batch verification)
- herb-print --glob --verify # All .html.erb files
+ herb-print --glob --verify # All .html.erb files
herb-print "app/views/**/*.html.erb" --glob --verify --stats
herb-print "*.erb" --glob --verify
- herb-print "/path/to/templates" --glob --verify # Directory (auto-appends /**/*.html.erb)
+ herb-print "/path/to/templates" --glob --verify # Directory (auto-appends /${HERB_FILES_GLOB})
herb-print "/path/to/templates/**/*.html.erb" --glob --verify
# The --verify flag is useful to test parser fidelity:
@@ -108,7 +110,7 @@ export class CLI {
await Herb.load()
if (options.glob) {
- const pattern = options.input || "**/*.html.erb"
+ const pattern = options.input || HERB_FILES_GLOB
const files = await glob(pattern)
if (files.length === 0) {
diff --git a/javascript/packages/printer/src/identity-printer.ts b/javascript/packages/printer/src/identity-printer.ts
index 6eb2c53c5..ad592cc2d 100644
--- a/javascript/packages/printer/src/identity-printer.ts
+++ b/javascript/packages/printer/src/identity-printer.ts
@@ -13,6 +13,14 @@ import type * as Nodes from "@herb-tools/core"
* - Verifying AST round-trip fidelity
*/
export class IdentityPrinter extends Printer {
+ static printERBNode(node: Nodes.ERBNode) {
+ const printer = new IdentityPrinter()
+
+ printer.printERBNode(node)
+
+ return printer.context.getOutput()
+ }
+
visitLiteralNode(node: Nodes.LiteralNode): void {
this.write(node.content)
}
diff --git a/javascript/packages/printer/src/printer.ts b/javascript/packages/printer/src/printer.ts
index 65af1dc08..ae56187aa 100644
--- a/javascript/packages/printer/src/printer.ts
+++ b/javascript/packages/printer/src/printer.ts
@@ -1,6 +1,8 @@
import { Node, Visitor, Token, ParseResult, isToken, isParseResult } from "@herb-tools/core"
import { PrintContext } from "./print-context.js"
+import type { ERBNode } from "@herb-tools/core"
+
/**
* Options for controlling the printing behavior
*/
diff --git a/javascript/packages/stimulus-lint/package.json b/javascript/packages/stimulus-lint/package.json
index f6f62f9b5..3297934fc 100644
--- a/javascript/packages/stimulus-lint/package.json
+++ b/javascript/packages/stimulus-lint/package.json
@@ -1,6 +1,6 @@
{
"name": "stimulus-lint",
- "version": "0.1.4",
+ "version": "0.1.5",
"description": "Linting rules for Stimulus controllers and HTML+ERB view templates.",
"license": "MIT",
"homepage": "https://herb-tools.dev",
@@ -34,10 +34,10 @@
"prepublishOnly": "yarn clean && yarn build && yarn test"
},
"dependencies": {
- "@herb-tools/linter": "0.7.4",
- "@herb-tools/core": "0.7.4",
- "@herb-tools/node-wasm": "0.7.4",
- "@herb-tools/highlighter": "0.7.4",
+ "@herb-tools/linter": "0.7.5",
+ "@herb-tools/core": "0.7.5",
+ "@herb-tools/node-wasm": "0.7.5",
+ "@herb-tools/highlighter": "0.7.5",
"stimulus-parser": "^0.3.0"
},
"files": [
diff --git a/javascript/packages/tailwind-class-sorter/package.json b/javascript/packages/tailwind-class-sorter/package.json
index 6b6d6eb0d..2bbee0cca 100644
--- a/javascript/packages/tailwind-class-sorter/package.json
+++ b/javascript/packages/tailwind-class-sorter/package.json
@@ -2,7 +2,7 @@
"type": "module",
"name": "@herb-tools/tailwind-class-sorter",
"description": "Standalone Tailwind CSS class sorter with Prettier plugin compatibility, extracted from tailwindlabs/prettier-plugin-tailwindcss",
- "version": "0.7.4",
+ "version": "0.7.5",
"license": "MIT",
"main": "./dist/tailwind-class-sorter.cjs",
"module": "./dist/tailwind-class-sorter.esm.js",
@@ -46,7 +46,7 @@
"devDependencies": {
"@types/node": "^24.1.0",
"dedent": "^1.6.0",
- "esbuild": "^0.25.10",
+ "esbuild": "^0.25.11",
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.17",
"tsup": "^8.5.0",
diff --git a/javascript/packages/vscode/package.json b/javascript/packages/vscode/package.json
index 8dd528949..0a26d49c8 100644
--- a/javascript/packages/vscode/package.json
+++ b/javascript/packages/vscode/package.json
@@ -2,7 +2,7 @@
"name": "herb-lsp",
"displayName": "Herb LSP - HTML+ERB Language Tools",
"description": "VS Code extension for connecting with the Herb Language Server and Language Tools for HTML+ERB files",
- "version": "0.7.4",
+ "version": "0.7.5",
"private": true,
"license": "MIT",
"pricing": "Free",
@@ -61,12 +61,16 @@
"items": {
"type": "string",
"enum": [
+ "erb-comment-syntax",
+ "erb-no-case-node-children",
"erb-no-empty-tags",
+ "erb-no-extra-newline",
"erb-no-output-control-flow",
"erb-no-silent-tag-in-attribute-name",
"erb-prefer-image-tag-helper",
+ "erb-require-trailing-newline",
"erb-require-whitespace-inside-tags",
- "erb-requires-trailing-newline",
+ "erb-right-trim",
"html-anchor-require-href",
"html-aria-attribute-must-be-valid",
"html-aria-label-is-well-formatted",
@@ -77,19 +81,24 @@
"html-attribute-equals-spacing",
"html-attribute-values-require-quotes",
"html-avoid-both-disabled-and-aria-disabled",
+ "html-body-only-elements",
"html-boolean-attributes-no-value",
+ "html-head-only-elements",
"html-iframe-has-title",
"html-img-require-alt",
+ "html-input-require-autocomplete",
"html-navigation-has-label",
"html-no-aria-hidden-on-focusable",
"html-no-block-inside-inline",
"html-no-duplicate-attributes",
"html-no-duplicate-ids",
+ "html-no-duplicate-meta-names",
"html-no-empty-attributes",
"html-no-empty-headings",
"html-no-nested-links",
"html-no-positive-tab-index",
"html-no-self-closing",
+ "html-no-space-in-tag",
"html-no-title-attribute",
"html-no-underscores-in-attribute-names",
"html-tag-name-lowercase",
@@ -239,9 +248,9 @@
"prepublishOnly": "yarn clean && yarn build && yarn test"
},
"devDependencies": {
- "@herb-tools/formatter": "0.7.4",
- "@herb-tools/linter": "0.7.4",
- "@herb-tools/node-wasm": "0.7.4",
+ "@herb-tools/formatter": "0.7.5",
+ "@herb-tools/linter": "0.7.5",
+ "@herb-tools/node-wasm": "0.7.5",
"@types/node": "20.x",
"@types/vscode": "^1.43.0",
"@typescript-eslint/eslint-plugin": "^8.40.0",
@@ -249,7 +258,7 @@
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.5.0",
- "esbuild": "^0.25.10",
+ "esbuild": "^0.25.11",
"esbuild-plugin-copy": "^2.1.1",
"eslint": "^9.25.1",
"npm-run-all": "^4.1.5",
diff --git a/javascript/packages/vscode/scripts/sync-defaults.js b/javascript/packages/vscode/scripts/sync-defaults.js
index a177af031..892b0c702 100644
--- a/javascript/packages/vscode/scripts/sync-defaults.js
+++ b/javascript/packages/vscode/scripts/sync-defaults.js
@@ -18,7 +18,7 @@ function extractLinterRuleNames() {
if (file.endsWith('.ts') && file !== 'index.ts' && file !== 'rule-utils.ts') {
const filePath = path.join(rulesDir, file);
const content = fs.readFileSync(filePath, 'utf8');
- const match = content.match(/name\s*=\s*["']([^"']+)["']/);
+ const match = content.match(/ name\s*=\s*["']([^"']+)["']/);
if (match) {
ruleNames.push(match[1]);
diff --git a/javascript/packages/vscode/src/client.ts b/javascript/packages/vscode/src/client.ts
index caccf60be..3679baf6a 100644
--- a/javascript/packages/vscode/src/client.ts
+++ b/javascript/packages/vscode/src/client.ts
@@ -7,7 +7,7 @@ export class Client {
private client: LanguageClient
private serverModule: string
private languageClientId = "languageServerHerb"
- private languageClientName = "Herb LSP "
+ private languageClientName = "Herb Language Server "
private context: ExtensionContext
private configurationListener?: Disposable
diff --git a/javascript/packages/vscode/src/code-action-provider.ts b/javascript/packages/vscode/src/code-action-provider.ts
index 4a97ef0ca..e03f88497 100644
--- a/javascript/packages/vscode/src/code-action-provider.ts
+++ b/javascript/packages/vscode/src/code-action-provider.ts
@@ -7,18 +7,19 @@ export class HerbCodeActionProvider implements vscode.CodeActionProvider {
context: vscode.CodeActionContext,
_token: vscode.CancellationToken
): vscode.CodeAction[] {
+ if (context.diagnostics.length === 0) {
+ return []
+ }
+
const actions: vscode.CodeAction[] = []
- if (!document.fileName.endsWith('.html.erb')) {
- return actions
- }
+ for (const diagnostic of context.diagnostics) {
+ const source = typeof diagnostic.source === 'string' ? diagnostic.source.trim() : undefined
- const diagnostics = context.diagnostics
- if (diagnostics.length === 0) {
- return actions
- }
+ if (!source || !source.includes('Herb')) {
+ continue
+ }
- for (const diagnostic of diagnostics) {
let errorType = 'UNKNOWN_ERROR'
if (typeof diagnostic.code === 'string' && diagnostic.code) {
errorType = diagnostic.code
diff --git a/javascript/packages/vscode/src/extension.ts b/javascript/packages/vscode/src/extension.ts
index e66b88d0c..fb4bb0e2b 100644
--- a/javascript/packages/vscode/src/extension.ts
+++ b/javascript/packages/vscode/src/extension.ts
@@ -1,5 +1,7 @@
import * as vscode from "vscode"
+import { HERB_FILES_GLOB } from "@herb-tools/core"
+
import { Client } from "./client"
import { HerbAnalysisProvider } from "./herb-analysis-provider"
import { HerbCodeActionProvider } from "./code-action-provider"
@@ -63,8 +65,7 @@ export async function activate(context: vscode.ExtensionContext) {
})
)
- // Set up file watcher for HTML+ERB files
- const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*.html.erb')
+ const fileWatcher = vscode.workspace.createFileSystemWatcher(HERB_FILES_GLOB)
fileWatcher.onDidChange(async (uri) => {
console.log(`File changed: ${uri.fsPath}`)
@@ -83,7 +84,6 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(fileWatcher)
- // Auto-run analyze project if HTML+ERB files exist
await runAutoAnalysis()
console.log("Herb LSP is now active!")
@@ -94,12 +94,13 @@ async function runAutoAnalysis() {
return
}
- const erbFiles = await vscode.workspace.findFiles('**/*.html.erb')
- if (erbFiles.length === 0) {
+ const files = await vscode.workspace.findFiles(HERB_FILES_GLOB)
+ if (files.length === 0) {
return
}
- console.log(`Found ${erbFiles.length} HTML+ERB files. Running auto-analysis...`)
+ console.log(`Found ${files.length} HTML+ERB files. Running auto-analysis...`)
+
await analysisProvider.analyzeProject()
}
diff --git a/javascript/packages/vscode/src/herb-analysis-provider.ts b/javascript/packages/vscode/src/herb-analysis-provider.ts
index 13d148516..1121f4825 100644
--- a/javascript/packages/vscode/src/herb-analysis-provider.ts
+++ b/javascript/packages/vscode/src/herb-analysis-provider.ts
@@ -5,6 +5,8 @@ import { VersionService } from './version-service'
import { workspace, window, EventEmitter, ProgressLocation } from 'vscode'
+import { HERB_FILES_GLOB } from "@herb-tools/core"
+
import type { FileStatus, TreeNode } from './types'
import type { TreeDataProvider, Event, ExtensionContext, TreeItem, Uri } from 'vscode'
@@ -46,7 +48,7 @@ export class HerbAnalysisProvider implements TreeDataProvider {
}
async analyzeProject(): Promise {
- const uris = await workspace.findFiles('**/*.html.erb')
+ const uris = await workspace.findFiles(HERB_FILES_GLOB)
this.lastAnalysisTime = new Date()
diff --git a/lib/herb/engine.rb b/lib/herb/engine.rb
index 133db20d7..c4fe7752b 100644
--- a/lib/herb/engine.rb
+++ b/lib/herb/engine.rb
@@ -193,13 +193,17 @@ def add_text(text)
def add_code(code)
terminate_expression
- @src << " " << code
-
- # TODO: rework and check for Prism::InlineComment as soon as we expose the Prism Nodes in the Herb AST
- if code.include?("#")
- @src << "\n"
+ if code.include?("=begin") || code.include?("=end")
+ @src << "\n" << code << "\n"
else
- @src << ";" unless code[-1] == "\n"
+ @src << " " << code
+
+ # TODO: rework and check for Prism::InlineComment as soon as we expose the Prism Nodes in the Herb AST
+ if code.include?("#")
+ @src << "\n"
+ else
+ @src << ";" unless code[-1] == "\n"
+ end
end
@buffer_on_stack = false
diff --git a/lib/herb/engine/compiler.rb b/lib/herb/engine/compiler.rb
index 3e9870265..7e8e90f66 100644
--- a/lib/herb/engine/compiler.rb
+++ b/lib/herb/engine/compiler.rb
@@ -227,7 +227,7 @@ def visit_erb_end_node(node)
end
def visit_erb_case_match_node(node)
- visit_erb_control_with_parts(node, :children, :conditions, :else_clause, :end_node)
+ visit_erb_control_with_parts(node, :conditions, :else_clause, :end_node)
end
def visit_erb_in_node(node)
diff --git a/lib/herb/libherb.rb b/lib/herb/libherb.rb
index 401004303..ea670bc00 100644
--- a/lib/herb/libherb.rb
+++ b/lib/herb/libherb.rb
@@ -25,7 +25,6 @@ def self.library_path
ffi_lib(library_path)
attach_function :herb_lex_to_buffer, [:pointer, :pointer], :void
- attach_function :herb_lex_json_to_buffer, [:pointer, :pointer], :void
attach_function :herb_lex, [:pointer], :pointer
attach_function :herb_parse, [:pointer], :pointer
attach_function :herb_extract_ruby_to_buffer, [:pointer, :pointer], :void
diff --git a/lib/herb/libherb/array.rb b/lib/herb/libherb/array.rb
index 6ef705a1b..304252a43 100644
--- a/lib/herb/libherb/array.rb
+++ b/lib/herb/libherb/array.rb
@@ -7,9 +7,9 @@
module Herb
module LibHerb
- attach_function :array_get, [:pointer, :int], :pointer
- attach_function :array_capacity, [:pointer], :size_t
- attach_function :array_size, [:pointer], :size_t
+ attach_function :hb_array_get, [:pointer, :int], :pointer
+ attach_function :hb_array_capacity, [:pointer], :size_t
+ attach_function :hb_array_size, [:pointer], :size_t
class Array
extend Forwardable
@@ -24,16 +24,16 @@ def initialize(pointer, item_class)
end
def capacity
- LibHerb.array_capacity(pointer)
+ LibHerb.hb_array_capacity(pointer)
end
def size
- LibHerb.array_size(pointer)
+ LibHerb.hb_array_size(pointer)
end
def item_pointers
size.times.map { |item|
- LibHerb.array_get(pointer, item)
+ LibHerb.hb_array_get(pointer, item)
}
end
diff --git a/lib/herb/libherb/buffer.rb b/lib/herb/libherb/buffer.rb
index d682df170..5db3858dc 100644
--- a/lib/herb/libherb/buffer.rb
+++ b/lib/herb/libherb/buffer.rb
@@ -5,16 +5,16 @@
module Herb
module LibHerb
- attach_function :buffer_init, [:pointer], :bool
- attach_function :buffer_free, [:pointer], :void
- attach_function :buffer_value, [:pointer], :pointer
- attach_function :buffer_length, [:pointer], :size_t
- attach_function :buffer_capacity, [:pointer], :size_t
- attach_function :buffer_sizeof, [], :size_t
- attach_function :buffer_append, [:pointer, :pointer], :void
+ attach_function :hb_buffer_init, [:pointer], :bool
+ attach_function :hb_buffer_free, [:pointer], :void
+ attach_function :hb_buffer_value, [:pointer], :pointer
+ attach_function :hb_buffer_length, [:pointer], :size_t
+ attach_function :hb_buffer_capacity, [:pointer], :size_t
+ attach_function :hb_buffer_sizeof, [], :size_t
+ attach_function :hb_buffer_append, [:pointer, :pointer], :void
class Buffer
- SIZEOF = LibHerb.buffer_sizeof
+ SIZEOF = LibHerb.hb_buffer_sizeof
attr_reader :pointer
@@ -23,19 +23,19 @@ def initialize(pointer)
end
def append(string)
- LibHerb.buffer_append(pointer, string)
+ LibHerb.hb_buffer_append(pointer, string)
end
def value
- LibHerb.buffer_value(pointer)
+ LibHerb.hb_buffer_value(pointer)
end
def length
- LibHerb.buffer_length(pointer)
+ LibHerb.hb_buffer_length(pointer)
end
def capacity
- LibHerb.buffer_capacity(pointer)
+ LibHerb.hb_buffer_capacity(pointer)
end
def read
@@ -44,11 +44,11 @@ def read
def self.with
FFI::MemoryPointer.new(SIZEOF) do |pointer|
- raise "couldn't allocate Buffer" unless LibHerb.buffer_init(pointer)
+ raise "couldn't allocate Buffer" unless LibHerb.hb_buffer_init(pointer)
return yield new(pointer)
ensure
- LibHerb.buffer_free(pointer)
+ LibHerb.hb_buffer_free(pointer)
end
end
end
diff --git a/lib/herb/libherb/libherb.rb b/lib/herb/libherb/libherb.rb
index 38354e405..03c3b77d7 100644
--- a/lib/herb/libherb/libherb.rb
+++ b/lib/herb/libherb/libherb.rb
@@ -26,14 +26,6 @@ def self.lex(source)
)
end
- def self.lex_to_json(source)
- LibHerb::Buffer.with do |output|
- LibHerb.herb_lex_json_to_buffer(source, output.pointer)
-
- JSON.parse(output.read.force_encoding("utf-8"))
- end
- end
-
def self.extract_ruby(source)
LibHerb::Buffer.with do |output|
LibHerb.herb_extract_ruby_to_buffer(source, output.pointer)
diff --git a/lib/herb/project.rb b/lib/herb/project.rb
index aade882eb..bd44dc327 100644
--- a/lib/herb/project.rb
+++ b/lib/herb/project.rb
@@ -30,7 +30,7 @@ def initialize(project_path, output_file: nil)
end
def glob
- "**/*.html.erb"
+ "**/*.{html,rhtml,html.erb,html+*.erb,turbo_stream.erb}"
end
def full_path_glob
@@ -78,19 +78,23 @@ def parse!
failed_files = []
timeout_files = []
error_files = []
+ compilation_failed_files = []
error_outputs = {}
file_contents = {}
parse_errors = {}
+ compilation_errors = {}
files.each_with_index do |file_path, index|
total_failed = failed_files.count
total_timeout = timeout_files.count
total_errors = error_files.count
+ total_compilation_failed = compilation_failed_files.count
- lines_to_clear = 6 + total_failed + total_timeout + total_errors
+ lines_to_clear = 6 + total_failed + total_timeout + total_errors + total_compilation_failed
lines_to_clear += 3 if total_failed.positive?
lines_to_clear += 3 if total_timeout.positive?
lines_to_clear += 3 if total_errors.positive?
+ lines_to_clear += 3 if total_compilation_failed.positive?
lines_to_clear.times { print "\e[1A\e[K" } if index.positive? && interactive?
@@ -129,6 +133,13 @@ def parse!
error_files.each { |file| puts " - #{file}" }
puts
end
+
+ if compilation_failed_files.any?
+ puts
+ puts "Files with compilation errors:"
+ compilation_failed_files.each { |file| puts " - #{file}" }
+ puts
+ end
end
begin
@@ -173,7 +184,33 @@ def parse!
case $CHILD_STATUS.exitstatus
when 0
log.puts "✅ Parsed #{file_path} successfully"
- successful_files << file_path
+
+ begin
+ Herb::Engine.new(file_content, filename: file_path, escape: true)
+
+ log.puts "✅ Compiled #{file_path} successfully"
+ successful_files << file_path
+ rescue Herb::Engine::CompilationError => e
+ log.puts "❌ Compilation failed for #{file_path}"
+
+ compilation_failed_files << file_path
+ compilation_errors[file_path] = {
+ error: e.message,
+ backtrace: e.backtrace&.first(10) || [],
+ }
+
+ file_contents[file_path] = file_content
+ rescue StandardError => e
+ log.puts "❌ Unexpected compilation error for #{file_path}: #{e.class}: #{e.message}"
+
+ compilation_failed_files << file_path
+ compilation_errors[file_path] = {
+ error: "#{e.class}: #{e.message}",
+ backtrace: e.backtrace&.first(10) || [],
+ }
+
+ file_contents[file_path] = file_content
+ end
when 2
message = "⚠️ Parsing #{file_path} completed with errors"
log.puts message
@@ -248,8 +285,11 @@ def parse!
summary = [
heading("Summary"),
"Total files: #{files.count}",
- "✅ Successful: #{successful_files.count} (#{percentage(successful_files.count, files.count)}%)",
- "❌ Failed: #{failed_files.count} (#{percentage(failed_files.count, files.count)}%)",
+ "✅ Successful (parsed & compiled): #{successful_files.count} (#{percentage(successful_files.count,
+ files.count)}%)",
+ "❌ Compilation errors: #{compilation_failed_files.count} (#{percentage(compilation_failed_files.count,
+ files.count)}%)",
+ "❌ Failed to parse: #{failed_files.count} (#{percentage(failed_files.count, files.count)}%)",
"⚠️ Parse errors: #{error_files.count} (#{percentage(error_files.count, files.count)}%)",
"⏱️ Timed out: #{timeout_files.count} (#{percentage(timeout_files.count, files.count)}%)"
]
@@ -289,7 +329,17 @@ def parse!
end
end
- problem_files = failed_files + timeout_files + error_files
+ if compilation_failed_files.any?
+ log.puts "\n#{heading("Files with compilation errors")}"
+ puts "\nFiles with compilation errors:"
+
+ compilation_failed_files.each do |f|
+ log.puts f
+ puts " - #{f}"
+ end
+ end
+
+ problem_files = failed_files + timeout_files + error_files + compilation_failed_files
if problem_files.any?
log.puts "\n#{heading("FILE CONTENTS AND DETAILS")}"
@@ -331,28 +381,43 @@ def parse!
end
end
- next unless parse_errors[file]
+ if parse_errors[file]
+ if parse_errors[file][:stdout].strip.length.positive?
+ log.puts "\n#{heading("STANDARD OUTPUT")}"
+ log.puts "```"
+ log.puts parse_errors[file][:stdout]
+ log.puts "```"
+ end
- if parse_errors[file][:stdout].strip.length.positive?
- log.puts "\n#{heading("STANDARD OUTPUT")}"
- log.puts "```"
- log.puts parse_errors[file][:stdout]
- log.puts "```"
- end
+ if parse_errors[file][:stderr].strip.length.positive?
+ log.puts "\n#{heading("ERROR OUTPUT")}"
+ log.puts "```"
+ log.puts parse_errors[file][:stderr]
+ log.puts "```"
+ end
- if parse_errors[file][:stderr].strip.length.positive?
- log.puts "\n#{heading("ERROR OUTPUT")}"
- log.puts "```"
- log.puts parse_errors[file][:stderr]
- log.puts "```"
+ if parse_errors[file][:ast]
+ log.puts "\n#{heading("AST")}"
+ log.puts "```"
+ log.puts parse_errors[file][:ast]
+ log.puts "```"
+ log.puts
+ end
end
- next unless parse_errors[file][:ast]
+ next unless compilation_errors[file]
- log.puts "\n#{heading("AST")}"
+ log.puts "\n#{heading("COMPILATION ERROR")}"
log.puts "```"
- log.puts parse_errors[file][:ast]
+ log.puts compilation_errors[file][:error]
log.puts "```"
+
+ if compilation_errors[file][:backtrace].any?
+ log.puts "\n#{heading("BACKTRACE")}"
+ log.puts "```"
+ log.puts compilation_errors[file][:backtrace].join("\n")
+ log.puts "```"
+ end
log.puts
end
end
diff --git a/lib/herb/version.rb b/lib/herb/version.rb
index 47f0c2796..b3a0aa4e9 100644
--- a/lib/herb/version.rb
+++ b/lib/herb/version.rb
@@ -2,5 +2,5 @@
# typed: true
module Herb
- VERSION = "0.7.4"
+ VERSION = "0.7.5"
end
diff --git a/package.json b/package.json
index c02b85dfb..a684519d5 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
"minimatch": "^10.0.3",
"nx": "21.1.2",
"oxlint": "^1.13.0",
- "playwright": "^1.51.0",
+ "playwright": "^1.55.1",
"prettier": "^3.6.2",
"rimraf": "^6.0.1",
"rollup": "^4.47.1",
diff --git a/playground/index.html b/playground/index.html
index 24e47c0f6..f976d6d6d 100644
--- a/playground/index.html
+++ b/playground/index.html
@@ -173,6 +173,29 @@
+
+
+
+
+
+ Autofix
+
+
+ Autocorrect autocorrectable Herb Linter offenses
+
+
+
+
0 && typeof result.source === "string") {
+ if (this.editor) {
+ this.editor.setValue(result.source)
+ } else {
+ this.inputTarget.value = result.source
+ }
+
+ if (wandIcon && checkIcon) {
+ wandIcon.classList.add("hidden")
+ checkIcon.classList.remove("hidden")
+ checkIcon.style.display = ""
+
+ setTimeout(() => {
+ this.resetAutofixButtonIcons()
+ }, 1000)
+ }
+
+ const offensesLabel = fixedCount === 1 ? "offense" : "offenses"
+ this.showTemporaryMessage(`Autofixed ${fixedCount} linter ${offensesLabel}`, "success")
+
+ await this.analyze()
+ this.resetAutofixButtonIcons()
+ } else {
+ this.showTemporaryMessage("No autocorrectable linter offenses found", "info")
+ }
+ } else {
+ this.showTemporaryMessage("Failed to autofix linter offenses", "error")
+ }
+ } catch (error) {
+ console.error("Autofix error:", error)
+ this.showTemporaryMessage("Failed to autofix linter offenses", "error")
+ }
+ }
+
async analyze() {
this.updateURL()
@@ -816,6 +873,24 @@ export default class extends Controller {
}
}
+ if (this.hasAutofixButtonTarget) {
+ const hasParserErrors = result.parseResult ? result.parseResult.recursiveErrors().length > 0 : false
+ const hasLintOffenses = !!(result.lintResult && Array.isArray(result.lintResult.offenses) && result.lintResult.offenses.length > 0)
+
+ if (hasParserErrors) {
+ this.disableAutofixButton()
+ this.setupAutofixTooltip()
+ this.updateAutofixTooltipText('Cannot autofix code due to parser errors. Fix parser errors in Diagnostics tab first.')
+ } else if (!hasLintOffenses) {
+ this.disableAutofixButton()
+ this.setupAutofixTooltip()
+ this.updateAutofixTooltipText('No Herb Linter offenses found to autofix.')
+ } else {
+ this.enableAutofixButton()
+ this.updateAutofixTooltipText('Autocorrect autocorrectable Herb Linter offenses')
+ }
+ }
+
if (this.hasRubyViewerTarget) {
this.rubyViewerTarget.classList.add("language-ruby")
this.rubyViewerTarget.textContent = result.ruby
@@ -1236,6 +1311,72 @@ export default class extends Controller {
}
}
+ setupAutofixTooltip() {
+ if (this.hasAutofixTooltipTarget) {
+ this.autofixButtonTarget.addEventListener('mouseenter', this.showAutofixTooltip)
+ this.autofixButtonTarget.addEventListener('mouseleave', this.hideAutofixTooltip)
+ }
+ }
+
+ removeAutofixTooltip() {
+ if (this.hasAutofixTooltipTarget) {
+ this.autofixButtonTarget.removeEventListener('mouseenter', this.showAutofixTooltip)
+ this.autofixButtonTarget.removeEventListener('mouseleave', this.hideAutofixTooltip)
+
+ this.hideAutofixTooltip()
+ }
+ }
+
+ showAutofixTooltip = () => {
+ if (this.hasAutofixTooltipTarget) {
+ this.autofixTooltipTarget.classList.remove('hidden')
+ }
+ }
+
+ hideAutofixTooltip = () => {
+ if (this.hasAutofixTooltipTarget) {
+ this.autofixTooltipTarget.classList.add('hidden')
+ }
+ }
+
+ updateAutofixTooltipText(text) {
+ if (this.hasAutofixTooltipTarget) {
+ const textNode = this.autofixTooltipTarget.firstChild
+ if (textNode && textNode.nodeType === Node.TEXT_NODE) {
+ textNode.textContent = text
+ }
+ }
+ }
+
+ enableAutofixButton() {
+ this.autofixButtonTarget.disabled = false
+ this.autofixButtonTarget.classList.remove('opacity-50', 'cursor-not-allowed')
+ this.autofixButtonTarget.classList.add('hover:bg-gray-200', 'dark:hover:bg-gray-700')
+ }
+
+ disableAutofixButton() {
+ this.autofixButtonTarget.disabled = true
+ this.autofixButtonTarget.classList.add('opacity-50', 'cursor-not-allowed')
+ this.autofixButtonTarget.classList.remove('hover:bg-gray-200', 'dark:hover:bg-gray-700')
+ this.resetAutofixButtonIcons()
+ }
+
+ resetAutofixButtonIcons() {
+ if (!this.hasAutofixButtonTarget) return
+
+ const wandIcon = this.autofixButtonTarget.querySelector(".fa-wand-magic-sparkles")
+ const checkIcon = this.autofixButtonTarget.querySelector(".fa-circle-check")
+
+ if (wandIcon) {
+ wandIcon.classList.remove("hidden")
+ }
+
+ if (checkIcon) {
+ checkIcon.classList.add("hidden")
+ checkIcon.style.display = ""
+ }
+ }
+
setupShareTooltip() {
if (this.hasShareButtonTarget && this.hasShareTooltipTarget) {
this.shareButtonTarget.addEventListener('mouseenter', this.showShareTooltip)
diff --git a/printer.md b/printer.md
deleted file mode 100644
index 0bcf4fea5..000000000
--- a/printer.md
+++ /dev/null
@@ -1,472 +0,0 @@
-
-
-------
-
-
-
-
-
-
-# Printer
-
-AST-to-source code conversion for the Herb ecosystem.
-
-## Status
-**In Development** - Core functionality available
-
-## Overview
-
-The Herb Printer converts Herb AST nodes back into source code, enabling tools like the formatter and minifier to reconstruct templates after transformation. It provides fine-grained control over output formatting and serves as the foundation for code generation workflows.
-
-## Features
-
-### AST Reconstruction
-- **Complete fidelity**: Reconstructs exact source code from AST
-- **Configurable formatting**: Control indentation, spacing, and line breaks
-- **Comment preservation**: Maintains comments with position information
-- **Error handling**: Graceful handling of malformed AST nodes
-
-### Output Customization
-- **Indentation styles**: Spaces, tabs, custom patterns
-- **Line ending control**: LF, CRLF, or system default
-- **Whitespace handling**: Configurable whitespace preservation
-- **ERB tag formatting**: Control ERB tag spacing and style
-
-### Source Map Generation
-- **Position mapping**: Maps output positions back to original AST nodes
-- **Debugging support**: Enables source-level debugging of generated code
-- **Tool integration**: Compatible with development tools and debuggers
-
-## Installation
-
-```bash
-npm install @herb-tools/printer
-```
-
-For Ruby:
-```bash
-gem install herb-printer
-```
-
-## Usage
-
-### JavaScript/Node.js
-
-```javascript
-import { Printer } from '@herb-tools/printer'
-import { Herb } from '@herb-tools/node'
-
-// Parse template to AST
-const ast = Herb.parse('<%= @user.name %>
')
-
-// Create printer with custom formatting
-const printer = new Printer({
- indentSize: 2,
- indentType: 'space',
- lineEnding: '\n',
- preserveWhitespace: true
-})
-
-// Convert AST back to source
-const source = printer.print(ast)
-console.log(source)
-// <%= @user.name %>
-```
-
-### Advanced Configuration
-
-```javascript
-const printer = new Printer({
- // Indentation
- indentSize: 2,
- indentType: 'space', // 'space', 'tab', 'mixed'
-
- // Line handling
- lineEnding: '\n', // '\n', '\r\n', 'auto'
- maxLineLength: 80,
- wrapLongLines: true,
-
- // ERB formatting
- erbTagSpacing: 'compact', // 'compact', 'spaced', 'preserve'
- erbOutputPadding: true,
-
- // HTML formatting
- htmlAttributeWrap: 'auto', // 'auto', 'force', 'never'
- htmlSelfClosing: 'xml', // 'xml', 'html', 'preserve'
-
- // Comment handling
- preserveComments: true,
- commentIndentation: 'align',
-
- // Source maps
- generateSourceMap: true,
- sourceMapMode: 'inline' // 'inline', 'external', 'comment'
-})
-```
-
-### Ruby API
-
-```ruby
-require 'herb/printer'
-
-# Parse template
-ast = Herb.parse('<%= @user.name %>
')
-
-# Create printer
-printer = Herb::Printer.new(
- indent_size: 2,
- indent_type: :space,
- erb_tag_spacing: :compact
-)
-
-# Generate source code
-source = printer.print(ast)
-puts source
-```
-
-## Formatting Styles
-
-### ERB Tag Styles
-
-```javascript
-// Compact style (default)
-printer.setOption('erbTagSpacing', 'compact')
-// Output: <%=@user.name%>
-
-// Spaced style
-printer.setOption('erbTagSpacing', 'spaced')
-// Output: <%= @user.name %>
-
-// Preserve original
-printer.setOption('erbTagSpacing', 'preserve')
-// Output: (maintains original spacing)
-```
-
-### HTML Attribute Formatting
-
-```javascript
-const ast = Herb.parse('')
-
-// Auto wrap (default)
-printer.setOption('htmlAttributeWrap', 'auto')
-printer.setOption('maxLineLength', 40)
-// Output:
-//
-
-// Force single line
-printer.setOption('htmlAttributeWrap', 'never')
-// Output:
-```
-
-### Indentation Styles
-
-```javascript
-// Spaces (default)
-printer.setOption('indentType', 'space')
-printer.setOption('indentSize', 2)
-
-// Tabs
-printer.setOption('indentType', 'tab')
-printer.setOption('indentSize', 1)
-
-// Mixed (tabs for structure, spaces for alignment)
-printer.setOption('indentType', 'mixed')
-```
-
-## Integration with Tools
-
-### Formatter Integration
-
-```javascript
-// formatter.js
-import { Formatter } from '@herb-tools/formatter'
-import { Printer } from '@herb-tools/printer'
-
-class Formatter {
- constructor(options) {
- this.printer = new Printer(options.printer)
- }
-
- format(source) {
- // Parse source to AST
- const ast = this.parse(source)
-
- // Apply formatting transformations
- const formattedAST = this.transform(ast)
-
- // Use printer to generate formatted source
- return this.printer.print(formattedAST)
- }
-}
-```
-
-### Minifier Integration
-
-```javascript
-// minifier.js
-import { Minifier } from '@herb-tools/minifier'
-import { Printer } from '@herb-tools/printer'
-
-class Minifier {
- constructor() {
- this.printer = new Printer({
- indentSize: 0,
- preserveWhitespace: false,
- erbTagSpacing: 'compact',
- generateSourceMap: false
- })
- }
-
- minify(source) {
- const ast = this.parse(source)
- const minifiedAST = this.optimize(ast)
- return this.printer.print(minifiedAST)
- }
-}
-```
-
-### Custom Code Generator
-
-```javascript
-class TemplateGenerator {
- constructor() {
- this.printer = new Printer({
- indentSize: 2,
- erbTagSpacing: 'spaced'
- })
- }
-
- generateTemplate(data) {
- // Build AST programmatically
- const ast = this.buildAST(data)
-
- // Generate source code
- return this.printer.print(ast)
- }
-
- buildAST(data) {
- return {
- type: 'Document',
- children: [
- {
- type: 'HTMLElement',
- tagName: 'div',
- attributes: [
- {
- type: 'HTMLAttribute',
- name: 'class',
- value: data.className
- }
- ],
- children: [
- {
- type: 'ERBContent',
- rubyCode: `@${data.variable}.${data.method}`
- }
- ]
- }
- ]
- }
- }
-}
-```
-
-## Source Maps
-
-### Generating Source Maps
-
-```javascript
-const printer = new Printer({
- generateSourceMap: true,
- sourceMapMode: 'external'
-})
-
-const result = printer.printWithSourceMap(ast)
-console.log(result.code) // Generated source code
-console.log(result.map) // Source map object
-
-// Save source map to file
-fs.writeFileSync('output.erb.map', JSON.stringify(result.map))
-```
-
-### Source Map Format
-
-```json
-{
- "version": 3,
- "sources": ["original.erb"],
- "names": [],
- "mappings": "AAAA,KAAK,CAAC,KAAK",
- "sourceRoot": "",
- "sourcesContent": ["
<%= @user.name %>
"]
-}
-```
-
-### Using Source Maps
-
-```javascript
-// Debug formatted code
-const { SourceMapConsumer } = require('source-map')
-
-async function debugPosition(line, column) {
- const consumer = await new SourceMapConsumer(sourceMap)
- const original = consumer.originalPositionFor({ line, column })
-
- console.log(`Generated position ${line}:${column} maps to original ${original.line}:${original.column}`)
-}
-```
-
-## API Reference
-
-### Printer Class
-
-```typescript
-class Printer {
- constructor(options: PrinterOptions)
-
- // Core printing methods
- print(ast: ASTNode): string
- printWithSourceMap(ast: ASTNode): { code: string, map: SourceMap }
-
- // Configuration
- setOption(key: string, value: any): void
- setOptions(options: Partial
): void
- getOptions(): PrinterOptions
-
- // Utility methods
- static defaultOptions(): PrinterOptions
- static validateAST(ast: ASTNode): ValidationResult
-}
-
-interface PrinterOptions {
- // Indentation
- indentSize: number
- indentType: 'space' | 'tab' | 'mixed'
-
- // Line handling
- lineEnding: '\n' | '\r\n' | 'auto'
- maxLineLength: number
- wrapLongLines: boolean
-
- // ERB formatting
- erbTagSpacing: 'compact' | 'spaced' | 'preserve'
- erbOutputPadding: boolean
-
- // HTML formatting
- htmlAttributeWrap: 'auto' | 'force' | 'never'
- htmlSelfClosing: 'xml' | 'html' | 'preserve'
-
- // Comments
- preserveComments: boolean
- commentIndentation: 'align' | 'indent' | 'preserve'
-
- // Source maps
- generateSourceMap: boolean
- sourceMapMode: 'inline' | 'external' | 'comment'
-}
-```
-
-### Node Handlers
-
-```javascript
-class Printer {
- // Override specific node handlers
- printHTMLElement(node, context) {
- // Custom HTML element printing
- return this.defaultPrintHTMLElement(node, context)
- }
-
- printERBContent(node, context) {
- // Custom ERB content printing
- return this.defaultPrintERBContent(node, context)
- }
-
- printHTMLAttribute(node, context) {
- // Custom attribute printing
- return this.defaultPrintHTMLAttribute(node, context)
- }
-}
-```
-
-## Performance
-
-### Benchmarks
-
-| AST Size | Print Time | Memory Usage |
-|----------|------------|--------------|
-| Small (10 nodes) | 0.1ms | 50 KB |
-| Medium (100 nodes) | 0.8ms | 200 KB |
-| Large (1000 nodes) | 6ms | 1.5 MB |
-
-### Optimization Tips
-
-```javascript
-// Reuse printer instances
-const printer = new Printer(options)
-
-// Batch processing
-const results = asts.map(ast => printer.print(ast))
-
-// Disable source maps for production
-const prodPrinter = new Printer({
- ...options,
- generateSourceMap: false
-})
-
-// Stream large ASTs
-const stream = printer.printStream(largeAST)
-```
-
-## Testing
-
-### Unit Tests
-
-```javascript
-import { Printer } from '@herb-tools/printer'
-import { Herb } from '@herb-tools/node'
-
-describe('Printer', () => {
- test('round-trip preservation', () => {
- const source = '<%= @user.name %>
'
- const ast = Herb.parse(source)
- const printed = new Printer().print(ast)
-
- expect(printed).toBe(source)
- })
-
- test('custom formatting', () => {
- const ast = Herb.parse('<%=@user.name%>')
- const printer = new Printer({ erbTagSpacing: 'spaced' })
- const printed = printer.print(ast)
-
- expect(printed).toBe('<%= @user.name %>')
- })
-})
-```
-
-### Integration Tests
-
-```javascript
-test('formatter integration', () => {
- const unformatted = '<%=@user.name%>
'
- const formatted = new Formatter().format(unformatted)
-
- expect(formatted).toBe('\n <%= @user.name %>\n
')
-})
-```
-
-## Contributing
-
-Areas for contribution:
-- Additional formatting options
-- Performance optimizations
-- Source map enhancements
-- Tool integrations
-
-## Resources
-
-- [GitHub Repository](https://github.com/marcoroth/herb/tree/main/javascript/packages/printer)
-- [API Documentation](https://herb-tools.dev/printer/api)
-- [Source Map Specification](https://sourcemaps.info/spec.html)
diff --git a/src/analyze.c b/src/analyze.c
index 8c0a7ea4a..c6204e27e 100644
--- a/src/analyze.c
+++ b/src/analyze.c
@@ -1,7 +1,6 @@
#include "include/analyze.h"
#include "include/analyze_helpers.h"
#include "include/analyzed_ruby.h"
-#include "include/array.h"
#include "include/ast_nodes.h"
#include "include/errors.h"
#include "include/extract.h"
@@ -11,6 +10,8 @@
#include "include/prism_helpers.h"
#include "include/token_struct.h"
#include "include/util.h"
+#include "include/util/hb_array.h"
+#include "include/util/hb_string.h"
#include "include/visitor.h"
#include
@@ -19,7 +20,7 @@
#include
#include
-static analyzed_ruby_T* herb_analyze_ruby(char* source) {
+static analyzed_ruby_T* herb_analyze_ruby(hb_string_T source) {
analyzed_ruby_T* analyzed = init_analyzed_ruby(source);
pm_visit_node(analyzed->root, search_if_nodes, analyzed);
@@ -52,9 +53,7 @@ static bool analyze_erb_content(const AST_NODE_T* node, void* data) {
const char* opening = erb_content_node->tag_opening->value;
if (strcmp(opening, "<%%") != 0 && strcmp(opening, "<%%=") != 0 && strcmp(opening, "<%#") != 0) {
- analyzed_ruby_T* analyzed = herb_analyze_ruby(erb_content_node->content->value);
-
- if (false) { pretty_print_analyzed_ruby(analyzed, erb_content_node->content->value); }
+ analyzed_ruby_T* analyzed = herb_analyze_ruby(hb_string(erb_content_node->content->value));
erb_content_node->parsed = true;
erb_content_node->valid = analyzed->valid;
@@ -73,16 +72,16 @@ static bool analyze_erb_content(const AST_NODE_T* node, void* data) {
static size_t process_block_children(
AST_NODE_T* node,
- array_T* array,
+ hb_array_T* array,
size_t index,
- array_T* children_array,
+ hb_array_T* children_array,
analyze_ruby_context_T* context,
control_type_t parent_type
);
static size_t process_subsequent_block(
AST_NODE_T* node,
- array_T* array,
+ hb_array_T* array,
size_t index,
AST_NODE_T** subsequent_out,
analyze_ruby_context_T* context,
@@ -96,12 +95,8 @@ static control_type_t detect_control_type(AST_ERB_CONTENT_NODE_T* erb_node) {
if (!ruby) { return CONTROL_TYPE_UNKNOWN; }
- if (ruby->valid) {
- if (has_yield_node(ruby)) { return CONTROL_TYPE_YIELD; }
- return CONTROL_TYPE_UNKNOWN;
- }
+ if (ruby->valid) { return CONTROL_TYPE_UNKNOWN; }
- if (has_yield_node(ruby)) { return CONTROL_TYPE_YIELD; }
if (has_block_node(ruby)) { return CONTROL_TYPE_BLOCK; }
if (has_if_node(ruby)) { return CONTROL_TYPE_IF; }
if (has_elsif_node(ruby)) { return CONTROL_TYPE_ELSIF; }
@@ -119,6 +114,7 @@ static control_type_t detect_control_type(AST_ERB_CONTENT_NODE_T* erb_node) {
if (has_until_node(ruby)) { return CONTROL_TYPE_UNTIL; }
if (has_for_node(ruby)) { return CONTROL_TYPE_FOR; }
if (has_block_closing(ruby)) { return CONTROL_TYPE_BLOCK_CLOSE; }
+ if (has_yield_node(ruby)) { return CONTROL_TYPE_YIELD; }
return CONTROL_TYPE_UNKNOWN;
}
@@ -152,22 +148,22 @@ static bool is_terminator_type(control_type_t parent_type, control_type_t child_
static AST_NODE_T* create_control_node(
AST_ERB_CONTENT_NODE_T* erb_node,
- array_T* children,
+ hb_array_T* children,
AST_NODE_T* subsequent,
AST_ERB_END_NODE_T* end_node,
control_type_t control_type
) {
- array_T* errors = array_init(8);
- position_T* start_position = erb_node->tag_opening->location->start;
- position_T* end_position = erb_node->tag_closing->location->end;
+ hb_array_T* errors = hb_array_init(8);
+ position_T start_position = erb_node->tag_opening->location.start;
+ position_T end_position = erb_node->tag_closing->location.end;
if (end_node) {
- end_position = end_node->base.location->end;
- } else if (children && array_size(children) > 0) {
- AST_NODE_T* last_child = array_get(children, array_size(children) - 1);
- end_position = last_child->location->end;
+ end_position = end_node->base.location.end;
+ } else if (children && hb_array_size(children) > 0) {
+ AST_NODE_T* last_child = hb_array_get(children, hb_array_size(children) - 1);
+ end_position = last_child->location.end;
} else if (subsequent) {
- end_position = subsequent->location->end;
+ end_position = subsequent->location.end;
}
token_T* tag_opening = erb_node->tag_opening;
@@ -190,30 +186,31 @@ static AST_NODE_T* create_control_node(
);
case CONTROL_TYPE_ELSE:
- return (AST_NODE_T*)
- ast_erb_else_node_init(tag_opening, content, tag_closing, children, start_position, end_position, errors);
+ return (
+ AST_NODE_T*
+ ) ast_erb_else_node_init(tag_opening, content, tag_closing, children, start_position, end_position, errors);
case CONTROL_TYPE_CASE:
case CONTROL_TYPE_CASE_MATCH: {
AST_ERB_ELSE_NODE_T* else_node = NULL;
if (subsequent && subsequent->type == AST_ERB_ELSE_NODE) { else_node = (AST_ERB_ELSE_NODE_T*) subsequent; }
- array_T* when_conditions = array_init(8);
- array_T* in_conditions = array_init(8);
- array_T* non_when_non_in_children = array_init(8);
+ hb_array_T* when_conditions = hb_array_init(8);
+ hb_array_T* in_conditions = hb_array_init(8);
+ hb_array_T* non_when_non_in_children = hb_array_init(8);
- for (size_t i = 0; i < array_size(children); i++) {
- AST_NODE_T* child = array_get(children, i);
+ for (size_t i = 0; i < hb_array_size(children); i++) {
+ AST_NODE_T* child = hb_array_get(children, i);
if (child && child->type == AST_ERB_WHEN_NODE) {
- array_append(when_conditions, child);
+ hb_array_append(when_conditions, child);
} else if (child && child->type == AST_ERB_IN_NODE) {
- array_append(in_conditions, child);
+ hb_array_append(in_conditions, child);
} else {
- array_append(non_when_non_in_children, child);
+ hb_array_append(non_when_non_in_children, child);
}
}
- if (array_size(in_conditions) > 0) {
+ if (hb_array_size(in_conditions) > 0) {
return (AST_NODE_T*) ast_erb_case_match_node_init(
tag_opening,
content,
@@ -243,13 +240,15 @@ static AST_NODE_T* create_control_node(
}
case CONTROL_TYPE_WHEN: {
- return (AST_NODE_T*)
- ast_erb_when_node_init(tag_opening, content, tag_closing, children, start_position, end_position, errors);
+ return (
+ AST_NODE_T*
+ ) ast_erb_when_node_init(tag_opening, content, tag_closing, children, start_position, end_position, errors);
}
case CONTROL_TYPE_IN: {
- return (AST_NODE_T*)
- ast_erb_in_node_init(tag_opening, content, tag_closing, children, start_position, end_position, errors);
+ return (
+ AST_NODE_T*
+ ) ast_erb_in_node_init(tag_opening, content, tag_closing, children, start_position, end_position, errors);
}
case CONTROL_TYPE_BEGIN: {
@@ -300,8 +299,9 @@ static AST_NODE_T* create_control_node(
}
case CONTROL_TYPE_ENSURE: {
- return (AST_NODE_T*)
- ast_erb_ensure_node_init(tag_opening, content, tag_closing, children, start_position, end_position, errors);
+ return (
+ AST_NODE_T*
+ ) ast_erb_ensure_node_init(tag_opening, content, tag_closing, children, start_position, end_position, errors);
}
case CONTROL_TYPE_UNLESS: {
@@ -375,34 +375,35 @@ static AST_NODE_T* create_control_node(
}
case CONTROL_TYPE_YIELD: {
- return (AST_NODE_T*)
- ast_erb_yield_node_init(tag_opening, content, tag_closing, start_position, end_position, errors);
+ return (
+ AST_NODE_T*
+ ) ast_erb_yield_node_init(tag_opening, content, tag_closing, start_position, end_position, errors);
}
- default: array_free(&errors); return NULL;
+ default: hb_array_free(&errors); return NULL;
}
}
static size_t process_control_structure(
AST_NODE_T* node,
- array_T* array,
+ hb_array_T* array,
size_t index,
- array_T* output_array,
+ hb_array_T* output_array,
analyze_ruby_context_T* context,
control_type_t initial_type
) {
- AST_ERB_CONTENT_NODE_T* erb_node = (AST_ERB_CONTENT_NODE_T*) array_get(array, index);
- array_T* children = array_init(8);
+ AST_ERB_CONTENT_NODE_T* erb_node = (AST_ERB_CONTENT_NODE_T*) hb_array_get(array, index);
+ hb_array_T* children = hb_array_init(8);
index++;
if (initial_type == CONTROL_TYPE_CASE || initial_type == CONTROL_TYPE_CASE_MATCH) {
- array_T* when_conditions = array_init(8);
- array_T* in_conditions = array_init(8);
- array_T* non_when_non_in_children = array_init(8);
+ hb_array_T* when_conditions = hb_array_init(8);
+ hb_array_T* in_conditions = hb_array_init(8);
+ hb_array_T* non_when_non_in_children = hb_array_init(8);
- while (index < array_size(array)) {
- AST_NODE_T* next_node = array_get(array, index);
+ while (index < hb_array_size(array)) {
+ AST_NODE_T* next_node = hb_array_get(array, index);
if (!next_node) { break; }
@@ -413,17 +414,17 @@ static size_t process_control_structure(
if (next_type == CONTROL_TYPE_WHEN || next_type == CONTROL_TYPE_IN) { break; }
}
- array_append(non_when_non_in_children, next_node);
+ hb_array_append(non_when_non_in_children, next_node);
index++;
}
- while (index < array_size(array)) {
- AST_NODE_T* next_node = array_get(array, index);
+ while (index < hb_array_size(array)) {
+ AST_NODE_T* next_node = hb_array_get(array, index);
if (!next_node) { break; }
if (next_node->type != AST_ERB_CONTENT_NODE) {
- array_append(non_when_non_in_children, next_node);
+ hb_array_append(non_when_non_in_children, next_node);
index++;
continue;
}
@@ -432,101 +433,67 @@ static size_t process_control_structure(
control_type_t next_type = detect_control_type(erb_content);
if (next_type == CONTROL_TYPE_WHEN) {
- array_T* when_statements = array_init(8);
+ hb_array_T* when_statements = hb_array_init(8);
index++;
- while (index < array_size(array)) {
- AST_NODE_T* child = array_get(array, index);
-
- if (!child) { break; }
-
- if (child->type == AST_ERB_CONTENT_NODE) {
- AST_ERB_CONTENT_NODE_T* child_erb = (AST_ERB_CONTENT_NODE_T*) child;
- control_type_t child_type = detect_control_type(child_erb);
-
- if (child_type == CONTROL_TYPE_WHEN || child_type == CONTROL_TYPE_IN || child_type == CONTROL_TYPE_ELSE
- || child_type == CONTROL_TYPE_END) {
- break;
- }
- }
-
- array_append(when_statements, child);
- index++;
- }
+ index = process_block_children(node, array, index, when_statements, context, CONTROL_TYPE_WHEN);
AST_ERB_WHEN_NODE_T* when_node = ast_erb_when_node_init(
erb_content->tag_opening,
erb_content->content,
erb_content->tag_closing,
when_statements,
- erb_content->tag_opening->location->start,
- erb_content->tag_closing->location->end,
- array_init(8)
+ erb_content->tag_opening->location.start,
+ erb_content->tag_closing->location.end,
+ hb_array_init(8)
);
- array_append(when_conditions, (AST_NODE_T*) when_node);
+ hb_array_append(when_conditions, (AST_NODE_T*) when_node);
continue;
} else if (next_type == CONTROL_TYPE_IN) {
- array_T* in_statements = array_init(8);
+ hb_array_T* in_statements = hb_array_init(8);
index++;
- while (index < array_size(array)) {
- AST_NODE_T* child = array_get(array, index);
-
- if (!child) { break; }
-
- if (child->type == AST_ERB_CONTENT_NODE) {
- AST_ERB_CONTENT_NODE_T* child_erb = (AST_ERB_CONTENT_NODE_T*) child;
- control_type_t child_type = detect_control_type(child_erb);
-
- if (child_type == CONTROL_TYPE_IN || child_type == CONTROL_TYPE_WHEN || child_type == CONTROL_TYPE_ELSE
- || child_type == CONTROL_TYPE_END) {
- break;
- }
- }
-
- array_append(in_statements, child);
- index++;
- }
+ index = process_block_children(node, array, index, in_statements, context, CONTROL_TYPE_IN);
AST_ERB_IN_NODE_T* in_node = ast_erb_in_node_init(
erb_content->tag_opening,
erb_content->content,
erb_content->tag_closing,
in_statements,
- erb_content->tag_opening->location->start,
- erb_content->tag_closing->location->end,
- array_init(8)
+ erb_content->tag_opening->location.start,
+ erb_content->tag_closing->location.end,
+ hb_array_init(8)
);
- array_append(in_conditions, (AST_NODE_T*) in_node);
+ hb_array_append(in_conditions, (AST_NODE_T*) in_node);
continue;
} else if (next_type == CONTROL_TYPE_ELSE || next_type == CONTROL_TYPE_END) {
break;
} else {
- array_append(non_when_non_in_children, next_node);
+ hb_array_append(non_when_non_in_children, next_node);
index++;
}
}
AST_ERB_ELSE_NODE_T* else_clause = NULL;
- if (index < array_size(array)) {
- AST_NODE_T* next_node = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* next_node = hb_array_get(array, index);
if (next_node && next_node->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* next_erb = (AST_ERB_CONTENT_NODE_T*) next_node;
control_type_t next_type = detect_control_type(next_erb);
if (next_type == CONTROL_TYPE_ELSE) {
- array_T* else_children = array_init(8);
+ hb_array_T* else_children = hb_array_init(8);
index++;
- while (index < array_size(array)) {
- AST_NODE_T* child = array_get(array, index);
+ while (index < hb_array_size(array)) {
+ AST_NODE_T* child = hb_array_get(array, index);
if (!child) { break; }
@@ -537,7 +504,7 @@ static size_t process_control_structure(
if (child_type == CONTROL_TYPE_END) { break; }
}
- array_append(else_children, child);
+ hb_array_append(else_children, child);
index++;
}
@@ -546,9 +513,9 @@ static size_t process_control_structure(
next_erb->content,
next_erb->tag_closing,
else_children,
- next_erb->tag_opening->location->start,
- next_erb->tag_closing->location->end,
- array_init(8)
+ next_erb->tag_opening->location.start,
+ next_erb->tag_closing->location.end,
+ hb_array_init(8)
);
}
}
@@ -556,8 +523,8 @@ static size_t process_control_structure(
AST_ERB_END_NODE_T* end_node = NULL;
- if (index < array_size(array)) {
- AST_NODE_T* potential_end = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* potential_end = hb_array_get(array, index);
if (potential_end && potential_end->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* end_erb = (AST_ERB_CONTENT_NODE_T*) potential_end;
@@ -567,8 +534,8 @@ static size_t process_control_structure(
end_erb->tag_opening,
end_erb->content,
end_erb->tag_closing,
- end_erb->tag_opening->location->start,
- end_erb->tag_closing->location->end,
+ end_erb->tag_opening->location.start,
+ end_erb->tag_closing->location.end,
end_erb->base.errors
);
@@ -577,22 +544,22 @@ static size_t process_control_structure(
}
}
- position_T* start_position = erb_node->tag_opening->location->start;
- position_T* end_position = erb_node->tag_closing->location->end;
+ position_T start_position = erb_node->tag_opening->location.start;
+ position_T end_position = erb_node->tag_closing->location.end;
if (end_node) {
- end_position = end_node->base.location->end;
+ end_position = end_node->base.location.end;
} else if (else_clause) {
- end_position = else_clause->base.location->end;
- } else if (array_size(when_conditions) > 0) {
- AST_NODE_T* last_when = array_get(when_conditions, array_size(when_conditions) - 1);
- end_position = last_when->location->end;
- } else if (array_size(in_conditions) > 0) {
- AST_NODE_T* last_in = array_get(in_conditions, array_size(in_conditions) - 1);
- end_position = last_in->location->end;
+ end_position = else_clause->base.location.end;
+ } else if (hb_array_size(when_conditions) > 0) {
+ AST_NODE_T* last_when = hb_array_get(when_conditions, hb_array_size(when_conditions) - 1);
+ end_position = last_when->location.end;
+ } else if (hb_array_size(in_conditions) > 0) {
+ AST_NODE_T* last_in = hb_array_get(in_conditions, hb_array_size(in_conditions) - 1);
+ end_position = last_in->location.end;
}
- if (array_size(in_conditions) > 0) {
+ if (hb_array_size(in_conditions) > 0) {
AST_ERB_CASE_MATCH_NODE_T* case_match_node = ast_erb_case_match_node_init(
erb_node->tag_opening,
erb_node->content,
@@ -603,10 +570,10 @@ static size_t process_control_structure(
end_node,
start_position,
end_position,
- array_init(8)
+ hb_array_init(8)
);
- array_append(output_array, (AST_NODE_T*) case_match_node);
+ hb_array_append(output_array, (AST_NODE_T*) case_match_node);
return index;
}
@@ -620,10 +587,10 @@ static size_t process_control_structure(
end_node,
start_position,
end_position,
- array_init(8)
+ hb_array_init(8)
);
- array_append(output_array, (AST_NODE_T*) case_node);
+ hb_array_append(output_array, (AST_NODE_T*) case_node);
return index;
}
@@ -634,8 +601,8 @@ static size_t process_control_structure(
AST_ERB_ELSE_NODE_T* else_clause = NULL;
AST_ERB_ENSURE_NODE_T* ensure_clause = NULL;
- if (index < array_size(array)) {
- AST_NODE_T* next_node = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* next_node = hb_array_get(array, index);
if (next_node && next_node->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* next_erb = (AST_ERB_CONTENT_NODE_T*) next_node;
@@ -649,20 +616,20 @@ static size_t process_control_structure(
}
}
- if (index < array_size(array)) {
- AST_NODE_T* next_node = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* next_node = hb_array_get(array, index);
if (next_node && next_node->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* next_erb = (AST_ERB_CONTENT_NODE_T*) next_node;
control_type_t next_type = detect_control_type(next_erb);
if (next_type == CONTROL_TYPE_ELSE) {
- array_T* else_children = array_init(8);
+ hb_array_T* else_children = hb_array_init(8);
index++;
- while (index < array_size(array)) {
- AST_NODE_T* child = array_get(array, index);
+ while (index < hb_array_size(array)) {
+ AST_NODE_T* child = hb_array_get(array, index);
if (!child) { break; }
@@ -673,7 +640,7 @@ static size_t process_control_structure(
if (child_type == CONTROL_TYPE_ENSURE || child_type == CONTROL_TYPE_END) { break; }
}
- array_append(else_children, child);
+ hb_array_append(else_children, child);
index++;
}
@@ -682,28 +649,28 @@ static size_t process_control_structure(
next_erb->content,
next_erb->tag_closing,
else_children,
- next_erb->tag_opening->location->start,
- next_erb->tag_closing->location->end,
- array_init(8)
+ next_erb->tag_opening->location.start,
+ next_erb->tag_closing->location.end,
+ hb_array_init(8)
);
}
}
}
- if (index < array_size(array)) {
- AST_NODE_T* next_node = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* next_node = hb_array_get(array, index);
if (next_node && next_node->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* next_erb = (AST_ERB_CONTENT_NODE_T*) next_node;
control_type_t next_type = detect_control_type(next_erb);
if (next_type == CONTROL_TYPE_ENSURE) {
- array_T* ensure_children = array_init(8);
+ hb_array_T* ensure_children = hb_array_init(8);
index++;
- while (index < array_size(array)) {
- AST_NODE_T* child = array_get(array, index);
+ while (index < hb_array_size(array)) {
+ AST_NODE_T* child = hb_array_get(array, index);
if (!child) { break; }
@@ -714,7 +681,7 @@ static size_t process_control_structure(
if (child_type == CONTROL_TYPE_END) { break; }
}
- array_append(ensure_children, child);
+ hb_array_append(ensure_children, child);
index++;
}
@@ -723,9 +690,9 @@ static size_t process_control_structure(
next_erb->content,
next_erb->tag_closing,
ensure_children,
- next_erb->tag_opening->location->start,
- next_erb->tag_closing->location->end,
- array_init(8)
+ next_erb->tag_opening->location.start,
+ next_erb->tag_closing->location.end,
+ hb_array_init(8)
);
}
}
@@ -733,8 +700,8 @@ static size_t process_control_structure(
AST_ERB_END_NODE_T* end_node = NULL;
- if (index < array_size(array)) {
- AST_NODE_T* potential_end = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* potential_end = hb_array_get(array, index);
if (potential_end && potential_end->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* end_erb = (AST_ERB_CONTENT_NODE_T*) potential_end;
@@ -744,8 +711,8 @@ static size_t process_control_structure(
end_erb->tag_opening,
end_erb->content,
end_erb->tag_closing,
- end_erb->tag_opening->location->start,
- end_erb->tag_closing->location->end,
+ end_erb->tag_opening->location.start,
+ end_erb->tag_closing->location.end,
end_erb->base.errors
);
@@ -754,17 +721,17 @@ static size_t process_control_structure(
}
}
- position_T* start_position = erb_node->tag_opening->location->start;
- position_T* end_position = erb_node->tag_closing->location->end;
+ position_T start_position = erb_node->tag_opening->location.start;
+ position_T end_position = erb_node->tag_closing->location.end;
if (end_node) {
- end_position = end_node->base.location->end;
+ end_position = end_node->base.location.end;
} else if (ensure_clause) {
- end_position = ensure_clause->base.location->end;
+ end_position = ensure_clause->base.location.end;
} else if (else_clause) {
- end_position = else_clause->base.location->end;
+ end_position = else_clause->base.location.end;
} else if (rescue_clause) {
- end_position = rescue_clause->base.location->end;
+ end_position = rescue_clause->base.location.end;
}
AST_ERB_BEGIN_NODE_T* begin_node = ast_erb_begin_node_init(
@@ -778,10 +745,10 @@ static size_t process_control_structure(
end_node,
start_position,
end_position,
- array_init(8)
+ hb_array_init(8)
);
- array_append(output_array, (AST_NODE_T*) begin_node);
+ hb_array_append(output_array, (AST_NODE_T*) begin_node);
return index;
}
@@ -790,8 +757,8 @@ static size_t process_control_structure(
AST_ERB_END_NODE_T* end_node = NULL;
- if (index < array_size(array)) {
- AST_NODE_T* potential_close = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* potential_close = hb_array_get(array, index);
if (potential_close && potential_close->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* close_erb = (AST_ERB_CONTENT_NODE_T*) potential_close;
@@ -802,8 +769,8 @@ static size_t process_control_structure(
close_erb->tag_opening,
close_erb->content,
close_erb->tag_closing,
- close_erb->tag_opening->location->start,
- close_erb->tag_closing->location->end,
+ close_erb->tag_opening->location.start,
+ close_erb->tag_closing->location.end,
close_erb->base.errors
);
@@ -812,14 +779,14 @@ static size_t process_control_structure(
}
}
- position_T* start_position = erb_node->tag_opening->location->start;
- position_T* end_position = erb_node->tag_closing->location->end;
+ position_T start_position = erb_node->tag_opening->location.start;
+ position_T end_position = erb_node->tag_closing->location.end;
if (end_node) {
- end_position = end_node->base.location->end;
- } else if (children && array_size(children) > 0) {
- AST_NODE_T* last_child = array_get(children, array_size(children) - 1);
- end_position = last_child->location->end;
+ end_position = end_node->base.location.end;
+ } else if (children && hb_array_size(children) > 0) {
+ AST_NODE_T* last_child = hb_array_get(children, hb_array_size(children) - 1);
+ end_position = last_child->location.end;
}
AST_ERB_BLOCK_NODE_T* block_node = ast_erb_block_node_init(
@@ -830,10 +797,10 @@ static size_t process_control_structure(
end_node,
start_position,
end_position,
- array_init(8)
+ hb_array_init(8)
);
- array_append(output_array, (AST_NODE_T*) block_node);
+ hb_array_append(output_array, (AST_NODE_T*) block_node);
return index;
}
@@ -842,8 +809,8 @@ static size_t process_control_structure(
AST_NODE_T* subsequent = NULL;
AST_ERB_END_NODE_T* end_node = NULL;
- if (index < array_size(array)) {
- AST_NODE_T* next_node = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* next_node = hb_array_get(array, index);
if (next_node && next_node->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* next_erb = (AST_ERB_CONTENT_NODE_T*) next_node;
@@ -855,8 +822,8 @@ static size_t process_control_structure(
}
}
- if (index < array_size(array)) {
- AST_NODE_T* potential_end = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* potential_end = hb_array_get(array, index);
if (potential_end && potential_end->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* end_erb = (AST_ERB_CONTENT_NODE_T*) potential_end;
@@ -866,8 +833,8 @@ static size_t process_control_structure(
end_erb->tag_opening,
end_erb->content,
end_erb->tag_closing,
- end_erb->tag_opening->location->start,
- end_erb->tag_closing->location->end,
+ end_erb->tag_opening->location.start,
+ end_erb->tag_closing->location.end,
end_erb->base.errors
);
@@ -878,22 +845,22 @@ static size_t process_control_structure(
AST_NODE_T* control_node = create_control_node(erb_node, children, subsequent, end_node, initial_type);
- if (control_node) { array_append(output_array, control_node); }
+ if (control_node) { hb_array_append(output_array, control_node); }
return index;
}
static size_t process_subsequent_block(
AST_NODE_T* node,
- array_T* array,
+ hb_array_T* array,
size_t index,
AST_NODE_T** subsequent_out,
analyze_ruby_context_T* context,
control_type_t parent_type
) {
- AST_ERB_CONTENT_NODE_T* erb_node = (AST_ERB_CONTENT_NODE_T*) array_get(array, index);
+ AST_ERB_CONTENT_NODE_T* erb_node = (AST_ERB_CONTENT_NODE_T*) hb_array_get(array, index);
control_type_t type = detect_control_type(erb_node);
- array_T* children = array_init(8);
+ hb_array_T* children = hb_array_init(8);
index++;
@@ -901,8 +868,8 @@ static size_t process_subsequent_block(
AST_NODE_T* subsequent_node = create_control_node(erb_node, children, NULL, NULL, type);
- if (index < array_size(array)) {
- AST_NODE_T* next_node = array_get(array, index);
+ if (index < hb_array_size(array)) {
+ AST_NODE_T* next_node = hb_array_get(array, index);
if (next_node && next_node->type == AST_ERB_CONTENT_NODE) {
AST_ERB_CONTENT_NODE_T* next_erb = (AST_ERB_CONTENT_NODE_T*) next_node;
@@ -953,19 +920,19 @@ static size_t process_subsequent_block(
static size_t process_block_children(
AST_NODE_T* node,
- array_T* array,
+ hb_array_T* array,
size_t index,
- array_T* children_array,
+ hb_array_T* children_array,
analyze_ruby_context_T* context,
control_type_t parent_type
) {
- while (index < array_size(array)) {
- AST_NODE_T* child = array_get(array, index);
+ while (index < hb_array_size(array)) {
+ AST_NODE_T* child = hb_array_get(array, index);
if (!child) { break; }
if (child->type != AST_ERB_CONTENT_NODE) {
- array_append(children_array, child);
+ hb_array_append(children_array, child);
index++;
continue;
}
@@ -978,35 +945,35 @@ static size_t process_block_children(
if (child_type == CONTROL_TYPE_IF || child_type == CONTROL_TYPE_CASE || child_type == CONTROL_TYPE_CASE_MATCH
|| child_type == CONTROL_TYPE_BEGIN || child_type == CONTROL_TYPE_UNLESS || child_type == CONTROL_TYPE_WHILE
|| child_type == CONTROL_TYPE_UNTIL || child_type == CONTROL_TYPE_FOR || child_type == CONTROL_TYPE_BLOCK) {
- array_T* temp_array = array_init(1);
+ hb_array_T* temp_array = hb_array_init(1);
size_t new_index = process_control_structure(node, array, index, temp_array, context, child_type);
- if (array_size(temp_array) > 0) { array_append(children_array, array_get(temp_array, 0)); }
+ if (hb_array_size(temp_array) > 0) { hb_array_append(children_array, hb_array_get(temp_array, 0)); }
- array_free(&temp_array);
+ hb_array_free(&temp_array);
index = new_index;
continue;
}
- array_append(children_array, child);
+ hb_array_append(children_array, child);
index++;
}
return index;
}
-static array_T* rewrite_node_array(AST_NODE_T* node, array_T* array, analyze_ruby_context_T* context) {
- array_T* new_array = array_init(array_size(array));
+static hb_array_T* rewrite_node_array(AST_NODE_T* node, hb_array_T* array, analyze_ruby_context_T* context) {
+ hb_array_T* new_array = hb_array_init(hb_array_size(array));
size_t index = 0;
- while (index < array_size(array)) {
- AST_NODE_T* item = array_get(array, index);
+ while (index < hb_array_size(array)) {
+ AST_NODE_T* item = hb_array_get(array, index);
if (!item) { break; }
if (item->type != AST_ERB_CONTENT_NODE) {
- array_append(new_array, item);
+ hb_array_append(new_array, item);
index++;
continue;
}
@@ -1028,12 +995,12 @@ static array_T* rewrite_node_array(AST_NODE_T* node, array_T* array, analyze_rub
continue;
case CONTROL_TYPE_YIELD: {
- AST_NODE_T* yield_node = create_control_node(erb_node, array_init(8), NULL, NULL, type);
+ AST_NODE_T* yield_node = create_control_node(erb_node, hb_array_init(8), NULL, NULL, type);
if (yield_node) {
- array_append(new_array, yield_node);
+ hb_array_append(new_array, yield_node);
} else {
- array_append(new_array, item);
+ hb_array_append(new_array, item);
}
index++;
@@ -1041,7 +1008,7 @@ static array_T* rewrite_node_array(AST_NODE_T* node, array_T* array, analyze_rub
}
default:
- array_append(new_array, item);
+ hb_array_append(new_array, item);
index++;
break;
}
@@ -1056,30 +1023,30 @@ static bool transform_erb_nodes(const AST_NODE_T* node, void* data) {
if (node->type == AST_DOCUMENT_NODE) {
AST_DOCUMENT_NODE_T* document_node = (AST_DOCUMENT_NODE_T*) node;
- array_T* old_array = document_node->children;
+ hb_array_T* old_array = document_node->children;
document_node->children = rewrite_node_array((AST_NODE_T*) node, document_node->children, context);
- array_free(&old_array);
+ hb_array_free(&old_array);
}
if (node->type == AST_HTML_ELEMENT_NODE) {
AST_HTML_ELEMENT_NODE_T* element_node = (AST_HTML_ELEMENT_NODE_T*) node;
- array_T* old_array = element_node->body;
+ hb_array_T* old_array = element_node->body;
element_node->body = rewrite_node_array((AST_NODE_T*) node, element_node->body, context);
- array_free(&old_array);
+ hb_array_free(&old_array);
}
if (node->type == AST_HTML_OPEN_TAG_NODE) {
AST_HTML_OPEN_TAG_NODE_T* open_tag = (AST_HTML_OPEN_TAG_NODE_T*) node;
- array_T* old_array = open_tag->children;
+ hb_array_T* old_array = open_tag->children;
open_tag->children = rewrite_node_array((AST_NODE_T*) node, open_tag->children, context);
- array_free(&old_array);
+ hb_array_free(&old_array);
}
if (node->type == AST_HTML_ATTRIBUTE_VALUE_NODE) {
AST_HTML_ATTRIBUTE_VALUE_NODE_T* value_node = (AST_HTML_ATTRIBUTE_VALUE_NODE_T*) node;
- array_T* old_array = value_node->children;
+ hb_array_T* old_array = value_node->children;
value_node->children = rewrite_node_array((AST_NODE_T*) node, value_node->children, context);
- array_free(&old_array);
+ hb_array_free(&old_array);
}
herb_visit_child_nodes(node, transform_erb_nodes, data);
@@ -1093,19 +1060,21 @@ void herb_analyze_parse_tree(AST_DOCUMENT_NODE_T* document, const char* source)
analyze_ruby_context_T* context = malloc(sizeof(analyze_ruby_context_T));
context->document = document;
context->parent = NULL;
- context->ruby_context_stack = array_init(8);
+ context->ruby_context_stack = hb_array_init(8);
herb_visit_node((AST_NODE_T*) document, transform_erb_nodes, context);
herb_analyze_parse_errors(document, source);
- array_free(&context->ruby_context_stack);
+ hb_array_free(&context->ruby_context_stack);
free(context);
}
void herb_analyze_parse_errors(AST_DOCUMENT_NODE_T* document, const char* source) {
char* extracted_ruby = herb_extract_ruby_with_semicolons(source);
+ if (!extracted_ruby) { return; }
+
pm_parser_t parser;
pm_options_t options = { 0, .partial_script = true };
pm_parser_init(&parser, (const uint8_t*) extracted_ruby, strlen(extracted_ruby), &options);
@@ -1116,7 +1085,7 @@ void herb_analyze_parse_errors(AST_DOCUMENT_NODE_T* document, const char* source
error = (const pm_diagnostic_t*) error->node.next) {
RUBY_PARSE_ERROR_T* parse_error = ruby_parse_error_from_prism_error(error, (AST_NODE_T*) document, source, &parser);
- array_append(document->base.errors, parse_error);
+ hb_array_append(document->base.errors, parse_error);
}
pm_node_destroy(&parser, root);
diff --git a/src/analyzed_ruby.c b/src/analyzed_ruby.c
index a2cd0947b..10e5f585f 100644
--- a/src/analyzed_ruby.c
+++ b/src/analyzed_ruby.c
@@ -1,12 +1,13 @@
#include "include/analyzed_ruby.h"
+#include "include/util/hb_string.h"
#include
#include
-analyzed_ruby_T* init_analyzed_ruby(char* source) {
+analyzed_ruby_T* init_analyzed_ruby(hb_string_T source) {
analyzed_ruby_T* analyzed = malloc(sizeof(analyzed_ruby_T));
- pm_parser_init(&analyzed->parser, (const uint8_t*) source, strlen(source), NULL);
+ pm_parser_init(&analyzed->parser, (const uint8_t*) source.data, source.length, NULL);
analyzed->root = pm_parse(&analyzed->parser);
analyzed->valid = (analyzed->parser.error_list.size == 0);
diff --git a/src/ast_node.c b/src/ast_node.c
index 350599c13..e66414249 100644
--- a/src/ast_node.c
+++ b/src/ast_node.c
@@ -12,14 +12,15 @@ size_t ast_node_sizeof(void) {
return sizeof(struct AST_NODE_STRUCT);
}
-void ast_node_init(AST_NODE_T* node, const ast_node_type_T type, position_T* start, position_T* end, array_T* errors) {
+void ast_node_init(AST_NODE_T* node, const ast_node_type_T type, position_T start, position_T end, hb_array_T* errors) {
if (!node) { return; }
node->type = type;
- node->location = location_init(position_copy(start), position_copy(end));
+ node->location.start = start;
+ node->location.end = end;
if (errors == NULL) {
- node->errors = array_init(8);
+ node->errors = hb_array_init(8);
} else {
node->errors = errors;
}
@@ -28,7 +29,7 @@ void ast_node_init(AST_NODE_T* node, const ast_node_type_T type, position_T* sta
AST_LITERAL_NODE_T* ast_literal_node_init_from_token(const token_T* token) {
AST_LITERAL_NODE_T* literal = malloc(sizeof(AST_LITERAL_NODE_T));
- ast_node_init(&literal->base, AST_LITERAL_NODE, token->location->start, token->location->end, NULL);
+ ast_node_init(&literal->base, AST_LITERAL_NODE, token->location.start, token->location.end, NULL);
literal->content = herb_strdup(token->value);
@@ -40,35 +41,31 @@ ast_node_type_T ast_node_type(const AST_NODE_T* node) {
}
size_t ast_node_errors_count(const AST_NODE_T* node) {
- return array_size(node->errors);
+ return hb_array_size(node->errors);
}
-array_T* ast_node_errors(const AST_NODE_T* node) {
+hb_array_T* ast_node_errors(const AST_NODE_T* node) {
return node->errors;
}
void ast_node_append_error(const AST_NODE_T* node, ERROR_T* error) {
- array_append(node->errors, error);
+ hb_array_append(node->errors, error);
}
-void ast_node_set_start(AST_NODE_T* node, position_T* position) {
- if (node->location->start != NULL) { position_free(node->location->start); }
-
- node->location->start = position_copy(position);
+void ast_node_set_start(AST_NODE_T* node, position_T position) {
+ node->location.start = position;
}
-void ast_node_set_end(AST_NODE_T* node, position_T* position) {
- if (node->location->end != NULL) { position_free(node->location->end); }
-
- node->location->end = position_copy(position);
+void ast_node_set_end(AST_NODE_T* node, position_T position) {
+ node->location.end = position;
}
void ast_node_set_start_from_token(AST_NODE_T* node, const token_T* token) {
- ast_node_set_start(node, token->location->start);
+ ast_node_set_start(node, token->location.start);
}
void ast_node_set_end_from_token(AST_NODE_T* node, const token_T* token) {
- ast_node_set_end(node, token->location->end);
+ ast_node_set_end(node, token->location.end);
}
void ast_node_set_positions_from_token(AST_NODE_T* node, const token_T* token) {
diff --git a/src/buffer.c b/src/buffer.c
deleted file mode 100644
index 750e2190d..000000000
--- a/src/buffer.c
+++ /dev/null
@@ -1,232 +0,0 @@
-#include
-#include
-#include
-
-#include "include/buffer.h"
-#include "include/macros.h"
-#include "include/memory.h"
-#include "include/util.h"
-
-bool buffer_init(buffer_T* buffer) {
- buffer->capacity = 1024;
- buffer->length = 0;
- buffer->value = nullable_safe_malloc((buffer->capacity + 1) * sizeof(char));
-
- if (!buffer->value) {
- fprintf(stderr, "Error: Failed to initialize buffer with capacity of %zu.\n", buffer->capacity);
- return false;
- }
-
- buffer->value[0] = '\0';
-
- return true;
-}
-
-buffer_T buffer_new(void) {
- buffer_T buffer;
- buffer_init(&buffer);
- return buffer;
-}
-
-char* buffer_value(const buffer_T* buffer) {
- return buffer->value;
-}
-
-size_t buffer_length(const buffer_T* buffer) {
- return buffer->length;
-}
-
-size_t buffer_capacity(const buffer_T* buffer) {
- return buffer->capacity;
-}
-
-size_t buffer_sizeof(void) {
- return sizeof(buffer_T);
-}
-
-/**
- * Increases the capacity of the buffer if needed to accommodate additional content.
- * This function only handles memory allocation and does not modify the buffer content
- * or null termination.
- *
- * @param buffer The buffer to increase capacity for
- * @param additional_capacity The additional length needed beyond current buffer capacity
- * @return true if capacity was increased, false if reallocation failed
- */
-bool buffer_increase_capacity(buffer_T* buffer, const size_t additional_capacity) {
- if (additional_capacity + 1 >= SIZE_MAX) {
- fprintf(stderr, "Error: Buffer capacity would overflow system limits.\n");
- exit(1);
- }
-
- const size_t new_capacity = buffer->capacity + additional_capacity;
-
- return buffer_resize(buffer, new_capacity);
-}
-
-/**
- * Resizes the capacity of the buffer to the specified new capacity.
- *
- * @param buffer The buffer to resize
- * @param new_capacity The new capacity to resize the buffer to
- * @return true if capacity was resized, false if reallocation failed
- */
-bool buffer_resize(buffer_T* buffer, const size_t new_capacity) {
- if (new_capacity + 1 >= SIZE_MAX) {
- fprintf(stderr, "Error: Buffer capacity would overflow system limits.\n");
- exit(1);
- }
-
- char* new_value = nullable_safe_realloc(buffer->value, new_capacity + 1);
-
- if (unlikely(new_value == NULL)) {
- fprintf(stderr, "Error: Failed to resize buffer to %zu.\n", new_capacity);
- exit(1);
- }
-
- buffer->value = new_value;
- buffer->capacity = new_capacity;
-
- return true;
-}
-
-/**
- * Expands the capacity of the buffer by doubling its current capacity.
- * This function is a convenience function that calls buffer_increase_capacity
- * with a factor of 2.
- *
- * @param buffer The buffer to expand capacity for
- * @return true if capacity was increased, false if reallocation failed
- */
-bool buffer_expand_capacity(buffer_T* buffer) {
- return buffer_resize(buffer, buffer->capacity * 2);
-}
-
-/**
- * Expands the capacity of the buffer if needed to accommodate additional content.
- * This function is a convenience function that calls buffer_has_capacity and
- * buffer_expand_capacity.
- *
- * @param buffer The buffer to expand capacity for
- * @param required_length The additional length needed beyond current buffer capacity
- * @return true if capacity was increased, false if reallocation failed
- */
-bool buffer_expand_if_needed(buffer_T* buffer, const size_t required_length) {
- if (buffer_has_capacity(buffer, required_length)) { return true; }
-
- return buffer_resize(buffer, buffer->capacity + (required_length * 2));
-}
-
-/**
- * Appends a null-terminated string to the buffer.
- * @note This function requires that 'text' is a properly null-terminated string.
- * When reading data from files or other non-string sources, ensure the data is
- * null-terminated before calling this function, or use buffer_append_with_length instead.
- *
- * @param buffer The buffer to append to
- * @param text A null-terminated string to append
- * @return void
- */
-void buffer_append(buffer_T* buffer, const char* text) {
- if (!buffer || !text) { return; }
- if (text[0] == '\0') { return; }
-
- size_t text_length = strlen(text);
-
- if (!buffer_expand_if_needed(buffer, text_length)) { return; }
-
- memcpy(buffer->value + buffer->length, text, text_length);
- buffer->length += text_length;
- buffer->value[buffer->length] = '\0';
-}
-
-/**
- * Appends a string of specified length to the buffer.
- * Unlike buffer_append(), this function does not require the text to be
- * null-terminated as it uses the provided length instead of strlen().
- * This is particularly useful when working with data from files, network
- * buffers, or other non-null-terminated sources.
- *
- * @param buffer The buffer to append to
- * @param text The text to append (doesn't need to be null-terminated)
- * @param length The number of bytes to append from text
- * @return void
- */
-void buffer_append_with_length(buffer_T* buffer, const char* text, const size_t length) {
- if (!buffer || !text || length == 0) { return; }
- if (!buffer_expand_if_needed(buffer, length)) { return; }
-
- memcpy(buffer->value + buffer->length, text, length);
-
- buffer->length += length;
- buffer->value[buffer->length] = '\0';
-}
-
-void buffer_append_char(buffer_T* buffer, const char character) {
- static char string[2];
-
- string[0] = character;
- string[1] = '\0';
-
- buffer_append(buffer, string);
-}
-
-void buffer_append_repeated(buffer_T* buffer, const char character, size_t length) {
- if (length == 0) { return; }
-
- char* spaces = malloc(length + 1);
- if (!spaces) { return; }
-
- memset(spaces, character, length);
- spaces[length] = '\0';
-
- buffer_append(buffer, spaces);
-
- free(spaces);
-}
-
-void buffer_append_whitespace(buffer_T* buffer, const size_t length) {
- buffer_append_repeated(buffer, ' ', length);
-}
-
-void buffer_prepend(buffer_T* buffer, const char* text) {
- if (!buffer || !text) { return; }
- if (text[0] == '\0') { return; }
-
- size_t text_length = strlen(text);
-
- if (!buffer_expand_if_needed(buffer, text_length)) { return; }
-
- memmove(buffer->value + text_length, buffer->value, buffer->length + 1);
- memcpy(buffer->value, text, text_length);
-
- buffer->length += text_length;
-}
-
-void buffer_concat(buffer_T* destination, buffer_T* source) {
- if (source->length == 0) { return; }
- if (!buffer_expand_if_needed(destination, source->length)) { return; }
-
- memcpy(destination->value + destination->length, source->value, source->length);
-
- destination->length += source->length;
- destination->value[destination->length] = '\0';
-}
-
-bool buffer_has_capacity(buffer_T* buffer, const size_t required_length) {
- return (buffer->length + required_length <= buffer->capacity);
-}
-
-void buffer_clear(buffer_T* buffer) {
- buffer->length = 0;
- buffer->value[0] = '\0';
-}
-
-void buffer_free(buffer_T* buffer) {
- if (!buffer) { return; }
-
- if (buffer->value != NULL) { free(buffer->value); }
-
- buffer->value = NULL;
- buffer->length = buffer->capacity = 0;
-}
diff --git a/src/extract.c b/src/extract.c
index cc7bedb49..e927632a4 100644
--- a/src/extract.c
+++ b/src/extract.c
@@ -1,21 +1,22 @@
-#include "include/array.h"
-#include "include/buffer.h"
#include "include/herb.h"
#include "include/io.h"
#include "include/lexer.h"
+#include "include/util/hb_array.h"
+#include "include/util/hb_buffer.h"
#include
+#include
-void herb_extract_ruby_to_buffer_with_semicolons(const char* source, buffer_T* output) {
- array_T* tokens = herb_lex(source);
+void herb_extract_ruby_to_buffer_with_semicolons(const char* source, hb_buffer_T* output) {
+ hb_array_T* tokens = herb_lex(source);
bool skip_erb_content = false;
- for (size_t i = 0; i < array_size(tokens); i++) {
- const token_T* token = array_get(tokens, i);
+ for (size_t i = 0; i < hb_array_size(tokens); i++) {
+ const token_T* token = hb_array_get(tokens, i);
switch (token->type) {
case TOKEN_NEWLINE: {
- buffer_append(output, token->value);
+ hb_buffer_append(output, token->value);
break;
}
@@ -24,15 +25,15 @@ void herb_extract_ruby_to_buffer_with_semicolons(const char* source, buffer_T* o
skip_erb_content = true;
}
- buffer_append_whitespace(output, range_length(token->range));
+ hb_buffer_append_whitespace(output, range_length(token->range));
break;
}
case TOKEN_ERB_CONTENT: {
if (skip_erb_content == false) {
- buffer_append(output, token->value);
+ hb_buffer_append(output, token->value);
} else {
- buffer_append_whitespace(output, range_length(token->range));
+ hb_buffer_append_whitespace(output, range_length(token->range));
}
break;
@@ -41,13 +42,13 @@ void herb_extract_ruby_to_buffer_with_semicolons(const char* source, buffer_T* o
case TOKEN_ERB_END: {
skip_erb_content = false;
- buffer_append_char(output, ';');
- buffer_append_whitespace(output, range_length(token->range) - 1);
+ hb_buffer_append_char(output, ';');
+ hb_buffer_append_whitespace(output, range_length(token->range) - 1);
break;
}
default: {
- buffer_append_whitespace(output, range_length(token->range));
+ hb_buffer_append_whitespace(output, range_length(token->range));
}
}
}
@@ -55,16 +56,16 @@ void herb_extract_ruby_to_buffer_with_semicolons(const char* source, buffer_T* o
herb_free_tokens(&tokens);
}
-void herb_extract_ruby_to_buffer(const char* source, buffer_T* output) {
- array_T* tokens = herb_lex(source);
+void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output) {
+ hb_array_T* tokens = herb_lex(source);
bool skip_erb_content = false;
- for (size_t i = 0; i < array_size(tokens); i++) {
- const token_T* token = array_get(tokens, i);
+ for (size_t i = 0; i < hb_array_size(tokens); i++) {
+ const token_T* token = hb_array_get(tokens, i);
switch (token->type) {
case TOKEN_NEWLINE: {
- buffer_append(output, token->value);
+ hb_buffer_append(output, token->value);
break;
}
@@ -73,15 +74,15 @@ void herb_extract_ruby_to_buffer(const char* source, buffer_T* output) {
skip_erb_content = true;
}
- buffer_append_whitespace(output, range_length(token->range));
+ hb_buffer_append_whitespace(output, range_length(token->range));
break;
}
case TOKEN_ERB_CONTENT: {
if (skip_erb_content == false) {
- buffer_append(output, token->value);
+ hb_buffer_append(output, token->value);
} else {
- buffer_append_whitespace(output, range_length(token->range));
+ hb_buffer_append_whitespace(output, range_length(token->range));
}
break;
@@ -90,12 +91,12 @@ void herb_extract_ruby_to_buffer(const char* source, buffer_T* output) {
case TOKEN_ERB_END: {
skip_erb_content = false;
- buffer_append_whitespace(output, range_length(token->range));
+ hb_buffer_append_whitespace(output, range_length(token->range));
break;
}
default: {
- buffer_append_whitespace(output, range_length(token->range));
+ hb_buffer_append_whitespace(output, range_length(token->range));
}
}
}
@@ -103,17 +104,17 @@ void herb_extract_ruby_to_buffer(const char* source, buffer_T* output) {
herb_free_tokens(&tokens);
}
-void herb_extract_html_to_buffer(const char* source, buffer_T* output) {
- array_T* tokens = herb_lex(source);
+void herb_extract_html_to_buffer(const char* source, hb_buffer_T* output) {
+ hb_array_T* tokens = herb_lex(source);
- for (size_t i = 0; i < array_size(tokens); i++) {
- const token_T* token = array_get(tokens, i);
+ for (size_t i = 0; i < hb_array_size(tokens); i++) {
+ const token_T* token = hb_array_get(tokens, i);
switch (token->type) {
case TOKEN_ERB_START:
case TOKEN_ERB_CONTENT:
- case TOKEN_ERB_END: buffer_append_whitespace(output, range_length(token->range)); break;
- default: buffer_append(output, token->value);
+ case TOKEN_ERB_END: hb_buffer_append_whitespace(output, range_length(token->range)); break;
+ default: hb_buffer_append(output, token->value);
}
}
@@ -121,8 +122,10 @@ void herb_extract_html_to_buffer(const char* source, buffer_T* output) {
}
char* herb_extract_ruby_with_semicolons(const char* source) {
- buffer_T output;
- buffer_init(&output);
+ if (!source) { return NULL; }
+
+ hb_buffer_T output;
+ hb_buffer_init(&output, strlen(source));
herb_extract_ruby_to_buffer_with_semicolons(source, &output);
@@ -130,8 +133,10 @@ char* herb_extract_ruby_with_semicolons(const char* source) {
}
char* herb_extract(const char* source, const herb_extract_language_T language) {
- buffer_T output;
- buffer_init(&output);
+ if (!source) { return NULL; }
+
+ hb_buffer_T output;
+ hb_buffer_init(&output, strlen(source));
switch (language) {
case HERB_EXTRACT_LANGUAGE_RUBY: herb_extract_ruby_to_buffer(source, &output); break;
diff --git a/src/herb.c b/src/herb.c
index d057b28a8..a48a56f3a 100644
--- a/src/herb.c
+++ b/src/herb.c
@@ -1,97 +1,85 @@
#include "include/herb.h"
-#include "include/array.h"
-#include "include/buffer.h"
#include "include/io.h"
-#include "include/json.h"
#include "include/lexer.h"
#include "include/parser.h"
#include "include/token.h"
+#include "include/util/hb_array.h"
+#include "include/util/hb_buffer.h"
#include "include/version.h"
#include
#include
-array_T* herb_lex(const char* source) {
- lexer_T* lexer = lexer_init(source);
+hb_array_T* herb_lex(const char* source) {
+ lexer_T lexer = { 0 };
+ lexer_init(&lexer, source);
+
token_T* token = NULL;
- array_T* tokens = array_init(128);
+ hb_array_T* tokens = hb_array_init(128);
- while ((token = lexer_next_token(lexer))->type != TOKEN_EOF) {
- array_append(tokens, token);
+ while ((token = lexer_next_token(&lexer))->type != TOKEN_EOF) {
+ hb_array_append(tokens, token);
}
- array_append(tokens, token);
-
- lexer_free(lexer);
+ hb_array_append(tokens, token);
return tokens;
}
AST_DOCUMENT_NODE_T* herb_parse(const char* source, parser_options_T* options) {
- lexer_T* lexer = lexer_init(source);
- parser_T* parser = herb_parser_init(lexer, options);
+ if (!source) { source = ""; }
+
+ lexer_T lexer = { 0 };
+ lexer_init(&lexer, source);
+ parser_T parser = { 0 };
+
+ parser_options_T parser_options = HERB_DEFAULT_PARSER_OPTIONS;
- AST_DOCUMENT_NODE_T* document = herb_parser_parse(parser);
+ if (options != NULL) { parser_options = *options; }
- parser_free(parser);
+ herb_parser_init(&parser, &lexer, parser_options);
+
+ AST_DOCUMENT_NODE_T* document = herb_parser_parse(&parser);
+
+ herb_parser_deinit(&parser);
return document;
}
-array_T* herb_lex_file(const char* path) {
+hb_array_T* herb_lex_file(const char* path) {
char* source = herb_read_file(path);
- array_T* tokens = herb_lex(source);
+ hb_array_T* tokens = herb_lex(source);
free(source);
return tokens;
}
-void herb_lex_to_buffer(const char* source, buffer_T* output) {
- array_T* tokens = herb_lex(source);
+void herb_lex_to_buffer(const char* source, hb_buffer_T* output) {
+ hb_array_T* tokens = herb_lex(source);
- for (size_t i = 0; i < array_size(tokens); i++) {
- token_T* token = array_get(tokens, i);
+ for (size_t i = 0; i < hb_array_size(tokens); i++) {
+ token_T* token = hb_array_get(tokens, i);
char* type = token_to_string(token);
- buffer_append(output, type);
+ hb_buffer_append(output, type);
free(type);
- buffer_append(output, "\n");
- }
-
- herb_free_tokens(&tokens);
-}
-
-void herb_lex_json_to_buffer(const char* source, buffer_T* output) {
- array_T* tokens = herb_lex(source);
-
- buffer_T json = buffer_new();
- json_start_root_array(&json);
-
- for (size_t i = 0; i < array_size(tokens); i++) {
- token_T* token = array_get(tokens, i);
- char* token_json = token_to_json(token);
- json_add_raw_string(&json, token_json);
- free(token_json);
+ hb_buffer_append(output, "\n");
}
- json_end_array(&json);
- buffer_concat(output, &json);
-
- buffer_free(&json);
herb_free_tokens(&tokens);
}
-void herb_free_tokens(array_T** tokens) {
+void herb_free_tokens(hb_array_T** tokens) {
if (!tokens || !*tokens) { return; }
- for (size_t i = 0; i < array_size(*tokens); i++) {
- token_T* token = array_get(*tokens, i);
+ for (size_t i = 0; i < hb_array_size(*tokens); i++) {
+ token_T* token = hb_array_get(*tokens, i);
if (token) { token_free(token); }
}
- array_free(tokens);
+ hb_array_free(tokens);
}
const char* herb_version(void) {
diff --git a/src/html_util.c b/src/html_util.c
index b400d0709..4371ee5f9 100644
--- a/src/html_util.c
+++ b/src/html_util.c
@@ -1,76 +1,31 @@
#include "include/html_util.h"
#include "include/util.h"
+#include "include/util/hb_buffer.h"
+#include "include/util/hb_string.h"
#include
#include
#include
#include
#include
-#include
// https://developer.mozilla.org/en-US/docs/Glossary/Void_element
-bool is_void_element(const char* tag_name) {
- if (tag_name == NULL) { return false; }
+bool is_void_element(hb_string_T tag_name) {
+ if (hb_string_is_empty(tag_name)) { return false; }
- const char* void_tags[] = {
- "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr",
+ hb_string_T void_tags[14] = {
+ hb_string("area"), hb_string("base"), hb_string("br"), hb_string("col"), hb_string("embed"),
+ hb_string("hr"), hb_string("img"), hb_string("input"), hb_string("link"), hb_string("meta"),
+ hb_string("param"), hb_string("source"), hb_string("track"), hb_string("wbr"),
};
- for (size_t i = 0; i < sizeof(void_tags) / sizeof(char*); i++) {
- if (strcasecmp(tag_name, void_tags[i]) == 0) { return true; }
- }
-
- return false;
-}
-
-bool is_html4_void_element(const char* tag_name) {
- if (tag_name == NULL) { return false; }
-
- const char* html4_void_tags[] = {
- "basefont", "bgsound", "command", "frame", "image", "keygen",
- };
-
- for (size_t i = 0; i < sizeof(html4_void_tags) / sizeof(char*); i++) {
- if (strcasecmp(tag_name, html4_void_tags[i]) == 0) { return true; }
+ for (size_t i = 0; i < 14; i++) {
+ if (hb_string_equals_case_insensitive(tag_name, void_tags[i])) { return true; }
}
return false;
}
-/**
- * @brief Creates an opening HTML tag string like ""
- *
- * @param tag_name The name of the HTML tag to be enclosed in a opening tag
- * @return A newly allocated string containing the opening tag, or NULL if memory allocation fails
- * @note The caller is responsible for freeing the returned string
- *
- * Example:
- * @code
- * char* tag = html_opening_tag_string("div");
- * if (tag) {
- * printf("%s\n", tag); // Prints:
- * free(tag);
- * }
- * @endcode
- */
-char* html_opening_tag_string(const char* tag_name) {
- if (tag_name == NULL) { return herb_strdup("<>"); }
-
- size_t length = strlen(tag_name);
- char* result = (char*) malloc(length + 3); // +3 for '<', '>', and '\0'
-
- if (result == NULL) { return NULL; }
-
- result[0] = '<';
-
- memcpy(result + 1, tag_name, length);
-
- result[length + 1] = '>';
- result[length + 2] = '\0';
-
- return result;
-}
-
/**
* @brief Creates a closing HTML tag string like ""
*
@@ -80,30 +35,22 @@ char* html_opening_tag_string(const char* tag_name) {
*
* Example:
* @code
- * char* tag = html_closing_tag_string("div");
- * if (tag) {
- * printf("%s\n", tag); // Prints:
- * free(tag);
- * }
+ * hb_string_T tag = html_closing_tag_string(hb_string("div"));
+ *
+ * printf("%.*s\n", tag.length, tag.data); // Prints:
+ * free(tag.data);
* @endcode
*/
-char* html_closing_tag_string(const char* tag_name) {
- if (tag_name == NULL) { return herb_strdup(">"); }
+hb_string_T html_closing_tag_string(hb_string_T tag_name) {
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, tag_name.length + 3);
- size_t length = strlen(tag_name);
- char* result = (char*) malloc(length + 4); // +4 for '<', '/', '>', and '\0'
+ hb_buffer_append_char(&buffer, '<');
+ hb_buffer_append_char(&buffer, '/');
+ hb_buffer_append_string(&buffer, tag_name);
+ hb_buffer_append_char(&buffer, '>');
- if (result == NULL) { return NULL; }
-
- result[0] = '<';
- result[1] = '/';
-
- memcpy(result + 2, tag_name, length);
-
- result[length + 2] = '>';
- result[length + 3] = '\0';
-
- return result;
+ return hb_string(buffer.value);
}
/**
@@ -115,29 +62,20 @@ char* html_closing_tag_string(const char* tag_name) {
*
* Example:
* @code
- * char* tag = html_self_closing_tag_string("br");
- * if (tag) {
- * printf("%s\n", tag); // Prints:
- * free(tag);
- * }
+ * hb_string_T tag = html_self_closing_tag_string(hb_string("br"));
+ * printf("%.*s\n", tag.length, tag.data); // Prints:
+ * free(tag);
* @endcode
*/
-char* html_self_closing_tag_string(const char* tag_name) {
- if (tag_name == NULL) { return herb_strdup("< />"); }
-
- size_t length = strlen(tag_name);
- char* result = (char*) malloc(length + 5); // +5 for '<', ' ', '/', '>', and '\0'
-
- if (result == NULL) { return NULL; }
-
- result[0] = '<';
-
- memcpy(result + 1, tag_name, length);
+hb_string_T html_self_closing_tag_string(hb_string_T tag_name) {
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, tag_name.length + 4);
- result[length + 1] = ' ';
- result[length + 2] = '/';
- result[length + 3] = '>';
- result[length + 4] = '\0';
+ hb_buffer_append_char(&buffer, '<');
+ hb_buffer_append_string(&buffer, tag_name);
+ hb_buffer_append_char(&buffer, ' ');
+ hb_buffer_append_char(&buffer, '/');
+ hb_buffer_append_char(&buffer, '>');
- return result;
+ return hb_string(buffer.value);
}
diff --git a/src/include/analyze.h b/src/include/analyze.h
index d423856d3..d6f5a1049 100644
--- a/src/include/analyze.h
+++ b/src/include/analyze.h
@@ -2,13 +2,13 @@
#define HERB_ANALYZE_H
#include "analyzed_ruby.h"
-#include "array.h"
#include "ast_nodes.h"
+#include "util/hb_array.h"
typedef struct ANALYZE_RUBY_CONTEXT_STRUCT {
AST_DOCUMENT_NODE_T* document;
AST_NODE_T* parent;
- array_T* ruby_context_stack;
+ hb_array_T* ruby_context_stack;
} analyze_ruby_context_T;
typedef enum {
diff --git a/src/include/analyzed_ruby.h b/src/include/analyzed_ruby.h
index 67b2aa9a9..fc31037af 100644
--- a/src/include/analyzed_ruby.h
+++ b/src/include/analyzed_ruby.h
@@ -1,7 +1,8 @@
#ifndef HERB_ANALYZED_RUBY_H
#define HERB_ANALYZED_RUBY_H
-#include "array.h"
+#include "util/hb_array.h"
+#include "util/hb_string.h"
#include
@@ -30,7 +31,7 @@ typedef struct ANALYZED_RUBY_STRUCT {
bool has_yield_node;
} analyzed_ruby_T;
-analyzed_ruby_T* init_analyzed_ruby(char* source);
+analyzed_ruby_T* init_analyzed_ruby(hb_string_T source);
void free_analyzed_ruby(analyzed_ruby_T* analyzed);
#endif
diff --git a/src/include/array.h b/src/include/array.h
deleted file mode 100644
index f9715e83f..000000000
--- a/src/include/array.h
+++ /dev/null
@@ -1,33 +0,0 @@
-#ifndef HERB_ARRAY_H
-#define HERB_ARRAY_H
-
-#include
-
-typedef struct ARRAY_STRUCT {
- void** items;
- size_t size;
- size_t capacity;
-} array_T;
-
-array_T* array_init(size_t capacity);
-
-void* array_get(const array_T* array, size_t index);
-void* array_first(array_T* array);
-void* array_last(array_T* array);
-
-void array_append(array_T* array, void* item);
-void array_set(const array_T* array, size_t index, void* item);
-void array_free(array_T** array);
-void array_remove(array_T* array, size_t index);
-
-size_t array_index_of(array_T* array, void* item);
-void array_remove_item(array_T* array, void* item);
-
-void array_push(array_T* array, void* item);
-void* array_pop(array_T* array);
-
-size_t array_capacity(const array_T* array);
-size_t array_size(const array_T* array);
-size_t array_sizeof(void);
-
-#endif
diff --git a/src/include/ast_node.h b/src/include/ast_node.h
index e41ece34a..144e35334 100644
--- a/src/include/ast_node.h
+++ b/src/include/ast_node.h
@@ -6,7 +6,7 @@
#include "position.h"
#include "token_struct.h"
-void ast_node_init(AST_NODE_T* node, ast_node_type_T type, position_T* start, position_T* end, array_T* errors);
+void ast_node_init(AST_NODE_T* node, ast_node_type_T type, position_T start, position_T end, hb_array_T* errors);
void ast_node_free(AST_NODE_T* node);
AST_LITERAL_NODE_T* ast_literal_node_init_from_token(const token_T* token);
@@ -18,11 +18,11 @@ ast_node_type_T ast_node_type(const AST_NODE_T* node);
char* ast_node_name(AST_NODE_T* node);
-void ast_node_set_start(AST_NODE_T* node, position_T* position);
-void ast_node_set_end(AST_NODE_T* node, position_T* position);
+void ast_node_set_start(AST_NODE_T* node, position_T position);
+void ast_node_set_end(AST_NODE_T* node, position_T position);
size_t ast_node_errors_count(const AST_NODE_T* node);
-array_T* ast_node_errors(const AST_NODE_T* node);
+hb_array_T* ast_node_errors(const AST_NODE_T* node);
void ast_node_append_error(const AST_NODE_T* node, ERROR_T* error);
void ast_node_set_start_from_token(AST_NODE_T* node, const token_T* token);
diff --git a/src/include/buffer.h b/src/include/buffer.h
deleted file mode 100644
index a684e9ded..000000000
--- a/src/include/buffer.h
+++ /dev/null
@@ -1,39 +0,0 @@
-#ifndef HERB_BUFFER_H
-#define HERB_BUFFER_H
-
-#include
-#include
-
-typedef struct BUFFER_STRUCT {
- char* value;
- size_t length;
- size_t capacity;
-} buffer_T;
-
-bool buffer_init(buffer_T* buffer);
-buffer_T buffer_new(void);
-
-bool buffer_increase_capacity(buffer_T* buffer, size_t additional_capacity);
-bool buffer_has_capacity(buffer_T* buffer, size_t required_length);
-bool buffer_expand_capacity(buffer_T* buffer);
-bool buffer_expand_if_needed(buffer_T* buffer, size_t required_length);
-bool buffer_resize(buffer_T* buffer, size_t new_capacity);
-
-void buffer_append(buffer_T* buffer, const char* text);
-void buffer_append_with_length(buffer_T* buffer, const char* text, size_t length);
-void buffer_append_char(buffer_T* buffer, char character);
-void buffer_append_repeated(buffer_T* buffer, char character, size_t length);
-void buffer_append_whitespace(buffer_T* buffer, size_t length);
-void buffer_prepend(buffer_T* buffer, const char* text);
-void buffer_concat(buffer_T* destination, buffer_T* source);
-
-char* buffer_value(const buffer_T* buffer);
-
-size_t buffer_length(const buffer_T* buffer);
-size_t buffer_capacity(const buffer_T* buffer);
-size_t buffer_sizeof(void);
-
-void buffer_clear(buffer_T* buffer);
-void buffer_free(buffer_T* buffer);
-
-#endif
diff --git a/src/include/extract.h b/src/include/extract.h
index 668f821a3..2f1704dcc 100644
--- a/src/include/extract.h
+++ b/src/include/extract.h
@@ -1,18 +1,18 @@
#ifndef HERB_EXTRACT_H
#define HERB_EXTRACT_H
-#include "buffer.h"
+#include "util/hb_buffer.h"
typedef enum {
HERB_EXTRACT_LANGUAGE_RUBY,
HERB_EXTRACT_LANGUAGE_HTML,
} herb_extract_language_T;
-void herb_extract_ruby_to_buffer(const char* source, buffer_T* output);
-void herb_extract_html_to_buffer(const char* source, buffer_T* output);
+void herb_extract_ruby_to_buffer(const char* source, hb_buffer_T* output);
+void herb_extract_html_to_buffer(const char* source, hb_buffer_T* output);
char* herb_extract_ruby_with_semicolons(const char* source);
-void herb_extract_ruby_to_buffer_with_semicolons(const char* source, buffer_T* output);
+void herb_extract_ruby_to_buffer_with_semicolons(const char* source, hb_buffer_T* output);
char* herb_extract(const char* source, herb_extract_language_T language);
char* herb_extract_from_file(const char* path, herb_extract_language_T language);
diff --git a/src/include/herb.h b/src/include/herb.h
index d4ec2c7c6..207053e45 100644
--- a/src/include/herb.h
+++ b/src/include/herb.h
@@ -1,11 +1,11 @@
#ifndef HERB_H
#define HERB_H
-#include "array.h"
#include "ast_node.h"
-#include "buffer.h"
#include "extract.h"
#include "parser.h"
+#include "util/hb_array.h"
+#include "util/hb_buffer.h"
#include
@@ -13,18 +13,17 @@
extern "C" {
#endif
-void herb_lex_to_buffer(const char* source, buffer_T* output);
-void herb_lex_json_to_buffer(const char* source, buffer_T* output);
+void herb_lex_to_buffer(const char* source, hb_buffer_T* output);
-array_T* herb_lex(const char* source);
-array_T* herb_lex_file(const char* path);
+hb_array_T* herb_lex(const char* source);
+hb_array_T* herb_lex_file(const char* path);
AST_DOCUMENT_NODE_T* herb_parse(const char* source, parser_options_T* options);
const char* herb_version(void);
const char* herb_prism_version(void);
-void herb_free_tokens(array_T** tokens);
+void herb_free_tokens(hb_array_T** tokens);
#ifdef __cplusplus
}
diff --git a/src/include/html_util.h b/src/include/html_util.h
index 8412c9490..51dbaf71a 100644
--- a/src/include/html_util.h
+++ b/src/include/html_util.h
@@ -1,13 +1,12 @@
#ifndef HERB_HTML_UTIL_H
#define HERB_HTML_UTIL_H
+#include "util/hb_string.h"
#include
-bool is_void_element(const char* tag_name);
-bool is_html4_void_element(const char* tag_name);
+bool is_void_element(hb_string_T tag_name);
-char* html_opening_tag_string(const char* tag_name);
-char* html_closing_tag_string(const char* tag_name);
-char* html_self_closing_tag_string(const char* tag_name);
+hb_string_T html_closing_tag_string(hb_string_T tag_name);
+hb_string_T html_self_closing_tag_string(hb_string_T tag_name);
#endif
diff --git a/src/include/json.h b/src/include/json.h
deleted file mode 100644
index b48428c3a..000000000
--- a/src/include/json.h
+++ /dev/null
@@ -1,28 +0,0 @@
-#ifndef HERB_JSON_H
-#define HERB_JSON_H
-
-#include "buffer.h"
-
-void json_start_root_object(buffer_T* json);
-void json_start_root_array(buffer_T* json);
-
-void json_escape_string(buffer_T* json, const char* string);
-
-void json_add_string(buffer_T* json, const char* key, const char* value);
-void json_add_int(buffer_T* json, const char* key, int value);
-void json_add_size_t(buffer_T* json, const char* key, size_t value);
-void json_add_double(buffer_T* json, const char* key, double value);
-void json_add_bool(buffer_T* json, const char* key, int value);
-
-void json_add_raw_string(buffer_T* json, const char* string);
-
-void json_start_object(buffer_T* json, const char* key);
-void json_end_object(buffer_T* json);
-
-void json_start_array(buffer_T* json, const char* key);
-void json_end_array(buffer_T* json);
-
-void json_double_to_string(double value, char* buffer);
-void json_int_to_string(int value, char* buffer);
-
-#endif
diff --git a/src/include/lexer.h b/src/include/lexer.h
index 01ac0551f..142f3fb1c 100644
--- a/src/include/lexer.h
+++ b/src/include/lexer.h
@@ -4,10 +4,8 @@
#include "lexer_struct.h"
#include "token_struct.h"
-lexer_T* lexer_init(const char* source);
+void lexer_init(lexer_T* lexer, const char* source);
token_T* lexer_next_token(lexer_T* lexer);
token_T* lexer_error(lexer_T* lexer, const char* message);
-void lexer_free(lexer_T* lexer);
-
#endif
diff --git a/src/include/lexer_peek_helpers.h b/src/include/lexer_peek_helpers.h
index ae78bee03..9cc7e94c9 100644
--- a/src/include/lexer_peek_helpers.h
+++ b/src/include/lexer_peek_helpers.h
@@ -5,38 +5,40 @@
#include "token_struct.h"
#include
+#include
#include
#include
typedef struct {
- size_t position;
- size_t line;
- size_t column;
- size_t previous_position;
- size_t previous_line;
- size_t previous_column;
+ uint32_t position;
+ uint32_t line;
+ uint32_t column;
+ uint32_t previous_position;
+ uint32_t previous_line;
+ uint32_t previous_column;
char current_character;
lexer_state_T state;
} lexer_state_snapshot_T;
-char lexer_peek(const lexer_T* lexer, int offset);
-bool lexer_peek_for_doctype(const lexer_T* lexer, int offset);
-bool lexer_peek_for_xml_declaration(const lexer_T* lexer, int offset);
-bool lexer_peek_for_cdata_start(const lexer_T* lexer, int offset);
-bool lexer_peek_for_cdata_end(const lexer_T* lexer, int offset);
+char lexer_peek(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_for_doctype(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_for_xml_declaration(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_for_cdata_start(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_for_cdata_end(const lexer_T* lexer, uint32_t offset);
-bool lexer_peek_for_html_comment_start(const lexer_T* lexer, int offset);
-bool lexer_peek_for_html_comment_end(const lexer_T* lexer, int offset);
+bool lexer_peek_for_html_comment_start(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_for_html_comment_end(const lexer_T* lexer, uint32_t offset);
-bool lexer_peek_erb_close_tag(const lexer_T* lexer, int offset);
-bool lexer_peek_erb_dash_close_tag(const lexer_T* lexer, int offset);
-bool lexer_peek_erb_percent_close_tag(const lexer_T* lexer, int offset);
-bool lexer_peek_erb_end(const lexer_T* lexer, int offset);
+bool lexer_peek_erb_close_tag(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_erb_dash_close_tag(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_erb_percent_close_tag(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_erb_equals_close_tag(const lexer_T* lexer, uint32_t offset);
+bool lexer_peek_erb_end(const lexer_T* lexer, uint32_t offset);
-char lexer_backtrack(const lexer_T* lexer, int offset);
+char lexer_backtrack(const lexer_T* lexer, uint32_t offset);
bool lexer_peek_for_token_type_after_whitespace(lexer_T* lexer, token_type_T token_type);
-bool lexer_peek_for_close_tag_start(const lexer_T* lexer, int offset);
+bool lexer_peek_for_close_tag_start(const lexer_T* lexer, uint32_t offset);
lexer_state_snapshot_T lexer_save_state(lexer_T* lexer);
void lexer_restore_state(lexer_T* lexer, lexer_state_snapshot_T snapshot);
diff --git a/src/include/lexer_struct.h b/src/include/lexer_struct.h
index 709233884..94b132559 100644
--- a/src/include/lexer_struct.h
+++ b/src/include/lexer_struct.h
@@ -1,7 +1,10 @@
#ifndef HERB_LEXER_STRUCT_H
#define HERB_LEXER_STRUCT_H
+#include "util/hb_string.h"
+
#include
+#include
#include
typedef enum {
@@ -11,21 +14,20 @@ typedef enum {
} lexer_state_T;
typedef struct LEXER_STRUCT {
- const char* source;
- size_t source_length;
+ hb_string_T source;
- size_t current_line;
- size_t current_column;
- size_t current_position;
+ uint32_t current_line;
+ uint32_t current_column;
+ uint32_t current_position;
- size_t previous_line;
- size_t previous_column;
- size_t previous_position;
+ uint32_t previous_line;
+ uint32_t previous_column;
+ uint32_t previous_position;
char current_character;
lexer_state_T state;
- size_t stall_counter;
- size_t last_position;
+ uint32_t stall_counter;
+ uint32_t last_position;
bool stalled;
} lexer_T;
diff --git a/src/include/location.h b/src/include/location.h
index ef07ba688..d01c94aa9 100644
--- a/src/include/location.h
+++ b/src/include/location.h
@@ -1,25 +1,22 @@
#ifndef HERB_LOCATION_H
#define HERB_LOCATION_H
+#include
#include
#include "position.h"
typedef struct LOCATION_STRUCT {
- position_T* start;
- position_T* end;
+ position_T start;
+ position_T end;
} location_T;
-location_T* location_init(position_T* start, position_T* end);
-location_T* location_from(size_t start_line, size_t start_column, size_t end_line, size_t end_column);
-
-position_T* location_start(location_T* location);
-position_T* location_end_(location_T* location);
-
-size_t location_sizeof(void);
-
-location_T* location_copy(location_T* location);
-
-void location_free(location_T* location);
+void location_from(
+ location_T* location,
+ uint32_t start_line,
+ uint32_t start_column,
+ uint32_t end_line,
+ uint32_t end_column
+);
#endif
diff --git a/src/include/memory.h b/src/include/memory.h
deleted file mode 100644
index b6f942e82..000000000
--- a/src/include/memory.h
+++ /dev/null
@@ -1,12 +0,0 @@
-#ifndef HERB_MEMORY_H
-#define HERB_MEMORY_H
-
-#include
-
-void* safe_malloc(size_t size);
-void* safe_realloc(void* pointer, size_t new_size);
-
-void* nullable_safe_malloc(size_t size);
-void* nullable_safe_realloc(void* pointer, size_t new_size);
-
-#endif
diff --git a/src/include/parser.h b/src/include/parser.h
index 138c53594..4eb92f3fb 100644
--- a/src/include/parser.h
+++ b/src/include/parser.h
@@ -1,9 +1,9 @@
#ifndef HERB_PARSER_H
#define HERB_PARSER_H
-#include "array.h"
#include "ast_node.h"
#include "lexer.h"
+#include "util/hb_array.h"
typedef enum {
FOREIGN_CONTENT_UNKNOWN = 0,
@@ -19,21 +19,23 @@ typedef struct PARSER_OPTIONS_STRUCT {
bool track_whitespace;
} parser_options_T;
+extern const parser_options_T HERB_DEFAULT_PARSER_OPTIONS;
+
typedef struct PARSER_STRUCT {
lexer_T* lexer;
token_T* current_token;
- array_T* open_tags_stack;
+ hb_array_T* open_tags_stack;
parser_state_T state;
foreign_content_type_T foreign_content_type;
- parser_options_T* options;
+ parser_options_T options;
} parser_T;
-parser_T* herb_parser_init(lexer_T* lexer, parser_options_T* options);
+void herb_parser_init(parser_T* parser, lexer_T* lexer, parser_options_T options);
AST_DOCUMENT_NODE_T* herb_parser_parse(parser_T* parser);
size_t parser_sizeof(void);
-void parser_free(parser_T* parser);
+void herb_parser_deinit(parser_T* parser);
#endif
diff --git a/src/include/parser_helpers.h b/src/include/parser_helpers.h
index aff4ac8ae..b3ab98300 100644
--- a/src/include/parser_helpers.h
+++ b/src/include/parser_helpers.h
@@ -1,47 +1,57 @@
#ifndef HERB_PARSER_HELPERS_H
#define HERB_PARSER_HELPERS_H
-#include "array.h"
#include "ast_nodes.h"
-#include "buffer.h"
#include "errors.h"
#include "parser.h"
#include "token.h"
+#include "util/hb_array.h"
+#include "util/hb_buffer.h"
+#include "util/hb_string.h"
void parser_push_open_tag(const parser_T* parser, token_T* tag_name);
-bool parser_check_matching_tag(const parser_T* parser, const char* tag_name);
+bool parser_check_matching_tag(const parser_T* parser, hb_string_T tag_name);
token_T* parser_pop_open_tag(const parser_T* parser);
-void parser_append_unexpected_error(parser_T* parser, const char* description, const char* expected, array_T* errors);
-void parser_append_unexpected_token_error(parser_T* parser, token_type_T expected_type, array_T* errors);
+void parser_append_unexpected_error(
+ parser_T* parser,
+ const char* description,
+ const char* expected,
+ hb_array_T* errors
+);
+void parser_append_unexpected_token_error(parser_T* parser, token_type_T expected_type, hb_array_T* errors);
void parser_append_literal_node_from_buffer(
const parser_T* parser,
- buffer_T* buffer,
- array_T* children,
- position_T* start
+ hb_buffer_T* buffer,
+ hb_array_T* children,
+ position_T start
);
bool parser_in_svg_context(const parser_T* parser);
-foreign_content_type_T parser_get_foreign_content_type(const char* tag_name);
-bool parser_is_foreign_content_tag(const char* tag_name);
-const char* parser_get_foreign_content_closing_tag(foreign_content_type_T type);
+foreign_content_type_T parser_get_foreign_content_type(hb_string_T tag_name);
+bool parser_is_foreign_content_tag(hb_string_T tag_name);
+hb_string_T parser_get_foreign_content_closing_tag(foreign_content_type_T type);
void parser_enter_foreign_content(parser_T* parser, foreign_content_type_T type);
void parser_exit_foreign_content(parser_T* parser);
-bool parser_is_expected_closing_tag_name(const char* tag_name, foreign_content_type_T expected_type);
+bool parser_is_expected_closing_tag_name(hb_string_T tag_name, foreign_content_type_T expected_type);
token_T* parser_advance(parser_T* parser);
token_T* parser_consume_if_present(parser_T* parser, token_type_T type);
-token_T* parser_consume_expected(parser_T* parser, token_type_T type, array_T* array);
+token_T* parser_consume_expected(parser_T* parser, token_type_T type, hb_array_T* array);
AST_HTML_ELEMENT_NODE_T* parser_handle_missing_close_tag(
AST_HTML_OPEN_TAG_NODE_T* open_tag,
- array_T* body,
- array_T* errors
+ hb_array_T* body,
+ hb_array_T* errors
+);
+void parser_handle_mismatched_tags(
+ const parser_T* parser,
+ const AST_HTML_CLOSE_TAG_NODE_T* close_tag,
+ hb_array_T* errors
);
-void parser_handle_mismatched_tags(const parser_T* parser, const AST_HTML_CLOSE_TAG_NODE_T* close_tag, array_T* errors);
#endif
diff --git a/src/include/position.h b/src/include/position.h
index 95e64044b..8fad03d5f 100644
--- a/src/include/position.h
+++ b/src/include/position.h
@@ -1,22 +1,11 @@
#ifndef HERB_POSITION_H
#define HERB_POSITION_H
-#include
+#include
typedef struct POSITION_STRUCT {
- size_t line;
- size_t column;
+ uint32_t line;
+ uint32_t column;
} position_T;
-position_T* position_init(size_t line, size_t column);
-
-size_t position_line(const position_T* position);
-size_t position_column(const position_T* position);
-
-size_t position_sizeof(void);
-
-position_T* position_copy(position_T* position);
-
-void position_free(position_T* position);
-
#endif
diff --git a/src/include/pretty_print.h b/src/include/pretty_print.h
index 610bc3f73..d7921a8c0 100644
--- a/src/include/pretty_print.h
+++ b/src/include/pretty_print.h
@@ -3,14 +3,20 @@
#include "analyzed_ruby.h"
#include "ast_nodes.h"
-#include "buffer.h"
#include "location.h"
+#include "util/hb_buffer.h"
#include
-void pretty_print_indent(buffer_T* buffer, size_t indent);
-void pretty_print_newline(size_t indent, size_t relative_indent, buffer_T* buffer);
-void pretty_print_label(const char* name, size_t indent, size_t relative_indent, bool last_property, buffer_T* buffer);
+void pretty_print_indent(hb_buffer_T* buffer, size_t indent);
+void pretty_print_newline(size_t indent, size_t relative_indent, hb_buffer_T* buffer);
+void pretty_print_label(
+ const char* name,
+ size_t indent,
+ size_t relative_indent,
+ bool last_property,
+ hb_buffer_T* buffer
+);
void pretty_print_position_property(
position_T* position,
@@ -18,10 +24,10 @@ void pretty_print_position_property(
size_t indent,
size_t relative_indent,
bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
-void pretty_print_location(location_T* location, buffer_T* buffer);
+void pretty_print_location(location_T location, hb_buffer_T* buffer);
void pretty_print_property(
const char* name,
@@ -29,7 +35,7 @@ void pretty_print_property(
size_t indent,
size_t relative_indent,
bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
void pretty_print_size_t_property(
@@ -38,7 +44,7 @@ void pretty_print_size_t_property(
size_t indent,
size_t relative_indent,
bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
void pretty_print_string_property(
@@ -47,7 +53,7 @@ void pretty_print_string_property(
size_t indent,
size_t relative_indent,
bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
void pretty_print_quoted_property(
@@ -56,7 +62,7 @@ void pretty_print_quoted_property(
size_t indent,
size_t relative_indent,
bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
void pretty_print_boolean_property(
@@ -65,7 +71,7 @@ void pretty_print_boolean_property(
size_t indent,
size_t relative_indent,
bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
void pretty_print_token_property(
@@ -74,20 +80,24 @@ void pretty_print_token_property(
size_t indent,
size_t relative_indent,
bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
void pretty_print_array(
const char* name,
- array_T* array,
+ hb_array_T* array,
size_t indent,
size_t relative_indent,
bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
-void pretty_print_errors(AST_NODE_T* node, size_t indent, size_t relative_indent, bool last_property, buffer_T* buffer);
-
-void pretty_print_analyzed_ruby(analyzed_ruby_T* analyzed, const char* source);
+void pretty_print_errors(
+ AST_NODE_T* node,
+ size_t indent,
+ size_t relative_indent,
+ bool last_property,
+ hb_buffer_T* buffer
+);
#endif
diff --git a/src/include/prism_helpers.h b/src/include/prism_helpers.h
index 66438c342..ebfb53b4f 100644
--- a/src/include/prism_helpers.h
+++ b/src/include/prism_helpers.h
@@ -16,6 +16,6 @@ RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error(
pm_parser_t* parser
);
-position_T* position_from_source_with_offset(const char* source, size_t offset);
+position_T position_from_source_with_offset(const char* source, size_t offset);
#endif
diff --git a/src/include/range.h b/src/include/range.h
index c5c28f8bb..019b1962b 100644
--- a/src/include/range.h
+++ b/src/include/range.h
@@ -1,23 +1,14 @@
#ifndef HERB_RANGE_H
#define HERB_RANGE_H
+#include
#include
typedef struct RANGE_STRUCT {
- size_t from;
- size_t to;
+ uint32_t from;
+ uint32_t to;
} range_T;
-range_T* range_init(size_t from, size_t to);
-
-size_t range_from(const range_T* range);
-size_t range_to(const range_T* range);
-size_t range_length(range_T* range);
-
-range_T* range_copy(range_T* range);
-
-size_t range_sizeof(void);
-
-void range_free(range_T* range);
+uint32_t range_length(range_T range);
#endif
diff --git a/src/include/token.h b/src/include/token.h
index 6930439e9..5628e2f0d 100644
--- a/src/include/token.h
+++ b/src/include/token.h
@@ -7,15 +7,11 @@
token_T* token_init(const char* value, token_type_T type, lexer_T* lexer);
char* token_to_string(const token_T* token);
-char* token_to_json(const token_T* token);
const char* token_type_to_string(token_type_T type);
char* token_value(const token_T* token);
int token_type(const token_T* token);
-position_T* token_start_position(token_T* token);
-position_T* token_end_position(token_T* token);
-
size_t token_sizeof(void);
token_T* token_copy(token_T* token);
diff --git a/src/include/token_struct.h b/src/include/token_struct.h
index e5192165b..2727d2a4c 100644
--- a/src/include/token_struct.h
+++ b/src/include/token_struct.h
@@ -50,8 +50,8 @@ typedef enum {
typedef struct TOKEN_STRUCT {
char* value;
- range_T* range;
- location_T* location;
+ range_T range;
+ location_T location;
token_type_T type;
} token_T;
diff --git a/src/include/util.h b/src/include/util.h
index 18df10360..978abeb92 100644
--- a/src/include/util.h
+++ b/src/include/util.h
@@ -1,25 +1,14 @@
#ifndef HERB_UTIL_H
#define HERB_UTIL_H
+#include "util/hb_string.h"
#include
#include
-int is_whitespace(int character);
int is_newline(int character);
-int count_in_string(const char* string, char character);
-int count_newlines(const char* string);
-
-char* replace_char(char* string, char find, char replace);
char* escape_newlines(const char* input);
-char* quoted_string(const char* input);
-char* wrap_string(const char* input, char character);
-
-bool string_blank(const char* input);
-bool string_present(const char* input);
-
+hb_string_T quoted_string(hb_string_T input);
char* herb_strdup(const char* s);
-char* size_t_to_string(size_t value);
-
#endif
diff --git a/src/include/util/hb_arena.h b/src/include/util/hb_arena.h
new file mode 100644
index 000000000..f58df0a21
--- /dev/null
+++ b/src/include/util/hb_arena.h
@@ -0,0 +1,20 @@
+#ifndef HERB_ARENA_H
+#define HERB_ARENA_H
+
+#include
+#include
+
+typedef struct HB_ARENA_ALLOCATOR_STRUCT {
+ char* memory;
+ size_t capacity;
+ size_t position;
+} hb_arena_T;
+
+bool hb_arena_init(hb_arena_T* allocator, size_t size);
+void* hb_arena_alloc(hb_arena_T* allocator, size_t size);
+size_t hb_arena_position(hb_arena_T* allocator);
+void hb_arena_reset(hb_arena_T* allocator);
+void hb_arena_reset_to(hb_arena_T* allocator, size_t new_position);
+void hb_arena_free(hb_arena_T* allocator);
+
+#endif
diff --git a/src/include/util/hb_array.h b/src/include/util/hb_array.h
new file mode 100644
index 000000000..ccc3b043a
--- /dev/null
+++ b/src/include/util/hb_array.h
@@ -0,0 +1,33 @@
+#ifndef HERB_ARRAY_H
+#define HERB_ARRAY_H
+
+#include
+
+typedef struct HB_ARRAY_STRUCT {
+ void** items;
+ size_t size;
+ size_t capacity;
+} hb_array_T;
+
+hb_array_T* hb_array_init(size_t capacity);
+
+void* hb_array_get(const hb_array_T* array, size_t index);
+void* hb_array_first(hb_array_T* array);
+void* hb_array_last(hb_array_T* array);
+
+void hb_array_append(hb_array_T* array, void* item);
+void hb_array_set(const hb_array_T* array, size_t index, void* item);
+void hb_array_free(hb_array_T** array);
+void hb_array_remove(hb_array_T* array, size_t index);
+
+size_t hb_array_index_of(hb_array_T* array, void* item);
+void hb_array_remove_item(hb_array_T* array, void* item);
+
+void hb_array_push(hb_array_T* array, void* item);
+void* hb_array_pop(hb_array_T* array);
+
+size_t hb_array_capacity(const hb_array_T* array);
+size_t hb_array_size(const hb_array_T* array);
+size_t hb_array_sizeof(void);
+
+#endif
diff --git a/src/include/util/hb_buffer.h b/src/include/util/hb_buffer.h
new file mode 100644
index 000000000..d9395009f
--- /dev/null
+++ b/src/include/util/hb_buffer.h
@@ -0,0 +1,34 @@
+#ifndef HERB_BUFFER_H
+#define HERB_BUFFER_H
+
+#include "hb_string.h"
+
+#include
+#include
+
+typedef struct HB_BUFFER_STRUCT {
+ char* value;
+ size_t length;
+ size_t capacity;
+} hb_buffer_T;
+
+bool hb_buffer_init(hb_buffer_T* buffer, size_t capacity);
+
+void hb_buffer_append(hb_buffer_T* buffer, const char* text);
+void hb_buffer_append_with_length(hb_buffer_T* buffer, const char* text, size_t length);
+void hb_buffer_append_string(hb_buffer_T* buffer, hb_string_T string);
+void hb_buffer_append_char(hb_buffer_T* buffer, char character);
+void hb_buffer_append_whitespace(hb_buffer_T* buffer, size_t length);
+void hb_buffer_prepend(hb_buffer_T* buffer, const char* text);
+void hb_buffer_concat(hb_buffer_T* destination, hb_buffer_T* source);
+
+char* hb_buffer_value(const hb_buffer_T* buffer);
+
+size_t hb_buffer_length(const hb_buffer_T* buffer);
+size_t hb_buffer_capacity(const hb_buffer_T* buffer);
+size_t hb_buffer_sizeof(void);
+
+void hb_buffer_clear(hb_buffer_T* buffer);
+void hb_buffer_free(hb_buffer_T** buffer);
+
+#endif
diff --git a/src/include/util/hb_string.h b/src/include/util/hb_string.h
new file mode 100644
index 000000000..a0b90a5a7
--- /dev/null
+++ b/src/include/util/hb_string.h
@@ -0,0 +1,24 @@
+#ifndef HERB_STRING_H
+#define HERB_STRING_H
+
+#include
+#include
+#include
+
+#include "hb_arena.h"
+
+typedef struct HB_STRING_STRUCT {
+ char* data;
+ uint32_t length;
+} hb_string_T;
+
+hb_string_T hb_string(const char* null_terminated_c_string);
+hb_string_T hb_string_slice(hb_string_T string, uint32_t offset);
+bool hb_string_equals(hb_string_T a, hb_string_T b);
+bool hb_string_equals_case_insensitive(hb_string_T a, hb_string_T b);
+bool hb_string_starts_with(hb_string_T string, hb_string_T expected_prefix);
+bool hb_string_is_empty(hb_string_T string);
+
+char* hb_string_to_c_string(hb_arena_T* allocator, hb_string_T string);
+
+#endif
diff --git a/src/include/version.h b/src/include/version.h
index 4185a598a..a75f8cba0 100644
--- a/src/include/version.h
+++ b/src/include/version.h
@@ -1,6 +1,6 @@
#ifndef HERB_VERSION_H
#define HERB_VERSION_H
-#define HERB_VERSION "0.7.4"
+#define HERB_VERSION "0.7.5"
#endif
diff --git a/src/include/visitor.h b/src/include/visitor.h
index 01d5c9e44..bc6a6414f 100644
--- a/src/include/visitor.h
+++ b/src/include/visitor.h
@@ -1,9 +1,9 @@
#ifndef HERB_VISITOR_H
#define HERB_VISITOR_H
-#include "array.h"
#include "ast_node.h"
#include "ast_nodes.h"
+#include "util/hb_array.h"
void herb_visit_node(const AST_NODE_T* node, bool (*visitor)(const AST_NODE_T*, void*), void* data);
void herb_visit_child_nodes(const AST_NODE_T* node, bool (*visitor)(const AST_NODE_T* node, void* data), void* data);
diff --git a/src/io.c b/src/io.c
index a1ef45632..1c6e68f6d 100644
--- a/src/io.c
+++ b/src/io.c
@@ -1,5 +1,5 @@
#include "include/io.h"
-#include "include/buffer.h"
+#include "include/util/hb_buffer.h"
#include
#include
@@ -8,6 +8,8 @@
#define FILE_READ_CHUNK 4096
char* herb_read_file(const char* filename) {
+ if (!filename) { return NULL; }
+
FILE* fp = fopen(filename, "rb");
if (fp == NULL) {
@@ -15,16 +17,17 @@ char* herb_read_file(const char* filename) {
exit(1);
}
- buffer_T buffer = buffer_new();
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 4096);
char chunk[FILE_READ_CHUNK];
size_t bytes_read;
while ((bytes_read = fread(chunk, 1, FILE_READ_CHUNK, fp)) > 0) {
- buffer_append_with_length(&buffer, chunk, bytes_read);
+ hb_buffer_append_with_length(&buffer, chunk, bytes_read);
}
fclose(fp);
- return buffer_value(&buffer);
+ return hb_buffer_value(&buffer);
}
diff --git a/src/json.c b/src/json.c
deleted file mode 100644
index a1bc80662..000000000
--- a/src/json.c
+++ /dev/null
@@ -1,205 +0,0 @@
-#include "include/json.h"
-#include "include/buffer.h"
-
-void json_escape_string(buffer_T* json, const char* string) {
- if (!string) {
- buffer_append(json, "null");
- return;
- }
-
- buffer_append(json, "\"");
-
- while (*string) {
- switch (*string) {
- case '\"': buffer_append(json, "\\\""); break;
- case '\\': buffer_append(json, "\\\\"); break;
- case '\n': buffer_append(json, "\\n"); break;
- case '\t': buffer_append(json, "\\t"); break;
- default: buffer_append_char(json, *string); break;
- }
- string++;
- }
-
- buffer_append(json, "\"");
-}
-
-void json_int_to_string(const int value, char* buffer) {
- char string[20]; // Enough to hold all possible int values
- int i = 0;
-
- // Handle negative numbers
- unsigned int abs_value = (unsigned int) abs(value);
-
- do {
- string[i++] = (char) ((abs_value % 10) + '0');
- abs_value /= 10;
- } while (abs_value > 0);
-
- if (value < 0) { string[i++] = '-'; }
-
- int j = 0;
-
- while (i > 0) {
- buffer[j++] = string[--i];
- }
-
- buffer[j] = '\0';
-}
-
-void json_double_to_string(const double value, char* buffer) {
- const int int_part = (int) value;
- const double frac_part = value - (double) int_part;
- const int frac_as_int = (int) (frac_part * 100); // Keep 2 decimal places
-
- char int_buffer[20];
- char frac_buffer[5];
-
- json_int_to_string(int_part, int_buffer);
- json_int_to_string(frac_as_int < 0 ? -frac_as_int : frac_as_int, frac_buffer);
-
- char* pointer = buffer;
- for (const char* source = int_buffer; *source != '\0'; ++source) {
- *pointer++ = *source;
- }
-
- *pointer++ = '.';
-
- for (const char* source = frac_buffer; *source != '\0'; ++source) {
- *pointer++ = *source;
- }
-
- *pointer = '\0';
-}
-
-void json_add_string(buffer_T* json, const char* key, const char* value) {
- if (!json) { return; }
-
- if (json->length > 1) { buffer_append(json, ", "); }
-
- if (key) {
- json_escape_string(json, key);
- buffer_append(json, ": ");
- }
-
- json_escape_string(json, value);
-}
-
-void json_add_double(buffer_T* json, const char* key, const double value) {
- if (!json) { return; }
-
- char number[32];
- json_double_to_string(value, number);
-
- if (json->length > 1) { buffer_append(json, ", "); }
-
- if (key) {
- json_escape_string(json, key);
- buffer_append(json, ": ");
- }
-
- buffer_append(json, number);
-}
-
-void json_add_int(buffer_T* json, const char* key, const int value) {
- if (!json) { return; }
-
- char number[20];
- json_int_to_string(value, number);
-
- if (json->length > 1) { buffer_append(json, ", "); }
-
- if (key) {
- json_escape_string(json, key);
- buffer_append(json, ": ");
- }
-
- buffer_append(json, number);
- if (json->length == 1) { buffer_append(json, " "); }
-}
-
-void json_add_size_t(buffer_T* json, const char* key, size_t value) {
- if (!json) { return; }
-
- char number[32];
- char temp[32];
- int i = 0;
-
- do {
- temp[i++] = (char) ((value % 10) + '0');
- value /= 10;
- } while (value > 0);
-
- int j = 0;
- while (i > 0) {
- number[j++] = temp[--i];
- }
- number[j] = '\0';
-
- if (json->length > 1) { buffer_append(json, ", "); }
-
- if (key) {
- json_escape_string(json, key);
- buffer_append(json, ": ");
- }
-
- buffer_append(json, number);
- if (json->length == 1) { buffer_append(json, " "); }
-}
-
-void json_add_bool(buffer_T* json, const char* key, const int value) {
- if (!json) { return; }
-
- if (json->length > 1) { buffer_append(json, ", "); }
-
- if (key) {
- json_escape_string(json, key);
- buffer_append(json, ": ");
- }
-
- buffer_append(json, value ? "true" : "false");
-}
-
-void json_add_raw_string(buffer_T* json, const char* string) {
- if (!json) { return; }
-
- if (json->length > 1) { buffer_append(json, ", "); }
-
- buffer_append(json, string);
-}
-
-void json_start_root_object(buffer_T* json) {
- if (json) { buffer_append(json, "{"); }
-}
-
-void json_start_object(buffer_T* json, const char* key) {
- if (!json) { return; }
-
- if (json->length > 1) { buffer_append(json, ", "); }
-
- if (key) {
- json_escape_string(json, key);
- buffer_append(json, ": ");
- }
-
- buffer_append(json, "{");
-}
-
-void json_end_object(buffer_T* json) {
- if (json) { buffer_append(json, "}"); }
-}
-
-void json_start_root_array(buffer_T* json) {
- if (json) { buffer_append(json, "["); }
-}
-
-void json_start_array(buffer_T* json, const char* key) {
- if (!json) { return; }
-
- if (json->length > 1) { buffer_append(json, ", "); }
- json_escape_string(json, key);
- buffer_append(json, ": [");
-}
-
-void json_end_array(buffer_T* json) {
- if (json) { buffer_append(json, "]"); }
-}
diff --git a/src/lexer.c b/src/lexer.c
index 6e91e180c..45995751c 100644
--- a/src/lexer.c
+++ b/src/lexer.c
@@ -1,24 +1,21 @@
-#include "include/buffer.h"
#include "include/lexer_peek_helpers.h"
#include "include/token.h"
#include "include/utf8.h"
#include "include/util.h"
+#include "include/util/hb_buffer.h"
+#include "include/util/hb_string.h"
#include
#include
#define LEXER_STALL_LIMIT 5
-static size_t lexer_sizeof(void) {
- return sizeof(struct LEXER_STRUCT);
-}
-
static bool lexer_eof(const lexer_T* lexer) {
return lexer->current_character == '\0' || lexer->stalled;
}
static bool lexer_has_more_characters(const lexer_T* lexer) {
- return lexer->current_position < lexer->source_length;
+ return lexer->current_position < lexer->source.length;
}
static bool lexer_stalled(lexer_T* lexer) {
@@ -34,17 +31,16 @@ static bool lexer_stalled(lexer_T* lexer) {
return lexer->stalled;
}
-lexer_T* lexer_init(const char* source) {
- if (source == NULL) { source = ""; }
-
- lexer_T* lexer = calloc(1, lexer_sizeof());
+void lexer_init(lexer_T* lexer, const char* source) {
+ if (source != NULL) {
+ lexer->source = hb_string(source);
+ } else {
+ lexer->source = hb_string("");
+ }
+ lexer->current_character = lexer->source.data[0];
lexer->state = STATE_DATA;
- lexer->source = source;
- lexer->source_length = strlen(source);
- lexer->current_character = source[0];
-
lexer->current_line = 1;
lexer->current_column = 0;
lexer->current_position = 0;
@@ -56,8 +52,6 @@ lexer_T* lexer_init(const char* source) {
lexer->stall_counter = 0;
lexer->last_position = 0;
lexer->stalled = false;
-
- return lexer;
}
token_T* lexer_error(lexer_T* lexer, const char* message) {
@@ -66,7 +60,7 @@ token_T* lexer_error(lexer_T* lexer, const char* message) {
snprintf(
error_message,
sizeof(error_message),
- "[Lexer] Error: %s (character '%c', line %zu, col %zu)\n",
+ "[Lexer] Error: %s (character '%c', line %u, col %u)\n",
message,
lexer->current_character,
lexer->current_line,
@@ -81,7 +75,7 @@ static void lexer_advance(lexer_T* lexer) {
if (!is_newline(lexer->current_character)) { lexer->current_column++; }
lexer->current_position++;
- lexer->current_character = lexer->source[lexer->current_position];
+ lexer->current_character = lexer->source.data[lexer->current_position];
}
}
@@ -93,11 +87,11 @@ static void lexer_advance_utf8_bytes(lexer_T* lexer, int byte_count) {
lexer->current_position += byte_count;
- if (lexer->current_position >= lexer->source_length) {
- lexer->current_position = lexer->source_length;
+ if (lexer->current_position >= lexer->source.length) {
+ lexer->current_position = lexer->source.length;
lexer->current_character = '\0';
} else {
- lexer->current_character = lexer->source[lexer->current_position];
+ lexer->current_character = lexer->source.data[lexer->current_position];
}
}
}
@@ -135,7 +129,7 @@ static token_T* lexer_advance_current(lexer_T* lexer, const token_type_T type) {
}
static token_T* lexer_advance_utf8_character(lexer_T* lexer, const token_type_T type) {
- int char_byte_length = utf8_sequence_length(lexer->source, lexer->current_position, lexer->source_length);
+ int char_byte_length = utf8_sequence_length(lexer->source.data, lexer->current_position, lexer->source.length);
if (char_byte_length <= 1) { return lexer_advance_current(lexer, type); }
@@ -144,12 +138,12 @@ static token_T* lexer_advance_utf8_character(lexer_T* lexer, const token_type_T
if (!utf8_char) { return lexer_advance_current(lexer, type); }
for (int i = 0; i < char_byte_length; i++) {
- if (lexer->current_position + i >= lexer->source_length) {
+ if (lexer->current_position + i >= lexer->source.length) {
free(utf8_char);
return lexer_advance_current(lexer, type);
}
- utf8_char[i] = lexer->source[lexer->current_position + i];
+ utf8_char[i] = lexer->source.data[lexer->current_position + i];
}
utf8_char[char_byte_length] = '\0';
@@ -164,7 +158,7 @@ static token_T* lexer_advance_utf8_character(lexer_T* lexer, const token_type_T
}
static token_T* lexer_match_and_advance(lexer_T* lexer, const char* value, const token_type_T type) {
- if (strncmp(lexer->source + lexer->current_position, value, strlen(value)) == 0) {
+ if (strncmp(lexer->source.data + lexer->current_position, value, strlen(value)) == 0) {
return lexer_advance_with(lexer, value, type);
}
@@ -174,35 +168,37 @@ static token_T* lexer_match_and_advance(lexer_T* lexer, const char* value, const
// ===== Specialized Parsers
static token_T* lexer_parse_whitespace(lexer_T* lexer) {
- buffer_T buffer = buffer_new();
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 128);
while (isspace(lexer->current_character) && lexer->current_character != '\n' && lexer->current_character != '\r'
&& !lexer_eof(lexer)) {
- buffer_append_char(&buffer, lexer->current_character);
+ hb_buffer_append_char(&buffer, lexer->current_character);
lexer_advance(lexer);
}
token_T* token = token_init(buffer.value, TOKEN_WHITESPACE, lexer);
- buffer_free(&buffer);
+ free(buffer.value);
return token;
}
static token_T* lexer_parse_identifier(lexer_T* lexer) {
- buffer_T buffer = buffer_new();
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 128);
while ((isalnum(lexer->current_character) || lexer->current_character == '-' || lexer->current_character == '_'
|| lexer->current_character == ':')
&& !lexer_peek_for_html_comment_end(lexer, 0) && !lexer_eof(lexer)) {
- buffer_append_char(&buffer, lexer->current_character);
+ hb_buffer_append_char(&buffer, lexer->current_character);
lexer_advance(lexer);
}
token_T* token = token_init(buffer.value, TOKEN_IDENTIFIER, lexer);
- buffer_free(&buffer);
+ free(buffer.value);
return token;
}
@@ -223,14 +219,19 @@ static token_T* lexer_parse_erb_open(lexer_T* lexer) {
}
static token_T* lexer_parse_erb_content(lexer_T* lexer) {
- buffer_T buffer = buffer_new();
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
while (!lexer_peek_erb_end(lexer, 0)) {
if (lexer_eof(lexer)) {
- return token_init(buffer.value, TOKEN_ERROR, lexer); // Handle unexpected EOF
+ token_T* token = token_init(buffer.value, TOKEN_ERROR, lexer); // Handle unexpected EOF
+
+ free(buffer.value);
+
+ return token;
}
- buffer_append_char(&buffer, lexer->current_character);
+ hb_buffer_append_char(&buffer, lexer->current_character);
if (is_newline(lexer->current_character)) {
lexer->current_line++;
@@ -240,14 +241,14 @@ static token_T* lexer_parse_erb_content(lexer_T* lexer) {
}
lexer->current_position++;
- lexer->current_character = lexer->source[lexer->current_position];
+ lexer->current_character = lexer->source.data[lexer->current_position];
}
lexer->state = STATE_ERB_CLOSE;
token_T* token = token_init(buffer.value, TOKEN_ERB_CONTENT, lexer);
- buffer_free(&buffer);
+ free(buffer.value);
return token;
}
@@ -256,6 +257,7 @@ static token_T* lexer_parse_erb_close(lexer_T* lexer) {
lexer->state = STATE_DATA;
if (lexer_peek_erb_percent_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "%%>", TOKEN_ERB_END); }
+ if (lexer_peek_erb_equals_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "=%>", TOKEN_ERB_END); }
if (lexer_peek_erb_dash_close_tag(lexer, 0)) { return lexer_advance_with(lexer, "-%>", TOKEN_ERB_END); }
return lexer_advance_with(lexer, "%>", TOKEN_ERB_END);
@@ -353,9 +355,3 @@ token_T* lexer_next_token(lexer_T* lexer) {
}
}
}
-
-void lexer_free(lexer_T* lexer) {
- if (lexer == NULL) { return; }
-
- free(lexer);
-}
diff --git a/src/lexer_peek_helpers.c b/src/lexer_peek_helpers.c
index ca045ba31..86cbfe72a 100644
--- a/src/lexer_peek_helpers.c
+++ b/src/lexer_peek_helpers.c
@@ -3,80 +3,83 @@
#include "include/lexer_struct.h"
#include "include/macros.h"
#include "include/token.h"
+#include "include/util/hb_string.h"
#include
#include
-char lexer_backtrack(const lexer_T* lexer, const int offset) {
- return lexer->source[MAX(lexer->current_position - offset, 0)];
+char lexer_backtrack(const lexer_T* lexer, uint32_t offset) {
+ return lexer->source.data[MAX(lexer->current_position - offset, 0)];
}
-char lexer_peek(const lexer_T* lexer, const int offset) {
- return lexer->source[MIN(lexer->current_position + offset, lexer->source_length)];
+char lexer_peek(const lexer_T* lexer, uint32_t offset) {
+ return lexer->source.data[MIN(lexer->current_position + offset, lexer->source.length)];
}
-bool lexer_peek_for(const lexer_T* lexer, const int offset, const char* pattern, const bool case_insensitive) {
- for (int index = 0; pattern[index]; index++) {
- const char character = lexer_peek(lexer, offset + index);
+bool lexer_peek_for(const lexer_T* lexer, uint32_t offset, hb_string_T pattern, const bool case_insensitive) {
+ hb_string_T remaining_source = hb_string_slice(lexer->source, lexer->current_position + offset);
+ remaining_source.length = MIN(pattern.length, remaining_source.length);
- if (case_insensitive) {
- if (tolower(character) != tolower(pattern[index])) { return false; }
- } else {
- if (character != pattern[index]) { return false; }
- }
+ if (case_insensitive) {
+ return hb_string_equals_case_insensitive(remaining_source, pattern);
+ } else {
+ return hb_string_equals(remaining_source, pattern);
}
+}
- return true;
+bool lexer_peek_for_doctype(const lexer_T* lexer, uint32_t offset) {
+ return lexer_peek_for(lexer, offset, hb_string(""), false);
}
-bool lexer_peek_for_cdata_end(const lexer_T* lexer, const int offset) {
- return lexer_peek_for(lexer, offset, "]]>", false);
+bool lexer_peek_for_html_comment_start(const lexer_T* lexer, uint32_t offset) {
+ return lexer_peek_for(lexer, offset, hb_string(""), false);
}
-bool lexer_peek_for_html_comment_end(const lexer_T* lexer, const int offset) {
- return lexer_peek_for(lexer, offset, "-->", false);
+bool lexer_peek_erb_close_tag(const lexer_T* lexer, uint32_t offset) {
+ return lexer_peek_for(lexer, offset, hb_string("%>"), false);
}
-bool lexer_peek_erb_close_tag(const lexer_T* lexer, const int offset) {
- return lexer_peek_for(lexer, offset, "%>", false);
+bool lexer_peek_erb_dash_close_tag(const lexer_T* lexer, uint32_t offset) {
+ return lexer_peek_for(lexer, offset, hb_string("-%>"), false);
}
-bool lexer_peek_erb_dash_close_tag(const lexer_T* lexer, const int offset) {
- return lexer_peek_for(lexer, offset, "-%>", false);
+bool lexer_peek_erb_percent_close_tag(const lexer_T* lexer, uint32_t offset) {
+ return lexer_peek_for(lexer, offset, hb_string("%%>"), false);
}
-bool lexer_peek_erb_percent_close_tag(const lexer_T* lexer, const int offset) {
- return lexer_peek_for(lexer, offset, "%%>", false);
+bool lexer_peek_erb_equals_close_tag(const lexer_T* lexer, uint32_t offset) {
+ return lexer_peek_for(lexer, offset, hb_string("=%>"), false);
}
-bool lexer_peek_erb_end(const lexer_T* lexer, const int offset) {
+bool lexer_peek_erb_end(const lexer_T* lexer, uint32_t offset) {
return (
lexer_peek_erb_close_tag(lexer, offset) || lexer_peek_erb_dash_close_tag(lexer, offset)
- || lexer_peek_erb_percent_close_tag(lexer, offset)
+ || lexer_peek_erb_percent_close_tag(lexer, offset) || lexer_peek_erb_equals_close_tag(lexer, offset)
);
}
bool lexer_peek_for_token_type_after_whitespace(lexer_T* lexer, token_type_T token_type) {
- size_t saved_position = lexer->current_position;
- size_t saved_line = lexer->current_line;
- size_t saved_column = lexer->current_column;
+ uint32_t saved_position = lexer->current_position;
+ uint32_t saved_line = lexer->current_line;
+ uint32_t saved_column = lexer->current_column;
char saved_character = lexer->current_character;
+ lexer_state_T saved_state = lexer->state;
token_T* token = lexer_next_token(lexer);
@@ -93,11 +96,12 @@ bool lexer_peek_for_token_type_after_whitespace(lexer_T* lexer, token_type_T tok
lexer->current_line = saved_line;
lexer->current_column = saved_column;
lexer->current_character = saved_character;
+ lexer->state = saved_state;
return result;
}
-bool lexer_peek_for_close_tag_start(const lexer_T* lexer, const int offset) {
+bool lexer_peek_for_close_tag_start(const lexer_T* lexer, uint32_t offset) {
if (lexer_peek(lexer, offset) != '<' || lexer_peek(lexer, offset + 1) != '/') { return false; }
int pos = offset + 2;
diff --git a/src/location.c b/src/location.c
index 1c83c5e49..aec168d3c 100644
--- a/src/location.c
+++ b/src/location.c
@@ -1,41 +1,13 @@
#include "include/location.h"
-#include "include/memory.h"
#include "include/position.h"
-size_t location_sizeof(void) {
- return sizeof(location_T);
-}
-
-location_T* location_init(position_T* start, position_T* end) {
- location_T* location = safe_malloc(location_sizeof());
-
- location->start = start;
- location->end = end;
-
- return location;
-}
-
-location_T* location_from(size_t start_line, size_t start_column, size_t end_line, size_t end_column) {
- return location_init(position_init(start_line, start_column), position_init(end_line, end_column));
-}
-
-position_T* location_start(location_T* location) {
- return location->start;
-}
-
-position_T* location_end(location_T* location) {
- return location->end;
-}
-
-location_T* location_copy(location_T* location) {
- if (location == NULL) { return NULL; }
-
- return location_init(position_copy(location->start), position_copy(location->end));
-}
-
-void location_free(location_T* location) {
- if (location->start != NULL) { position_free(location->start); }
- if (location->end != NULL) { position_free(location->end); }
-
- free(location);
+void location_from(
+ location_T* location,
+ uint32_t start_line,
+ uint32_t start_column,
+ uint32_t end_line,
+ uint32_t end_column
+) {
+ location->start = (position_T) { .line = start_line, .column = start_column };
+ location->end = (position_T) { .line = end_line, .column = end_column };
}
diff --git a/src/main.c b/src/main.c
index d89af37b0..52b1f370c 100644
--- a/src/main.c
+++ b/src/main.c
@@ -4,11 +4,11 @@
#include "include/ast_node.h"
#include "include/ast_nodes.h"
#include "include/ast_pretty_print.h"
-#include "include/buffer.h"
#include "include/extract.h"
#include "include/herb.h"
#include "include/io.h"
#include "include/ruby_parser.h"
+#include "include/util/hb_buffer.h"
#include
#include
@@ -39,7 +39,6 @@ int main(const int argc, char* argv[]) {
printf("Herb 🌿 Powerful and seamless HTML-aware ERB parsing and tooling.\n\n");
printf("./herb lex [file] - Lex a file\n");
- printf("./herb lex_json [file] - Lex a file and return the result as json.\n");
printf("./herb parse [file] - Parse a file\n");
printf("./herb ruby [file] - Extract Ruby from a file\n");
printf("./herb html [file] - Extract HTML from a file\n");
@@ -53,9 +52,9 @@ int main(const int argc, char* argv[]) {
return 1;
}
- buffer_T output;
+ hb_buffer_T output;
- if (!buffer_init(&output)) { return 1; }
+ if (!hb_buffer_init(&output, 4096)) { return 1; }
char* source = herb_read_file(argv[2]);
@@ -74,7 +73,7 @@ int main(const int argc, char* argv[]) {
print_time_diff(start, end, "visiting");
ast_node_free((AST_NODE_T*) root);
- buffer_free(&output);
+ free(output.value);
free(source);
return 0;
@@ -87,18 +86,7 @@ int main(const int argc, char* argv[]) {
printf("%s\n", output.value);
print_time_diff(start, end, "lexing");
- buffer_free(&output);
- free(source);
-
- return 0;
- }
-
- if (strcmp(argv[1], "lex_json") == 0) {
- herb_lex_json_to_buffer(source, &output);
-
- printf("%s\n", output.value);
-
- buffer_free(&output);
+ free(output.value);
free(source);
return 0;
@@ -106,15 +94,23 @@ int main(const int argc, char* argv[]) {
if (strcmp(argv[1], "parse") == 0) {
AST_DOCUMENT_NODE_T* root = herb_parse(source, NULL);
+
+ herb_analyze_parse_tree(root, source);
+
clock_gettime(CLOCK_MONOTONIC, &end);
- ast_pretty_print_node((AST_NODE_T*) root, 0, 0, &output);
- printf("%s\n", output.value);
+ int silent = 0;
+ if (argc > 3 && strcmp(argv[3], "--silent") == 0) { silent = 1; }
+
+ if (!silent) {
+ ast_pretty_print_node((AST_NODE_T*) root, 0, 0, &output);
+ printf("%s\n", output.value);
- print_time_diff(start, end, "parsing");
+ print_time_diff(start, end, "parsing");
+ }
ast_node_free((AST_NODE_T*) root);
- buffer_free(&output);
+ free(output.value);
free(source);
return 0;
@@ -127,7 +123,7 @@ int main(const int argc, char* argv[]) {
printf("%s\n", output.value);
print_time_diff(start, end, "extracting Ruby");
- buffer_free(&output);
+ free(output.value);
free(source);
return 0;
@@ -140,7 +136,7 @@ int main(const int argc, char* argv[]) {
printf("%s\n", output.value);
print_time_diff(start, end, "extracting HTML");
- buffer_free(&output);
+ free(output.value);
free(source);
return 0;
diff --git a/src/memory.c b/src/memory.c
deleted file mode 100644
index f0f3ed8dc..000000000
--- a/src/memory.c
+++ /dev/null
@@ -1,53 +0,0 @@
-#include "memory.h"
-
-#include
-#include
-#include
-
-static void* safe_malloc_internal(const size_t size, const bool fail_fast) {
- if (size == 0) { return NULL; }
-
- void* pointer = malloc(size);
-
- if (!pointer) {
- fprintf(stderr, "Error: Failed to allocate %zu bytes.\n", size);
- fflush(stderr);
- if (fail_fast) { exit(1); }
- return NULL;
- }
-
- return pointer;
-}
-
-static void* safe_realloc_internal(void* pointer, const size_t new_size, const bool fail_fast) {
- if (new_size == 0) { return NULL; }
-
- if (!pointer) { return safe_malloc_internal(new_size, fail_fast); }
-
- void* new_pointer = realloc(pointer, new_size);
-
- if (!new_pointer) {
- fprintf(stderr, "Error: Memory reallocation failed (size: %zu bytes).\n", new_size);
- fflush(stderr);
- if (fail_fast) { exit(1); }
- return NULL;
- }
-
- return new_pointer;
-}
-
-void* safe_malloc(const size_t size) {
- return safe_malloc_internal(size, true);
-}
-
-void* nullable_safe_malloc(const size_t size) {
- return safe_malloc_internal(size, false);
-}
-
-void* safe_realloc(void* pointer, const size_t new_size) {
- return safe_realloc_internal(pointer, new_size, true);
-}
-
-void* nullable_safe_realloc(void* pointer, const size_t new_size) {
- return safe_realloc_internal(pointer, new_size, false);
-}
diff --git a/src/parser.c b/src/parser.c
index 3e4e4ffde..3cc25791a 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -1,8 +1,6 @@
#include "include/parser.h"
-#include "include/array.h"
#include "include/ast_node.h"
#include "include/ast_nodes.h"
-#include "include/buffer.h"
#include "include/errors.h"
#include "include/html_util.h"
#include "include/lexer.h"
@@ -11,65 +9,60 @@
#include "include/token.h"
#include "include/token_matchers.h"
#include "include/util.h"
+#include "include/util/hb_array.h"
+#include "include/util/hb_buffer.h"
+#include "include/util/hb_string.h"
#include
#include
#include
#include
-static void parser_parse_in_data_state(parser_T* parser, array_T* children, array_T* errors);
-static void parser_parse_foreign_content(parser_T* parser, array_T* children, array_T* errors);
+static void parser_parse_in_data_state(parser_T* parser, hb_array_T* children, hb_array_T* errors);
+static void parser_parse_foreign_content(parser_T* parser, hb_array_T* children, hb_array_T* errors);
static AST_ERB_CONTENT_NODE_T* parser_parse_erb_tag(parser_T* parser);
-static void parser_handle_whitespace(parser_T* parser, token_T* whitespace_token, array_T* children);
-static void parser_consume_whitespace(parser_T* parser, array_T* children);
+static void parser_handle_whitespace(parser_T* parser, token_T* whitespace_token, hb_array_T* children);
+static void parser_consume_whitespace(parser_T* parser, hb_array_T* children);
static void parser_skip_erb_content(lexer_T* lexer);
static bool parser_lookahead_erb_is_attribute(lexer_T* lexer);
-static void parser_handle_erb_in_open_tag(parser_T* parser, array_T* children);
-static void parser_handle_whitespace_in_open_tag(parser_T* parser, array_T* children);
+static void parser_handle_erb_in_open_tag(parser_T* parser, hb_array_T* children);
+static void parser_handle_whitespace_in_open_tag(parser_T* parser, hb_array_T* children);
+
+const parser_options_T HERB_DEFAULT_PARSER_OPTIONS = { .track_whitespace = false };
size_t parser_sizeof(void) {
return sizeof(struct PARSER_STRUCT);
}
-parser_T* herb_parser_init(lexer_T* lexer, parser_options_T* options) {
- parser_T* parser = calloc(1, parser_sizeof());
-
+void herb_parser_init(parser_T* parser, lexer_T* lexer, parser_options_T options) {
parser->lexer = lexer;
parser->current_token = lexer_next_token(lexer);
- parser->open_tags_stack = array_init(16);
+ parser->open_tags_stack = hb_array_init(16);
parser->state = PARSER_STATE_DATA;
parser->foreign_content_type = FOREIGN_CONTENT_UNKNOWN;
-
- if (options) {
- parser->options = calloc(1, sizeof(parser_options_T));
- parser->options->track_whitespace = options->track_whitespace;
- } else {
- parser->options = NULL;
- }
-
- return parser;
+ parser->options = options;
}
static AST_CDATA_NODE_T* parser_parse_cdata(parser_T* parser) {
- array_T* errors = array_init(8);
- array_T* children = array_init(8);
- buffer_T content = buffer_new();
+ hb_array_T* errors = hb_array_init(8);
+ hb_array_T* children = hb_array_init(8);
+ hb_buffer_T content;
+ hb_buffer_init(&content, 128);
token_T* tag_opening = parser_consume_expected(parser, TOKEN_CDATA_START, errors);
- position_T* start = position_copy(parser->current_token->location->start);
+ position_T start = parser->current_token->location.start;
while (token_is_none_of(parser, TOKEN_CDATA_END, TOKEN_EOF)) {
if (token_is(parser, TOKEN_ERB_START)) {
parser_append_literal_node_from_buffer(parser, &content, children, start);
AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser);
- array_append(children, erb_node);
- position_free(start);
- start = position_copy(parser->current_token->location->start);
+ hb_array_append(children, erb_node);
+ start = parser->current_token->location.start;
continue;
}
token_T* token = parser_advance(parser);
- buffer_append(&content, token->value);
+ hb_buffer_append(&content, token->value);
token_free(token);
}
@@ -80,13 +73,12 @@ static AST_CDATA_NODE_T* parser_parse_cdata(parser_T* parser) {
tag_opening,
children,
tag_closing,
- tag_opening->location->start,
- tag_closing->location->end,
+ tag_opening->location.start,
+ tag_closing->location.end,
errors
);
- position_free(start);
- buffer_free(&content);
+ free(content.value);
token_free(tag_opening);
token_free(tag_closing);
@@ -94,28 +86,28 @@ static AST_CDATA_NODE_T* parser_parse_cdata(parser_T* parser) {
}
static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) {
- array_T* errors = array_init(8);
- array_T* children = array_init(8);
+ hb_array_T* errors = hb_array_init(8);
+ hb_array_T* children = hb_array_init(8);
token_T* comment_start = parser_consume_expected(parser, TOKEN_HTML_COMMENT_START, errors);
- position_T* start = position_copy(parser->current_token->location->start);
+ position_T start = parser->current_token->location.start;
- buffer_T comment = buffer_new();
+ hb_buffer_T comment;
+ hb_buffer_init(&comment, 512);
while (token_is_none_of(parser, TOKEN_HTML_COMMENT_END, TOKEN_EOF)) {
if (token_is(parser, TOKEN_ERB_START)) {
parser_append_literal_node_from_buffer(parser, &comment, children, start);
AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser);
- array_append(children, erb_node);
+ hb_array_append(children, erb_node);
- position_free(start);
- start = position_copy(parser->current_token->location->start);
+ start = parser->current_token->location.start;
continue;
}
token_T* token = parser_advance(parser);
- buffer_append(&comment, token->value);
+ hb_buffer_append(&comment, token->value);
token_free(token);
}
@@ -127,13 +119,12 @@ static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) {
comment_start,
children,
comment_end,
- comment_start->location->start,
- comment_end->location->end,
+ comment_start->location.start,
+ comment_end->location.end,
errors
);
- buffer_free(&comment);
- position_free(start);
+ free(comment.value);
token_free(comment_start);
token_free(comment_end);
@@ -141,26 +132,27 @@ static AST_HTML_COMMENT_NODE_T* parser_parse_html_comment(parser_T* parser) {
}
static AST_HTML_DOCTYPE_NODE_T* parser_parse_html_doctype(parser_T* parser) {
- array_T* errors = array_init(8);
- array_T* children = array_init(8);
- buffer_T content = buffer_new();
+ hb_array_T* errors = hb_array_init(8);
+ hb_array_T* children = hb_array_init(8);
+ hb_buffer_T content;
+ hb_buffer_init(&content, 64);
token_T* tag_opening = parser_consume_expected(parser, TOKEN_HTML_DOCTYPE, errors);
- position_T* start = position_copy(parser->current_token->location->start);
+ position_T start = parser->current_token->location.start;
while (token_is_none_of(parser, TOKEN_HTML_TAG_END, TOKEN_EOF)) {
if (token_is(parser, TOKEN_ERB_START)) {
parser_append_literal_node_from_buffer(parser, &content, children, start);
AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser);
- array_append(children, erb_node);
+ hb_array_append(children, erb_node);
continue;
}
token_T* token = parser_consume_expected(parser, parser->current_token->type, errors);
- buffer_append(&content, token->value);
+ hb_buffer_append(&content, token->value);
token_free(token);
}
@@ -172,43 +164,42 @@ static AST_HTML_DOCTYPE_NODE_T* parser_parse_html_doctype(parser_T* parser) {
tag_opening,
children,
tag_closing,
- tag_opening->location->start,
- tag_closing->location->end,
+ tag_opening->location.start,
+ tag_closing->location.end,
errors
);
- position_free(start);
token_free(tag_opening);
token_free(tag_closing);
- buffer_free(&content);
+ free(content.value);
return doctype;
}
static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser) {
- array_T* errors = array_init(8);
- array_T* children = array_init(8);
- buffer_T content = buffer_new();
+ hb_array_T* errors = hb_array_init(8);
+ hb_array_T* children = hb_array_init(8);
+ hb_buffer_T content;
+ hb_buffer_init(&content, 64);
token_T* tag_opening = parser_consume_expected(parser, TOKEN_XML_DECLARATION, errors);
- position_T* start = position_copy(parser->current_token->location->start);
+ position_T start = parser->current_token->location.start;
while (token_is_none_of(parser, TOKEN_XML_DECLARATION_END, TOKEN_EOF)) {
if (token_is(parser, TOKEN_ERB_START)) {
parser_append_literal_node_from_buffer(parser, &content, children, start);
AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser);
- array_append(children, erb_node);
+ hb_array_append(children, erb_node);
- position_free(start);
- start = position_copy(parser->current_token->location->start);
+ start = parser->current_token->location.start;
continue;
}
token_T* token = parser_advance(parser);
- buffer_append(&content, token->value);
+ hb_buffer_append(&content, token->value);
token_free(token);
}
@@ -220,23 +211,23 @@ static AST_XML_DECLARATION_NODE_T* parser_parse_xml_declaration(parser_T* parser
tag_opening,
children,
tag_closing,
- tag_opening->location->start,
- tag_closing->location->end,
+ tag_opening->location.start,
+ tag_closing->location.end,
errors
);
- position_free(start);
token_free(tag_opening);
token_free(tag_closing);
- buffer_free(&content);
+ free(content.value);
return xml_declaration;
}
-static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T* document_errors) {
- position_T* start = position_copy(parser->current_token->location->start);
+static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, hb_array_T* document_errors) {
+ position_T start = parser->current_token->location.start;
- buffer_T content = buffer_new();
+ hb_buffer_T content;
+ hb_buffer_init(&content, 2048);
while (token_is_none_of(
parser,
@@ -248,54 +239,50 @@ static AST_HTML_TEXT_NODE_T* parser_parse_text_content(parser_T* parser, array_T
TOKEN_EOF
)) {
if (token_is(parser, TOKEN_ERROR)) {
- buffer_free(&content);
+ free(content.value);
token_T* token = parser_consume_expected(parser, TOKEN_ERROR, document_errors);
append_unexpected_error(
"Token Error",
"not TOKEN_ERROR",
token->value,
- token->location->start,
- token->location->end,
+ token->location.start,
+ token->location.end,
document_errors
);
token_free(token);
- position_free(start);
return NULL;
}
token_T* token = parser_advance(parser);
- buffer_append(&content, token->value);
+ hb_buffer_append(&content, token->value);
token_free(token);
}
- array_T* errors = array_init(8);
+ hb_array_T* errors = hb_array_init(8);
- if (buffer_length(&content) > 0) {
- AST_HTML_TEXT_NODE_T* text_node =
- ast_html_text_node_init(buffer_value(&content), start, parser->current_token->location->start, errors);
+ AST_HTML_TEXT_NODE_T* text_node = NULL;
- position_free(start);
- buffer_free(&content);
-
- return text_node;
+ if (hb_buffer_length(&content) > 0) {
+ text_node =
+ ast_html_text_node_init(hb_buffer_value(&content), start, parser->current_token->location.start, errors);
+ } else {
+ text_node = ast_html_text_node_init("", start, parser->current_token->location.start, errors);
}
- AST_HTML_TEXT_NODE_T* text_node = ast_html_text_node_init("", start, parser->current_token->location->start, errors);
-
- position_free(start);
- buffer_free(&content);
+ free(content.value);
return text_node;
}
static AST_HTML_ATTRIBUTE_NAME_NODE_T* parser_parse_html_attribute_name(parser_T* parser) {
- array_T* errors = array_init(8);
- array_T* children = array_init(8);
- buffer_T buffer = buffer_new();
- position_T* start = position_copy(parser->current_token->location->start);
+ hb_array_T* errors = hb_array_init(8);
+ hb_array_T* children = hb_array_init(8);
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 128);
+ position_T start = parser->current_token->location.start;
while (token_is_none_of(
parser,
@@ -310,53 +297,50 @@ static AST_HTML_ATTRIBUTE_NAME_NODE_T* parser_parse_html_attribute_name(parser_T
parser_append_literal_node_from_buffer(parser, &buffer, children, start);
AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser);
- array_append(children, erb_node);
+ hb_array_append(children, erb_node);
- position_free(start);
- start = position_copy(parser->current_token->location->start);
+ start = parser->current_token->location.start;
continue;
}
token_T* token = parser_advance(parser);
- buffer_append(&buffer, token->value);
+ hb_buffer_append(&buffer, token->value);
token_free(token);
}
parser_append_literal_node_from_buffer(parser, &buffer, children, start);
- position_T* node_start = NULL;
- position_T* node_end = NULL;
+ position_T node_start = { 0 };
+ position_T node_end = { 0 };
if (children->size > 0) {
- AST_NODE_T* first_child = array_get(children, 0);
- AST_NODE_T* last_child = array_get(children, children->size - 1);
+ AST_NODE_T* first_child = hb_array_get(children, 0);
+ AST_NODE_T* last_child = hb_array_get(children, children->size - 1);
- node_start = position_copy(first_child->location->start);
- node_end = position_copy(last_child->location->end);
+ node_start = first_child->location.start;
+ node_end = last_child->location.end;
} else {
- node_start = position_copy(parser->current_token->location->start);
- node_end = position_copy(parser->current_token->location->start);
+ node_start = parser->current_token->location.start;
+ node_end = parser->current_token->location.start;
}
AST_HTML_ATTRIBUTE_NAME_NODE_T* attribute_name =
ast_html_attribute_name_node_init(children, node_start, node_end, errors);
- position_free(start);
- position_free(node_start);
- position_free(node_end);
- buffer_free(&buffer);
+ free(buffer.value);
return attribute_name;
}
static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value(
parser_T* parser,
- array_T* children,
- array_T* errors
+ hb_array_T* children,
+ hb_array_T* errors
) {
- buffer_T buffer = buffer_new();
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 512);
token_T* opening_quote = parser_consume_expected(parser, TOKEN_QUOTE, errors);
- position_T* start = position_copy(parser->current_token->location->start);
+ position_T start = parser->current_token->location.start;
while (!token_is(parser, TOKEN_EOF)
&& !(
@@ -366,10 +350,9 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
if (token_is(parser, TOKEN_ERB_START)) {
parser_append_literal_node_from_buffer(parser, &buffer, children, start);
- array_append(children, parser_parse_erb_tag(parser));
+ hb_array_append(children, parser_parse_erb_tag(parser));
- position_free(start);
- start = position_copy(parser->current_token->location->start);
+ start = parser->current_token->location.start;
continue;
}
@@ -381,8 +364,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
if (next_token && next_token->type == TOKEN_QUOTE && opening_quote != NULL
&& strcmp(next_token->value, opening_quote->value) == 0) {
- buffer_append(&buffer, parser->current_token->value);
- buffer_append(&buffer, next_token->value);
+ hb_buffer_append(&buffer, parser->current_token->value);
+ hb_buffer_append(&buffer, next_token->value);
token_free(parser->current_token);
token_free(next_token);
@@ -396,7 +379,7 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
}
}
- buffer_append(&buffer, parser->current_token->value);
+ hb_buffer_append(&buffer, parser->current_token->value);
token_free(parser->current_token);
parser->current_token = lexer_next_token(parser->lexer);
@@ -414,8 +397,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
"Unescaped quote character in attribute value",
"escaped quote (\\') or different quote style (\")",
opening_quote->value,
- potential_closing->location->start,
- potential_closing->location->end,
+ potential_closing->location.start,
+ potential_closing->location.end,
errors
);
@@ -424,7 +407,7 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
token_free(parser->current_token);
parser->current_token = potential_closing;
- buffer_append(&buffer, parser->current_token->value);
+ hb_buffer_append(&buffer, parser->current_token->value);
token_free(parser->current_token);
parser->current_token = lexer_next_token(parser->lexer);
@@ -436,15 +419,14 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
if (token_is(parser, TOKEN_ERB_START)) {
parser_append_literal_node_from_buffer(parser, &buffer, children, start);
- array_append(children, parser_parse_erb_tag(parser));
+ hb_array_append(children, parser_parse_erb_tag(parser));
- position_free(start);
- start = position_copy(parser->current_token->location->start);
+ start = parser->current_token->location.start;
continue;
}
- buffer_append(&buffer, parser->current_token->value);
+ hb_buffer_append(&buffer, parser->current_token->value);
token_free(parser->current_token);
parser->current_token = lexer_next_token(parser->lexer);
@@ -458,8 +440,7 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
}
parser_append_literal_node_from_buffer(parser, &buffer, children, start);
- position_free(start);
- buffer_free(&buffer);
+ free(buffer.value);
token_T* closing_quote = parser_consume_expected(parser, TOKEN_QUOTE, errors);
@@ -467,8 +448,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
append_quotes_mismatch_error(
opening_quote,
closing_quote,
- closing_quote->location->start,
- closing_quote->location->end,
+ closing_quote->location.start,
+ closing_quote->location.end,
errors
);
}
@@ -478,8 +459,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
children,
closing_quote,
true,
- opening_quote->location->start,
- closing_quote->location->end,
+ opening_quote->location.start,
+ closing_quote->location.end,
errors
);
@@ -490,21 +471,21 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_quoted_html_attribute_value
}
static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser_T* parser) {
- array_T* children = array_init(8);
- array_T* errors = array_init(8);
+ hb_array_T* children = hb_array_init(8);
+ hb_array_T* errors = hb_array_init(8);
// >
if (token_is(parser, TOKEN_ERB_START)) {
AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser);
- array_append(children, erb_node);
+ hb_array_append(children, erb_node);
return ast_html_attribute_value_node_init(
NULL,
children,
NULL,
false,
- erb_node->base.location->start,
- erb_node->base.location->end,
+ erb_node->base.location.start,
+ erb_node->base.location.end,
errors
);
}
@@ -515,15 +496,15 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser
AST_LITERAL_NODE_T* literal = ast_literal_node_init_from_token(identifier);
token_free(identifier);
- array_append(children, literal);
+ hb_array_append(children, literal);
return ast_html_attribute_value_node_init(
NULL,
children,
NULL,
false,
- literal->base.location->start,
- literal->base.location->end,
+ literal->base.location.start,
+ literal->base.location.end,
errors
);
}
@@ -533,8 +514,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser
if (token_is(parser, TOKEN_BACKTICK)) {
token_T* token = parser_advance(parser);
- position_T* start = position_copy(token->location->start);
- position_T* end = position_copy(token->location->end);
+ position_T start = token->location.start;
+ position_T end = token->location.end;
append_unexpected_error(
"Invalid quote character for HTML attribute",
@@ -548,8 +529,6 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser
AST_HTML_ATTRIBUTE_VALUE_NODE_T* value =
ast_html_attribute_value_node_init(NULL, children, NULL, false, start, end, errors);
- position_free(start);
- position_free(end);
token_free(token);
return value;
@@ -559,8 +538,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser
"Unexpected Token",
"TOKEN_IDENTIFIER, TOKEN_QUOTE, TOKEN_ERB_START",
token_type_to_string(parser->current_token->type),
- parser->current_token->location->start,
- parser->current_token->location->end,
+ parser->current_token->location.start,
+ parser->current_token->location.end,
errors
);
@@ -569,8 +548,8 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser
children,
NULL,
false,
- parser->current_token->location->start,
- parser->current_token->location->end,
+ parser->current_token->location.start,
+ parser->current_token->location.end,
errors
);
@@ -580,56 +559,60 @@ static AST_HTML_ATTRIBUTE_VALUE_NODE_T* parser_parse_html_attribute_value(parser
static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser) {
AST_HTML_ATTRIBUTE_NAME_NODE_T* attribute_name = parser_parse_html_attribute_name(parser);
- if (parser->options && parser->options->track_whitespace) {
+ if (parser->options.track_whitespace) {
bool has_equals = (parser->current_token->type == TOKEN_EQUALS)
|| lexer_peek_for_token_type_after_whitespace(parser->lexer, TOKEN_EQUALS);
if (has_equals) {
- buffer_T equals_buffer = buffer_new();
- position_T* equals_start = NULL;
- position_T* equals_end = NULL;
- size_t range_start = 0;
- size_t range_end = 0;
-
+ hb_buffer_T equals_buffer;
+ hb_buffer_init(&equals_buffer, 256);
+ position_T equals_start = { 0 };
+ position_T equals_end = { 0 };
+ uint32_t range_start = 0;
+ uint32_t range_end = 0;
+
+ bool equals_start_present = false;
while (token_is_any_of(parser, TOKEN_WHITESPACE, TOKEN_NEWLINE)) {
token_T* whitespace = parser_advance(parser);
- if (equals_start == NULL) {
- equals_start = position_copy(whitespace->location->start);
- range_start = whitespace->range->from;
+ if (equals_start_present == false) {
+ equals_start_present = true;
+ equals_start = whitespace->location.start;
+ range_start = whitespace->range.from;
}
- buffer_append(&equals_buffer, whitespace->value);
+ hb_buffer_append(&equals_buffer, whitespace->value);
token_free(whitespace);
}
token_T* equals = parser_advance(parser);
- if (equals_start == NULL) {
- equals_start = position_copy(equals->location->start);
- range_start = equals->range->from;
+ if (equals_start_present == false) {
+ equals_start_present = true;
+ equals_start = equals->location.start;
+ range_start = equals->range.from;
}
- buffer_append(&equals_buffer, equals->value);
- equals_end = position_copy(equals->location->end);
- range_end = equals->range->to;
+ hb_buffer_append(&equals_buffer, equals->value);
+ equals_end = equals->location.end;
+ range_end = equals->range.to;
token_free(equals);
while (token_is_any_of(parser, TOKEN_WHITESPACE, TOKEN_NEWLINE)) {
token_T* whitespace = parser_advance(parser);
- buffer_append(&equals_buffer, whitespace->value);
- equals_end = position_copy(whitespace->location->end);
- range_end = whitespace->range->to;
+ hb_buffer_append(&equals_buffer, whitespace->value);
+ equals_end = whitespace->location.end;
+ range_end = whitespace->range.to;
token_free(whitespace);
}
token_T* equals_with_whitespace = calloc(1, sizeof(token_T));
equals_with_whitespace->type = TOKEN_EQUALS;
equals_with_whitespace->value = herb_strdup(equals_buffer.value);
- equals_with_whitespace->location = location_init(equals_start, equals_end);
- equals_with_whitespace->range = range_init(range_start, range_end);
+ equals_with_whitespace->location = (location_T) { .start = equals_start, .end = equals_end };
+ equals_with_whitespace->range = (range_T) { .from = range_start, .to = range_end };
- buffer_free(&equals_buffer);
+ free(equals_buffer.value);
AST_HTML_ATTRIBUTE_VALUE_NODE_T* attribute_value = parser_parse_html_attribute_value(parser);
@@ -637,8 +620,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser)
attribute_name,
equals_with_whitespace,
attribute_value,
- attribute_name->base.location->start,
- attribute_value->base.location->end,
+ attribute_name->base.location.start,
+ attribute_value->base.location.end,
NULL
);
} else {
@@ -646,8 +629,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser)
attribute_name,
NULL,
NULL,
- attribute_name->base.location->start,
- attribute_name->base.location->end,
+ attribute_name->base.location.start,
+ attribute_name->base.location.end,
NULL
);
}
@@ -666,8 +649,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser)
attribute_name,
equals,
attribute_value,
- attribute_name->base.location->start,
- attribute_value->base.location->end,
+ attribute_name->base.location.start,
+ attribute_value->base.location.end,
NULL
);
@@ -680,8 +663,8 @@ static AST_HTML_ATTRIBUTE_NODE_T* parser_parse_html_attribute(parser_T* parser)
attribute_name,
NULL,
NULL,
- attribute_name->base.location->start,
- attribute_name->base.location->end,
+ attribute_name->base.location.start,
+ attribute_name->base.location.end,
NULL
);
}
@@ -735,12 +718,12 @@ static bool parser_lookahead_erb_is_attribute(lexer_T* lexer) {
} while (true);
}
-static void parser_handle_erb_in_open_tag(parser_T* parser, array_T* children) {
+static void parser_handle_erb_in_open_tag(parser_T* parser, hb_array_T* children) {
bool is_output_tag = parser->current_token->value && strlen(parser->current_token->value) >= 3
&& strncmp(parser->current_token->value, "<%=", 3) == 0;
if (!is_output_tag) {
- array_append(children, parser_parse_erb_tag(parser));
+ hb_array_append(children, parser_parse_erb_tag(parser));
return;
}
@@ -754,13 +737,13 @@ static void parser_handle_erb_in_open_tag(parser_T* parser, array_T* children) {
bool looks_like_attribute = parser_lookahead_erb_is_attribute(&lexer_copy);
if (looks_like_attribute) {
- array_append(children, parser_parse_html_attribute(parser));
+ hb_array_append(children, parser_parse_html_attribute(parser));
} else {
- array_append(children, parser_parse_erb_tag(parser));
+ hb_array_append(children, parser_parse_erb_tag(parser));
}
}
-static void parser_handle_whitespace_in_open_tag(parser_T* parser, array_T* children) {
+static void parser_handle_whitespace_in_open_tag(parser_T* parser, hb_array_T* children) {
token_T* whitespace = parser_consume_if_present(parser, TOKEN_WHITESPACE);
if (whitespace != NULL) {
@@ -774,8 +757,8 @@ static void parser_handle_whitespace_in_open_tag(parser_T* parser, array_T* chil
}
static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) {
- array_T* errors = array_init(8);
- array_T* children = array_init(8);
+ hb_array_T* errors = hb_array_init(8);
+ hb_array_T* children = hb_array_init(8);
token_T* tag_start = parser_consume_expected(parser, TOKEN_HTML_TAG_START, errors);
token_T* tag_name = parser_consume_expected(parser, TOKEN_IDENTIFIER, errors);
@@ -787,7 +770,7 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) {
}
if (parser->current_token->type == TOKEN_IDENTIFIER) {
- array_append(children, parser_parse_html_attribute(parser));
+ hb_array_append(children, parser_parse_html_attribute(parser));
continue;
}
@@ -797,7 +780,7 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) {
}
if (parser->current_token->type == TOKEN_AT) {
- array_append(children, parser_parse_html_attribute(parser));
+ hb_array_append(children, parser_parse_html_attribute(parser));
continue;
}
@@ -807,7 +790,7 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) {
if (next_token && next_token->type == TOKEN_IDENTIFIER) {
token_free(next_token);
- array_append(children, parser_parse_html_attribute(parser));
+ hb_array_append(children, parser_parse_html_attribute(parser));
continue;
}
@@ -834,8 +817,8 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) {
token_free(tag_start);
token_free(tag_name);
- array_free(&children);
- array_free(&errors);
+ hb_array_free(&children);
+ hb_array_free(&errors);
return NULL;
}
@@ -849,8 +832,8 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) {
tag_end,
children,
is_self_closing,
- tag_start->location->start,
- tag_end->location->end,
+ tag_start->location.start,
+ tag_end->location.end,
errors
);
@@ -862,8 +845,8 @@ static AST_HTML_OPEN_TAG_NODE_T* parser_parse_html_open_tag(parser_T* parser) {
}
static AST_HTML_CLOSE_TAG_NODE_T* parser_parse_html_close_tag(parser_T* parser) {
- array_T* errors = array_init(8);
- array_T* children = array_init(8);
+ hb_array_T* errors = hb_array_init(8);
+ hb_array_T* children = hb_array_init(8);
token_T* tag_opening = parser_consume_expected(parser, TOKEN_HTML_TAG_START_CLOSE, errors);
@@ -875,21 +858,21 @@ static AST_HTML_CLOSE_TAG_NODE_T* parser_parse_html_close_tag(parser_T* parser)
token_T* tag_closing = parser_consume_expected(parser, TOKEN_HTML_TAG_END, errors);
- if (tag_name != NULL && is_void_element(tag_name->value) && parser_in_svg_context(parser) == false) {
- char* expected = html_self_closing_tag_string(tag_name->value);
- char* got = html_closing_tag_string(tag_name->value);
+ if (tag_name != NULL && is_void_element(hb_string(tag_name->value)) && parser_in_svg_context(parser) == false) {
+ hb_string_T expected = html_self_closing_tag_string(hb_string(tag_name->value));
+ hb_string_T got = html_closing_tag_string(hb_string(tag_name->value));
append_void_element_closing_tag_error(
tag_name,
- expected,
- got,
- tag_opening->location->start,
- tag_closing->location->end,
+ expected.data,
+ got.data,
+ tag_opening->location.start,
+ tag_closing->location.end,
errors
);
- free(expected);
- free(got);
+ free(expected.data);
+ free(got.data);
}
AST_HTML_CLOSE_TAG_NODE_T* close_tag = ast_html_close_tag_node_init(
@@ -897,8 +880,8 @@ static AST_HTML_CLOSE_TAG_NODE_T* parser_parse_html_close_tag(parser_T* parser)
tag_name,
children,
tag_closing,
- tag_opening->location->start,
- tag_closing->location->end,
+ tag_opening->location.start,
+ tag_closing->location.end,
errors
);
@@ -921,8 +904,8 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_self_closing_element(
NULL,
true,
ELEMENT_SOURCE_HTML,
- open_tag->base.location->start,
- open_tag->base.location->end,
+ open_tag->base.location.start,
+ open_tag->base.location.end,
NULL
);
}
@@ -931,13 +914,13 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_regular_element(
parser_T* parser,
AST_HTML_OPEN_TAG_NODE_T* open_tag
) {
- array_T* errors = array_init(8);
- array_T* body = array_init(8);
+ hb_array_T* errors = hb_array_init(8);
+ hb_array_T* body = hb_array_init(8);
parser_push_open_tag(parser, open_tag->tag_name);
- if (open_tag->tag_name->value && parser_is_foreign_content_tag(open_tag->tag_name->value)) {
- foreign_content_type_T content_type = parser_get_foreign_content_type(open_tag->tag_name->value);
+ if (open_tag->tag_name->value && parser_is_foreign_content_tag(hb_string(open_tag->tag_name->value))) {
+ foreign_content_type_T content_type = parser_get_foreign_content_type(hb_string(open_tag->tag_name->value));
parser_enter_foreign_content(parser, content_type);
parser_parse_foreign_content(parser, body, errors);
} else {
@@ -948,13 +931,13 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_regular_element(
AST_HTML_CLOSE_TAG_NODE_T* close_tag = parser_parse_html_close_tag(parser);
- if (parser_in_svg_context(parser) == false && is_void_element(close_tag->tag_name->value)) {
- array_push(body, close_tag);
+ if (parser_in_svg_context(parser) == false && is_void_element(hb_string(close_tag->tag_name->value))) {
+ hb_array_push(body, close_tag);
parser_parse_in_data_state(parser, body, errors);
close_tag = parser_parse_html_close_tag(parser);
}
- bool matches_stack = parser_check_matching_tag(parser, close_tag->tag_name->value);
+ bool matches_stack = parser_check_matching_tag(parser, hb_string(close_tag->tag_name->value));
if (matches_stack) {
token_T* popped_token = parser_pop_open_tag(parser);
@@ -970,8 +953,8 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_regular_element(
close_tag,
false,
ELEMENT_SOURCE_HTML,
- open_tag->base.location->start,
- close_tag->base.location->end,
+ open_tag->base.location.start,
+ close_tag->base.location.end,
errors
);
}
@@ -983,14 +966,14 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_element(parser_T* parser) {
if (open_tag->is_void) { return parser_parse_html_self_closing_element(parser, open_tag); }
//
, in void element list, and not in inside an element
- if (!open_tag->is_void && is_void_element(open_tag->tag_name->value) && !parser_in_svg_context(parser)) {
+ if (!open_tag->is_void && is_void_element(hb_string(open_tag->tag_name->value)) && !parser_in_svg_context(parser)) {
return parser_parse_html_self_closing_element(parser, open_tag);
}
AST_HTML_ELEMENT_NODE_T* regular_element = parser_parse_html_regular_element(parser, open_tag);
if (regular_element != NULL) { return regular_element; }
- array_T* errors = array_init(8);
+ hb_array_T* errors = hb_array_init(8);
parser_append_unexpected_error(parser, "Unknown HTML open tag type", "HTMLOpenTag or HTMLSelfCloseTag", errors);
@@ -1001,14 +984,14 @@ static AST_HTML_ELEMENT_NODE_T* parser_parse_html_element(parser_T* parser) {
NULL,
false,
ELEMENT_SOURCE_HTML,
- open_tag->base.location->start,
- open_tag->base.location->end,
+ open_tag->base.location.start,
+ open_tag->base.location.end,
errors
);
}
static AST_ERB_CONTENT_NODE_T* parser_parse_erb_tag(parser_T* parser) {
- array_T* errors = array_init(8);
+ hb_array_T* errors = hb_array_init(8);
token_T* opening_tag = parser_consume_expected(parser, TOKEN_ERB_START, errors);
token_T* content = parser_consume_expected(parser, TOKEN_ERB_CONTENT, errors);
@@ -1021,8 +1004,8 @@ static AST_ERB_CONTENT_NODE_T* parser_parse_erb_tag(parser_T* parser) {
NULL,
false,
false,
- opening_tag->location->start,
- closing_tag->location->end,
+ opening_tag->location.start,
+ closing_tag->location.end,
errors
);
@@ -1033,15 +1016,15 @@ static AST_ERB_CONTENT_NODE_T* parser_parse_erb_tag(parser_T* parser) {
return erb_node;
}
-static void parser_parse_foreign_content(parser_T* parser, array_T* children, array_T* errors) {
- buffer_T content = buffer_new();
- position_T* start = position_copy(parser->current_token->location->start);
- const char* expected_closing_tag = parser_get_foreign_content_closing_tag(parser->foreign_content_type);
+static void parser_parse_foreign_content(parser_T* parser, hb_array_T* children, hb_array_T* errors) {
+ hb_buffer_T content;
+ hb_buffer_init(&content, 1024);
+ position_T start = parser->current_token->location.start;
+ hb_string_T expected_closing_tag = parser_get_foreign_content_closing_tag(parser->foreign_content_type);
- if (expected_closing_tag == NULL) {
+ if (hb_string_is_empty(expected_closing_tag)) {
parser_exit_foreign_content(parser);
- position_free(start);
- buffer_free(&content);
+ free(content.value);
return;
}
@@ -1051,10 +1034,9 @@ static void parser_parse_foreign_content(parser_T* parser, array_T* children, ar
parser_append_literal_node_from_buffer(parser, &content, children, start);
AST_ERB_CONTENT_NODE_T* erb_node = parser_parse_erb_tag(parser);
- array_append(children, erb_node);
+ hb_array_append(children, erb_node);
- position_free(start);
- start = position_copy(parser->current_token->location->start);
+ start = parser->current_token->location.start;
continue;
}
@@ -1066,7 +1048,8 @@ static void parser_parse_foreign_content(parser_T* parser, array_T* children, ar
bool is_potential_match = false;
if (next_token && next_token->type == TOKEN_IDENTIFIER && next_token->value) {
- is_potential_match = parser_is_expected_closing_tag_name(next_token->value, parser->foreign_content_type);
+ is_potential_match =
+ parser_is_expected_closing_tag_name(hb_string(next_token->value), parser->foreign_content_type);
}
lexer_restore_state(parser->lexer, saved_state);
@@ -1077,53 +1060,51 @@ static void parser_parse_foreign_content(parser_T* parser, array_T* children, ar
parser_append_literal_node_from_buffer(parser, &content, children, start);
parser_exit_foreign_content(parser);
- position_free(start);
- buffer_free(&content);
+ free(content.value);
return;
}
}
token_T* token = parser_advance(parser);
- buffer_append(&content, token->value);
+ hb_buffer_append(&content, token->value);
token_free(token);
}
parser_append_literal_node_from_buffer(parser, &content, children, start);
parser_exit_foreign_content(parser);
- position_free(start);
- buffer_free(&content);
+ free(content.value);
}
-static void parser_parse_in_data_state(parser_T* parser, array_T* children, array_T* errors) {
+static void parser_parse_in_data_state(parser_T* parser, hb_array_T* children, hb_array_T* errors) {
while (token_is_none_of(parser, TOKEN_HTML_TAG_START_CLOSE, TOKEN_EOF)) {
if (token_is(parser, TOKEN_ERB_START)) {
- array_append(children, parser_parse_erb_tag(parser));
+ hb_array_append(children, parser_parse_erb_tag(parser));
continue;
}
if (token_is(parser, TOKEN_HTML_DOCTYPE)) {
- array_append(children, parser_parse_html_doctype(parser));
+ hb_array_append(children, parser_parse_html_doctype(parser));
continue;
}
if (token_is(parser, TOKEN_XML_DECLARATION)) {
- array_append(children, parser_parse_xml_declaration(parser));
+ hb_array_append(children, parser_parse_xml_declaration(parser));
continue;
}
if (token_is(parser, TOKEN_CDATA_START)) {
- array_append(children, parser_parse_cdata(parser));
+ hb_array_append(children, parser_parse_cdata(parser));
continue;
}
if (token_is(parser, TOKEN_HTML_COMMENT_START)) {
- array_append(children, parser_parse_html_comment(parser));
+ hb_array_append(children, parser_parse_html_comment(parser));
continue;
}
if (token_is(parser, TOKEN_HTML_TAG_START)) {
- array_append(children, parser_parse_html_element(parser));
+ hb_array_append(children, parser_parse_html_element(parser));
continue;
}
@@ -1131,6 +1112,7 @@ static void parser_parse_in_data_state(parser_T* parser, array_T* children, arra
parser,
TOKEN_AMPERSAND,
TOKEN_AT,
+ TOKEN_BACKSLASH,
TOKEN_BACKTICK,
TOKEN_CHARACTER,
TOKEN_COLON,
@@ -1147,7 +1129,7 @@ static void parser_parse_in_data_state(parser_T* parser, array_T* children, arra
TOKEN_UNDERSCORE,
TOKEN_WHITESPACE
)) {
- array_append(children, parser_parse_text_content(parser, errors));
+ hb_array_append(children, parser_parse_text_content(parser, errors));
continue;
}
@@ -1155,20 +1137,20 @@ static void parser_parse_in_data_state(parser_T* parser, array_T* children, arra
parser,
"Unexpected token",
"TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, "
- "TOKEN_NBSP, TOKEN_AT, or TOKEN_NEWLINE",
+ "TOKEN_NBSP, TOKEN_AT, TOKEN_BACKSLASH, or TOKEN_NEWLINE",
errors
);
}
}
-static void parser_parse_unclosed_html_tags(const parser_T* parser, array_T* errors) {
- while (array_size(parser->open_tags_stack) > 0) {
+static void parser_parse_unclosed_html_tags(const parser_T* parser, hb_array_T* errors) {
+ while (hb_array_size(parser->open_tags_stack) > 0) {
token_T* unclosed_tag = parser_pop_open_tag(parser);
append_unclosed_element_error(
unclosed_tag,
- parser->current_token->location->start,
- parser->current_token->location->end,
+ parser->current_token->location.start,
+ parser->current_token->location.end,
errors
);
@@ -1176,7 +1158,7 @@ static void parser_parse_unclosed_html_tags(const parser_T* parser, array_T* err
}
}
-static void parser_parse_stray_closing_tags(parser_T* parser, array_T* children, array_T* errors) {
+static void parser_parse_stray_closing_tags(parser_T* parser, hb_array_T* children, hb_array_T* errors) {
while (token_is_not(parser, TOKEN_EOF)) {
if (token_is_not(parser, TOKEN_HTML_TAG_START_CLOSE)) {
parser_append_unexpected_token_error(parser, TOKEN_HTML_TAG_START_CLOSE, errors);
@@ -1189,25 +1171,25 @@ static void parser_parse_stray_closing_tags(parser_T* parser, array_T* children,
AST_HTML_CLOSE_TAG_NODE_T* close_tag = parser_parse_html_close_tag(parser);
- if (!is_void_element(close_tag->tag_name->value)) {
+ if (!is_void_element(hb_string(close_tag->tag_name->value))) {
append_missing_opening_tag_error(
close_tag->tag_name,
- close_tag->base.location->start,
- close_tag->base.location->end,
+ close_tag->base.location.start,
+ close_tag->base.location.end,
close_tag->base.errors
);
}
- array_append(children, close_tag);
+ hb_array_append(children, close_tag);
parser_parse_in_data_state(parser, children, errors);
}
}
static AST_DOCUMENT_NODE_T* parser_parse_document(parser_T* parser) {
- array_T* children = array_init(8);
- array_T* errors = array_init(8);
- position_T* start = position_copy(parser->current_token->location->start);
+ hb_array_T* children = hb_array_init(8);
+ hb_array_T* errors = hb_array_init(8);
+ position_T start = parser->current_token->location.start;
parser_parse_in_data_state(parser, children, errors);
parser_parse_unclosed_html_tags(parser, errors);
@@ -1215,9 +1197,8 @@ static AST_DOCUMENT_NODE_T* parser_parse_document(parser_T* parser) {
token_T* eof = parser_consume_expected(parser, TOKEN_EOF, errors);
- AST_DOCUMENT_NODE_T* document_node = ast_document_node_init(children, start, eof->location->end, errors);
+ AST_DOCUMENT_NODE_T* document_node = ast_document_node_init(children, start, eof->location.end, errors);
- position_free(start);
token_free(eof);
return document_node;
@@ -1227,26 +1208,26 @@ AST_DOCUMENT_NODE_T* herb_parser_parse(parser_T* parser) {
return parser_parse_document(parser);
}
-static void parser_handle_whitespace(parser_T* parser, token_T* whitespace_token, array_T* children) {
- if (parser->options && parser->options->track_whitespace) {
- array_T* errors = array_init(8);
+static void parser_handle_whitespace(parser_T* parser, token_T* whitespace_token, hb_array_T* children) {
+ if (parser->options.track_whitespace) {
+ hb_array_T* errors = hb_array_init(8);
AST_WHITESPACE_NODE_T* whitespace_node = ast_whitespace_node_init(
whitespace_token,
- whitespace_token->location->start,
- whitespace_token->location->end,
+ whitespace_token->location.start,
+ whitespace_token->location.end,
errors
);
- array_append(children, whitespace_node);
+ hb_array_append(children, whitespace_node);
}
token_free(whitespace_token);
}
-static void parser_consume_whitespace(parser_T* parser, array_T* children) {
+static void parser_consume_whitespace(parser_T* parser, hb_array_T* children) {
while (token_is_any_of(parser, TOKEN_WHITESPACE, TOKEN_NEWLINE)) {
token_T* whitespace = parser_advance(parser);
- if (parser->options && parser->options->track_whitespace && children != NULL) {
+ if (parser->options.track_whitespace && children != NULL) {
parser_handle_whitespace(parser, whitespace, children);
} else {
token_free(whitespace);
@@ -1254,13 +1235,9 @@ static void parser_consume_whitespace(parser_T* parser, array_T* children) {
}
}
-void parser_free(parser_T* parser) {
+void herb_parser_deinit(parser_T* parser) {
if (parser == NULL) { return; }
- if (parser->lexer != NULL) { lexer_free(parser->lexer); }
if (parser->current_token != NULL) { token_free(parser->current_token); }
- if (parser->open_tags_stack != NULL) { array_free(&parser->open_tags_stack); }
- if (parser->options != NULL) { free(parser->options); }
-
- free(parser);
+ if (parser->open_tags_stack != NULL) { hb_array_free(&parser->open_tags_stack); }
}
diff --git a/src/parser_helpers.c b/src/parser_helpers.c
index bf92a5523..f34d97864 100644
--- a/src/parser_helpers.c
+++ b/src/parser_helpers.c
@@ -1,36 +1,36 @@
#include "include/parser_helpers.h"
-#include "include/array.h"
#include "include/ast_node.h"
#include "include/ast_nodes.h"
-#include "include/buffer.h"
#include "include/errors.h"
#include "include/html_util.h"
#include "include/lexer.h"
#include "include/parser.h"
#include "include/token.h"
#include "include/token_matchers.h"
+#include "include/util/hb_array.h"
+#include "include/util/hb_buffer.h"
+#include "include/util/hb_string.h"
#include
-#include
void parser_push_open_tag(const parser_T* parser, token_T* tag_name) {
token_T* copy = token_copy(tag_name);
- array_push(parser->open_tags_stack, copy);
+ hb_array_push(parser->open_tags_stack, copy);
}
-bool parser_check_matching_tag(const parser_T* parser, const char* tag_name) {
- if (array_size(parser->open_tags_stack) == 0) { return false; }
+bool parser_check_matching_tag(const parser_T* parser, hb_string_T tag_name) {
+ if (hb_array_size(parser->open_tags_stack) == 0) { return false; }
- token_T* top_token = array_last(parser->open_tags_stack);
+ token_T* top_token = hb_array_last(parser->open_tags_stack);
if (top_token == NULL || top_token->value == NULL) { return false; };
- return (strcasecmp(top_token->value, tag_name) == 0);
+ return hb_string_equals(hb_string(top_token->value), tag_name);
}
token_T* parser_pop_open_tag(const parser_T* parser) {
- if (array_size(parser->open_tags_stack) == 0) { return NULL; }
+ if (hb_array_size(parser->open_tags_stack) == 0) { return NULL; }
- return array_pop(parser->open_tags_stack);
+ return hb_array_pop(parser->open_tags_stack);
}
/**
@@ -42,13 +42,14 @@ token_T* parser_pop_open_tag(const parser_T* parser) {
bool parser_in_svg_context(const parser_T* parser) {
if (!parser || !parser->open_tags_stack) { return false; }
- size_t stack_size = array_size(parser->open_tags_stack);
+ size_t stack_size = hb_array_size(parser->open_tags_stack);
for (size_t i = 0; i < stack_size; i++) {
- token_T* tag = (token_T*) array_get(parser->open_tags_stack, i);
+ token_T* tag = (token_T*) hb_array_get(parser->open_tags_stack, i);
if (tag && tag->value) {
- if (strcasecmp(tag->value, "svg") == 0) { return true; }
+ hb_string_T tag_value_string = hb_string(tag->value);
+ if (hb_string_equals(tag_value_string, hb_string("svg"))) { return true; }
}
}
@@ -57,24 +58,24 @@ bool parser_in_svg_context(const parser_T* parser) {
// ===== Foreign Content Handling =====
-foreign_content_type_T parser_get_foreign_content_type(const char* tag_name) {
- if (tag_name == NULL) { return FOREIGN_CONTENT_UNKNOWN; }
+foreign_content_type_T parser_get_foreign_content_type(hb_string_T tag_name) {
+ if (hb_string_is_empty(tag_name)) { return FOREIGN_CONTENT_UNKNOWN; }
- if (strcasecmp(tag_name, "script") == 0) { return FOREIGN_CONTENT_SCRIPT; }
- if (strcasecmp(tag_name, "style") == 0) { return FOREIGN_CONTENT_STYLE; }
+ if (hb_string_equals(tag_name, hb_string("script"))) { return FOREIGN_CONTENT_SCRIPT; }
+ if (hb_string_equals(tag_name, hb_string("style"))) { return FOREIGN_CONTENT_STYLE; }
return FOREIGN_CONTENT_UNKNOWN;
}
-bool parser_is_foreign_content_tag(const char* tag_name) {
+bool parser_is_foreign_content_tag(hb_string_T tag_name) {
return parser_get_foreign_content_type(tag_name) != FOREIGN_CONTENT_UNKNOWN;
}
-const char* parser_get_foreign_content_closing_tag(foreign_content_type_T type) {
+hb_string_T parser_get_foreign_content_closing_tag(foreign_content_type_T type) {
switch (type) {
- case FOREIGN_CONTENT_SCRIPT: return "script";
- case FOREIGN_CONTENT_STYLE: return "style";
- default: return NULL;
+ case FOREIGN_CONTENT_SCRIPT: return hb_string("script");
+ case FOREIGN_CONTENT_STYLE: return hb_string("style");
+ default: return hb_string("");
}
}
@@ -92,44 +93,49 @@ void parser_exit_foreign_content(parser_T* parser) {
parser->foreign_content_type = FOREIGN_CONTENT_UNKNOWN;
}
-void parser_append_unexpected_error(parser_T* parser, const char* description, const char* expected, array_T* errors) {
+void parser_append_unexpected_error(
+ parser_T* parser,
+ const char* description,
+ const char* expected,
+ hb_array_T* errors
+) {
token_T* token = parser_advance(parser);
append_unexpected_error(
description,
expected,
token_type_to_string(token->type),
- token->location->start,
- token->location->end,
+ token->location.start,
+ token->location.end,
errors
);
token_free(token);
}
-void parser_append_unexpected_token_error(parser_T* parser, token_type_T expected_type, array_T* errors) {
+void parser_append_unexpected_token_error(parser_T* parser, token_type_T expected_type, hb_array_T* errors) {
append_unexpected_token_error(
expected_type,
parser->current_token,
- parser->current_token->location->start,
- parser->current_token->location->end,
+ parser->current_token->location.start,
+ parser->current_token->location.end,
errors
);
}
void parser_append_literal_node_from_buffer(
const parser_T* parser,
- buffer_T* buffer,
- array_T* children,
- position_T* start
+ hb_buffer_T* buffer,
+ hb_array_T* children,
+ position_T start
) {
- if (buffer_length(buffer) == 0) { return; }
+ if (hb_buffer_length(buffer) == 0) { return; }
AST_LITERAL_NODE_T* literal =
- ast_literal_node_init(buffer_value(buffer), start, parser->current_token->location->start, NULL);
+ ast_literal_node_init(hb_buffer_value(buffer), start, parser->current_token->location.start, NULL);
- if (children != NULL) { array_append(children, literal); }
- buffer_clear(buffer);
+ if (children != NULL) { hb_array_append(children, literal); }
+ hb_buffer_clear(buffer);
}
token_T* parser_advance(parser_T* parser) {
@@ -143,13 +149,13 @@ token_T* parser_consume_if_present(parser_T* parser, const token_type_T type) {
return parser_advance(parser);
}
-token_T* parser_consume_expected(parser_T* parser, const token_type_T expected_type, array_T* array) {
+token_T* parser_consume_expected(parser_T* parser, const token_type_T expected_type, hb_array_T* array) {
token_T* token = parser_consume_if_present(parser, expected_type);
if (token == NULL) {
token = parser_advance(parser);
- append_unexpected_token_error(expected_type, token, token->location->start, token->location->end, array);
+ append_unexpected_token_error(expected_type, token, token->location.start, token->location.end, array);
}
return token;
@@ -157,13 +163,13 @@ token_T* parser_consume_expected(parser_T* parser, const token_type_T expected_t
AST_HTML_ELEMENT_NODE_T* parser_handle_missing_close_tag(
AST_HTML_OPEN_TAG_NODE_T* open_tag,
- array_T* body,
- array_T* errors
+ hb_array_T* body,
+ hb_array_T* errors
) {
append_missing_closing_tag_error(
open_tag->tag_name,
- open_tag->tag_name->location->start,
- open_tag->tag_name->location->end,
+ open_tag->tag_name->location.start,
+ open_tag->tag_name->location.end,
errors
);
@@ -174,8 +180,8 @@ AST_HTML_ELEMENT_NODE_T* parser_handle_missing_close_tag(
NULL,
false,
ELEMENT_SOURCE_HTML,
- open_tag->base.location->start,
- open_tag->base.location->end,
+ open_tag->base.location.start,
+ open_tag->base.location.end,
errors
);
}
@@ -183,33 +189,33 @@ AST_HTML_ELEMENT_NODE_T* parser_handle_missing_close_tag(
void parser_handle_mismatched_tags(
const parser_T* parser,
const AST_HTML_CLOSE_TAG_NODE_T* close_tag,
- array_T* errors
+ hb_array_T* errors
) {
- if (array_size(parser->open_tags_stack) > 0) {
- token_T* expected_tag = array_last(parser->open_tags_stack);
+ if (hb_array_size(parser->open_tags_stack) > 0) {
+ token_T* expected_tag = hb_array_last(parser->open_tags_stack);
token_T* actual_tag = close_tag->tag_name;
append_tag_names_mismatch_error(
expected_tag,
actual_tag,
- actual_tag->location->start,
- actual_tag->location->end,
+ actual_tag->location.start,
+ actual_tag->location.end,
errors
);
} else {
append_missing_opening_tag_error(
close_tag->tag_name,
- close_tag->tag_name->location->start,
- close_tag->tag_name->location->end,
+ close_tag->tag_name->location.start,
+ close_tag->tag_name->location.end,
errors
);
}
}
-bool parser_is_expected_closing_tag_name(const char* tag_name, foreign_content_type_T expected_type) {
- const char* expected_tag_name = parser_get_foreign_content_closing_tag(expected_type);
+bool parser_is_expected_closing_tag_name(hb_string_T tag_name, foreign_content_type_T expected_type) {
+ hb_string_T expected_tag_name = parser_get_foreign_content_closing_tag(expected_type);
- if (expected_tag_name == NULL || tag_name == NULL) { return false; }
+ if (hb_string_is_empty(tag_name) || hb_string_is_empty(expected_tag_name)) { return false; }
- return strcmp(tag_name, expected_tag_name) == 0;
+ return hb_string_equals(expected_tag_name, tag_name);
}
diff --git a/src/position.c b/src/position.c
deleted file mode 100644
index 7b946b764..000000000
--- a/src/position.c
+++ /dev/null
@@ -1,33 +0,0 @@
-#include "include/position.h"
-#include "include/memory.h"
-
-size_t position_sizeof(void) {
- return sizeof(position_T);
-}
-
-position_T* position_init(const size_t line, const size_t column) {
- position_T* position = safe_malloc(position_sizeof());
-
- position->line = line;
- position->column = column;
-
- return position;
-}
-
-size_t position_line(const position_T* position) {
- return position->line;
-}
-
-size_t position_column(const position_T* position) {
- return position->column;
-}
-
-position_T* position_copy(position_T* position) {
- if (position == NULL) { return NULL; }
-
- return position_init(position_line(position), position_column(position));
-}
-
-void position_free(position_T* position) {
- free(position);
-}
diff --git a/src/pretty_print.c b/src/pretty_print.c
index ee79d4e34..e480dd0b7 100644
--- a/src/pretty_print.c
+++ b/src/pretty_print.c
@@ -3,25 +3,26 @@
#include "include/ast_node.h"
#include "include/ast_nodes.h"
#include "include/ast_pretty_print.h"
-#include "include/buffer.h"
#include "include/errors.h"
#include "include/token_struct.h"
#include "include/util.h"
+#include "include/util/hb_buffer.h"
+#include "include/util/hb_string.h"
#include
#include
#include
-void pretty_print_indent(buffer_T* buffer, const size_t indent) {
+void pretty_print_indent(hb_buffer_T* buffer, const size_t indent) {
for (size_t i = 0; i < indent; i++) {
- buffer_append(buffer, " ");
+ hb_buffer_append(buffer, " ");
}
}
-void pretty_print_newline(const size_t indent, const size_t relative_indent, buffer_T* buffer) {
+void pretty_print_newline(const size_t indent, const size_t relative_indent, hb_buffer_T* buffer) {
pretty_print_indent(buffer, indent);
pretty_print_indent(buffer, relative_indent);
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
}
void pretty_print_label(
@@ -29,19 +30,19 @@ void pretty_print_label(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
pretty_print_indent(buffer, indent);
pretty_print_indent(buffer, relative_indent);
if (last_property) {
- buffer_append(buffer, "└── ");
+ hb_buffer_append(buffer, "└── ");
} else {
- buffer_append(buffer, "├── ");
+ hb_buffer_append(buffer, "├── ");
}
- buffer_append(buffer, name);
- buffer_append(buffer, ": ");
+ hb_buffer_append(buffer, name);
+ hb_buffer_append(buffer, ": ");
}
void pretty_print_quoted_property(
@@ -50,11 +51,11 @@ void pretty_print_quoted_property(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
- char* quoted = quoted_string(value);
- pretty_print_property(name, quoted, indent, relative_indent, last_property, buffer);
- free(quoted);
+ hb_string_T quoted = quoted_string(hb_string(value));
+ pretty_print_property(name, quoted.data, indent, relative_indent, last_property, buffer);
+ free(quoted.data);
}
void pretty_print_boolean_property(
@@ -63,7 +64,7 @@ void pretty_print_boolean_property(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
pretty_print_property(name, value ? "true" : "false", indent, relative_indent, last_property, buffer);
}
@@ -74,11 +75,11 @@ void pretty_print_property(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
pretty_print_label(name, indent, relative_indent, last_property, buffer);
- buffer_append(buffer, value);
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, value);
+ hb_buffer_append(buffer, "\n");
}
void pretty_print_size_t_property(
@@ -87,22 +88,24 @@ void pretty_print_size_t_property(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
pretty_print_label(name, indent, relative_indent, last_property, buffer);
- char* string = size_t_to_string(value);
- buffer_append(buffer, string);
- buffer_append(buffer, "\n");
- free(string);
+
+ char size_string[21];
+ snprintf(size_string, 21, "%zu", value);
+
+ hb_buffer_append(buffer, size_string);
+ hb_buffer_append(buffer, "\n");
}
void pretty_print_array(
const char* name,
- array_T* array,
+ hb_array_T* array,
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
if (array == NULL) {
pretty_print_property(name, "∅", indent, relative_indent, last_property, buffer);
@@ -110,7 +113,7 @@ void pretty_print_array(
return;
}
- if (array_size(array) == 0) {
+ if (hb_array_size(array) == 0) {
pretty_print_property(name, "[]", indent, relative_indent, last_property, buffer);
return;
@@ -118,31 +121,31 @@ void pretty_print_array(
pretty_print_label(name, indent, relative_indent, last_property, buffer);
- buffer_append(buffer, "(");
+ hb_buffer_append(buffer, "(");
char count[16];
- sprintf(count, "%zu", array_size(array));
- buffer_append(buffer, count);
- buffer_append(buffer, ")\n");
+ sprintf(count, "%zu", hb_array_size(array));
+ hb_buffer_append(buffer, count);
+ hb_buffer_append(buffer, ")\n");
if (indent < 20) {
- for (size_t i = 0; i < array_size(array); i++) {
- AST_NODE_T* child = array_get(array, i);
+ for (size_t i = 0; i < hb_array_size(array); i++) {
+ AST_NODE_T* child = hb_array_get(array, i);
pretty_print_indent(buffer, indent);
pretty_print_indent(buffer, relative_indent + 1);
- if (i == array_size(array) - 1) {
- buffer_append(buffer, "└── ");
+ if (i == hb_array_size(array) - 1) {
+ hb_buffer_append(buffer, "└── ");
} else {
- buffer_append(buffer, "├── ");
+ hb_buffer_append(buffer, "├── ");
}
ast_pretty_print_node(child, indent + 1, relative_indent + 1, buffer);
- if (i != array_size(array) - 1) { pretty_print_newline(indent + 1, relative_indent, buffer); }
+ if (i != hb_array_size(array) - 1) { pretty_print_newline(indent + 1, relative_indent, buffer); }
}
}
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
}
void pretty_print_errors(
@@ -150,27 +153,27 @@ void pretty_print_errors(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
- if (node->errors != NULL && array_size(node->errors) > 0) {
+ if (node->errors != NULL && hb_array_size(node->errors) > 0) {
error_pretty_print_array("errors", node->errors, indent, relative_indent, last_property, buffer);
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
}
}
-void pretty_print_location(location_T* location, buffer_T* buffer) {
- buffer_append(buffer, "(location: (");
+void pretty_print_location(location_T location, hb_buffer_T* buffer) {
+ hb_buffer_append(buffer, "(location: (");
char location_string[128];
sprintf(
location_string,
- "%zu,%zu)-(%zu,%zu",
- (location->start && location->start->line) ? location->start->line : 0,
- (location->start && location->start->column) ? location->start->column : 0,
- (location->end && location->end->line) ? location->end->line : 0,
- (location->end && location->end->column) ? location->end->column : 0
+ "%u,%u)-(%u,%u",
+ location.start.line,
+ location.start.column,
+ location.end.line,
+ location.end.column
);
- buffer_append(buffer, location_string);
- buffer_append(buffer, "))");
+ hb_buffer_append(buffer, location_string);
+ hb_buffer_append(buffer, "))");
}
void pretty_print_position_property(
@@ -179,29 +182,24 @@ void pretty_print_position_property(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
pretty_print_label(name, indent, relative_indent, last_property, buffer);
if (position != NULL) {
- buffer_append(buffer, "(");
+ hb_buffer_append(buffer, "(");
char position_string[128];
- sprintf(
- position_string,
- "%zu:%zu",
- (position->line) ? position->line : 0,
- (position->column) ? position->column : 0
- );
+ sprintf(position_string, "%u:%u", (position->line) ? position->line : 0, (position->column) ? position->column : 0);
- buffer_append(buffer, position_string);
- buffer_append(buffer, ")");
+ hb_buffer_append(buffer, position_string);
+ hb_buffer_append(buffer, ")");
} else {
- buffer_append(buffer, "∅");
+ hb_buffer_append(buffer, "∅");
}
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
}
void pretty_print_token_property(
@@ -210,22 +208,22 @@ void pretty_print_token_property(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
pretty_print_label(name, indent, relative_indent, last_property, buffer);
if (token != NULL && token->value != NULL) {
- char* quoted = quoted_string(token->value);
- buffer_append(buffer, quoted);
- free(quoted);
+ hb_string_T quoted = quoted_string(hb_string(token->value));
+ hb_buffer_append_string(buffer, quoted);
+ free(quoted.data);
- buffer_append(buffer, " ");
+ hb_buffer_append(buffer, " ");
pretty_print_location(token->location, buffer);
} else {
- buffer_append(buffer, "∅");
+ hb_buffer_append(buffer, "∅");
}
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
}
void pretty_print_string_property(
@@ -234,49 +232,22 @@ void pretty_print_string_property(
const size_t indent,
const size_t relative_indent,
const bool last_property,
- buffer_T* buffer
+ hb_buffer_T* buffer
) {
const char* value = "∅";
char* escaped = NULL;
- char* quoted = NULL;
+ hb_string_T quoted;
if (string != NULL) {
escaped = escape_newlines(string);
- quoted = quoted_string(escaped);
- value = quoted;
+ quoted = quoted_string(hb_string(escaped));
+ value = quoted.data;
}
pretty_print_property(name, value, indent, relative_indent, last_property, buffer);
if (string != NULL) {
if (escaped != NULL) { free(escaped); }
- if (quoted != NULL) { free(quoted); }
+ if (!hb_string_is_empty(quoted)) { free(quoted.data); }
}
}
-
-void pretty_print_analyzed_ruby(analyzed_ruby_T* analyzed, const char* source) {
- printf(
- "------------------------\nanalyzed (%p)\n------------------------\n%s\n------------------------\n if: %i\n "
- " elsif: %i\n else: %i\n end: %i\n block: %i\n block_closing: %i\n case: %i\n when: %i\n for: "
- "%i\n while: %i\n "
- " until: %i\n begin: %i\n "
- "rescue: %i\n ensure: %i\n unless: %i\n==================\n\n",
- (void*) analyzed,
- source,
- analyzed->has_if_node,
- analyzed->has_elsif_node,
- analyzed->has_else_node,
- analyzed->has_end,
- analyzed->has_block_node,
- analyzed->has_block_closing,
- analyzed->has_case_node,
- analyzed->has_when_node,
- analyzed->has_for_node,
- analyzed->has_while_node,
- analyzed->has_until_node,
- analyzed->has_begin_node,
- analyzed->has_rescue_node,
- analyzed->has_ensure_node,
- analyzed->has_unless_node
- );
-}
diff --git a/src/prism_helpers.c b/src/prism_helpers.c
index 93c4857f2..06ac155b5 100644
--- a/src/prism_helpers.c
+++ b/src/prism_helpers.c
@@ -16,15 +16,15 @@ const char* pm_error_level_to_string(pm_error_level_t level) {
}
}
-position_T* position_from_source_with_offset(const char* source, size_t offset) {
- position_T* position = position_init(1, 0);
+position_T position_from_source_with_offset(const char* source, size_t offset) {
+ position_T position = { .line = 1, .column = 0 };
for (size_t i = 0; i < offset; i++) {
if (is_newline(source[i])) {
- position->line++;
- position->column = 0;
+ position.line++;
+ position.column = 0;
} else {
- position->column++;
+ position.column++;
}
}
@@ -40,8 +40,8 @@ RUBY_PARSE_ERROR_T* ruby_parse_error_from_prism_error(
size_t start_offset = (size_t) (error->location.start - parser->start);
size_t end_offset = (size_t) (error->location.end - parser->start);
- position_T* start = position_from_source_with_offset(source, start_offset);
- position_T* end = position_from_source_with_offset(source, end_offset);
+ position_T start = position_from_source_with_offset(source, start_offset);
+ position_T end = position_from_source_with_offset(source, end_offset);
return ruby_parse_error_init(
error->message,
diff --git a/src/range.c b/src/range.c
index 030b87b71..6e6abea50 100644
--- a/src/range.c
+++ b/src/range.c
@@ -1,38 +1,5 @@
#include "include/range.h"
-size_t range_sizeof(void) {
- return sizeof(range_T);
-}
-
-range_T* range_init(const size_t from, const size_t to) {
- range_T* range = calloc(1, range_sizeof());
-
- range->from = from;
- range->to = to;
-
- return range;
-}
-
-size_t range_from(const range_T* range) {
- return range->from;
-}
-
-size_t range_to(const range_T* range) {
- return range->to;
-}
-
-size_t range_length(range_T* range) {
- return range_to(range) - range_from(range);
-}
-
-range_T* range_copy(range_T* range) {
- if (!range) { return NULL; }
-
- return range_init(range_from(range), range_to(range));
-}
-
-void range_free(range_T* range) {
- if (range == NULL) { return; }
-
- free(range);
+uint32_t range_length(range_T range) {
+ return range.to - range.from;
}
diff --git a/src/token.c b/src/token.c
index a8cb5fb3f..b84362754 100644
--- a/src/token.c
+++ b/src/token.c
@@ -1,7 +1,7 @@
#include "include/token.h"
-#include "include/json.h"
#include "include/lexer.h"
#include "include/position.h"
+#include "include/range.h"
#include "include/token_struct.h"
#include "include/util.h"
@@ -28,10 +28,15 @@ token_T* token_init(const char* value, const token_type_T type, lexer_T* lexer)
}
token->type = type;
- token->range = range_init(lexer->previous_position, lexer->current_position);
-
- token->location =
- location_from(lexer->previous_line, lexer->previous_column, lexer->current_line, lexer->current_column);
+ token->range = (range_T) { .from = lexer->previous_position, .to = lexer->current_position };
+
+ location_from(
+ &token->location,
+ lexer->previous_line,
+ lexer->previous_column,
+ lexer->current_line,
+ lexer->current_column
+ );
lexer->previous_line = lexer->current_line;
lexer->previous_column = lexer->current_column;
@@ -84,7 +89,7 @@ const char* token_type_to_string(const token_type_T type) {
char* token_to_string(const token_T* token) {
const char* type_string = token_type_to_string(token->type);
- const char* template = "#";
+ const char* template = "#";
char* string = calloc(strlen(type_string) + strlen(template) + strlen(token->value) + 16, sizeof(char));
char* escaped;
@@ -100,12 +105,12 @@ char* token_to_string(const token_T* token) {
template,
type_string,
escaped,
- token->range->from,
- token->range->to,
- token->location->start->line,
- token->location->start->column,
- token->location->end->line,
- token->location->end->column
+ token->range.from,
+ token->range.to,
+ token->location.start.line,
+ token->location.start.column,
+ token->location.end.line,
+ token->location.end.column
);
free(escaped);
@@ -113,42 +118,6 @@ char* token_to_string(const token_T* token) {
return string;
}
-char* token_to_json(const token_T* token) {
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
- json_add_string(&json, "type", token_type_to_string(token->type));
- json_add_string(&json, "value", token->value);
-
- buffer_T range = buffer_new();
- json_start_array(&json, "range");
- json_add_size_t(&range, NULL, token->range->from);
- json_add_size_t(&range, NULL, token->range->to);
- buffer_concat(&json, &range);
- buffer_free(&range);
- json_end_array(&json);
-
- buffer_T start = buffer_new();
- json_start_object(&json, "start");
- json_add_size_t(&start, "line", token->location->start->line);
- json_add_size_t(&start, "column", token->location->start->column);
- buffer_concat(&json, &start);
- buffer_free(&start);
- json_end_object(&json);
-
- buffer_T end = buffer_new();
- json_start_object(&json, "end");
- json_add_size_t(&end, "line", token->location->end->line);
- json_add_size_t(&end, "column", token->location->end->column);
- buffer_concat(&json, &end);
- buffer_free(&end);
- json_end_object(&json);
-
- json_end_object(&json);
-
- return buffer_value(&json);
-}
-
char* token_value(const token_T* token) {
return token->value;
}
@@ -157,14 +126,6 @@ int token_type(const token_T* token) {
return token->type;
}
-position_T* token_start_position(token_T* token) {
- return token->location->start;
-}
-
-position_T* token_end_position(token_T* token) {
- return token->location->end;
-}
-
token_T* token_copy(token_T* token) {
if (!token) { return NULL; }
@@ -184,8 +145,8 @@ token_T* token_copy(token_T* token) {
}
new_token->type = token->type;
- new_token->range = range_copy(token->range);
- new_token->location = location_copy(token->location);
+ new_token->range = token->range;
+ new_token->location = token->location;
return new_token;
}
@@ -194,8 +155,6 @@ void token_free(token_T* token) {
if (!token) { return; }
if (token->value != NULL) { free(token->value); }
- if (token->range != NULL) { range_free(token->range); }
- if (token->location != NULL) { location_free(token->location); }
free(token);
}
diff --git a/src/util.c b/src/util.c
index 94589917b..95b7f4cdc 100644
--- a/src/util.c
+++ b/src/util.c
@@ -1,118 +1,56 @@
#include "include/util.h"
+#include "include/util/hb_buffer.h"
+#include "include/util/hb_string.h"
#include
#include
#include
#include
-int is_whitespace(const int character) {
- return character == ' ' || character == '\t';
-}
-
-int is_newline(const int character) {
- return character == 13 || character == 10;
-}
-
-int count_in_string(const char* string, const char character) {
- int count = 0;
-
- while (*string != '\0') {
- if (*string == character) { count++; }
-
- string++;
- }
-
- return count;
-}
-
-int count_newlines(const char* string) {
- int count = 0;
-
- while (*string) {
- if (*string == '\r') {
- count++;
- if (*(string + 1) == '\n') { string++; }
- } else if (*string == '\n') {
- count++;
- }
-
- string++;
- }
-
- return count;
-}
-
-char* replace_char(char* string, const char find, const char replace) {
- char* original_string = string;
-
- while (*string != '\0') {
- if (*string == find) { *string = replace; }
-
- string++;
- }
-
- return original_string;
+int is_newline(int character) {
+ return character == '\n' || character == '\r';
}
char* escape_newlines(const char* input) {
- char* output = calloc(strlen(input) * 2 + 1, sizeof(char));
- char* orig_output = output;
-
- while (*input) {
- if (*input == '\n') {
- *output++ = '\\';
- *output++ = 'n';
- } else if (*input == '\r') {
- *output++ = '\\';
- *output++ = 'r';
- } else {
- *output++ = *input;
+ hb_buffer_T buffer;
+
+ hb_buffer_init(&buffer, strlen(input));
+
+ for (size_t i = 0; i < strlen(input); ++i) {
+ switch (input[i]) {
+ case '\n': {
+ hb_buffer_append_char(&buffer, '\\');
+ hb_buffer_append_char(&buffer, 'n');
+ } break;
+ case '\r': {
+ hb_buffer_append_char(&buffer, '\\');
+ hb_buffer_append_char(&buffer, 'r');
+ } break;
+ default: {
+ hb_buffer_append_char(&buffer, input[i]);
+ }
}
-
- input++;
}
- *output = '\0';
-
- return orig_output;
+ return buffer.value;
}
-char* wrap_string(const char* input, const char character) {
- if (input == NULL) { return NULL; }
-
- const size_t length = strlen(input);
- char* wrapped = malloc(length + 3);
+static hb_string_T wrap_string(hb_string_T input, char character) {
+ hb_buffer_T buffer;
- if (wrapped == NULL) { return NULL; }
+ hb_buffer_init(&buffer, input.length + 2);
- wrapped[0] = character;
- strcpy(wrapped + 1, input);
- wrapped[length + 1] = character;
- wrapped[length + 2] = '\0';
+ hb_buffer_append_char(&buffer, character);
+ hb_buffer_append_string(&buffer, input);
+ hb_buffer_append_char(&buffer, character);
- return wrapped;
+ return hb_string(buffer.value);
}
-char* quoted_string(const char* input) {
+hb_string_T quoted_string(hb_string_T input) {
return wrap_string(input, '"');
}
-// Check if a string is blank (NULL, empty, or only contains whitespace)
-bool string_blank(const char* input) {
- if (input == NULL || input[0] == '\0') { return true; }
-
- for (const char* p = input; *p != '\0'; p++) {
- if (!isspace(*p)) { return false; }
- }
-
- return true;
-}
-
-// Check if a string is present (not blank)
-bool string_present(const char* input) {
- return !string_blank(input);
-}
-
char* herb_strdup(const char* s) {
size_t len = strlen(s) + 1;
char* copy = malloc(len);
@@ -121,10 +59,3 @@ char* herb_strdup(const char* s) {
return copy;
}
-
-char* size_t_to_string(const size_t value) {
- char* buffer = malloc(21);
- snprintf(buffer, 21, "%zu", value);
-
- return buffer;
-}
diff --git a/src/util/hb_arena.c b/src/util/hb_arena.c
new file mode 100644
index 000000000..3eb0e70a0
--- /dev/null
+++ b/src/util/hb_arena.c
@@ -0,0 +1,62 @@
+#ifdef __linux__
+#define _GNU_SOURCE
+#endif
+
+#include "../include/util/hb_arena.h"
+
+#include
+#include
+
+#define KB(kb) (1024 * kb)
+#define MB(mb) (1024 * KB(mb))
+
+bool hb_arena_init(hb_arena_T* allocator, size_t size) {
+ allocator->memory = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+ if (allocator->memory == MAP_FAILED) {
+ allocator->memory = NULL;
+ allocator->position = 0;
+ allocator->capacity = 0;
+
+ return false;
+ }
+
+ allocator->position = 0;
+ allocator->capacity = size;
+
+ return true;
+}
+
+void* hb_arena_alloc(hb_arena_T* allocator, size_t size) {
+ size_t required_size = ((size + 7) / 8) * 8; // rounds up to the next 8 bytes
+
+ if (allocator->position + required_size > allocator->capacity) { return NULL; }
+
+ size_t offset = allocator->position;
+ void* ptr = &allocator->memory[offset];
+ allocator->position += required_size;
+
+ return ptr;
+}
+
+size_t hb_arena_position(hb_arena_T* allocator) {
+ return allocator->position;
+}
+
+void hb_arena_reset(hb_arena_T* allocator) {
+ allocator->position = 0;
+}
+
+void hb_arena_reset_to(hb_arena_T* allocator, size_t new_position) {
+ if (new_position <= allocator->capacity) { allocator->position = new_position; }
+}
+
+void hb_arena_free(hb_arena_T* allocator) {
+ if (allocator->memory != NULL) {
+ munmap(allocator->memory, allocator->capacity);
+
+ allocator->memory = NULL;
+ allocator->position = 0;
+ allocator->capacity = 0;
+ }
+}
diff --git a/src/array.c b/src/util/hb_array.c
similarity index 61%
rename from src/array.c
rename to src/util/hb_array.c
index cd3210b8f..4d3c26b24 100644
--- a/src/array.c
+++ b/src/util/hb_array.c
@@ -1,20 +1,19 @@
#include
#include
-#include "include/array.h"
-#include "include/macros.h"
-#include "include/memory.h"
+#include "../include/macros.h"
+#include "../include/util/hb_array.h"
-size_t array_sizeof(void) {
- return sizeof(array_T);
+size_t hb_array_sizeof(void) {
+ return sizeof(hb_array_T);
}
-array_T* array_init(const size_t capacity) {
- array_T* array = safe_malloc(array_sizeof());
+hb_array_T* hb_array_init(const size_t capacity) {
+ hb_array_T* array = malloc(hb_array_sizeof());
array->size = 0;
array->capacity = capacity;
- array->items = nullable_safe_malloc(sizeof(void*) * capacity);
+ array->items = malloc(capacity * sizeof(void*));
if (!array->items) {
free(array);
@@ -24,7 +23,7 @@ array_T* array_init(const size_t capacity) {
return array;
}
-void array_append(array_T* array, void* item) {
+void hb_array_append(hb_array_T* array, void* item) {
if (array->size >= array->capacity) {
size_t new_capacity;
@@ -45,7 +44,7 @@ void array_append(array_T* array, void* item) {
}
size_t new_size_bytes = new_capacity * sizeof(void*);
- void* new_items = safe_realloc(array->items, new_size_bytes);
+ void* new_items = realloc(array->items, new_size_bytes);
if (unlikely(new_items == NULL)) { return; }
@@ -57,29 +56,29 @@ void array_append(array_T* array, void* item) {
array->size++;
}
-void* array_get(const array_T* array, const size_t index) {
+void* hb_array_get(const hb_array_T* array, const size_t index) {
if (index >= array->size) { return NULL; }
return array->items[index];
}
-void* array_first(array_T* array) {
+void* hb_array_first(hb_array_T* array) {
if (!array || array->size == 0) { return NULL; }
return array->items[0];
}
-void* array_last(array_T* array) {
+void* hb_array_last(hb_array_T* array) {
if (!array || array->size == 0) { return NULL; }
return array->items[array->size - 1];
}
-void array_set(const array_T* array, const size_t index, void* item) {
+void hb_array_set(const hb_array_T* array, const size_t index, void* item) {
if (index >= array->size) { return; }
array->items[index] = item;
}
-void array_remove(array_T* array, const size_t index) {
+void hb_array_remove(hb_array_T* array, const size_t index) {
if (index >= array->size) { return; }
for (size_t i = index; i < array->size - 1; i++) {
@@ -89,7 +88,7 @@ void array_remove(array_T* array, const size_t index) {
array->size--;
}
-size_t array_index_of(array_T* array, void* item) {
+size_t hb_array_index_of(hb_array_T* array, void* item) {
for (size_t i = 0; i < array->size; i++) {
if (array->items[i] == item) { return i; }
}
@@ -97,37 +96,37 @@ size_t array_index_of(array_T* array, void* item) {
return SIZE_MAX;
}
-void array_remove_item(array_T* array, void* item) {
- size_t index = array_index_of(array, item);
+void hb_array_remove_item(hb_array_T* array, void* item) {
+ size_t index = hb_array_index_of(array, item);
- if (index != SIZE_MAX) { array_remove(array, index); }
+ if (index != SIZE_MAX) { hb_array_remove(array, index); }
}
-// Alias for array_append
-void array_push(array_T* array, void* item) {
- array_append(array, item);
+// Alias for hb_array_append
+void hb_array_push(hb_array_T* array, void* item) {
+ hb_array_append(array, item);
}
-void* array_pop(array_T* array) {
+void* hb_array_pop(hb_array_T* array) {
if (!array || array->size == 0) { return NULL; }
- void* last_item = array_last(array);
+ void* last_item = hb_array_last(array);
array->size--;
return last_item;
}
-size_t array_size(const array_T* array) {
+size_t hb_array_size(const hb_array_T* array) {
if (array == NULL) { return 0; }
return array->size;
}
-size_t array_capacity(const array_T* array) {
+size_t hb_array_capacity(const hb_array_T* array) {
return array->capacity;
}
-void array_free(array_T** array) {
+void hb_array_free(hb_array_T** array) {
if (!array || !*array) { return; }
free((*array)->items);
diff --git a/src/util/hb_buffer.c b/src/util/hb_buffer.c
new file mode 100644
index 000000000..34eae2c91
--- /dev/null
+++ b/src/util/hb_buffer.c
@@ -0,0 +1,203 @@
+#include
+#include
+#include
+
+#include "../include/macros.h"
+#include "../include/util.h"
+#include "../include/util/hb_buffer.h"
+
+static bool hb_buffer_has_capacity(hb_buffer_T* buffer, const size_t required_length) {
+ return (buffer->length + required_length <= buffer->capacity);
+}
+
+/**
+ * Resizes the capacity of the buffer to the specified new capacity.
+ *
+ * @param buffer The buffer to resize
+ * @param new_capacity The new capacity to resize the buffer to
+ * @return true if capacity was resized, false if reallocation failed
+ */
+static bool hb_buffer_resize(hb_buffer_T* buffer, const size_t new_capacity) {
+ if (new_capacity + 1 >= SIZE_MAX) {
+ fprintf(stderr, "Error: Buffer capacity would overflow system limits.\n");
+ exit(1);
+ }
+
+ char* new_value = realloc(buffer->value, new_capacity + 1);
+
+ if (unlikely(new_value == NULL)) {
+ fprintf(stderr, "Error: Failed to resize buffer to %zu.\n", new_capacity);
+ exit(1);
+ }
+
+ buffer->value = new_value;
+ buffer->capacity = new_capacity;
+
+ return true;
+}
+
+/**
+ * Expands the capacity of the buffer if needed to accommodate additional content.
+ * This function is a convenience function that calls hb_buffer_has_capacity and
+ * hb_buffer_expand_capacity.
+ *
+ * @param buffer The buffer to expand capacity for
+ * @param required_length The additional length needed beyond current buffer capacity
+ * @return true if capacity was increased, false if reallocation failed
+ */
+static bool hb_buffer_expand_if_needed(hb_buffer_T* buffer, const size_t required_length) {
+ if (hb_buffer_has_capacity(buffer, required_length)) { return true; }
+
+ bool should_double_capacity = required_length < buffer->capacity;
+ size_t new_capacity = 0;
+
+ if (should_double_capacity) {
+ new_capacity = buffer->capacity * 2;
+ } else {
+ new_capacity = buffer->capacity + (required_length * 2);
+ }
+
+ return hb_buffer_resize(buffer, new_capacity);
+}
+
+bool hb_buffer_init(hb_buffer_T* buffer, const size_t capacity) {
+ buffer->capacity = capacity;
+ buffer->length = 0;
+ buffer->value = malloc(sizeof(char) * (buffer->capacity + 1));
+
+ if (!buffer->value) {
+ fprintf(stderr, "Error: Failed to initialize buffer with capacity of %zu.\n", buffer->capacity);
+
+ return false;
+ }
+
+ buffer->value[0] = '\0';
+
+ return true;
+}
+
+char* hb_buffer_value(const hb_buffer_T* buffer) {
+ return buffer->value;
+}
+
+size_t hb_buffer_length(const hb_buffer_T* buffer) {
+ return buffer->length;
+}
+
+size_t hb_buffer_capacity(const hb_buffer_T* buffer) {
+ return buffer->capacity;
+}
+
+size_t hb_buffer_sizeof(void) {
+ return sizeof(hb_buffer_T);
+}
+
+/**
+ * Appends a null-terminated string to the buffer.
+ * @note This function requires that 'text' is a properly null-terminated string.
+ * When reading data from files or other non-string sources, ensure the data is
+ * null-terminated before calling this function, or use hb_buffer_append_with_length instead.
+ *
+ * @param buffer The buffer to append to
+ * @param text A null-terminated string to append
+ * @return void
+ */
+void hb_buffer_append(hb_buffer_T* buffer, const char* text) {
+ if (!buffer || !text) { return; }
+ if (text[0] == '\0') { return; }
+
+ size_t text_length = strlen(text);
+
+ if (!hb_buffer_expand_if_needed(buffer, text_length)) { return; }
+
+ memcpy(buffer->value + buffer->length, text, text_length);
+ buffer->length += text_length;
+ buffer->value[buffer->length] = '\0';
+}
+
+/**
+ * Appends a string of specified length to the buffer.
+ * Unlike hb_buffer_append(), this function does not require the text to be
+ * null-terminated as it uses the provided length instead of strlen().
+ * This is particularly useful when working with data from files, network
+ * buffers, or other non-null-terminated sources.
+ *
+ * @param buffer The buffer to append to
+ * @param text The text to append (doesn't need to be null-terminated)
+ * @param length The number of bytes to append from text
+ * @return void
+ */
+void hb_buffer_append_with_length(hb_buffer_T* buffer, const char* text, const size_t length) {
+ if (!buffer || !text || length == 0) { return; }
+ if (!hb_buffer_expand_if_needed(buffer, length)) { return; }
+
+ memcpy(buffer->value + buffer->length, text, length);
+
+ buffer->length += length;
+ buffer->value[buffer->length] = '\0';
+}
+
+void hb_buffer_append_string(hb_buffer_T* buffer, hb_string_T string) {
+ if (!hb_string_is_empty(string)) { hb_buffer_append_with_length(buffer, string.data, string.length); }
+}
+
+void hb_buffer_append_char(hb_buffer_T* buffer, const char character) {
+ static char string[2];
+
+ string[0] = character;
+ string[1] = '\0';
+
+ hb_buffer_append(buffer, string);
+}
+
+static void hb_buffer_append_repeated(hb_buffer_T* buffer, const char character, size_t length) {
+ if (!buffer || length == 0) { return; }
+ if (!hb_buffer_expand_if_needed(buffer, length)) { return; }
+
+ memset(buffer->value + buffer->length, character, length);
+
+ buffer->length += length;
+ buffer->value[buffer->length] = '\0';
+}
+
+void hb_buffer_append_whitespace(hb_buffer_T* buffer, const size_t length) {
+ hb_buffer_append_repeated(buffer, ' ', length);
+}
+
+void hb_buffer_prepend(hb_buffer_T* buffer, const char* text) {
+ if (!buffer || !text) { return; }
+ if (text[0] == '\0') { return; }
+
+ size_t text_length = strlen(text);
+
+ if (!hb_buffer_expand_if_needed(buffer, text_length)) { return; }
+
+ memmove(buffer->value + text_length, buffer->value, buffer->length + 1);
+ memcpy(buffer->value, text, text_length);
+
+ buffer->length += text_length;
+}
+
+void hb_buffer_concat(hb_buffer_T* destination, hb_buffer_T* source) {
+ if (source->length == 0) { return; }
+ if (!hb_buffer_expand_if_needed(destination, source->length)) { return; }
+
+ memcpy(destination->value + destination->length, source->value, source->length);
+
+ destination->length += source->length;
+ destination->value[destination->length] = '\0';
+}
+
+void hb_buffer_clear(hb_buffer_T* buffer) {
+ buffer->length = 0;
+ buffer->value[0] = '\0';
+}
+
+void hb_buffer_free(hb_buffer_T** buffer) {
+ if (!buffer || !*buffer) { return; }
+
+ if ((*buffer)->value != NULL) { free((*buffer)->value); }
+
+ free(*buffer);
+ *buffer = NULL;
+}
diff --git a/src/util/hb_string.c b/src/util/hb_string.c
new file mode 100644
index 000000000..b348ef7e6
--- /dev/null
+++ b/src/util/hb_string.c
@@ -0,0 +1,63 @@
+#include "../include/util/hb_string.h"
+
+#include
+#include
+#include
+
+hb_string_T hb_string(const char* null_terminated_c_string) {
+ hb_string_T string;
+
+ string.data = (char*) null_terminated_c_string;
+ string.length = (uint32_t) strlen(null_terminated_c_string);
+
+ return string;
+}
+
+hb_string_T hb_string_slice(hb_string_T string, uint32_t offset) {
+ hb_string_T slice;
+ if (string.length < offset) {
+ slice.data = NULL;
+ slice.length = 0;
+
+ return slice;
+ }
+
+ slice.data = string.data + offset;
+ slice.length = string.length - offset;
+
+ return slice;
+}
+
+bool hb_string_equals(hb_string_T a, hb_string_T b) {
+ if (a.length != b.length) { return false; }
+
+ return strncmp(a.data, b.data, a.length) == 0;
+}
+
+bool hb_string_equals_case_insensitive(hb_string_T a, hb_string_T b) {
+ if (a.length != b.length) { return false; }
+
+ return strncasecmp(a.data, b.data, a.length) == 0;
+}
+
+bool hb_string_starts_with(hb_string_T string, hb_string_T expected_prefix) {
+ if (hb_string_is_empty(string) || hb_string_is_empty(expected_prefix)) { return false; }
+ if (string.length < expected_prefix.length) { return false; }
+
+ return strncmp(string.data, expected_prefix.data, expected_prefix.length) == 0;
+}
+
+bool hb_string_is_empty(hb_string_T string) {
+ return string.length == 0;
+}
+
+char* hb_string_to_c_string(hb_arena_T* allocator, hb_string_T string) {
+ size_t string_length_in_bytes = sizeof(char) * (string.length);
+ char* buffer = hb_arena_alloc(allocator, string_length_in_bytes + sizeof(char) * 1);
+
+ if (!hb_string_is_empty(string)) { memcpy(buffer, string.data, string_length_in_bytes); }
+
+ buffer[string_length_in_bytes] = '\0';
+
+ return buffer;
+}
diff --git a/templates/ext/herb/error_helpers.c.erb b/templates/ext/herb/error_helpers.c.erb
index 5771a91eb..7ededea6d 100644
--- a/templates/ext/herb/error_helpers.c.erb
+++ b/templates/ext/herb/error_helpers.c.erb
@@ -67,12 +67,12 @@ VALUE rb_error_from_c_struct(ERROR_T* error) {
return Qnil;
}
-VALUE rb_errors_array_from_c_array(array_T* array) {
+VALUE rb_errors_array_from_c_array(hb_array_T* array) {
VALUE rb_array = rb_ary_new();
if (array) {
- for (size_t i = 0; i < array_size(array); i++) {
- ERROR_T* child_node = (ERROR_T*) array_get(array, i);
+ for (size_t i = 0; i < hb_array_size(array); i++) {
+ ERROR_T* child_node = (ERROR_T*) hb_array_get(array, i);
if (child_node) {
VALUE rb_child = rb_error_from_c_struct(child_node);
diff --git a/templates/ext/herb/error_helpers.h.erb b/templates/ext/herb/error_helpers.h.erb
index d30be39ae..3a7a88c47 100644
--- a/templates/ext/herb/error_helpers.h.erb
+++ b/templates/ext/herb/error_helpers.h.erb
@@ -7,6 +7,6 @@
#include
VALUE rb_error_from_c_struct(ERROR_T* error);
-VALUE rb_errors_array_from_c_array(array_T* array);
+VALUE rb_errors_array_from_c_array(hb_array_T* array);
#endif
diff --git a/templates/ext/herb/nodes.c.erb b/templates/ext/herb/nodes.c.erb
index 5fd5d677f..a778196c1 100644
--- a/templates/ext/herb/nodes.c.erb
+++ b/templates/ext/herb/nodes.c.erb
@@ -9,7 +9,7 @@
#include "../../src/include/token.h"
VALUE rb_node_from_c_struct(AST_NODE_T* node);
-static VALUE rb_nodes_array_from_c_array(array_T* array);
+static VALUE rb_nodes_array_from_c_array(hb_array_T* array);
<%- nodes.each do |node| -%>
static VALUE rb_<%= node.human %>_from_c_struct(<%= node.struct_type %>* <%= node.human %>) {
@@ -72,12 +72,12 @@ VALUE rb_node_from_c_struct(AST_NODE_T* node) {
return Qnil;
}
-static VALUE rb_nodes_array_from_c_array(array_T* array) {
+static VALUE rb_nodes_array_from_c_array(hb_array_T* array) {
VALUE rb_array = rb_ary_new();
if (array) {
- for (size_t i = 0; i < array_size(array); i++) {
- AST_NODE_T* child_node = (AST_NODE_T*) array_get(array, i);
+ for (size_t i = 0; i < hb_array_size(array); i++) {
+ AST_NODE_T* child_node = (AST_NODE_T*) hb_array_get(array, i);
if (child_node) {
VALUE rb_child = rb_node_from_c_struct(child_node);
diff --git a/templates/javascript/packages/core/src/visitor.ts.erb b/templates/javascript/packages/core/src/visitor.ts.erb
index 47a7cfade..f38a060fc 100644
--- a/templates/javascript/packages/core/src/visitor.ts.erb
+++ b/templates/javascript/packages/core/src/visitor.ts.erb
@@ -1,11 +1,27 @@
import {
Node,
+ ERBNode,
<%- nodes.each do |node| -%>
<%= node.name %>,
<%- end -%>
} from "./nodes.js"
-export class Visitor {
+/**
+ * Interface that enforces all node visit methods are implemented
+ * This ensures that any class implementing IVisitor must have a visit method for every node type
+ */
+export interface IVisitor {
+ visit(node: Node | null | undefined): void
+ visitAll(nodes: (Node | null | undefined)[]): void
+ visitChildNodes(node: Node): void
+ <%- nodes.each do |node| -%>
+ visit<%= node.name %>(node: <%= node.name %>): void
+ <%- end -%>
+ visitNode(node: Node): void
+ visitERBNode(node: ERBNode): void
+}
+
+export class Visitor implements IVisitor {
visit(node: Node | null | undefined): void {
if (!node) return
@@ -20,8 +36,20 @@ export class Visitor {
node.compactChildNodes().forEach(node => node.accept(this))
}
+ visitNode(_node: Node): void {
+ // Default implementation does nothing
+ }
+
+ visitERBNode(_node: ERBNode): void {
+ // Default implementation does nothing
+ }
+
<%- nodes.each do |node| -%>
visit<%= node.name %>(node: <%= node.name %>): void {
+ this.visitNode(node)
+ <%- if node.name.start_with?("ERB") -%>
+ this.visitERBNode(node)
+ <%- end -%>
this.visitChildNodes(node)
}
diff --git a/templates/javascript/packages/node/extension/error_helpers.cpp.erb b/templates/javascript/packages/node/extension/error_helpers.cpp.erb
index 390848a22..712960efe 100644
--- a/templates/javascript/packages/node/extension/error_helpers.cpp.erb
+++ b/templates/javascript/packages/node/extension/error_helpers.cpp.erb
@@ -4,16 +4,16 @@
#include "nodes.h"
extern "C" {
-#include "../extension/libherb/include/herb.h"
-#include "../extension/libherb/include/token.h"
-#include "../extension/libherb/include/array.h"
-#include "../extension/libherb/include/errors.h"
#include "../extension/libherb/include/ast_node.h"
#include "../extension/libherb/include/ast_nodes.h"
+#include "../extension/libherb/include/errors.h"
+#include "../extension/libherb/include/herb.h"
+#include "../extension/libherb/include/token.h"
+#include "../extension/libherb/include/util/hb_array.h"
}
napi_value ErrorFromCStruct(napi_env env, ERROR_T* error);
-napi_value ErrorsArrayFromCArray(napi_env env, array_T* array);
+napi_value ErrorsArrayFromCArray(napi_env env, hb_array_T* array);
<%- errors.each do |error| -%>
napi_value <%= error.name %>FromCStruct(napi_env env, <%= error.struct_type %>* <%= error.human %>) {
@@ -75,13 +75,13 @@ napi_value <%= error.name %>FromCStruct(napi_env env, <%= error.struct_type %>*
<%- end -%>
-napi_value ErrorsArrayFromCArray(napi_env env, array_T* array) {
+napi_value ErrorsArrayFromCArray(napi_env env, hb_array_T* array) {
napi_value result;
napi_create_array(env, &result);
if (array) {
- for (size_t i = 0; i < array_size(array); i++) {
- ERROR_T* error = (ERROR_T*) array_get(array, i);
+ for (size_t i = 0; i < hb_array_size(array); i++) {
+ ERROR_T* error = (ERROR_T*) hb_array_get(array, i);
if (error) {
napi_value js_error = ErrorFromCStruct(env, error);
napi_set_element(env, result, i, js_error);
diff --git a/templates/javascript/packages/node/extension/error_helpers.h.erb b/templates/javascript/packages/node/extension/error_helpers.h.erb
index 003dd4a40..c10df4fec 100644
--- a/templates/javascript/packages/node/extension/error_helpers.h.erb
+++ b/templates/javascript/packages/node/extension/error_helpers.h.erb
@@ -8,7 +8,7 @@ extern "C" {
}
napi_value ErrorFromCStruct(napi_env env, ERROR_T* error);
-napi_value ErrorsArrayFromCArray(napi_env env, array_T* array);
+napi_value ErrorsArrayFromCArray(napi_env env, hb_array_T* array);
<%- errors.each do |error| -%>
napi_value <%= error.name %>FromCStruct(napi_env env, <%= error.struct_type %>* <%= error.human %>);
diff --git a/templates/javascript/packages/node/extension/nodes.cpp.erb b/templates/javascript/packages/node/extension/nodes.cpp.erb
index f5ecc95b7..a8de107df 100644
--- a/templates/javascript/packages/node/extension/nodes.cpp.erb
+++ b/templates/javascript/packages/node/extension/nodes.cpp.erb
@@ -4,15 +4,15 @@
#include "nodes.h"
extern "C" {
-#include "../extension/libherb/include/herb.h"
-#include "../extension/libherb/include/token.h"
-#include "../extension/libherb/include/array.h"
#include "../extension/libherb/include/ast_node.h"
#include "../extension/libherb/include/ast_nodes.h"
+#include "../extension/libherb/include/herb.h"
+#include "../extension/libherb/include/token.h"
+#include "../extension/libherb/include/util/hb_array.h"
}
napi_value NodeFromCStruct(napi_env env, AST_NODE_T* node);
-napi_value NodesArrayFromCArray(napi_env env, array_T* array);
+napi_value NodesArrayFromCArray(napi_env env, hb_array_T* array);
<%- nodes.each do |node| -%>
napi_value <%= node.human %>NodeFromCStruct(napi_env env, <%= node.struct_type %>* <%= node.human %>) {
@@ -73,13 +73,13 @@ napi_value <%= node.human %>NodeFromCStruct(napi_env env, <%= node.struct_type %
}
<%- end -%>
-napi_value NodesArrayFromCArray(napi_env env, array_T* array) {
+napi_value NodesArrayFromCArray(napi_env env, hb_array_T* array) {
napi_value result;
napi_create_array(env, &result);
if (array) {
- for (size_t i = 0; i < array_size(array); i++) {
- AST_NODE_T* child_node = (AST_NODE_T*) array_get(array, i);
+ for (size_t i = 0; i < hb_array_size(array); i++) {
+ AST_NODE_T* child_node = (AST_NODE_T*) hb_array_get(array, i);
if (child_node) {
napi_value js_child = NodeFromCStruct(env, child_node);
napi_set_element(env, result, i, js_child);
diff --git a/templates/javascript/packages/node/extension/nodes.h.erb b/templates/javascript/packages/node/extension/nodes.h.erb
index 1f83989df..78f97c849 100644
--- a/templates/javascript/packages/node/extension/nodes.h.erb
+++ b/templates/javascript/packages/node/extension/nodes.h.erb
@@ -8,7 +8,7 @@ extern "C" {
}
napi_value NodeFromCStruct(napi_env env, AST_NODE_T* node);
-napi_value NodesArrayFromCArray(napi_env env, array_T* array);
+napi_value NodesArrayFromCArray(napi_env env, hb_array_T* array);
<%- nodes.each do |node| -%>
napi_value <%= node.human %>NodeFromCStruct(napi_env env, <%= node.struct_type %>* <%= node.human %>);
diff --git a/templates/src/ast_nodes.c.erb b/templates/src/ast_nodes.c.erb
index c89debea1..658bf5061 100644
--- a/templates/src/ast_nodes.c.erb
+++ b/templates/src/ast_nodes.c.erb
@@ -4,16 +4,16 @@
#include
#include "include/analyzed_ruby.h"
-#include "include/array.h"
#include "include/ast_node.h"
#include "include/ast_nodes.h"
#include "include/errors.h"
#include "include/token.h"
#include "include/util.h"
+#include "include/util/hb_array.h"
<%- nodes.each do |node| -%>
<%- node_arguments = node.fields.any? ? node.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%>
-<%- arguments = node_arguments + ["position_T* start_position", "position_T* end_position", "array_T* errors"] -%>
+<%- arguments = node_arguments + ["position_T start_position", "position_T end_position", "hb_array_T* errors"] -%>
<%= node.struct_type %>* ast_<%= node.human %>_init(<%= arguments.join(", ") %>) {
<%= node.struct_type %>* <%= node.human %> = malloc(sizeof(<%= node.struct_type %>));
@@ -73,16 +73,14 @@ void ast_free_base_node(AST_NODE_T* node) {
if (node == NULL) { return; }
if (node->errors) {
- for (size_t i = 0; i < array_size(node->errors); i++) {
- ERROR_T* child = array_get(node->errors, i);
+ for (size_t i = 0; i < hb_array_size(node->errors); i++) {
+ ERROR_T* child = hb_array_get(node->errors, i);
if (child != NULL) { error_free(child); }
}
- array_free(&node->errors);
+ hb_array_free(&node->errors);
}
- if (node->location) { location_free(node->location); }
-
free(node);
}
@@ -101,12 +99,12 @@ static void ast_free_<%= node.human %>(<%= node.struct_type %>* <%= node.human %
ast_node_free((AST_NODE_T*) <%= node.human %>-><%= field.name %>);
<%- when Herb::Template::ArrayField -%>
if (<%= node.human %>-><%= field.name %> != NULL) {
- for (size_t i = 0; i < array_size(<%= node.human %>-><%= field.name %>); i++) {
- AST_NODE_T* child = array_get(<%= node.human %>-><%= field.name %>, i);
+ for (size_t i = 0; i < hb_array_size(<%= node.human %>-><%= field.name %>); i++) {
+ AST_NODE_T* child = hb_array_get(<%= node.human %>-><%= field.name %>, i);
if (child) { ast_node_free(child); }
}
- array_free(&<%= node.human %>-><%= field.name %>);
+ hb_array_free(&<%= node.human %>-><%= field.name %>);
}
<%- when Herb::Template::StringField -%>
if (<%= node.human %>-><%= field.name %> != NULL) { free((char*) <%= node.human %>-><%= field.name %>); }
diff --git a/templates/src/ast_pretty_print.c.erb b/templates/src/ast_pretty_print.c.erb
index 845ab35bb..78e371937 100644
--- a/templates/src/ast_pretty_print.c.erb
+++ b/templates/src/ast_pretty_print.c.erb
@@ -1,27 +1,27 @@
#include "include/ast_node.h"
#include "include/ast_nodes.h"
-#include "include/buffer.h"
#include "include/errors.h"
#include "include/pretty_print.h"
#include "include/token_struct.h"
#include "include/util.h"
+#include "include/util/hb_buffer.h"
#include
#include
#include
-void ast_pretty_print_node(AST_NODE_T* node, const size_t indent, const size_t relative_indent, buffer_T* buffer) {
+void ast_pretty_print_node(AST_NODE_T* node, const size_t indent, const size_t relative_indent, hb_buffer_T* buffer) {
if (!node) { return; }
bool print_location = true;
- buffer_append(buffer, "@ ");
- buffer_append(buffer, ast_node_human_type(node));
- buffer_append(buffer, " ");
+ hb_buffer_append(buffer, "@ ");
+ hb_buffer_append(buffer, ast_node_human_type(node));
+ hb_buffer_append(buffer, " ");
if (print_location) { pretty_print_location(node->location, buffer); }
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
switch (node->type) {
<%- nodes.each do |node| -%>
@@ -49,16 +49,16 @@ void ast_pretty_print_node(AST_NODE_T* node, const size_t indent, const size_t r
pretty_print_label("<%= field.name %>", indent, relative_indent, <%= last %>, buffer);
if (<%= node.human %>-><%= field.name %>) {
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
pretty_print_indent(buffer, indent);
pretty_print_indent(buffer, relative_indent + 1);
- buffer_append(buffer, "└── ");
+ hb_buffer_append(buffer, "└── ");
ast_pretty_print_node((AST_NODE_T*) <%= node.human %>-><%= field.name %>, indent, relative_indent + 2, buffer);
} else {
- buffer_append(buffer, " ∅\n");
+ hb_buffer_append(buffer, " ∅\n");
}
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
<%- when Herb::Template::AnalyzedRubyField -%>
if (<%= node.human %>-><%= field.name %>) {
@@ -79,12 +79,12 @@ void ast_pretty_print_node(AST_NODE_T* node, const size_t indent, const size_t r
pretty_print_boolean_property("unless_node", <%= node.human %>-><%= field.name %>->has_unless_node, indent, relative_indent, <%= last %>, buffer);
} else {
pretty_print_label("<%= field.name %>", indent, relative_indent, <%= last %>, buffer);
- buffer_append(buffer, " ∅\n");
+ hb_buffer_append(buffer, " ∅\n");
}
<%- when Herb::Template::VoidPointerField -%>
pretty_print_label("<%= field.name %>", indent, relative_indent, <%= last %>, buffer);
- buffer_append(buffer, " ?\n");
+ hb_buffer_append(buffer, " ?\n");
<%- else -%>
<%= field.inspect %>
diff --git a/templates/src/errors.c.erb b/templates/src/errors.c.erb
index a3336a711..1c8e1408c 100644
--- a/templates/src/errors.c.erb
+++ b/templates/src/errors.c.erb
@@ -1,10 +1,10 @@
-#include "include/array.h"
#include "include/errors.h"
#include "include/location.h"
#include "include/position.h"
#include "include/pretty_print.h"
#include "include/token.h"
#include "include/util.h"
+#include "include/util/hb_array.h"
#include
#include
@@ -17,15 +17,16 @@ size_t error_sizeof(void) {
return sizeof(struct ERROR_STRUCT);
}
-void error_init(ERROR_T* error, const error_type_T type, position_T* start, position_T* end) {
+void error_init(ERROR_T* error, const error_type_T type, position_T start, position_T end) {
if (!error) { return; }
error->type = type;
- error->location = location_init(position_copy(start), position_copy(end));
+ error->location.start = start;
+ error->location.end = end;
}
<%- errors.each do |error| -%>
<%- error_arguments = error.fields.any? ? error.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%>
-<%- arguments = error_arguments + ["position_T* start", "position_T* end"] -%>
+<%- arguments = error_arguments + ["position_T start", "position_T end"] -%>
<%= error.struct_type %>* <%= error.human %>_init(<%= arguments.join(", ") %>) {
<%= error.struct_type %>* <%= error.human %> = malloc(sizeof(<%= error.struct_type %>));
@@ -72,7 +73,7 @@ void error_init(ERROR_T* error, const error_type_T type, position_T* start, posi
<%- error.fields.each do |field| -%>
<%- case field -%>
<%- when Herb::Template::PositionField -%>
- <%= error.human %>-><%= field.name %> = position_copy(<%= field.name %>);
+ <%= error.human %>-><%= field.name %> = <%= field.name %>;
<%- when Herb::Template::TokenField -%>
<%= error.human %>-><%= field.name %> = token_copy(<%= field.name %>);
<%- when Herb::Template::TokenTypeField -%>
@@ -88,8 +89,8 @@ void error_init(ERROR_T* error, const error_type_T type, position_T* start, posi
return <%= error.human %>;
}
-void append_<%= error.human %>(<%= (arguments + ["array_T* errors"]).join(", ") %>) {
- array_append(errors, <%= error.human %>_init(<%= arguments.map { |argument| argument.split(" ").last.strip }.join(", ") %>));
+void append_<%= error.human %>(<%= (arguments + ["hb_array_T* errors"]).join(", ") %>) {
+ hb_array_append(errors, <%= error.human %>_init(<%= arguments.map { |argument| argument.split(" ").last.strip }.join(", ") %>));
}
<%- end -%>
@@ -116,7 +117,6 @@ const char* error_human_type(ERROR_T* error) {
void error_free_base_error(ERROR_T* error) {
if (error == NULL) { return; }
- if (error->location != NULL) { location_free(error->location); }
if (error->message != NULL) { free(error->message); }
free(error);
@@ -130,8 +130,6 @@ static void error_free_<%= error.human %>(<%= error.struct_type %>* <%= error.hu
<%- end -%>
<%- error.fields.each do |field| -%>
<%- case field -%>
- <%- when Herb::Template::PositionField -%>
- if (<%= error.human %>-><%= field.name %> != NULL) { position_free(<%= error.human %>-><%= field.name %>); }
<%- when Herb::Template::TokenField -%>
if (<%= error.human %>-><%= field.name %> != NULL) { token_free(<%= error.human %>-><%= field.name %>); }
<%- when Herb::Template::TokenTypeField -%>
@@ -160,8 +158,8 @@ void error_free(ERROR_T* error) {
}
void error_pretty_print_array(
- const char* name, array_T* array, const size_t indent, const size_t relative_indent, const bool last_property,
- buffer_T* buffer
+ const char* name, hb_array_T* array, const size_t indent, const size_t relative_indent, const bool last_property,
+ hb_buffer_T* buffer
) {
if (array == NULL) {
pretty_print_property(name, "∅", indent, relative_indent, last_property, buffer);
@@ -169,7 +167,7 @@ void error_pretty_print_array(
return;
}
- if (array_size(array) == 0) {
+ if (hb_array_size(array) == 0) {
pretty_print_property(name, "[]", indent, relative_indent, last_property, buffer);
return;
@@ -177,42 +175,42 @@ void error_pretty_print_array(
pretty_print_label(name, indent, relative_indent, last_property, buffer);
- buffer_append(buffer, "(");
+ hb_buffer_append(buffer, "(");
char count[16];
- sprintf(count, "%zu", array_size(array));
- buffer_append(buffer, count);
- buffer_append(buffer, ")\n");
+ sprintf(count, "%zu", hb_array_size(array));
+ hb_buffer_append(buffer, count);
+ hb_buffer_append(buffer, ")\n");
if (indent < 20) {
- for (size_t i = 0; i < array_size(array); i++) {
- ERROR_T* child = array_get(array, i);
+ for (size_t i = 0; i < hb_array_size(array); i++) {
+ ERROR_T* child = hb_array_get(array, i);
pretty_print_indent(buffer, indent);
pretty_print_indent(buffer, relative_indent + 1);
- if (i == array_size(array) - 1) {
- buffer_append(buffer, "└── ");
+ if (i == hb_array_size(array) - 1) {
+ hb_buffer_append(buffer, "└── ");
} else {
- buffer_append(buffer, "├── ");
+ hb_buffer_append(buffer, "├── ");
}
error_pretty_print(child, indent + 1, relative_indent + 1, buffer);
- if (i != array_size(array) - 1) { pretty_print_newline(indent + 1, relative_indent, buffer); }
+ if (i != hb_array_size(array) - 1) { pretty_print_newline(indent + 1, relative_indent, buffer); }
}
}
}
<%- errors.each do |error| -%>
-static void error_pretty_print_<%= error.human %>(<%= error.struct_type %>* error, const size_t indent, const size_t relative_indent, buffer_T* buffer) {
+static void error_pretty_print_<%= error.human %>(<%= error.struct_type %>* error, const size_t indent, const size_t relative_indent, hb_buffer_T* buffer) {
if (!error) { return; }
- buffer_append(buffer, "@ ");
- buffer_append(buffer, error_human_type((ERROR_T*) error));
- buffer_append(buffer, " ");
+ hb_buffer_append(buffer, "@ ");
+ hb_buffer_append(buffer, error_human_type((ERROR_T*) error));
+ hb_buffer_append(buffer, " ");
pretty_print_location(error->base.location, buffer);
- buffer_append(buffer, "\n");
+ hb_buffer_append(buffer, "\n");
pretty_print_quoted_property("message", error->base.message, indent, relative_indent, <%= error.fields.none? %>, buffer);
<%- error.fields.each_with_index do |field, index| -%>
@@ -234,7 +232,7 @@ static void error_pretty_print_<%= error.human %>(<%= error.struct_type %>* erro
}
<%- end -%>
-void error_pretty_print(ERROR_T* error, const size_t indent, const size_t relative_indent, buffer_T* buffer) {
+void error_pretty_print(ERROR_T* error, const size_t indent, const size_t relative_indent, hb_buffer_T* buffer) {
if (!error) { return; }
switch (error->type) {
diff --git a/templates/src/include/ast_nodes.h.erb b/templates/src/include/ast_nodes.h.erb
index 9b6dca00f..9aad4ad94 100644
--- a/templates/src/include/ast_nodes.h.erb
+++ b/templates/src/include/ast_nodes.h.erb
@@ -4,13 +4,13 @@
#include
#include
-#include "array.h"
-#include "buffer.h"
-#include "position.h"
-#include "location.h"
-#include "token_struct.h"
#include "analyzed_ruby.h"
#include "element_source.h"
+#include "location.h"
+#include "position.h"
+#include "token_struct.h"
+#include "util/hb_array.h"
+#include "util/hb_buffer.h"
typedef enum {
<%- nodes.each do |node| -%>
@@ -20,9 +20,9 @@ typedef enum {
typedef struct AST_NODE_STRUCT {
ast_node_type_T type;
- location_T* location;
+ location_T location;
// maybe a range too?
- array_T* errors;
+ hb_array_T* errors;
} AST_NODE_T;
<%- nodes.each do |node| -%>
@@ -36,7 +36,7 @@ typedef struct <%= node.struct_name %> {
<%- nodes.each do |node| -%>
<%- node_arguments = node.fields.any? ? node.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%>
-<%- arguments = node_arguments + ["position_T* start_position", "position_T* end_position", "array_T* errors"] -%>
+<%- arguments = node_arguments + ["position_T start_position", "position_T end_position", "hb_array_T* errors"] -%>
<%= node.struct_type %>* ast_<%= node.human %>_init(<%= arguments.join(", ") %>);
<%- end -%>
diff --git a/templates/src/include/ast_pretty_print.h.erb b/templates/src/include/ast_pretty_print.h.erb
index e0dc4052b..47f91aa13 100644
--- a/templates/src/include/ast_pretty_print.h.erb
+++ b/templates/src/include/ast_pretty_print.h.erb
@@ -2,13 +2,13 @@
#define HERB_AST_PRETTY_PRINT_H
#include "ast_nodes.h"
-#include "buffer.h"
+#include "util/hb_buffer.h"
void ast_pretty_print_node(
AST_NODE_T* node,
size_t indent,
size_t relative_indent,
- buffer_T* buffer
+ hb_buffer_T* buffer
);
#endif
diff --git a/templates/src/include/errors.h.erb b/templates/src/include/errors.h.erb
index 1baca9430..4676c72d3 100644
--- a/templates/src/include/errors.h.erb
+++ b/templates/src/include/errors.h.erb
@@ -1,12 +1,12 @@
#ifndef HERB_ERRORS_H
#define HERB_ERRORS_H
-#include "array.h"
-#include "buffer.h"
#include "errors.h"
#include "location.h"
#include "position.h"
#include "token.h"
+#include "util/hb_array.h"
+#include "util/hb_buffer.h"
typedef enum {
<%- errors.each do |error| -%>
@@ -16,7 +16,7 @@ typedef enum {
typedef struct ERROR_STRUCT {
error_type_T type;
- location_T* location;
+ location_T location;
char* message;
} ERROR_T;
@@ -31,12 +31,12 @@ typedef struct {
<%- errors.each do |error| -%>
<%- error_arguments = error.fields.any? ? error.fields.map { |field| [field.c_type, " ", field.name].join } : [] -%>
-<%- arguments = error_arguments + ["position_T* start", "position_T* end"] -%>
+<%- arguments = error_arguments + ["position_T start", "position_T end"] -%>
<%= error.struct_type %>* <%= error.human %>_init(<%= arguments.join(", ") %>);
-void append_<%= error.human %>(<%= (arguments << "array_T* errors").join(", ") %>);
+void append_<%= error.human %>(<%= (arguments << "hb_array_T* errors").join(", ") %>);
<%- end -%>
-void error_init(ERROR_T* error, error_type_T type, position_T* start, position_T* end);
+void error_init(ERROR_T* error, error_type_T type, position_T start, position_T end);
size_t error_sizeof(void);
error_type_T error_type(ERROR_T* error);
@@ -48,11 +48,11 @@ const char* error_human_type(ERROR_T* error);
void error_free(ERROR_T* error);
-void error_pretty_print(ERROR_T* error, size_t indent, size_t relative_indent, buffer_T* buffer);
+void error_pretty_print(ERROR_T* error, size_t indent, size_t relative_indent, hb_buffer_T* buffer);
void error_pretty_print_array(
- const char* name, array_T* array, size_t indent, size_t relative_indent, bool last_property,
- buffer_T* buffer
+ const char* name, hb_array_T* array, size_t indent, size_t relative_indent, bool last_property,
+ hb_buffer_T* buffer
);
#endif
diff --git a/templates/src/visitor.c.erb b/templates/src/visitor.c.erb
index 41a5afd55..ab20a5bfa 100644
--- a/templates/src/visitor.c.erb
+++ b/templates/src/visitor.c.erb
@@ -1,9 +1,9 @@
#include
-#include "include/array.h"
-#include "include/visitor.h"
#include "include/ast_node.h"
#include "include/ast_nodes.h"
+#include "include/util/hb_array.h"
+#include "include/visitor.h"
void herb_visit_node(const AST_NODE_T* node, bool (*visitor)(const AST_NODE_T*, void*), void* data) {
if (visitor(node, data) && node != NULL) {
@@ -31,8 +31,8 @@ void herb_visit_child_nodes(const AST_NODE_T *node, bool (*visitor)(const AST_NO
<%- when Herb::Template::ArrayField -%>
if (<%= node.human %>-><%= field.name %> != NULL) {
- for (size_t index = 0; index < array_size(<%= node.human %>-><%= field.name %>); index++) {
- herb_visit_node(array_get(<%= node.human %>-><%= field.name %>, index), visitor, data);
+ for (size_t index = 0; index < hb_array_size(<%= node.human %>-><%= field.name %>); index++) {
+ herb_visit_node(hb_array_get(<%= node.human %>-><%= field.name %>, index), visitor, data);
}
}
diff --git a/templates/template.rb b/templates/template.rb
index 4d5973703..64097371f 100755
--- a/templates/template.rb
+++ b/templates/template.rb
@@ -30,7 +30,7 @@ def ruby_type
end
def c_type
- "array_T*"
+ "hb_array_T*"
end
def c_item_type
diff --git a/templates/wasm/error_helpers.cpp.erb b/templates/wasm/error_helpers.cpp.erb
index 427972a0f..7e0f43b7b 100644
--- a/templates/wasm/error_helpers.cpp.erb
+++ b/templates/wasm/error_helpers.cpp.erb
@@ -8,12 +8,12 @@
#include "extension_helpers.h"
extern "C" {
-#include "../src/include/herb.h"
-#include "../src/include/token.h"
-#include "../src/include/array.h"
-#include "../src/include/errors.h"
#include "../src/include/ast_node.h"
#include "../src/include/ast_nodes.h"
+#include "../src/include/errors.h"
+#include "../src/include/herb.h"
+#include "../src/include/token.h"
+#include "../src/include/util/hb_array.h"
}
using namespace emscripten;
@@ -21,9 +21,9 @@ using namespace emscripten;
val CreateLocation(location_T* location);
val CreateToken(token_T* token);
val NodeFromCStruct(AST_NODE_T* node);
-val NodesArrayFromCArray(array_T* array);
+val NodesArrayFromCArray(hb_array_T* array);
val ErrorFromCStruct(ERROR_T* error);
-val ErrorsArrayFromCArray(array_T* array);
+val ErrorsArrayFromCArray(hb_array_T* array);
<%- errors.each do |error| -%>
val <%= error.name %>FromCStruct(<%= error.struct_type %>* <%= error.human %>) {
@@ -61,13 +61,13 @@ val <%= error.name %>FromCStruct(<%= error.struct_type %>* <%= error.human %>) {
<%- end -%>
-val ErrorsArrayFromCArray(array_T* array) {
+val ErrorsArrayFromCArray(hb_array_T* array) {
val Array = val::global("Array");
val result = Array.new_();
if (array) {
- for (size_t i = 0; i < array_size(array); i++) {
- ERROR_T* error = (ERROR_T*)array_get(array, i);
+ for (size_t i = 0; i < hb_array_size(array); i++) {
+ ERROR_T* error = (ERROR_T*)hb_array_get(array, i);
if (error) {
result.call("push", ErrorFromCStruct(error));
}
diff --git a/templates/wasm/error_helpers.h.erb b/templates/wasm/error_helpers.h.erb
index bfa8971d3..32f65cefb 100644
--- a/templates/wasm/error_helpers.h.erb
+++ b/templates/wasm/error_helpers.h.erb
@@ -6,7 +6,7 @@ extern "C" {
}
emscripten::val ErrorFromCStruct(ERROR_T* error);
-emscripten::val ErrorsArrayFromCArray(array_T* array);
+emscripten::val ErrorsArrayFromCArray(hb_array_T* array);
<%- errors.each do |error| -%>
emscripten::val <%= error.name %>FromCStruct(<%= error.struct_type %>* <%= error.human %>);
diff --git a/templates/wasm/nodes.cpp.erb b/templates/wasm/nodes.cpp.erb
index 1580f22d4..d804a37c8 100644
--- a/templates/wasm/nodes.cpp.erb
+++ b/templates/wasm/nodes.cpp.erb
@@ -4,17 +4,17 @@
#include "error_helpers.h"
#include "extension_helpers.h"
-#include "../src/include/herb.h"
-#include "../src/include/token.h"
-#include "../src/include/array.h"
#include "../src/include/ast_node.h"
#include "../src/include/ast_nodes.h"
+#include "../src/include/herb.h"
#include "../src/include/location.h"
+#include "../src/include/token.h"
+#include "../src/include/util/hb_array.h"
using namespace emscripten;
val NodeFromCStruct(AST_NODE_T* node);
-val NodesArrayFromCArray(array_T* array);
+val NodesArrayFromCArray(hb_array_T* array);
<%- nodes.each do |node| -%>
val <%= node.name %>FromCStruct(<%= node.struct_type %>* <%= node.human %>) {
@@ -62,13 +62,13 @@ val NodeFromCStruct(AST_NODE_T* node) {
}
}
-val NodesArrayFromCArray(array_T* array) {
+val NodesArrayFromCArray(hb_array_T* array) {
if (!array) return val::null();
val jsArray = val::array();
- for (size_t i = 0; i < array_size(array); i++) {
- AST_NODE_T* child_node = (AST_NODE_T*) array_get(array, i);
+ for (size_t i = 0; i < hb_array_size(array); i++) {
+ AST_NODE_T* child_node = (AST_NODE_T*) hb_array_get(array, i);
if (child_node) {
jsArray.set(i, NodeFromCStruct(child_node));
diff --git a/templates/wasm/nodes.h.erb b/templates/wasm/nodes.h.erb
index a32c551ae..eeefc1177 100644
--- a/templates/wasm/nodes.h.erb
+++ b/templates/wasm/nodes.h.erb
@@ -6,7 +6,7 @@ extern "C" {
}
emscripten::val NodeFromCStruct(AST_NODE_T* node);
-emscripten::val NodesArrayFromCArray(array_T* array);
+emscripten::val NodesArrayFromCArray(hb_array_T* array);
<%- nodes.each do |node| -%>
emscripten::val <%= node.human %>NodeFromCStruct(<%= node.struct_type %>* <%= node.human %>);
diff --git a/test/analyze/case_in_test.rb b/test/analyze/case_in_test.rb
index a648ada19..8fb1a25ac 100644
--- a/test/analyze/case_in_test.rb
+++ b/test/analyze/case_in_test.rb
@@ -92,5 +92,41 @@ class CaseInTest < Minitest::Spec
<% end %>
HTML
end
+
+ test "case in with block inside in clause" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case result %>
+ <% in { status: :success } %>
+ <%= content_tag(:div) do %>
+ Success!
+ <% end %>
+ <% end %>
+ HTML
+ end
+
+ test "case in with multiple blocks in in clause" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case data %>
+ <% in { type: :alert } %>
+ <%= content_tag(:div) do %>
+ Alert
+ <% end %>
+ <%= content_tag(:span) do %>
+ Icon
+ <% end %>
+ <% end %>
+ HTML
+ end
+
+ test "case in with if statement inside in clause" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case value %>
+ <% in [Integer] %>
+ <% if positive? %>
+ Positive
+ <% end %>
+ <% end %>
+ HTML
+ end
end
end
diff --git a/test/analyze/case_test.rb b/test/analyze/case_test.rb
index 8a7d2b538..a4b7bf5d4 100644
--- a/test/analyze/case_test.rb
+++ b/test/analyze/case_test.rb
@@ -90,5 +90,78 @@ class CaseTest < Minitest::Spec
<% end %>
HTML
end
+
+ test "case with block inside when" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case 1 %>
+ <% when 1 %>
+ <%= content_tag(:p) do %>
+ Yep
+ <% end %>
+ <% end %>
+ HTML
+ end
+
+ test "case with multiple blocks in when" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case status %>
+ <% when :active %>
+ <%= content_tag(:div) do %>
+ Active
+ <% end %>
+ <%= content_tag(:span) do %>
+ Badge
+ <% end %>
+ <% end %>
+ HTML
+ end
+
+ test "case with nested blocks in when" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case level %>
+ <% when 1 %>
+ <%= content_tag(:div) do %>
+ <%= content_tag(:p) do %>
+ Nested
+ <% end %>
+ <% end %>
+ <% end %>
+ HTML
+ end
+
+ test "case with block in multiple when clauses" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case type %>
+ <% when :a %>
+ <%= form_for(obj) do |f| %>
+ Form A
+ <% end %>
+ <% when :b %>
+ <%= form_for(obj) do |f| %>
+ Form B
+ <% end %>
+ <% end %>
+ HTML
+ end
+
+ test "case with if statement inside when" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case status %>
+ <% when :active %>
+ <% if admin? %>
+ Admin view
+ <% end %>
+ <% end %>
+ HTML
+ end
+
+ test "case with yield" do
+ assert_parsed_snapshot(<<~HTML)
+ <% case yield(:a) %>
+ <% when 'a' %>
+ aaa
+ <% end %>
+ HTML
+ end
end
end
diff --git a/test/c/main.c b/test/c/main.c
index c61106da6..ef4238806 100644
--- a/test/c/main.c
+++ b/test/c/main.c
@@ -1,12 +1,13 @@
#include
#include
-TCase *array_tests(void);
-TCase *buffer_tests(void);
+TCase *hb_arena_tests(void);
+TCase *hb_array_tests(void);
+TCase *hb_buffer_tests(void);
+TCase *hb_string_tests(void);
TCase *herb_tests(void);
TCase *html_util_tests(void);
TCase *io_tests(void);
-TCase *json_tests(void);
TCase *lex_tests(void);
TCase *token_tests(void);
TCase *util_tests(void);
@@ -14,12 +15,13 @@ TCase *util_tests(void);
Suite *herb_suite(void) {
Suite *suite = suite_create("Herb Suite");
- suite_add_tcase(suite, array_tests());
- suite_add_tcase(suite, buffer_tests());
+ suite_add_tcase(suite, hb_arena_tests());
+ suite_add_tcase(suite, hb_array_tests());
+ suite_add_tcase(suite, hb_buffer_tests());
+ suite_add_tcase(suite, hb_string_tests());
suite_add_tcase(suite, herb_tests());
suite_add_tcase(suite, html_util_tests());
suite_add_tcase(suite, io_tests());
- suite_add_tcase(suite, json_tests());
suite_add_tcase(suite, lex_tests());
suite_add_tcase(suite, token_tests());
suite_add_tcase(suite, util_tests());
diff --git a/test/c/test_array.c b/test/c/test_array.c
deleted file mode 100644
index 5473d76fc..000000000
--- a/test/c/test_array.c
+++ /dev/null
@@ -1,105 +0,0 @@
-#include "include/test.h"
-#include "../../src/include/array.h"
-
-// Test array initialization
-TEST(test_array_init)
- array_T* array = array_init(10);
-
- ck_assert_ptr_nonnull(array);
- ck_assert_int_eq(array->size, 0);
- ck_assert_int_eq(array->capacity, 10);
- ck_assert_ptr_nonnull(array->items);
-
- array_free(&array);
-END
-
-// Test array appending
-TEST(test_array_append)
- array_T* array = array_init(2);
-
- size_t item1 = 42, item2 = 99, item3 = 100;
- array_append(array, &item1);
- array_append(array, &item2);
-
- ck_assert_int_eq(array->size, 2);
- ck_assert_int_eq(array->capacity, 2);
- ck_assert_ptr_eq(array->items[0], &item1);
- ck_assert_ptr_eq(array->items[1], &item2);
-
- // Trigger reallocation
- array_append(array, &item3);
- ck_assert_int_eq(array->size, 3);
- ck_assert_int_eq(array->capacity, 4);
-
- array_free(&array);
-END
-
-// Test getting elements
-TEST(test_array_get)
- array_T* array = array_init(3);
-
- size_t item1 = 42, item2 = 99;
- array_append(array, &item1);
- array_append(array, &item2);
-
- ck_assert_ptr_eq(array_get(array, 0), &item1);
- ck_assert_ptr_eq(array_get(array, 1), &item2);
- ck_assert_ptr_null(array_get(array, 2)); // Out of bounds check
-
- array_free(&array);
-END
-
-// Test setting elements
-TEST(test_array_set)
- array_T* array = array_init(3);
-
- size_t item1 = 42, item2 = 99;
- array_append(array, &item1);
- array_append(array, &item2);
-
- size_t new_item = 77;
- array_set(array, 1, &new_item);
-
- ck_assert_ptr_eq(array_get(array, 1), &new_item);
-
- array_free(&array);
-END
-
-// Test removing elements
-TEST(test_array_remove)
- array_T* array = array_init(3);
-
- size_t item1 = 42, item2 = 99, item3 = 100;
- array_append(array, &item1);
- array_append(array, &item2);
- array_append(array, &item3);
-
- array_remove(array, 1); // Remove item2
- ck_assert_int_eq(array->size, 2);
- ck_assert_ptr_eq(array_get(array, 0), &item1);
- ck_assert_ptr_eq(array_get(array, 1), &item3); // Shifted left
-
- array_free(&array);
-END
-
-// Test freeing the array
-TEST(test_array_free)
- array_T* array = array_init(5);
- array_free(&array);
-
- ck_assert_ptr_null(array);
-END
-
-// Register test cases
-TCase *array_tests(void) {
- TCase *array = tcase_create("Array");
-
- tcase_add_test(array, test_array_init);
- tcase_add_test(array, test_array_append);
- tcase_add_test(array, test_array_get);
- tcase_add_test(array, test_array_set);
- tcase_add_test(array, test_array_remove);
- tcase_add_test(array, test_array_free);
-
- return array;
-}
diff --git a/test/c/test_buffer.c b/test/c/test_buffer.c
deleted file mode 100644
index 27d7b8609..000000000
--- a/test/c/test_buffer.c
+++ /dev/null
@@ -1,241 +0,0 @@
-#include "include/test.h"
-#include "../../src/include/buffer.h"
-
-// Test buffer initialization
-TEST(test_buffer_init)
- buffer_T buffer;
-
- ck_assert(buffer_init(&buffer));
- ck_assert_int_eq(buffer.capacity, 1024);
- ck_assert_int_eq(buffer.length, 0);
- ck_assert_ptr_nonnull(buffer.value);
- ck_assert_str_eq(buffer.value, "");
-
- buffer_free(&buffer);
-END
-
-// Test appending text to buffer
-TEST(test_buffer_append)
- buffer_T buffer = buffer_new();
-
- ck_assert_str_eq(buffer.value, "");
-
- buffer_append(&buffer, "Hello");
- ck_assert_str_eq(buffer.value, "Hello");
- ck_assert_int_eq(buffer.length, 5);
-
- buffer_append(&buffer, " World");
- ck_assert_str_eq(buffer.value, "Hello World");
- ck_assert_int_eq(buffer.length, 11);
-
- buffer_free(&buffer);
-END
-
-// Test prepending text to buffer
-TEST(test_buffer_prepend)
- buffer_T buffer = buffer_new();
-
- buffer_append(&buffer, "World");
- buffer_prepend(&buffer, "Hello ");
- ck_assert_str_eq(buffer.value, "Hello World");
- ck_assert_int_eq(buffer.length, 11);
-
- buffer_free(&buffer);
-END
-
-// Test concatenating two buffers
-TEST(test_buffer_concat)
- buffer_T buffer1 = buffer_new();
- buffer_T buffer2 = buffer_new();
-
- buffer_append(&buffer1, "Hello");
- buffer_append(&buffer2, " World");
-
- buffer_concat(&buffer1, &buffer2);
- ck_assert_str_eq(buffer1.value, "Hello World");
- ck_assert_int_eq(buffer1.length, 11);
-
- buffer_free(&buffer1);
- buffer_free(&buffer2);
-END
-
-// Test increating
-TEST(test_buffer_increase_capacity)
- buffer_T buffer = buffer_new();
-
- ck_assert_int_eq(buffer.capacity, 1024);
-
- ck_assert(buffer_increase_capacity(&buffer, 1));
- ck_assert_int_eq(buffer.capacity, 1025);
-
- ck_assert(buffer_increase_capacity(&buffer, 1024 + 1));
- ck_assert_int_eq(buffer.capacity, 2050);
-
- buffer_free(&buffer);
-END
-
-// Test expanding capacity
-TEST(test_buffer_expand_capacity)
- buffer_T buffer = buffer_new();
-
- ck_assert_int_eq(buffer.capacity, 1024);
-
- ck_assert(buffer_expand_capacity(&buffer));
- ck_assert_int_eq(buffer.capacity, 2048);
-
- ck_assert(buffer_expand_capacity(&buffer));
- ck_assert_int_eq(buffer.capacity, 4096);
-
- ck_assert(buffer_expand_capacity(&buffer));
- ck_assert_int_eq(buffer.capacity, 8192);
-
- buffer_free(&buffer);
-END
-
-// Test expanding if needed
-TEST(test_buffer_expand_if_needed)
- buffer_T buffer = buffer_new();
-
- ck_assert_int_eq(buffer.capacity, 1024);
-
- ck_assert(buffer_expand_if_needed(&buffer, 1));
- ck_assert_int_eq(buffer.capacity, 1024);
-
- ck_assert(buffer_expand_if_needed(&buffer, 1023));
- ck_assert_int_eq(buffer.capacity, 1024);
-
- ck_assert(buffer_expand_if_needed(&buffer, 1024));
- ck_assert_int_eq(buffer.capacity, 1024);
-
- ck_assert(buffer_expand_if_needed(&buffer, 1025));
- ck_assert_int_eq(buffer.capacity, 3074); // initial capacity (1024) + (required (1025) * 2) = 3074
-
- buffer_free(&buffer);
-END
-
-// Test resizing buffer
-TEST(test_buffer_resize)
- buffer_T buffer = buffer_new();
-
- ck_assert_int_eq(buffer.capacity, 1024);
-
- ck_assert(buffer_resize(&buffer, 2048));
- ck_assert_int_eq(buffer.capacity, 2048);
-
- ck_assert(buffer_resize(&buffer, 4096));
- ck_assert_int_eq(buffer.capacity, 4096);
-
- ck_assert(buffer_resize(&buffer, 8192));
- ck_assert_int_eq(buffer.capacity, 8192);
-
- buffer_free(&buffer);
-END
-
-// Test clearing buffer without freeing memory
-TEST(test_buffer_clear)
- buffer_T buffer = buffer_new();
-
- ck_assert_int_eq(buffer.capacity, 1024);
-
- buffer_append(&buffer, "Hello");
- ck_assert_str_eq(buffer.value, "Hello");
- ck_assert_int_eq(buffer.length, 5);
- ck_assert_int_eq(buffer.capacity, 1024);
-
- buffer_clear(&buffer);
-
- ck_assert_str_eq(buffer.value, "");
- ck_assert_int_eq(buffer.length, 0);
- ck_assert_int_eq(buffer.capacity, 1024); // Capacity should remain unchanged
-
- buffer_free(&buffer);
-END
-
-// Test freeing buffer
-TEST(test_buffer_free)
- buffer_T buffer = buffer_new();
-
- buffer_append(&buffer, "Test");
- ck_assert_int_eq(buffer.length, 4);
- ck_assert_int_eq(buffer.capacity, 1024);
- buffer_free(&buffer);
-
- ck_assert_ptr_null(buffer.value);
- ck_assert_int_eq(buffer.length, 0);
- ck_assert_int_eq(buffer.capacity, 0);
-END
-
-// Test buffer UTF-8 integrity
-TEST(test_buffer_utf8_integrity)
- buffer_T buffer = buffer_new();
-
- // UTF-8 String
- const char *utf8_text = "こんにちは";
- buffer_append(&buffer, utf8_text);
-
- // Ensure length matches actual UTF-8 bytes
- ck_assert_int_eq(buffer.length, strlen(utf8_text));
- ck_assert_int_eq(buffer.length, 15);
- ck_assert_str_eq(buffer.value, utf8_text);
-
- buffer_free(&buffer);
-END
-
-// Test: Buffer Appending UTF-8 Characters
-TEST(test_buffer_append_utf8)
- buffer_T buffer = buffer_new();
-
- // Append UTF-8 string
- buffer_append(&buffer, "こんにちは"); // "Hello" in Japanese
- ck_assert_int_eq(strlen("こんにちは"), 15); // UTF-8 multibyte characters
- ck_assert_int_eq(buffer_length(&buffer), 15);
- ck_assert_str_eq(buffer_value(&buffer), "こんにちは");
-
- buffer_free(&buffer);
-END
-
-// Test buffer length correctness
-TEST(test_buffer_length_correctness)
- buffer_T buffer = buffer_new();
-
- buffer_append(&buffer, "Short");
- size_t length = buffer_length(&buffer);
- ck_assert_int_eq(length, 5);
-
- buffer_append(&buffer, "er test");
- length = buffer_length(&buffer);
- ck_assert_int_eq(length, 12);
-
- buffer_free(&buffer);
-END
-
-// Test: Buffer Null-Termination
-TEST(test_buffer_null_termination)
- buffer_T buffer = buffer_new();
-
- buffer_append(&buffer, "Test");
- ck_assert(buffer_value(&buffer)[buffer_length(&buffer)] == '\0'); // Ensure null termination
-
- buffer_free(&buffer);
-END
-
-TCase *buffer_tests(void) {
- TCase *buffer = tcase_create("Buffer");
-
- tcase_add_test(buffer, test_buffer_init);
- tcase_add_test(buffer, test_buffer_append);
- tcase_add_test(buffer, test_buffer_prepend);
- tcase_add_test(buffer, test_buffer_concat);
- tcase_add_test(buffer, test_buffer_increase_capacity);
- tcase_add_test(buffer, test_buffer_expand_capacity);
- tcase_add_test(buffer, test_buffer_expand_if_needed);
- tcase_add_test(buffer, test_buffer_resize);
- tcase_add_test(buffer, test_buffer_clear);
- tcase_add_test(buffer, test_buffer_free);
- tcase_add_test(buffer, test_buffer_utf8_integrity);
- tcase_add_test(buffer, test_buffer_append_utf8);
- tcase_add_test(buffer, test_buffer_length_correctness);
- tcase_add_test(buffer, test_buffer_null_termination);
-
- return buffer;
-}
diff --git a/test/c/test_hb_arena.c b/test/c/test_hb_arena.c
new file mode 100644
index 000000000..4196dfe27
--- /dev/null
+++ b/test/c/test_hb_arena.c
@@ -0,0 +1,57 @@
+#include "include/test.h"
+#include "../../src/include/util/hb_arena.h"
+
+// Test appending text to buffer
+TEST(test_arena_alloc)
+ hb_arena_T allocator;
+
+ hb_arena_init(&allocator, 1024);
+ ck_assert_int_eq(allocator.position, 0);
+ ck_assert_int_eq(allocator.capacity, 1024);
+
+ {
+ // Ensure memory is writable
+ char *memory = hb_arena_alloc(&allocator, 1);
+ *memory = 'a';
+ }
+
+ ck_assert(allocator.position % 8 == 0);
+ ck_assert_int_eq(allocator.position, 8);
+
+ char *memory = hb_arena_alloc(&allocator, allocator.capacity - allocator.position);
+ ck_assert_ptr_nonnull(memory);
+ ck_assert(allocator.position % 8 == 0);
+END
+
+TEST(test_arena_free)
+ hb_arena_T allocator;
+
+ {
+ hb_arena_init(&allocator, 1024);
+ hb_arena_free(&allocator);
+
+ ck_assert_ptr_null(allocator.memory);
+ ck_assert_int_eq(allocator.capacity, 0);
+ ck_assert_int_eq(allocator.position, 0);
+ }
+
+ {
+ hb_arena_init(&allocator, 1024);
+ hb_arena_free(&allocator);
+ hb_arena_free(&allocator);
+
+ ck_assert_ptr_null(allocator.memory);
+ ck_assert_int_eq(allocator.capacity, 0);
+ ck_assert_int_eq(allocator.position, 0);
+ }
+END
+
+
+TCase *hb_arena_tests(void) {
+ TCase *tags = tcase_create("arena");
+
+ tcase_add_test(tags, test_arena_alloc);
+ tcase_add_test(tags, test_arena_free);
+
+ return tags;
+}
diff --git a/test/c/test_hb_array.c b/test/c/test_hb_array.c
new file mode 100644
index 000000000..1abe357cc
--- /dev/null
+++ b/test/c/test_hb_array.c
@@ -0,0 +1,105 @@
+#include "include/test.h"
+#include "../../src/include/util/hb_array.h"
+
+// Test array initialization
+TEST(test_hb_array_init)
+ hb_array_T* array = hb_array_init(10);
+
+ ck_assert_ptr_nonnull(array);
+ ck_assert_int_eq(array->size, 0);
+ ck_assert_int_eq(array->capacity, 10);
+ ck_assert_ptr_nonnull(array->items);
+
+ hb_array_free(&array);
+END
+
+// Test array appending
+TEST(test_hb_array_append)
+ hb_array_T* array = hb_array_init(2);
+
+ size_t item1 = 42, item2 = 99, item3 = 100;
+ hb_array_append(array, &item1);
+ hb_array_append(array, &item2);
+
+ ck_assert_int_eq(array->size, 2);
+ ck_assert_int_eq(array->capacity, 2);
+ ck_assert_ptr_eq(array->items[0], &item1);
+ ck_assert_ptr_eq(array->items[1], &item2);
+
+ // Trigger reallocation
+ hb_array_append(array, &item3);
+ ck_assert_int_eq(array->size, 3);
+ ck_assert_int_eq(array->capacity, 4);
+
+ hb_array_free(&array);
+END
+
+// Test getting elements
+TEST(test_hb_array_get)
+ hb_array_T* array = hb_array_init(3);
+
+ size_t item1 = 42, item2 = 99;
+ hb_array_append(array, &item1);
+ hb_array_append(array, &item2);
+
+ ck_assert_ptr_eq(hb_array_get(array, 0), &item1);
+ ck_assert_ptr_eq(hb_array_get(array, 1), &item2);
+ ck_assert_ptr_null(hb_array_get(array, 2)); // Out of bounds check
+
+ hb_array_free(&array);
+END
+
+// Test setting elements
+TEST(test_hb_array_set)
+ hb_array_T* array = hb_array_init(3);
+
+ size_t item1 = 42, item2 = 99;
+ hb_array_append(array, &item1);
+ hb_array_append(array, &item2);
+
+ size_t new_item = 77;
+ hb_array_set(array, 1, &new_item);
+
+ ck_assert_ptr_eq(hb_array_get(array, 1), &new_item);
+
+ hb_array_free(&array);
+END
+
+// Test removing elements
+TEST(test_hb_array_remove)
+ hb_array_T* array = hb_array_init(3);
+
+ size_t item1 = 42, item2 = 99, item3 = 100;
+ hb_array_append(array, &item1);
+ hb_array_append(array, &item2);
+ hb_array_append(array, &item3);
+
+ hb_array_remove(array, 1); // Remove item2
+ ck_assert_int_eq(array->size, 2);
+ ck_assert_ptr_eq(hb_array_get(array, 0), &item1);
+ ck_assert_ptr_eq(hb_array_get(array, 1), &item3); // Shifted left
+
+ hb_array_free(&array);
+END
+
+// Test freeing the array
+TEST(test_hb_array_free)
+ hb_array_T* array = hb_array_init(5);
+ hb_array_free(&array);
+
+ ck_assert_ptr_null(array);
+END
+
+// Register test cases
+TCase *hb_array_tests(void) {
+ TCase *array = tcase_create("Herb Array");
+
+ tcase_add_test(array, test_hb_array_init);
+ tcase_add_test(array, test_hb_array_append);
+ tcase_add_test(array, test_hb_array_get);
+ tcase_add_test(array, test_hb_array_set);
+ tcase_add_test(array, test_hb_array_remove);
+ tcase_add_test(array, test_hb_array_free);
+
+ return array;
+}
diff --git a/test/c/test_hb_buffer.c b/test/c/test_hb_buffer.c
new file mode 100644
index 000000000..a5f3f169d
--- /dev/null
+++ b/test/c/test_hb_buffer.c
@@ -0,0 +1,211 @@
+#include "include/test.h"
+#include "../../src/include/util/hb_buffer.h"
+
+// Test buffer initialization
+TEST(test_hb_buffer_init)
+ hb_buffer_T buffer;
+
+ ck_assert(hb_buffer_init(&buffer, 1024));
+ ck_assert_int_eq(buffer.capacity, 1024);
+ ck_assert_int_eq(buffer.length, 0);
+ ck_assert_ptr_nonnull(buffer.value);
+ ck_assert_str_eq(buffer.value, "");
+
+ free(buffer.value);
+END
+
+// Test appending text to buffer
+TEST(test_hb_buffer_append)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ ck_assert_str_eq(buffer.value, "");
+
+ hb_buffer_append(&buffer, "Hello");
+ ck_assert_str_eq(buffer.value, "Hello");
+ ck_assert_int_eq(buffer.length, 5);
+
+ hb_buffer_append(&buffer, " World");
+ ck_assert_str_eq(buffer.value, "Hello World");
+ ck_assert_int_eq(buffer.length, 11);
+
+ free(buffer.value);
+END
+
+// Test prepending text to buffer
+TEST(test_hb_buffer_prepend)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ hb_buffer_append(&buffer, "World");
+ hb_buffer_prepend(&buffer, "Hello ");
+ ck_assert_str_eq(buffer.value, "Hello World");
+ ck_assert_int_eq(buffer.length, 11);
+
+ free(buffer.value);
+END
+
+// Test concatenating two buffers
+TEST(test_hb_buffer_concat)
+ hb_buffer_T buffer1;
+ hb_buffer_init(&buffer1, 1024);
+ hb_buffer_T buffer2;
+ hb_buffer_init(&buffer2, 1024);
+
+ hb_buffer_append(&buffer1, "Hello");
+ hb_buffer_append(&buffer2, " World");
+
+ hb_buffer_concat(&buffer1, &buffer2);
+ ck_assert_str_eq(buffer1.value, "Hello World");
+ ck_assert_int_eq(buffer1.length, 11);
+
+ free(buffer1.value);
+ free(buffer2.value);
+END
+
+// Test appending a string to the buffer
+TEST(test_hb_buffer_append_string)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1);
+
+ hb_buffer_append_string(&buffer, hb_string("Hello, world"));
+
+ ck_assert_str_eq(buffer.value, "Hello, world");
+ ck_assert_int_eq(buffer.length, 12);
+
+ free(buffer.value);
+END
+
+TEST(test_hb_buffer_resizing_behavior)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ ck_assert_int_eq(buffer.capacity, 1024);
+
+ hb_buffer_append_whitespace(&buffer, 1023);
+ ck_assert_int_eq(buffer.capacity, 1024);
+
+ hb_buffer_append_whitespace(&buffer, 2);
+ ck_assert_int_eq(buffer.capacity, 2048);
+
+ hb_buffer_append_whitespace(&buffer, 2048);
+ ck_assert_int_eq(buffer.capacity, 6144);
+
+ free(buffer.value);
+END
+
+// Test clearing buffer without freeing memory
+TEST(test_hb_buffer_clear)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ ck_assert_int_eq(buffer.capacity, 1024);
+
+ hb_buffer_append(&buffer, "Hello");
+ ck_assert_str_eq(buffer.value, "Hello");
+ ck_assert_int_eq(buffer.length, 5);
+ ck_assert_int_eq(buffer.capacity, 1024);
+
+ hb_buffer_clear(&buffer);
+
+ ck_assert_str_eq(buffer.value, "");
+ ck_assert_int_eq(buffer.length, 0);
+ ck_assert_int_eq(buffer.capacity, 1024); // Capacity should remain unchanged
+
+ free(buffer.value);
+END
+
+// Test freeing buffer
+TEST(test_hb_buffer_free)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ hb_buffer_append(&buffer, "Test");
+ ck_assert_int_eq(buffer.length, 4);
+ ck_assert_int_eq(buffer.capacity, 1024);
+ free(buffer.value);
+ buffer.value = NULL;
+ buffer.length = 0;
+ buffer.capacity = 0;
+
+ ck_assert_ptr_null(buffer.value);
+ ck_assert_int_eq(buffer.length, 0);
+ ck_assert_int_eq(buffer.capacity, 0);
+END
+
+// Test buffer UTF-8 integrity
+TEST(test_buffer_utf8_integrity)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ // UTF-8 String
+ const char *utf8_text = "こんにちは";
+ hb_buffer_append(&buffer, utf8_text);
+
+ // Ensure length matches actual UTF-8 bytes
+ ck_assert_int_eq(buffer.length, strlen(utf8_text));
+ ck_assert_int_eq(buffer.length, 15);
+ ck_assert_str_eq(buffer.value, utf8_text);
+
+ free(buffer.value);
+END
+
+// Test: Buffer Appending UTF-8 Characters
+TEST(test_hb_buffer_append_utf8)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ // Append UTF-8 string
+ hb_buffer_append(&buffer, "こんにちは"); // "Hello" in Japanese
+ ck_assert_int_eq(strlen("こんにちは"), 15); // UTF-8 multibyte characters
+ ck_assert_int_eq(hb_buffer_length(&buffer), 15);
+ ck_assert_str_eq(hb_buffer_value(&buffer), "こんにちは");
+
+ free(buffer.value);
+END
+
+// Test buffer length correctness
+TEST(test_hb_buffer_length_correctness)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ hb_buffer_append(&buffer, "Short");
+ size_t length = hb_buffer_length(&buffer);
+ ck_assert_int_eq(length, 5);
+
+ hb_buffer_append(&buffer, "er test");
+ length = hb_buffer_length(&buffer);
+ ck_assert_int_eq(length, 12);
+
+ free(buffer.value);
+END
+
+// Test: Buffer Null-Termination
+TEST(test_buffer_null_termination)
+ hb_buffer_T buffer;
+ hb_buffer_init(&buffer, 1024);
+
+ hb_buffer_append(&buffer, "Test");
+ ck_assert(hb_buffer_value(&buffer)[hb_buffer_length(&buffer)] == '\0'); // Ensure null termination
+
+ free(buffer.value);
+END
+
+TCase *hb_buffer_tests(void) {
+ TCase *buffer = tcase_create("Herb Buffer");
+
+ tcase_add_test(buffer, test_hb_buffer_init);
+ tcase_add_test(buffer, test_hb_buffer_append);
+ tcase_add_test(buffer, test_hb_buffer_prepend);
+ tcase_add_test(buffer, test_hb_buffer_concat);
+ tcase_add_test(buffer, test_hb_buffer_append_string);
+ tcase_add_test(buffer, test_hb_buffer_resizing_behavior);
+ tcase_add_test(buffer, test_hb_buffer_clear);
+ tcase_add_test(buffer, test_hb_buffer_free);
+ tcase_add_test(buffer, test_buffer_utf8_integrity);
+ tcase_add_test(buffer, test_hb_buffer_append_utf8);
+ tcase_add_test(buffer, test_hb_buffer_length_correctness);
+ tcase_add_test(buffer, test_buffer_null_termination);
+
+ return buffer;
+}
diff --git a/test/c/test_hb_string.c b/test/c/test_hb_string.c
new file mode 100644
index 000000000..70ef03270
--- /dev/null
+++ b/test/c/test_hb_string.c
@@ -0,0 +1,169 @@
+#include "include/test.h"
+#include "../../src/include/util/hb_string.h"
+#include
+
+TEST(hb_string_equals_tests)
+ {
+ hb_string_T a = hb_string("Hello, world.");
+ hb_string_T b = hb_string("Hello, world.");
+
+ ck_assert(hb_string_equals(a, b));
+ }
+
+ {
+ hb_string_T a = hb_string("Hello, world.");
+ hb_string_T b = hb_string("Hello, world. Longer text");
+
+ ck_assert(!hb_string_equals(a, b));
+ }
+
+ {
+ hb_string_T a = hb_string("Hello, world.");
+ hb_string_T b = hb_string("");
+
+ ck_assert(!hb_string_equals(a, b));
+ }
+END
+
+TEST(hb_string_offset_based_slice_tests)
+ {
+ hb_string_T source = hb_string("01234");
+ hb_string_T expected_slice = hb_string("234");
+
+ hb_string_T slice = hb_string_slice(source, 2);
+
+ ck_assert(hb_string_equals(slice, expected_slice));
+ }
+
+ {
+ hb_string_T source = hb_string("01234");
+ hb_string_T expected_slice = hb_string("4");
+
+ hb_string_T slice = hb_string_slice(source, 4);
+
+ ck_assert(hb_string_equals(slice, expected_slice));
+ }
+
+ {
+ hb_string_T source = hb_string("01234");
+ hb_string_T slice = hb_string_slice(source, 5);
+
+ ck_assert(hb_string_is_empty(slice));
+ }
+
+ {
+ hb_string_T source = hb_string("01234");
+ hb_string_T slice = hb_string_slice(source, 6);
+
+ ck_assert(hb_string_is_empty(slice));
+ }
+END
+
+
+TEST(hb_string_equals_case_insensitive_tests)
+ {
+ hb_string_T a = hb_string("Hello, world.");
+ hb_string_T b = hb_string("Hello, World. Really?");
+
+ ck_assert(!hb_string_equals_case_insensitive(a, b));
+ }
+
+ {
+ hb_string_T a = hb_string("Hello, world.");
+ hb_string_T b = hb_string("Hello, World.");
+
+ ck_assert(hb_string_equals_case_insensitive(a, b));
+ }
+
+ {
+ hb_string_T a = hb_string("This.");
+ hb_string_T b = hb_string("That.");
+
+ ck_assert(!hb_string_equals_case_insensitive(a, b));
+ }
+END
+
+TEST(hb_string_is_empty_tests)
+ {
+ hb_string_T str = {
+ .length = 0,
+ .data = NULL
+ };
+
+ ck_assert(hb_string_is_empty(str));
+ }
+
+ {
+ hb_string_T str = hb_string("");
+
+ ck_assert(hb_string_is_empty(str));
+ }
+
+ {
+ hb_string_T str = hb_string("Content");
+
+ ck_assert(!hb_string_is_empty(str));
+ }
+END
+
+TEST(hb_string_starts_with_tests)
+ {
+ hb_string_T str = hb_string("This.");
+ hb_string_T prefix = {
+ .length = 0,
+ .data = NULL,
+ };
+
+ ck_assert(!hb_string_starts_with(str, prefix));
+ }
+
+ {
+ hb_string_T str = {
+ .length = 0,
+ .data = NULL,
+ };
+ hb_string_T prefix = hb_string("This.");
+
+ ck_assert(!hb_string_starts_with(str, prefix));
+ }
+
+ {
+ hb_string_T str = hb_string("Long text.");
+ hb_string_T prefix = hb_string("Long text.");
+
+ ck_assert(hb_string_starts_with(str, prefix));
+ }
+
+ {
+ hb_string_T str = hb_string("Long text.");
+ hb_string_T prefix = hb_string("Long");
+
+ ck_assert(hb_string_starts_with(str, prefix));
+ }
+
+ {
+ hb_string_T str = hb_string("Long text.");
+ hb_string_T prefix = hb_string("No");
+
+ ck_assert(!hb_string_starts_with(str, prefix));
+ }
+
+ {
+ hb_string_T str = hb_string("Long text.");
+ hb_string_T prefix = hb_string("This prefix is longer than the text");
+
+ ck_assert(!hb_string_starts_with(str, prefix));
+ }
+END
+
+TCase *hb_string_tests(void) {
+ TCase *tags = tcase_create("Herb String");
+
+ tcase_add_test(tags, hb_string_equals_tests);
+ tcase_add_test(tags, hb_string_offset_based_slice_tests);
+ tcase_add_test(tags, hb_string_equals_case_insensitive_tests);
+ tcase_add_test(tags, hb_string_is_empty_tests);
+ tcase_add_test(tags, hb_string_starts_with_tests);
+
+ return tags;
+}
diff --git a/test/c/test_herb.c b/test/c/test_herb.c
index 7eab745f0..bc0c1272c 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.4");
+ ck_assert_str_eq(herb_version(), "0.7.5");
END
TCase *herb_tests(void) {
diff --git a/test/c/test_html_util.c b/test/c/test_html_util.c
index 4f9d50403..dcd592bd2 100644
--- a/test/c/test_html_util.c
+++ b/test/c/test_html_util.c
@@ -1,35 +1,30 @@
-#include
-#include "include/test.h"
#include "../../src/include/herb.h"
#include "../../src/include/html_util.h"
-
-TEST(html_util_html_opening_tag_string)
- ck_assert_str_eq(html_opening_tag_string(NULL), "<>");
- ck_assert_str_eq(html_opening_tag_string(""), "<>");
- ck_assert_str_eq(html_opening_tag_string(" "), "< >");
- ck_assert_str_eq(html_opening_tag_string("br"), " ");
- ck_assert_str_eq(html_opening_tag_string("div"), "");
- ck_assert_str_eq(html_opening_tag_string("somelongerstring"), "");
-END
+#include "include/test.h"
+#include
TEST(html_util_html_closing_tag_string)
- ck_assert_str_eq(html_closing_tag_string(NULL), ">");
- ck_assert_str_eq(html_closing_tag_string(""), ">");
- ck_assert_str_eq(html_closing_tag_string(" "), " >");
- ck_assert_str_eq(html_closing_tag_string("div"), "
");
- ck_assert_str_eq(html_closing_tag_string("somelongerstring"), "");
+ ck_assert(hb_string_equals(html_closing_tag_string((hb_string_T) { .data = NULL, .length = 0 }), hb_string(">")));
+ ck_assert(hb_string_equals(html_closing_tag_string(hb_string("")), hb_string(">")));
+ ck_assert(hb_string_equals(html_closing_tag_string(hb_string(" ")), hb_string(" >")));
+ ck_assert(hb_string_equals(html_closing_tag_string(hb_string("div")), hb_string(" ")));
+
+ ck_assert(hb_string_equals(
+ html_closing_tag_string(hb_string("somelongerstring")),
+ hb_string("")
+ ));
END
TEST(html_util_html_self_closing_tag_string)
- ck_assert_str_eq(html_self_closing_tag_string(NULL), "< />");
- ck_assert_str_eq(html_self_closing_tag_string(""), "< />");
- ck_assert_str_eq(html_self_closing_tag_string(" "), "< />");
- ck_assert_str_eq(html_self_closing_tag_string("br"), " ");
- ck_assert_str_eq(html_self_closing_tag_string("somelongerstring"), " ");
+ ck_assert(hb_string_equals(html_self_closing_tag_string((hb_string_T) { .data = NULL, .length = 0 }), hb_string("< />")));
+ ck_assert(hb_string_equals(html_self_closing_tag_string(hb_string("")), hb_string("< />")));
+ ck_assert(hb_string_equals(html_self_closing_tag_string(hb_string(" ")), hb_string("< />")));
+ ck_assert(hb_string_equals(html_self_closing_tag_string(hb_string("br")), hb_string(" ")));
+ ck_assert(hb_string_equals(html_self_closing_tag_string(hb_string("somelongerstring")), hb_string(" ")));
END
-TCase *html_util_tests(void) {
- TCase *html_util = tcase_create("HTML Util");
+TCase* html_util_tests(void) {
+ TCase* html_util = tcase_create("HTML Util");
tcase_add_test(html_util, html_util_html_closing_tag_string);
tcase_add_test(html_util, html_util_html_closing_tag_string);
diff --git a/test/c/test_json.c b/test/c/test_json.c
deleted file mode 100644
index 8dec4b990..000000000
--- a/test/c/test_json.c
+++ /dev/null
@@ -1,306 +0,0 @@
-#include
-
-#include "include/test.h"
-#include "../../src/include/buffer.h"
-#include "../../src/include/json.h"
-
-TEST(test_json_escape_basic)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
- json_add_string(&json, "key", "value");
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"key\": \"value\"}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_escape_quotes)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
- json_add_string(&json, "quote", "This is a \"quoted\" string");
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"quote\": \"This is a \\\"quoted\\\" string\"}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_escape_backslash)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
- json_add_string(&json, "path", "C:\\Users\\Test");
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"path\": \"C:\\\\Users\\\\Test\"}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_escape_newline)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
- json_add_string(&json, "text", "Line1\nLine2");
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"text\": \"Line1\\nLine2\"}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_escape_tab)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
- json_add_string(&json, "text", "Column1\tColumn2");
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"text\": \"Column1\\tColumn2\"}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_escape_mixed)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
- json_add_string(&json, "complex", "A \"quoted\" \\ path\nwith\ttabs.");
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"complex\": \"A \\\"quoted\\\" \\\\ path\\nwith\\ttabs.\"}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_root_object)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
-
- json_add_string(&json, "name", "John");
- json_add_int(&json, "age", 20);
- json_add_double(&json, "score", 99.5);
- json_add_bool(&json, "active", 1);
-
- buffer_T address = buffer_new();
- json_start_object(&json, "address");
- json_add_string(&address, "city", "Basel");
- json_add_string(&address, "country", "Switzerland");
- buffer_concat(&json, &address);
- json_end_object(&json);
-
- buffer_T languages = buffer_new();
- json_start_array(&json, "languages");
- json_add_string(&languages, NULL, "Ruby");
- json_add_string(&languages, NULL, "C");
- json_add_string(&languages, NULL, "JavaScript");
- buffer_concat(&json, &languages);
- json_end_array(&json);
-
- buffer_T ratings = buffer_new();
- json_start_array(&json, "ratings");
- json_add_double(&ratings, NULL, 4.5);
- json_add_int(&ratings, NULL, 3);
- json_add_double(&ratings, NULL, 5.0);
- json_add_double(&ratings, NULL, 3.8);
- json_add_int(&ratings, NULL, 5);
- buffer_concat(&json, &ratings);
- json_end_array(&json);
-
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"name\": \"John\", \"age\": 20, \"score\": 99.50, \"active\": true, \"address\": {\"city\": \"Basel\", \"country\": \"Switzerland\"}, \"languages\": [\"Ruby\", \"C\", \"JavaScript\"], \"ratings\": [4.50, 3, 5.0, 3.79, 5]}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_root_array)
- buffer_T json = buffer_new();
-
- json_start_root_array(&json);
-
- json_add_string(&json, NULL, "Ruby");
- json_add_string(&json, NULL, "C");
- json_add_string(&json, NULL, "JavaScript");
- json_add_int(&json, NULL, 42);
- json_add_double(&json, NULL, 3.14159);
- json_add_bool(&json, NULL, 1);
- json_add_bool(&json, NULL, 0);
-
- json_end_array(&json);
-
- ck_assert_str_eq(buffer_value(&json), "[\"Ruby\", \"C\", \"JavaScript\", 42, 3.14, true, false]");
-
- buffer_free(&json);
-END
-
-TEST(test_json_append_array_to_object)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
-
- buffer_T object = buffer_new();
- json_start_object(&json, "object");
- json_add_string(&object, "key", "value");
-
- buffer_T array = buffer_new();
- json_start_array(&object, "array");
- json_add_string(&array, NULL, "One");
- json_add_string(&array, NULL, "Two");
-
- buffer_concat(&object, &array);
- json_end_array(&object);
-
- buffer_concat(&json, &object);
- json_end_object(&json);
-
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"object\": {\"key\": \"value\", \"array\": [\"One\", \"Two\"]}}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_append_object_array)
- buffer_T json = buffer_new();
-
- json_start_root_object(&json);
-
- buffer_T array = buffer_new();
- json_start_array(&json, "array");
- json_add_string(&array, NULL, "One");
- json_add_string(&array, NULL, "Two");
-
- buffer_T object = buffer_new();
- json_start_object(&array, NULL);
- json_add_string(&object, "key", "value");
-
- buffer_concat(&array, &object);
- json_end_object(&array);
-
- buffer_concat(&json, &array);
- json_end_array(&json);
-
- json_end_object(&json);
-
- ck_assert_str_eq(buffer_value(&json), "{\"array\": [\"One\", \"Two\", {\"key\": \"value\"}]}");
-
- buffer_free(&json);
-END
-
-TEST(test_json_double_to_string_precision)
- char buffer[64];
-
- json_double_to_string(1.234567890123456, buffer);
- ck_assert_str_eq(buffer, "1.23");
-
- json_double_to_string(123456.7890123456789, buffer);
- ck_assert_str_eq(buffer, "123456.78");
-
- json_double_to_string(0.000000000000001, buffer);
- ck_assert_str_eq(buffer, "0.0");
-
- json_double_to_string(-42.987654321098765, buffer);
- ck_assert_str_eq(buffer, "-42.98");
-
- json_double_to_string(3.141592653589793, buffer);
- ck_assert_str_eq(buffer, "3.14");
-END
-
-TEST(test_json_int_to_string_positive)
- char buffer[20];
-
- json_int_to_string(12345, buffer);
- ck_assert_str_eq(buffer, "12345");
-
- json_int_to_string(987654321, buffer);
- ck_assert_str_eq(buffer, "987654321");
-
- json_int_to_string(0, buffer);
- ck_assert_str_eq(buffer, "0");
-END
-
-TEST(test_json_int_to_string_negative)
- char buffer[20];
-
- json_int_to_string(-1, buffer);
- ck_assert_str_eq(buffer, "-1");
-
- json_int_to_string(-42, buffer);
- ck_assert_str_eq(buffer, "-42");
-
- json_int_to_string(-987654321, buffer);
- ck_assert_str_eq(buffer, "-987654321");
-END
-
-TEST(test_json_int_to_string_min_max)
- char buffer[20];
-
- json_int_to_string(2147483647, buffer);
- ck_assert_str_eq(buffer, "2147483647");
-
- json_int_to_string(-2147483648, buffer);
- ck_assert_str_eq(buffer, "-2147483648");
-END
-
-TEST(test_json_add_size_t_basic)
- buffer_T json = buffer_new();
-
- json_add_size_t(&json, "size", 42);
- ck_assert_str_eq(buffer_value(&json), "\"size\": 42");
-
- buffer_free(&json);
-END
-
-TEST(test_json_add_size_t_large_number)
- buffer_T json = buffer_new();
-
- json_add_size_t(&json, "size", 9876543210UL);
- ck_assert_str_eq(buffer_value(&json), "\"size\": 9876543210");
-
- buffer_clear(&json);
- json_add_size_t(&json, "size", SIZE_MAX);
- ck_assert_str_eq(buffer_value(&json), "\"size\": 18446744073709551615");
-
- buffer_free(&json);
-END
-
-TEST(test_json_add_size_t_in_array)
- buffer_T json = buffer_new();
-
- json_add_size_t(&json, NULL, 1024);
- json_add_size_t(&json, NULL, 2048);
- json_add_size_t(&json, NULL, 4096);
-
- ck_assert_str_eq(buffer_value(&json), "1024, 2048, 4096");
-
- buffer_free(&json);
-END
-
-
-TCase* json_tests(void) {
- TCase* json = tcase_create("JSON");
-
- tcase_add_test(json, test_json_escape_basic);
- tcase_add_test(json, test_json_escape_quotes);
- tcase_add_test(json, test_json_escape_backslash);
- tcase_add_test(json, test_json_escape_tab);
- tcase_add_test(json, test_json_escape_mixed);
- tcase_add_test(json, test_json_root_object);
- tcase_add_test(json, test_json_root_array);
- tcase_add_test(json, test_json_append_array_to_object);
- tcase_add_test(json, test_json_append_object_array);
- tcase_add_test(json, test_json_double_to_string_precision);
- tcase_add_test(json, test_json_int_to_string_positive);
- tcase_add_test(json, test_json_int_to_string_negative);
- tcase_add_test(json, test_json_int_to_string_min_max);
- tcase_add_test(json, test_json_add_size_t_basic);
- tcase_add_test(json, test_json_add_size_t_large_number);
- tcase_add_test(json, test_json_add_size_t_in_array);
-
- return json;
-}
diff --git a/test/c/test_lex.c b/test/c/test_lex.c
index d0ed240a0..568615c94 100644
--- a/test/c/test_lex.c
+++ b/test/c/test_lex.c
@@ -3,18 +3,20 @@
TEST(herb_lex_to_buffer_empty_file)
char* html = "";
- buffer_T output = buffer_new();
+ hb_buffer_T output;
+ hb_buffer_init(&output, 1024);
herb_lex_to_buffer(html, &output);
ck_assert_str_eq(output.value, "#\" range=[0, 0] start=(1:0) end=(1:0)>\n");
- buffer_free(&output);
+ free(output.value);
END
TEST(herb_lex_to_buffer_basic_tag)
char* html = "";
- buffer_T output = buffer_new();
+ hb_buffer_T output;
+ hb_buffer_init(&output, 1024);
herb_lex_to_buffer(html, &output);
@@ -29,7 +31,7 @@ TEST(herb_lex_to_buffer_basic_tag)
"#\" range=[13, 13] start=(1:13) end=(1:13)>\n"
);
- buffer_free(&output);
+ free(output.value);
END
TCase *lex_tests(void) {
diff --git a/test/c/test_token.c b/test/c/test_token.c
index a0d06daa2..69335ad9c 100644
--- a/test/c/test_token.c
+++ b/test/c/test_token.c
@@ -8,7 +8,8 @@ TEST(test_token)
END
TEST(test_token_to_string)
- buffer_T output = buffer_new();
+ hb_buffer_T output;
+ hb_buffer_init(&output, 1024);
herb_lex_to_buffer("hello", &output);
ck_assert_str_eq(
@@ -17,21 +18,7 @@ TEST(test_token_to_string)
"#\" range=[5, 5] start=(1:5) end=(1:5)>\n"
);
- buffer_free(&output);
-END
-
-TEST(test_token_to_json)
- buffer_T output = buffer_new();
- herb_lex_json_to_buffer("hello", &output);
-
- const char* expected = "["
- "{\"type\": \"TOKEN_IDENTIFIER\", \"value\": \"hello\", \"range\": [0 , 5], \"start\": {\"line\": 1, \"column\": 0}, \"end\": {\"line\": 1, \"column\": 5}}, "
- "{\"type\": \"TOKEN_EOF\", \"value\": \"\", \"range\": [5 , 5], \"start\": {\"line\": 1, \"column\": 5}, \"end\": {\"line\": 1, \"column\": 5}}"
- "]";
-
- ck_assert_str_eq(output.value, expected);
-
- buffer_free(&output);
+ free(output.value);
END
TCase *token_tests(void) {
@@ -39,7 +26,6 @@ TCase *token_tests(void) {
tcase_add_test(token, test_token);
tcase_add_test(token, test_token_to_string);
- tcase_add_test(token, test_token_to_json);
return token;
}
diff --git a/test/c/test_util.c b/test/c/test_util.c
index dd678f3c5..ca249c8dc 100644
--- a/test/c/test_util.c
+++ b/test/c/test_util.c
@@ -3,16 +3,6 @@
#include "../../src/include/herb.h"
#include "../../src/include/util.h"
-TEST(util_count_newlines)
- ck_assert_int_eq(count_newlines("\n"), 1);
- ck_assert_int_eq(count_newlines("\n\n"), 2);
-
- ck_assert_int_eq(count_newlines("\r"), 1);
- ck_assert_int_eq(count_newlines("\r\r"), 2);
-
- ck_assert_int_eq(count_newlines("\r\n"), 1);
- ck_assert_int_eq(count_newlines("\r\n\r\n"), 2);
-END
TEST(util_is_newline)
ck_assert_int_eq(is_newline('\n'), 1);
@@ -22,20 +12,10 @@ TEST(util_is_newline)
ck_assert_int_eq(is_newline('a'), 0);
END
-TEST(util_replace_char_returns_original)
- char str[] = "abca";
- char *returned = replace_char(str, 'a', 'x');
-
- ck_assert_ptr_eq(returned, str);
- ck_assert_str_eq(str, "xbcx");
-END
-
TCase *util_tests(void) {
TCase *util = tcase_create("Util");
- tcase_add_test(util, util_count_newlines);
tcase_add_test(util, util_is_newline);
- tcase_add_test(util, util_replace_char_returns_original);
return util;
}
diff --git a/test/engine/block_comments_test.rb b/test/engine/block_comments_test.rb
new file mode 100644
index 000000000..b179f7854
--- /dev/null
+++ b/test/engine/block_comments_test.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+require_relative "../snapshot_utils"
+require_relative "../../lib/herb/engine"
+
+module Engine
+ class BlockCommentsTest < Minitest::Spec
+ include SnapshotUtils
+
+ test "ruby block comments with =begin and =end multiline" do
+ template = <<~ERB
+ <%
+ =begin %>
+ This, while unusual, is a legal form of commenting.
+ <%
+ =end %>
+ Hey there
+ ERB
+
+ assert_compiled_snapshot(template)
+ end
+
+ test "ruby block comments inside erb tags" do
+ template = <<~ERB
+ <%
+ =begin
+ This is a comment
+ =end
+ %>
+ Content
+ ERB
+
+ assert_compiled_snapshot(template)
+ end
+
+ test "ruby block comments with code before and after" do
+ template = <<~ERB
+ <% x = 1 %>
+ <%
+ =begin
+ Multi-line comment
+ spanning multiple lines
+ =end
+ %>
+ <% y = 2 %>
+ <%= x + y %>
+ ERB
+
+ assert_compiled_snapshot(template)
+ end
+
+ test "evaluation: ruby block comments with =begin and =end mutliline" do
+ template = <<~ERB
+ <%
+ =begin %>
+ This, while unusual, is a legal form of commenting.
+ <%
+ =end %>
+ Hey there
+ ERB
+
+ assert_evaluated_snapshot(template)
+ end
+
+ test "evaluation: ruby block comments inside erb tags" do
+ template = <<~ERB
+ <%
+ =begin
+ This is a comment
+ =end
+ %>
+ Content
+ ERB
+
+ assert_evaluated_snapshot(template)
+ end
+
+ test "evaluation: ruby block comments with code before and after" do
+ template = <<~ERB
+ <% x = 1 %>
+ <%
+ =begin
+ Multi-line comment
+ spanning multiple lines
+ =end
+ %>
+ <% y = 2 %>
+ <%= x + y %>
+ ERB
+
+ assert_evaluated_snapshot(template, { x: 1, y: 2 })
+ end
+ end
+end
diff --git a/test/engine/examples_compilation_test.rb b/test/engine/examples_compilation_test.rb
index 2b3f16da9..91e6b0315 100644
--- a/test/engine/examples_compilation_test.rb
+++ b/test/engine/examples_compilation_test.rb
@@ -11,6 +11,8 @@ class ExamplesCompilationTest < Minitest::Spec
example_files = Dir.glob(File.join(examples_dir, "*.html.erb"))
example_files.each do |file_path|
+ next if file_path.end_with?(".invalid.html.erb")
+
basename = File.basename(file_path, ".html.erb")
test_name = "#{basename.tr("-_", " ")} compilation"
diff --git a/test/herb_test.rb b/test/herb_test.rb
index 866423e31..e6705036c 100644
--- a/test/herb_test.rb
+++ b/test/herb_test.rb
@@ -4,6 +4,6 @@
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
+ assert_equal "herb gem v0.7.5, libprism v1.6.0, libherb v0.7.5 (Ruby C native extension)", Herb.version
end
end
diff --git a/test/lexer/erb_test.rb b/test/lexer/erb_test.rb
index b3c5b1ded..e821c7e8e 100644
--- a/test/lexer/erb_test.rb
+++ b/test/lexer/erb_test.rb
@@ -34,6 +34,14 @@ class ERBTest < Minitest::Spec
assert_lexed_snapshot(%(<%%= "Test" %%>))
end
+ test "erb <% =%>" do
+ assert_lexed_snapshot(%(<% "Test" =%>))
+ end
+
+ test "erb <%= =%>" do
+ assert_lexed_snapshot(%(<%= "Test" =%>))
+ end
+
test "erb output inside HTML attribute value" do
assert_lexed_snapshot(%( ))
end
diff --git a/test/lexer/text_content_test.rb b/test/lexer/text_content_test.rb
index 2306336a5..03934d21f 100644
--- a/test/lexer/text_content_test.rb
+++ b/test/lexer/text_content_test.rb
@@ -14,18 +14,18 @@ class TextContentTest < Minitest::Spec
assert_lexed_snapshot("Some. Text. ")
end
- test "text content that exceeds initial buffer_T size and needs to resize once (ca. 4K)" do
- initial_buffer_capacity = 1024 # bytes
- content = cyclic_string((((initial_buffer_capacity * 2) + 1) * 2) + 1)
+ test "text content that exceeds initial hb_buffer_T size and needs to resize once (ca. 4K)" do
+ initial_hb_buffer_capacity = 1024 # bytes
+ content = cyclic_string((((initial_hb_buffer_capacity * 2) + 1) * 2) + 1)
result = assert_lexed_snapshot(%(#{content}
))
identifier = result.value.find { |token| token.type == "TOKEN_IDENTIFIER" && token.value.length > 4000 }
assert_equal content, identifier.value
end
- test "text content that exceeds initial buffer_T size and needs to resize twice (ca. 8K)" do
- initial_buffer_capacity = 1024 # bytes
- content = cyclic_string((((((initial_buffer_capacity * 2) + 1) * 2) + 1) * 2) + 1)
+ test "text content that exceeds initial hb_buffer_T size and needs to resize twice (ca. 8K)" do
+ initial_hb_buffer_capacity = 1024 # bytes
+ content = cyclic_string((((((initial_hb_buffer_capacity * 2) + 1) * 2) + 1) * 2) + 1)
result = assert_lexed_snapshot(%(#{content}
))
identifier = result.value.find { |token| token.type == "TOKEN_IDENTIFIER" && token.value.length > 4000 }
diff --git a/test/parser/boolean_attributes_test.rb b/test/parser/boolean_attributes_test.rb
index 076935b8f..24d96191c 100644
--- a/test/parser/boolean_attributes_test.rb
+++ b/test/parser/boolean_attributes_test.rb
@@ -29,5 +29,21 @@ class BooleanAttributesTest < Minitest::Spec
test "boolean attribute surrounded by regular attributes" do
assert_parsed_snapshot(%( ))
end
+
+ test "boolean attribute on void element followed by newline and ERB tag with track_whitespace" do
+ assert_parsed_snapshot(%( \n<%= hello %>), track_whitespace: true)
+ end
+
+ test "boolean attribute on void element followed by ERB tag with track_whitespace" do
+ assert_parsed_snapshot(%( <%= hello %>), track_whitespace: true)
+ end
+
+ test "boolean attribute on void element followed by ERB tag with track_whitespace" do
+ assert_parsed_snapshot(<<~HTML, track_whitespace: true)
+
+ <%= "hi" %>
+
+ HTML
+ end
end
end
diff --git a/test/parser/erb_test.rb b/test/parser/erb_test.rb
index 1c95ef3f6..142a25eab 100644
--- a/test/parser/erb_test.rb
+++ b/test/parser/erb_test.rb
@@ -181,5 +181,29 @@ class ERBTest < Minitest::Spec
%>
HTML
end
+
+ test "erb output with =%> close tag" do
+ assert_parsed_snapshot(%(<%= "hello" =%>))
+ end
+
+ test "erb if with =%> close tag" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if true =%>
+ Content
+ <% end =%>
+ HTML
+ end
+
+ test "erb if-elsif-else with =%> close tag" do
+ assert_parsed_snapshot(<<~HTML)
+ <% if condition =%>
+ True
+ <% elsif other =%>
+ Other
+ <% else =%>
+ False
+ <% end =%>
+ HTML
+ end
end
end
diff --git a/test/parser/text_content_test.rb b/test/parser/text_content_test.rb
index 84e4e8d80..2f9686da7 100644
--- a/test/parser/text_content_test.rb
+++ b/test/parser/text_content_test.rb
@@ -26,17 +26,17 @@ class TextContentTest < Minitest::Spec
assert_parsed_snapshot("Hello World")
end
- test "text content that exceeds initial buffer_T size (ca. 4K)" do
- initial_buffer_capacity = 1024 # bytes
- content = cyclic_string((((initial_buffer_capacity * 2) + 1) * 2) + 1)
+ test "text content that exceeds initial hb_buffer_T size (ca. 4K)" do
+ initial_hb_buffer_capacity = 1024 # bytes
+ content = cyclic_string((((initial_hb_buffer_capacity * 2) + 1) * 2) + 1)
result = assert_parsed_snapshot(%(#{content}
))
assert_equal content, result.value.children.first.body.first.content
end
- test "text content that exceeds initial buffer_T size (ca. 8K)" do
- initial_buffer_capacity = 1024 # bytes
- content = cyclic_string((((((initial_buffer_capacity * 2) + 1) * 2) + 1) * 2) + 1)
+ test "text content that exceeds initial hb_buffer_T size (ca. 8K)" do
+ initial_hb_buffer_capacity = 1024 # bytes
+ content = cyclic_string((((((initial_hb_buffer_capacity * 2) + 1) * 2) + 1) * 2) + 1)
result = assert_parsed_snapshot(%(#{content}
))
assert_equal content, result.value.children.first.body.first.content
@@ -145,5 +145,16 @@ class TextContentTest < Minitest::Spec
test "backtick with HTML tags - issue 467" do
assert_parsed_snapshot("a ` ` c")
end
+
+ test "backslash-prefixed text stays literal - issue 635" do
+ assert_parsed_snapshot("\\Asome-regexp\\z
")
+ end
+
+ # https://github.com/lobsters/lobsters/blob/75f9a53077d5aeaeadbb8271def0479dd8fcd761/app/views/domains/edit.html.erb#L11
+ test "backslash-prefixed text - issue 633" do
+ assert_parsed_snapshot <<~HTML
+ Regexp with captures, must consume whole string like: \\Ahttps?://github.com/+([^/]+).*\\z
+ HTML
+ end
end
end
diff --git a/test/snapshot_utils.rb b/test/snapshot_utils.rb
index 2034fc552..4f848230d 100644
--- a/test/snapshot_utils.rb
+++ b/test/snapshot_utils.rb
@@ -145,7 +145,7 @@ def snapshot_file(source, options = {})
content_hash = Digest::MD5.hexdigest(source || "#{source.class}-#{source.inspect}")
- test_name = name.gsub(" ", "_").gsub("/", "_")
+ test_name = sanitize_name_for_filesystem(name)
if options && !options.empty?
options_hash = Digest::MD5.hexdigest(options.inspect)
@@ -178,6 +178,23 @@ def snapshot_file(source, options = {})
private
+ def sanitize_name_for_filesystem(name)
+ [
+ # ntfs reserved characters
+ # https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
+ ["<", "lt"],
+ [">", "gt"],
+ [":", ""],
+ ["/", "_"],
+ ["\\", ""],
+ ["|", ""],
+ ["?", ""],
+ ["*", ""],
+
+ [" ", "_"]
+ ].inject(name) { |name, substitution| name.gsub(substitution[0], substitution[1]) }
+ end
+
def underscore(string)
string.gsub("::", "/")
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
diff --git a/test/snapshots/analyze/case_in_test/test_0008_case_in_with_block_inside_in_clause_a5d7393ef0ab780cd777fd4d09413ac8.txt b/test/snapshots/analyze/case_in_test/test_0008_case_in_with_block_inside_in_clause_a5d7393ef0ab780cd777fd4d09413ac8.txt
new file mode 100644
index 000000000..c29867eb1
--- /dev/null
+++ b/test/snapshots/analyze/case_in_test/test_0008_case_in_with_block_inside_in_clause_a5d7393ef0ab780cd777fd4d09413ac8.txt
@@ -0,0 +1,48 @@
+@ DocumentNode (location: (1:0)-(7:0))
+└── children: (2 items)
+ ├── @ ERBCaseMatchNode (location: (1:0)-(6:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case result " (location: (1:2)-(1:15))
+ │ ├── tag_closing: "%>" (location: (1:15)-(1:17))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:17)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (1 item)
+ │ │ └── @ ERBInNode (location: (2:0)-(2:29))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " in { status: :success } " (location: (2:2)-(2:27))
+ │ │ ├── tag_closing: "%>" (location: (2:27)-(2:29))
+ │ │ └── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (2:29)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " content_tag(:div) do " (location: (3:5)-(3:27))
+ │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (3:29)-(5:2))
+ │ │ │ │ └── content: "\n Success!\n "
+ │ │ │ │
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4))
+ │ │ │ ├── content: " end " (location: (5:4)-(5:9))
+ │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11))
+ │ │ │
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (5:11)-(6:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (6:0)-(6:9))
+ │ ├── tag_opening: "<%" (location: (6:0)-(6:2))
+ │ ├── content: " end " (location: (6:2)-(6:7))
+ │ └── tag_closing: "%>" (location: (6:7)-(6:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (6:9)-(7:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/case_in_test/test_0009_case_in_with_multiple_blocks_in_in_clause_8cdfe954fcb68cf29f71d5c05893547c.txt b/test/snapshots/analyze/case_in_test/test_0009_case_in_with_multiple_blocks_in_in_clause_8cdfe954fcb68cf29f71d5c05893547c.txt
new file mode 100644
index 000000000..15df4180f
--- /dev/null
+++ b/test/snapshots/analyze/case_in_test/test_0009_case_in_with_multiple_blocks_in_in_clause_8cdfe954fcb68cf29f71d5c05893547c.txt
@@ -0,0 +1,66 @@
+@ DocumentNode (location: (1:0)-(10:0))
+└── children: (2 items)
+ ├── @ ERBCaseMatchNode (location: (1:0)-(9:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case data " (location: (1:2)-(1:13))
+ │ ├── tag_closing: "%>" (location: (1:13)-(1:15))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:15)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (1 item)
+ │ │ └── @ ERBInNode (location: (2:0)-(2:25))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " in { type: :alert } " (location: (2:2)-(2:23))
+ │ │ ├── tag_closing: "%>" (location: (2:23)-(2:25))
+ │ │ └── statements: (5 items)
+ │ │ ├── @ HTMLTextNode (location: (2:25)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " content_tag(:div) do " (location: (3:5)-(3:27))
+ │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (3:29)-(5:2))
+ │ │ │ │ └── content: "\n Alert\n "
+ │ │ │ │
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4))
+ │ │ │ ├── content: " end " (location: (5:4)-(5:9))
+ │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11))
+ │ │ │
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (5:11)-(6:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBBlockNode (location: (6:2)-(8:11))
+ │ │ │ ├── tag_opening: "<%=" (location: (6:2)-(6:5))
+ │ │ │ ├── content: " content_tag(:span) do " (location: (6:5)-(6:28))
+ │ │ │ ├── tag_closing: "%>" (location: (6:28)-(6:30))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (6:30)-(8:2))
+ │ │ │ │ └── content: "\n Icon\n "
+ │ │ │ │
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (8:2)-(8:11))
+ │ │ │ ├── tag_opening: "<%" (location: (8:2)-(8:4))
+ │ │ │ ├── content: " end " (location: (8:4)-(8:9))
+ │ │ │ └── tag_closing: "%>" (location: (8:9)-(8:11))
+ │ │ │
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (8:11)-(9:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (9:0)-(9:9))
+ │ ├── tag_opening: "<%" (location: (9:0)-(9:2))
+ │ ├── content: " end " (location: (9:2)-(9:7))
+ │ └── tag_closing: "%>" (location: (9:7)-(9:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (9:9)-(10:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/case_in_test/test_0010_case_in_with_if_statement_inside_in_clause_51fcf7566215a5284ae1ee34c920837e.txt b/test/snapshots/analyze/case_in_test/test_0010_case_in_with_if_statement_inside_in_clause_51fcf7566215a5284ae1ee34c920837e.txt
new file mode 100644
index 000000000..5acba3aa5
--- /dev/null
+++ b/test/snapshots/analyze/case_in_test/test_0010_case_in_with_if_statement_inside_in_clause_51fcf7566215a5284ae1ee34c920837e.txt
@@ -0,0 +1,49 @@
+@ DocumentNode (location: (1:0)-(7:0))
+└── children: (2 items)
+ ├── @ ERBCaseMatchNode (location: (1:0)-(6:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case value " (location: (1:2)-(1:14))
+ │ ├── tag_closing: "%>" (location: (1:14)-(1:16))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:16)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (1 item)
+ │ │ └── @ ERBInNode (location: (2:0)-(2:18))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " in [Integer] " (location: (2:2)-(2:16))
+ │ │ ├── tag_closing: "%>" (location: (2:16)-(2:18))
+ │ │ └── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (2:18)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBIfNode (location: (3:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%" (location: (3:2)-(3:4))
+ │ │ │ ├── content: " if positive? " (location: (3:4)-(3:18))
+ │ │ │ ├── tag_closing: "%>" (location: (3:18)-(3:20))
+ │ │ │ ├── statements: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (3:20)-(5:2))
+ │ │ │ │ └── content: "\n Positive\n "
+ │ │ │ │
+ │ │ │ ├── subsequent: ∅
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4))
+ │ │ │ ├── content: " end " (location: (5:4)-(5:9))
+ │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11))
+ │ │ │
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (5:11)-(6:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (6:0)-(6:9))
+ │ ├── tag_opening: "<%" (location: (6:0)-(6:2))
+ │ ├── content: " end " (location: (6:2)-(6:7))
+ │ └── tag_closing: "%>" (location: (6:7)-(6:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (6:9)-(7:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/case_test/test_0008_case_with_block_inside_when_adb20c2773fe1206d2b47123f8001e22.txt b/test/snapshots/analyze/case_test/test_0008_case_with_block_inside_when_adb20c2773fe1206d2b47123f8001e22.txt
new file mode 100644
index 000000000..e2e329787
--- /dev/null
+++ b/test/snapshots/analyze/case_test/test_0008_case_with_block_inside_when_adb20c2773fe1206d2b47123f8001e22.txt
@@ -0,0 +1,48 @@
+@ DocumentNode (location: (1:0)-(7:0))
+└── children: (2 items)
+ ├── @ ERBCaseNode (location: (1:0)-(6:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case 1 " (location: (1:2)-(1:10))
+ │ ├── tag_closing: "%>" (location: (1:10)-(1:12))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:12)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (1 item)
+ │ │ └── @ ERBWhenNode (location: (2:0)-(2:12))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " when 1 " (location: (2:2)-(2:10))
+ │ │ ├── tag_closing: "%>" (location: (2:10)-(2:12))
+ │ │ └── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (2:12)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " content_tag(:p) do " (location: (3:5)-(3:25))
+ │ │ │ ├── tag_closing: "%>" (location: (3:25)-(3:27))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (3:27)-(5:2))
+ │ │ │ │ └── content: "\n Yep\n "
+ │ │ │ │
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4))
+ │ │ │ ├── content: " end " (location: (5:4)-(5:9))
+ │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11))
+ │ │ │
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (5:11)-(6:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (6:0)-(6:9))
+ │ ├── tag_opening: "<%" (location: (6:0)-(6:2))
+ │ ├── content: " end " (location: (6:2)-(6:7))
+ │ └── tag_closing: "%>" (location: (6:7)-(6:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (6:9)-(7:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/case_test/test_0009_case_with_multiple_blocks_in_when_05f7d387314a95620a58ddc7f7d31b7b.txt b/test/snapshots/analyze/case_test/test_0009_case_with_multiple_blocks_in_when_05f7d387314a95620a58ddc7f7d31b7b.txt
new file mode 100644
index 000000000..ff5e466ab
--- /dev/null
+++ b/test/snapshots/analyze/case_test/test_0009_case_with_multiple_blocks_in_when_05f7d387314a95620a58ddc7f7d31b7b.txt
@@ -0,0 +1,66 @@
+@ DocumentNode (location: (1:0)-(10:0))
+└── children: (2 items)
+ ├── @ ERBCaseNode (location: (1:0)-(9:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case status " (location: (1:2)-(1:15))
+ │ ├── tag_closing: "%>" (location: (1:15)-(1:17))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:17)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (1 item)
+ │ │ └── @ ERBWhenNode (location: (2:0)-(2:18))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " when :active " (location: (2:2)-(2:16))
+ │ │ ├── tag_closing: "%>" (location: (2:16)-(2:18))
+ │ │ └── statements: (5 items)
+ │ │ ├── @ HTMLTextNode (location: (2:18)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " content_tag(:div) do " (location: (3:5)-(3:27))
+ │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (3:29)-(5:2))
+ │ │ │ │ └── content: "\n Active\n "
+ │ │ │ │
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4))
+ │ │ │ ├── content: " end " (location: (5:4)-(5:9))
+ │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11))
+ │ │ │
+ │ │ │
+ │ │ ├── @ HTMLTextNode (location: (5:11)-(6:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBBlockNode (location: (6:2)-(8:11))
+ │ │ │ ├── tag_opening: "<%=" (location: (6:2)-(6:5))
+ │ │ │ ├── content: " content_tag(:span) do " (location: (6:5)-(6:28))
+ │ │ │ ├── tag_closing: "%>" (location: (6:28)-(6:30))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (6:30)-(8:2))
+ │ │ │ │ └── content: "\n Badge\n "
+ │ │ │ │
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (8:2)-(8:11))
+ │ │ │ ├── tag_opening: "<%" (location: (8:2)-(8:4))
+ │ │ │ ├── content: " end " (location: (8:4)-(8:9))
+ │ │ │ └── tag_closing: "%>" (location: (8:9)-(8:11))
+ │ │ │
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (8:11)-(9:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (9:0)-(9:9))
+ │ ├── tag_opening: "<%" (location: (9:0)-(9:2))
+ │ ├── content: " end " (location: (9:2)-(9:7))
+ │ └── tag_closing: "%>" (location: (9:7)-(9:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (9:9)-(10:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/case_test/test_0010_case_with_nested_blocks_in_when_6e70c1bf60788969969296d35c409a47.txt b/test/snapshots/analyze/case_test/test_0010_case_with_nested_blocks_in_when_6e70c1bf60788969969296d35c409a47.txt
new file mode 100644
index 000000000..857244cb8
--- /dev/null
+++ b/test/snapshots/analyze/case_test/test_0010_case_with_nested_blocks_in_when_6e70c1bf60788969969296d35c409a47.txt
@@ -0,0 +1,66 @@
+@ DocumentNode (location: (1:0)-(9:0))
+└── children: (2 items)
+ ├── @ ERBCaseNode (location: (1:0)-(8:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case level " (location: (1:2)-(1:14))
+ │ ├── tag_closing: "%>" (location: (1:14)-(1:16))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:16)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (1 item)
+ │ │ └── @ ERBWhenNode (location: (2:0)-(2:12))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " when 1 " (location: (2:2)-(2:10))
+ │ │ ├── tag_closing: "%>" (location: (2:10)-(2:12))
+ │ │ └── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (2:12)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBBlockNode (location: (3:2)-(7:11))
+ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ ├── content: " content_tag(:div) do " (location: (3:5)-(3:27))
+ │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29))
+ │ │ │ ├── body: (3 items)
+ │ │ │ │ ├── @ HTMLTextNode (location: (3:29)-(4:4))
+ │ │ │ │ │ └── content: "\n "
+ │ │ │ │ │
+ │ │ │ │ ├── @ ERBBlockNode (location: (4:4)-(6:13))
+ │ │ │ │ │ ├── tag_opening: "<%=" (location: (4:4)-(4:7))
+ │ │ │ │ │ ├── content: " content_tag(:p) do " (location: (4:7)-(4:27))
+ │ │ │ │ │ ├── tag_closing: "%>" (location: (4:27)-(4:29))
+ │ │ │ │ │ ├── body: (1 item)
+ │ │ │ │ │ │ └── @ HTMLTextNode (location: (4:29)-(6:4))
+ │ │ │ │ │ │ └── content: "\n Nested\n "
+ │ │ │ │ │ │
+ │ │ │ │ │ └── end_node:
+ │ │ │ │ │ └── @ ERBEndNode (location: (6:4)-(6:13))
+ │ │ │ │ │ ├── tag_opening: "<%" (location: (6:4)-(6:6))
+ │ │ │ │ │ ├── content: " end " (location: (6:6)-(6:11))
+ │ │ │ │ │ └── tag_closing: "%>" (location: (6:11)-(6:13))
+ │ │ │ │ │
+ │ │ │ │ │
+ │ │ │ │ └── @ HTMLTextNode (location: (6:13)-(7:2))
+ │ │ │ │ └── content: "\n "
+ │ │ │ │
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (7:2)-(7:11))
+ │ │ │ ├── tag_opening: "<%" (location: (7:2)-(7:4))
+ │ │ │ ├── content: " end " (location: (7:4)-(7:9))
+ │ │ │ └── tag_closing: "%>" (location: (7:9)-(7:11))
+ │ │ │
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (7:11)-(8:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (8:0)-(8:9))
+ │ ├── tag_opening: "<%" (location: (8:0)-(8:2))
+ │ ├── content: " end " (location: (8:2)-(8:7))
+ │ └── tag_closing: "%>" (location: (8:7)-(8:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (8:9)-(9:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/case_test/test_0011_case_with_block_in_multiple_when_clauses_3f35d3ebf6ea6eaf8e7b9bc7f8393a0f.txt b/test/snapshots/analyze/case_test/test_0011_case_with_block_in_multiple_when_clauses_3f35d3ebf6ea6eaf8e7b9bc7f8393a0f.txt
new file mode 100644
index 000000000..818e209a5
--- /dev/null
+++ b/test/snapshots/analyze/case_test/test_0011_case_with_block_in_multiple_when_clauses_3f35d3ebf6ea6eaf8e7b9bc7f8393a0f.txt
@@ -0,0 +1,75 @@
+@ DocumentNode (location: (1:0)-(11:0))
+└── children: (2 items)
+ ├── @ ERBCaseNode (location: (1:0)-(10:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case type " (location: (1:2)-(1:13))
+ │ ├── tag_closing: "%>" (location: (1:13)-(1:15))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:15)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (2 items)
+ │ │ ├── @ ERBWhenNode (location: (2:0)-(2:13))
+ │ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ │ ├── content: " when :a " (location: (2:2)-(2:11))
+ │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13))
+ │ │ │ └── statements: (3 items)
+ │ │ │ ├── @ HTMLTextNode (location: (2:13)-(3:2))
+ │ │ │ │ └── content: "\n "
+ │ │ │ │
+ │ │ │ ├── @ ERBBlockNode (location: (3:2)-(5:11))
+ │ │ │ │ ├── tag_opening: "<%=" (location: (3:2)-(3:5))
+ │ │ │ │ ├── content: " form_for(obj) do |f| " (location: (3:5)-(3:27))
+ │ │ │ │ ├── tag_closing: "%>" (location: (3:27)-(3:29))
+ │ │ │ │ ├── body: (1 item)
+ │ │ │ │ │ └── @ HTMLTextNode (location: (3:29)-(5:2))
+ │ │ │ │ │ └── content: "\n Form A\n "
+ │ │ │ │ │
+ │ │ │ │ └── end_node:
+ │ │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11))
+ │ │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4))
+ │ │ │ │ ├── content: " end " (location: (5:4)-(5:9))
+ │ │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11))
+ │ │ │ │
+ │ │ │ │
+ │ │ │ └── @ HTMLTextNode (location: (5:11)-(6:0))
+ │ │ │ └── content: "\n"
+ │ │ │
+ │ │ │
+ │ │ └── @ ERBWhenNode (location: (6:0)-(6:13))
+ │ │ ├── tag_opening: "<%" (location: (6:0)-(6:2))
+ │ │ ├── content: " when :b " (location: (6:2)-(6:11))
+ │ │ ├── tag_closing: "%>" (location: (6:11)-(6:13))
+ │ │ └── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (6:13)-(7:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBBlockNode (location: (7:2)-(9:11))
+ │ │ │ ├── tag_opening: "<%=" (location: (7:2)-(7:5))
+ │ │ │ ├── content: " form_for(obj) do |f| " (location: (7:5)-(7:27))
+ │ │ │ ├── tag_closing: "%>" (location: (7:27)-(7:29))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (7:29)-(9:2))
+ │ │ │ │ └── content: "\n Form B\n "
+ │ │ │ │
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (9:2)-(9:11))
+ │ │ │ ├── tag_opening: "<%" (location: (9:2)-(9:4))
+ │ │ │ ├── content: " end " (location: (9:4)-(9:9))
+ │ │ │ └── tag_closing: "%>" (location: (9:9)-(9:11))
+ │ │ │
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (9:11)-(10:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (10:0)-(10:9))
+ │ ├── tag_opening: "<%" (location: (10:0)-(10:2))
+ │ ├── content: " end " (location: (10:2)-(10:7))
+ │ └── tag_closing: "%>" (location: (10:7)-(10:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (10:9)-(11:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/case_test/test_0012_case_with_if_statement_inside_when_ccc0bd511203c41d9274ccc360e736c2.txt b/test/snapshots/analyze/case_test/test_0012_case_with_if_statement_inside_when_ccc0bd511203c41d9274ccc360e736c2.txt
new file mode 100644
index 000000000..5cef31f1d
--- /dev/null
+++ b/test/snapshots/analyze/case_test/test_0012_case_with_if_statement_inside_when_ccc0bd511203c41d9274ccc360e736c2.txt
@@ -0,0 +1,49 @@
+@ DocumentNode (location: (1:0)-(7:0))
+└── children: (2 items)
+ ├── @ ERBCaseNode (location: (1:0)-(6:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case status " (location: (1:2)-(1:15))
+ │ ├── tag_closing: "%>" (location: (1:15)-(1:17))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:17)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (1 item)
+ │ │ └── @ ERBWhenNode (location: (2:0)-(2:18))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " when :active " (location: (2:2)-(2:16))
+ │ │ ├── tag_closing: "%>" (location: (2:16)-(2:18))
+ │ │ └── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (2:18)-(3:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBIfNode (location: (3:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%" (location: (3:2)-(3:4))
+ │ │ │ ├── content: " if admin? " (location: (3:4)-(3:15))
+ │ │ │ ├── tag_closing: "%>" (location: (3:15)-(3:17))
+ │ │ │ ├── statements: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (3:17)-(5:2))
+ │ │ │ │ └── content: "\n Admin view\n "
+ │ │ │ │
+ │ │ │ ├── subsequent: ∅
+ │ │ │ └── end_node:
+ │ │ │ └── @ ERBEndNode (location: (5:2)-(5:11))
+ │ │ │ ├── tag_opening: "<%" (location: (5:2)-(5:4))
+ │ │ │ ├── content: " end " (location: (5:4)-(5:9))
+ │ │ │ └── tag_closing: "%>" (location: (5:9)-(5:11))
+ │ │ │
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (5:11)-(6:0))
+ │ │ └── content: "\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (6:0)-(6:9))
+ │ ├── tag_opening: "<%" (location: (6:0)-(6:2))
+ │ ├── content: " end " (location: (6:2)-(6:7))
+ │ └── tag_closing: "%>" (location: (6:7)-(6:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (6:9)-(7:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/analyze/case_test/test_0013_case_with_yield_dc59b65969b8ef95076abc05c3907392.txt b/test/snapshots/analyze/case_test/test_0013_case_with_yield_dc59b65969b8ef95076abc05c3907392.txt
new file mode 100644
index 000000000..1a4e6654e
--- /dev/null
+++ b/test/snapshots/analyze/case_test/test_0013_case_with_yield_dc59b65969b8ef95076abc05c3907392.txt
@@ -0,0 +1,30 @@
+@ DocumentNode (location: (1:0)-(5:0))
+└── children: (2 items)
+ ├── @ ERBCaseNode (location: (1:0)-(4:9))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " case yield(:a) " (location: (1:2)-(1:18))
+ │ ├── tag_closing: "%>" (location: (1:18)-(1:20))
+ │ ├── children: (1 item)
+ │ │ └── @ HTMLTextNode (location: (1:20)-(2:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── conditions: (1 item)
+ │ │ └── @ ERBWhenNode (location: (2:0)-(2:14))
+ │ │ ├── tag_opening: "<%" (location: (2:0)-(2:2))
+ │ │ ├── content: " when 'a' " (location: (2:2)-(2:12))
+ │ │ ├── tag_closing: "%>" (location: (2:12)-(2:14))
+ │ │ └── statements: (1 item)
+ │ │ └── @ HTMLTextNode (location: (2:14)-(4:0))
+ │ │ └── content: "\n aaa\n"
+ │ │
+ │ │
+ │ ├── else_clause: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (4:0)-(4:9))
+ │ ├── tag_opening: "<%" (location: (4:0)-(4:2))
+ │ ├── content: " end " (location: (4:2)-(4:7))
+ │ └── tag_closing: "%>" (location: (4:7)-(4:9))
+ │
+ │
+ └── @ HTMLTextNode (location: (4:9)-(5:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/engine/block_comments_test/test_0001_ruby_block_comments_with_=begin_and_=end_multiline_ff404c4e708f59f532b4042441e458ad.txt b/test/snapshots/engine/block_comments_test/test_0001_ruby_block_comments_with_=begin_and_=end_multiline_ff404c4e708f59f532b4042441e458ad.txt
new file mode 100644
index 000000000..b86d395f2
--- /dev/null
+++ b/test/snapshots/engine/block_comments_test/test_0001_ruby_block_comments_with_=begin_and_=end_multiline_ff404c4e708f59f532b4042441e458ad.txt
@@ -0,0 +1,11 @@
+_buf = ::String.new;
+
+=begin
+ _buf << '
+ This, while unusual, is a legal form of commenting.
+'.freeze;
+=end
+ _buf << '
+Hey there
+'.freeze;
+_buf.to_s
diff --git a/test/snapshots/engine/block_comments_test/test_0002_ruby_block_comments_inside_erb_tags_95bcfa3f3e28634bf923f18df0cd38a1.txt b/test/snapshots/engine/block_comments_test/test_0002_ruby_block_comments_inside_erb_tags_95bcfa3f3e28634bf923f18df0cd38a1.txt
new file mode 100644
index 000000000..467a7f23a
--- /dev/null
+++ b/test/snapshots/engine/block_comments_test/test_0002_ruby_block_comments_inside_erb_tags_95bcfa3f3e28634bf923f18df0cd38a1.txt
@@ -0,0 +1,9 @@
+_buf = ::String.new;
+
+=begin
+This is a comment
+=end
+ _buf << '
+Content
+'.freeze;
+_buf.to_s
diff --git a/test/snapshots/engine/block_comments_test/test_0003_ruby_block_comments_with_code_before_and_after_0ee0ba8a9a25359175b884cf66f37b2c.txt b/test/snapshots/engine/block_comments_test/test_0003_ruby_block_comments_with_code_before_and_after_0ee0ba8a9a25359175b884cf66f37b2c.txt
new file mode 100644
index 000000000..545dc4e42
--- /dev/null
+++ b/test/snapshots/engine/block_comments_test/test_0003_ruby_block_comments_with_code_before_and_after_0ee0ba8a9a25359175b884cf66f37b2c.txt
@@ -0,0 +1,12 @@
+_buf = ::String.new;
+ x = 1; _buf << '
+'.freeze;
+=begin
+Multi-line comment
+spanning multiple lines
+=end
+ _buf << '
+'.freeze; y = 2; _buf << '
+'.freeze; _buf << (x + y).to_s; _buf << '
+'.freeze;
+_buf.to_s
diff --git a/test/snapshots/engine/block_comments_test/test_0004_evaluation_ruby_block_comments_with_=begin_and_=end_mutliline_6bf3affa07b559b73f6428e0dbd270d2.txt b/test/snapshots/engine/block_comments_test/test_0004_evaluation_ruby_block_comments_with_=begin_and_=end_mutliline_6bf3affa07b559b73f6428e0dbd270d2.txt
new file mode 100644
index 000000000..a6a4497b5
--- /dev/null
+++ b/test/snapshots/engine/block_comments_test/test_0004_evaluation_ruby_block_comments_with_=begin_and_=end_mutliline_6bf3affa07b559b73f6428e0dbd270d2.txt
@@ -0,0 +1,2 @@
+
+Hey there
diff --git a/test/snapshots/engine/block_comments_test/test_0005_evaluation_ruby_block_comments_inside_erb_tags_8a842d797202d0b31c9832cd43a22686.txt b/test/snapshots/engine/block_comments_test/test_0005_evaluation_ruby_block_comments_inside_erb_tags_8a842d797202d0b31c9832cd43a22686.txt
new file mode 100644
index 000000000..1efa30582
--- /dev/null
+++ b/test/snapshots/engine/block_comments_test/test_0005_evaluation_ruby_block_comments_inside_erb_tags_8a842d797202d0b31c9832cd43a22686.txt
@@ -0,0 +1,2 @@
+
+Content
diff --git a/test/snapshots/engine/block_comments_test/test_0006_evaluation_ruby_block_comments_with_code_before_and_after_c5e6a4e1e9e691af8dcc74ae6c9564c9.txt b/test/snapshots/engine/block_comments_test/test_0006_evaluation_ruby_block_comments_with_code_before_and_after_c5e6a4e1e9e691af8dcc74ae6c9564c9.txt
new file mode 100644
index 000000000..d630b5b60
--- /dev/null
+++ b/test/snapshots/engine/block_comments_test/test_0006_evaluation_ruby_block_comments_with_code_before_and_after_c5e6a4e1e9e691af8dcc74ae6c9564c9.txt
@@ -0,0 +1,4 @@
+
+
+
+3
diff --git a/test/snapshots/engine/examples_compilation_test/test_0004_block_comment_compilation_d5c98b9f230e001f8aabf838d3774698.txt b/test/snapshots/engine/examples_compilation_test/test_0004_block_comment_compilation_d5c98b9f230e001f8aabf838d3774698.txt
new file mode 100644
index 000000000..dd6f62d48
--- /dev/null
+++ b/test/snapshots/engine/examples_compilation_test/test_0004_block_comment_compilation_d5c98b9f230e001f8aabf838d3774698.txt
@@ -0,0 +1,12 @@
+_buf = ::String.new;
+
+=begin
+ _buf << '
+ This, while unusual, is a legal form of commenting.
+'.freeze;
+=end
+ _buf << '
+
+Content
+'.freeze;
+_buf.to_s
diff --git a/test/snapshots/engine/examples_compilation_test/test_0005_case_in_compilation_6f13b7233db413c987e289ad2b3e3a3a.txt b/test/snapshots/engine/examples_compilation_test/test_0005_case_in_compilation_6f13b7233db413c987e289ad2b3e3a3a.txt
new file mode 100644
index 000000000..218a1f6eb
--- /dev/null
+++ b/test/snapshots/engine/examples_compilation_test/test_0005_case_in_compilation_6f13b7233db413c987e289ad2b3e3a3a.txt
@@ -0,0 +1,8 @@
+_buf = ::String.new;
+ case {}; in {}; _buf << '
+ "matched"
+'.freeze; else; _buf << '
+ "not matched"
+'.freeze; end; _buf << '
+'.freeze;
+_buf.to_s
diff --git a/test/snapshots/engine/examples_compilation_test/test_0004_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt b/test/snapshots/engine/examples_compilation_test/test_0006_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0004_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt
rename to test/snapshots/engine/examples_compilation_test/test_0006_case_when_compilation_3ee70e5b90c1ff368a7783c49c6dc611.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0005_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt b/test/snapshots/engine/examples_compilation_test/test_0007_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0005_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt
rename to test/snapshots/engine/examples_compilation_test/test_0007_comment_compilation_da96aac1987dbb33d41eb4a121419b47.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0006_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt b/test/snapshots/engine/examples_compilation_test/test_0008_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0006_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt
rename to test/snapshots/engine/examples_compilation_test/test_0008_comment_before_content_compilation_2b3c4e3c244db77468813a7472298e82.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0009_complete_erb_compilation_a33dd3e79d24b4f31f6d4e92611f3f63.txt b/test/snapshots/engine/examples_compilation_test/test_0009_complete_erb_compilation_a33dd3e79d24b4f31f6d4e92611f3f63.txt
new file mode 100644
index 000000000..f48818019
--- /dev/null
+++ b/test/snapshots/engine/examples_compilation_test/test_0009_complete_erb_compilation_a33dd3e79d24b4f31f6d4e92611f3f63.txt
@@ -0,0 +1,4 @@
+_buf = ::String.new;
+ _buf << (hello).to_s; _buf << '
+'.freeze;
+_buf.to_s
diff --git a/test/snapshots/engine/examples_compilation_test/test_0007_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt b/test/snapshots/engine/examples_compilation_test/test_0010_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0007_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt
rename to test/snapshots/engine/examples_compilation_test/test_0010_doctype_compilation_8c18fb8399a2fd1e65b0333bc01e041c.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0008_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt b/test/snapshots/engine/examples_compilation_test/test_0011_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0008_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt
rename to test/snapshots/engine/examples_compilation_test/test_0011_erb_compilation_0109f6af5a474973b8b1b52636135c4f.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0009_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt b/test/snapshots/engine/examples_compilation_test/test_0012_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0009_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt
rename to test/snapshots/engine/examples_compilation_test/test_0012_for_compilation_eeecb969def01ab8005c7b9f23686fed.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0010_if_else_compilation_1437d0425640509570342c24b30de571.txt b/test/snapshots/engine/examples_compilation_test/test_0013_if_else_compilation_1437d0425640509570342c24b30de571.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0010_if_else_compilation_1437d0425640509570342c24b30de571.txt
rename to test/snapshots/engine/examples_compilation_test/test_0013_if_else_compilation_1437d0425640509570342c24b30de571.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0011_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt b/test/snapshots/engine/examples_compilation_test/test_0014_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0011_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt
rename to test/snapshots/engine/examples_compilation_test/test_0014_line_wrap_compilation_ba78b92bec7a4692a2043f2ba2ebe057.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0012_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt b/test/snapshots/engine/examples_compilation_test/test_0015_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0012_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt
rename to test/snapshots/engine/examples_compilation_test/test_0015_link_to_with_block_compilation_4ddb2a17755d3e775e3225970bc60a96.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0013_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt b/test/snapshots/engine/examples_compilation_test/test_0016_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0013_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt
rename to test/snapshots/engine/examples_compilation_test/test_0016_nested_if_and_blocks_compilation_aa8aed9ef61b138a28efed42f4b72251.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0014_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt b/test/snapshots/engine/examples_compilation_test/test_0017_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0014_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt
rename to test/snapshots/engine/examples_compilation_test/test_0017_simple_block_compilation_1729fad3a77618acdc687c9fb671b75b.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0015_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt b/test/snapshots/engine/examples_compilation_test/test_0018_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0015_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt
rename to test/snapshots/engine/examples_compilation_test/test_0018_simple_erb_compilation_d81de4ca83482c4836215ef7177d9eec.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0016_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt b/test/snapshots/engine/examples_compilation_test/test_0019_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0016_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt
rename to test/snapshots/engine/examples_compilation_test/test_0019_test_compilation_9c22f391d1d03fa66b3d18095354a236.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0017_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt b/test/snapshots/engine/examples_compilation_test/test_0020_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0017_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt
rename to test/snapshots/engine/examples_compilation_test/test_0020_until_compilation_0b1269c00fd15a74df125278ed6a9fc4.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0018_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt b/test/snapshots/engine/examples_compilation_test/test_0021_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0018_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt
rename to test/snapshots/engine/examples_compilation_test/test_0021_utf8_compilation_45fa7aa654c0dc06d1a1b9504002dfba.txt
diff --git a/test/snapshots/engine/examples_compilation_test/test_0019_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt b/test/snapshots/engine/examples_compilation_test/test_0022_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt
similarity index 100%
rename from test/snapshots/engine/examples_compilation_test/test_0019_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt
rename to test/snapshots/engine/examples_compilation_test/test_0022_while_compilation_a705eb5ed83b4db368d7204baa136b36.txt
diff --git a/test/snapshots/lexer/attributes_test/test_0009_attribute_value_single_quotes_with_>_value_2e196056650e47d46886938eb88136f7.txt b/test/snapshots/lexer/attributes_test/test_0009_attribute_value_single_quotes_with_gt_value_2e196056650e47d46886938eb88136f7.txt
similarity index 100%
rename from test/snapshots/lexer/attributes_test/test_0009_attribute_value_single_quotes_with_>_value_2e196056650e47d46886938eb88136f7.txt
rename to test/snapshots/lexer/attributes_test/test_0009_attribute_value_single_quotes_with_gt_value_2e196056650e47d46886938eb88136f7.txt
diff --git a/test/snapshots/lexer/attributes_test/test_0013_Alpine.js_:class_attribute_af387b2d214ba9baadbd062f15508d2d.txt b/test/snapshots/lexer/attributes_test/test_0013_Alpine.js_class_attribute_af387b2d214ba9baadbd062f15508d2d.txt
similarity index 100%
rename from test/snapshots/lexer/attributes_test/test_0013_Alpine.js_:class_attribute_af387b2d214ba9baadbd062f15508d2d.txt
rename to test/snapshots/lexer/attributes_test/test_0013_Alpine.js_class_attribute_af387b2d214ba9baadbd062f15508d2d.txt
diff --git a/test/snapshots/lexer/attributes_test/test_0014_Alpine.js_:value_attribute_dbd22dd4679133f8be271e39a3a16b03.txt b/test/snapshots/lexer/attributes_test/test_0014_Alpine.js_value_attribute_dbd22dd4679133f8be271e39a3a16b03.txt
similarity index 100%
rename from test/snapshots/lexer/attributes_test/test_0014_Alpine.js_:value_attribute_dbd22dd4679133f8be271e39a3a16b03.txt
rename to test/snapshots/lexer/attributes_test/test_0014_Alpine.js_value_attribute_dbd22dd4679133f8be271e39a3a16b03.txt
diff --git a/test/snapshots/lexer/attributes_test/test_0017_Alpine.js_:_without_identifier_ad89c5b890e891eda58e590a3eaf2e82.txt b/test/snapshots/lexer/attributes_test/test_0017_Alpine.js__without_identifier_ad89c5b890e891eda58e590a3eaf2e82.txt
similarity index 100%
rename from test/snapshots/lexer/attributes_test/test_0017_Alpine.js_:_without_identifier_ad89c5b890e891eda58e590a3eaf2e82.txt
rename to test/snapshots/lexer/attributes_test/test_0017_Alpine.js__without_identifier_ad89c5b890e891eda58e590a3eaf2e82.txt
diff --git a/test/snapshots/lexer/attributes_test/test_0018_attribute_value_double_quotes_with_>_value_bf55684b694d2aa471b24fa539546339.txt b/test/snapshots/lexer/attributes_test/test_0018_attribute_value_double_quotes_with_gt_value_bf55684b694d2aa471b24fa539546339.txt
similarity index 100%
rename from test/snapshots/lexer/attributes_test/test_0018_attribute_value_double_quotes_with_>_value_bf55684b694d2aa471b24fa539546339.txt
rename to test/snapshots/lexer/attributes_test/test_0018_attribute_value_double_quotes_with_gt_value_bf55684b694d2aa471b24fa539546339.txt
diff --git a/test/snapshots/lexer/erb_test/test_0001_erb_<%_%>_e97ed3ba194342cfe8febc1a40a3d603.txt b/test/snapshots/lexer/erb_test/test_0001_erb_lt%_%gt_e97ed3ba194342cfe8febc1a40a3d603.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0001_erb_<%_%>_e97ed3ba194342cfe8febc1a40a3d603.txt
rename to test/snapshots/lexer/erb_test/test_0001_erb_lt%_%gt_e97ed3ba194342cfe8febc1a40a3d603.txt
diff --git a/test/snapshots/lexer/erb_test/test_0002_erb_<%=_%>_2d5c53c5a986076f8a5df2585ce5aa74.txt b/test/snapshots/lexer/erb_test/test_0002_erb_lt%=_%gt_2d5c53c5a986076f8a5df2585ce5aa74.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0002_erb_<%=_%>_2d5c53c5a986076f8a5df2585ce5aa74.txt
rename to test/snapshots/lexer/erb_test/test_0002_erb_lt%=_%gt_2d5c53c5a986076f8a5df2585ce5aa74.txt
diff --git a/test/snapshots/lexer/erb_test/test_0003_erb_<%-_%>_b305aff64a566ea549c116aacb0ce86c.txt b/test/snapshots/lexer/erb_test/test_0003_erb_lt%-_%gt_b305aff64a566ea549c116aacb0ce86c.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0003_erb_<%-_%>_b305aff64a566ea549c116aacb0ce86c.txt
rename to test/snapshots/lexer/erb_test/test_0003_erb_lt%-_%gt_b305aff64a566ea549c116aacb0ce86c.txt
diff --git a/test/snapshots/lexer/erb_test/test_0004_erb_<%-_-%>_34d2696185efe003bf3f2c2dddb924e3.txt b/test/snapshots/lexer/erb_test/test_0004_erb_lt%-_-%gt_34d2696185efe003bf3f2c2dddb924e3.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0004_erb_<%-_-%>_34d2696185efe003bf3f2c2dddb924e3.txt
rename to test/snapshots/lexer/erb_test/test_0004_erb_lt%-_-%gt_34d2696185efe003bf3f2c2dddb924e3.txt
diff --git a/test/snapshots/lexer/erb_test/test_0005_erb_<%#_%>_d59bec1a45925c1618c6d42541cb652b.txt b/test/snapshots/lexer/erb_test/test_0005_erb_lt%#_%gt_d59bec1a45925c1618c6d42541cb652b.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0005_erb_<%#_%>_d59bec1a45925c1618c6d42541cb652b.txt
rename to test/snapshots/lexer/erb_test/test_0005_erb_lt%#_%gt_d59bec1a45925c1618c6d42541cb652b.txt
diff --git a/test/snapshots/lexer/erb_test/test_0006_erb_<%%_%%>_5856d025d70301b0cc95e2287a1d324f.txt b/test/snapshots/lexer/erb_test/test_0006_erb_lt%%_%%gt_5856d025d70301b0cc95e2287a1d324f.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0006_erb_<%%_%%>_5856d025d70301b0cc95e2287a1d324f.txt
rename to test/snapshots/lexer/erb_test/test_0006_erb_lt%%_%%gt_5856d025d70301b0cc95e2287a1d324f.txt
diff --git a/test/snapshots/lexer/erb_test/test_0007_erb_<%%=_%%>_8bba62f5739809ebc6cf44f4206dd610.txt b/test/snapshots/lexer/erb_test/test_0007_erb_lt%%=_%%gt_8bba62f5739809ebc6cf44f4206dd610.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0007_erb_<%%=_%%>_8bba62f5739809ebc6cf44f4206dd610.txt
rename to test/snapshots/lexer/erb_test/test_0007_erb_lt%%=_%%gt_8bba62f5739809ebc6cf44f4206dd610.txt
diff --git a/test/snapshots/lexer/erb_test/test_0008_erb_lt%_=%gt_344eadf6b1e04a6f534a3c7e38bbadf1.txt b/test/snapshots/lexer/erb_test/test_0008_erb_lt%_=%gt_344eadf6b1e04a6f534a3c7e38bbadf1.txt
new file mode 100644
index 000000000..f84f7c4c9
--- /dev/null
+++ b/test/snapshots/lexer/erb_test/test_0008_erb_lt%_=%gt_344eadf6b1e04a6f534a3c7e38bbadf1.txt
@@ -0,0 +1,4 @@
+#
+#
+#
+#
diff --git a/test/snapshots/lexer/erb_test/test_0009_erb_lt%=_=%gt_273e03432c2039cc05443a602e8c633a.txt b/test/snapshots/lexer/erb_test/test_0009_erb_lt%=_=%gt_273e03432c2039cc05443a602e8c633a.txt
new file mode 100644
index 000000000..62d751f5b
--- /dev/null
+++ b/test/snapshots/lexer/erb_test/test_0009_erb_lt%=_=%gt_273e03432c2039cc05443a602e8c633a.txt
@@ -0,0 +1,4 @@
+#
+#
+#
+#
diff --git a/test/snapshots/lexer/erb_test/test_0008_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt b/test/snapshots/lexer/erb_test/test_0010_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0008_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt
rename to test/snapshots/lexer/erb_test/test_0010_erb_output_inside_HTML_attribute_value_91d881ce0dd66286e4866c07a91e025c.txt
diff --git a/test/snapshots/lexer/erb_test/test_0009_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt b/test/snapshots/lexer/erb_test/test_0011_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0009_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt
rename to test/snapshots/lexer/erb_test/test_0011_erb_output_inside_HTML_attribute_value_with_value_before_e474134558bf8ffccb829ba880271d99.txt
diff --git a/test/snapshots/lexer/erb_test/test_0010_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt b/test/snapshots/lexer/erb_test/test_0012_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0010_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt
rename to test/snapshots/lexer/erb_test/test_0012_erb_output_inside_HTML_attribute_value_with_value_before_and_after_4ce9cd67e9f49e97d785cb27fb5e287a.txt
diff --git a/test/snapshots/lexer/erb_test/test_0011_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt b/test/snapshots/lexer/erb_test/test_0013_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0011_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt
rename to test/snapshots/lexer/erb_test/test_0013_erb_output_inside_HTML_attribute_value_with_value_and_after_e07f0e9a593189f256789bd0ab7b5a82.txt
diff --git a/test/snapshots/lexer/erb_test/test_0012_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt b/test/snapshots/lexer/erb_test/test_0014_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0012_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt
rename to test/snapshots/lexer/erb_test/test_0014_multi-line_erb_content_3b1dbeebb7d6cc88ee758abcd174c7c4.txt
diff --git a/test/snapshots/lexer/erb_test/test_0013_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt b/test/snapshots/lexer/erb_test/test_0015_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0013_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt
rename to test/snapshots/lexer/erb_test/test_0015_multi-line_erb_content_with_complex_ruby_71e2451824a84db91b7e1cf752872168.txt
diff --git a/test/snapshots/lexer/erb_test/test_0014_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt b/test/snapshots/lexer/erb_test/test_0016_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt
similarity index 100%
rename from test/snapshots/lexer/erb_test/test_0014_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt
rename to test/snapshots/lexer/erb_test/test_0016_multi-line_erb_silent_tag_fae7aab59f46d7906cdeccfc1fa2cb3e.txt
diff --git a/test/snapshots/lexer/text_content_test/test_0003_text_content_that_exceeds_initial_buffer_T_size_and_needs_to_resize_once_(ca._4K)_998d9cbd9b8794fa030c888bfa09cad4.txt b/test/snapshots/lexer/text_content_test/test_0003_text_content_that_exceeds_initial_hb_buffer_T_size_and_needs_to_resize_once_(ca._4K)_998d9cbd9b8794fa030c888bfa09cad4.txt
similarity index 100%
rename from test/snapshots/lexer/text_content_test/test_0003_text_content_that_exceeds_initial_buffer_T_size_and_needs_to_resize_once_(ca._4K)_998d9cbd9b8794fa030c888bfa09cad4.txt
rename to test/snapshots/lexer/text_content_test/test_0003_text_content_that_exceeds_initial_hb_buffer_T_size_and_needs_to_resize_once_(ca._4K)_998d9cbd9b8794fa030c888bfa09cad4.txt
diff --git a/test/snapshots/lexer/text_content_test/test_0004_text_content_that_exceeds_initial_buffer_T_size_and_needs_to_resize_twice_(ca._8K)_45e3ecb8edc7dbec7d68b72b5d083439.txt b/test/snapshots/lexer/text_content_test/test_0004_text_content_that_exceeds_initial_hb_buffer_T_size_and_needs_to_resize_twice_(ca._8K)_45e3ecb8edc7dbec7d68b72b5d083439.txt
similarity index 100%
rename from test/snapshots/lexer/text_content_test/test_0004_text_content_that_exceeds_initial_buffer_T_size_and_needs_to_resize_twice_(ca._8K)_45e3ecb8edc7dbec7d68b72b5d083439.txt
rename to test/snapshots/lexer/text_content_test/test_0004_text_content_that_exceeds_initial_hb_buffer_T_size_and_needs_to_resize_twice_(ca._8K)_45e3ecb8edc7dbec7d68b72b5d083439.txt
diff --git a/test/snapshots/parser/boolean_attributes_test/test_0007_boolean_attribute_on_void_element_followed_by_newline_and_ERB_tag_with_track_whitespace_865f46b916df0289c3b60c880f529e8b-b26dbda6d8a652930695c93bd07179f4.txt b/test/snapshots/parser/boolean_attributes_test/test_0007_boolean_attribute_on_void_element_followed_by_newline_and_ERB_tag_with_track_whitespace_865f46b916df0289c3b60c880f529e8b-b26dbda6d8a652930695c93bd07179f4.txt
new file mode 100644
index 000000000..6574e7dac
--- /dev/null
+++ b/test/snapshots/parser/boolean_attributes_test/test_0007_boolean_attribute_on_void_element_followed_by_newline_and_ERB_tag_with_track_whitespace_865f46b916df0289c3b60c880f529e8b-b26dbda6d8a652930695c93bd07179f4.txt
@@ -0,0 +1,40 @@
+@ DocumentNode (location: (1:0)-(2:12))
+└── children: (3 items)
+ ├── @ HTMLElementNode (location: (1:0)-(1:18))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:18))
+ │ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ │ ├── tag_name: "link" (location: (1:1)-(1:5))
+ │ │ ├── tag_closing: ">" (location: (1:17)-(1:18))
+ │ │ ├── children: (2 items)
+ │ │ │ ├── @ WhitespaceNode (location: (1:5)-(1:6))
+ │ │ │ │ └── value: " " (location: (1:5)-(1:6))
+ │ │ │ │
+ │ │ │ └── @ HTMLAttributeNode (location: (1:6)-(1:17))
+ │ │ │ ├── name:
+ │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:6)-(1:17))
+ │ │ │ │ └── children: (1 item)
+ │ │ │ │ └── @ LiteralNode (location: (1:6)-(1:17))
+ │ │ │ │ └── content: "crossorigin"
+ │ │ │ │
+ │ │ │ │
+ │ │ │ ├── equals: ∅
+ │ │ │ └── value: ∅
+ │ │ │
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "link" (location: (1:1)-(1:5))
+ │ ├── body: []
+ │ ├── close_tag: ∅
+ │ ├── is_void: true
+ │ └── source: "HTML"
+ │
+ ├── @ HTMLTextNode (location: (2:3)-(2:0))
+ │ └── content: "\n"
+ │
+ └── @ ERBContentNode (location: (2:0)-(2:12))
+ ├── tag_opening: "<%=" (location: (2:0)-(2:3))
+ ├── content: " hello " (location: (2:3)-(2:10))
+ ├── tag_closing: "%>" (location: (2:10)-(2:12))
+ ├── parsed: true
+ └── valid: true
\ No newline at end of file
diff --git a/test/snapshots/parser/boolean_attributes_test/test_0008_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_7a98c943c925e6943aaa2bec978901a9-b26dbda6d8a652930695c93bd07179f4.txt b/test/snapshots/parser/boolean_attributes_test/test_0008_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_7a98c943c925e6943aaa2bec978901a9-b26dbda6d8a652930695c93bd07179f4.txt
new file mode 100644
index 000000000..535ea29fc
--- /dev/null
+++ b/test/snapshots/parser/boolean_attributes_test/test_0008_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_7a98c943c925e6943aaa2bec978901a9-b26dbda6d8a652930695c93bd07179f4.txt
@@ -0,0 +1,37 @@
+@ DocumentNode (location: (1:0)-(1:30))
+└── children: (2 items)
+ ├── @ HTMLElementNode (location: (1:0)-(1:18))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:18))
+ │ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ │ ├── tag_name: "link" (location: (1:1)-(1:5))
+ │ │ ├── tag_closing: ">" (location: (1:17)-(1:18))
+ │ │ ├── children: (2 items)
+ │ │ │ ├── @ WhitespaceNode (location: (1:5)-(1:6))
+ │ │ │ │ └── value: " " (location: (1:5)-(1:6))
+ │ │ │ │
+ │ │ │ └── @ HTMLAttributeNode (location: (1:6)-(1:17))
+ │ │ │ ├── name:
+ │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:6)-(1:17))
+ │ │ │ │ └── children: (1 item)
+ │ │ │ │ └── @ LiteralNode (location: (1:6)-(1:17))
+ │ │ │ │ └── content: "crossorigin"
+ │ │ │ │
+ │ │ │ │
+ │ │ │ ├── equals: ∅
+ │ │ │ └── value: ∅
+ │ │ │
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "link" (location: (1:1)-(1:5))
+ │ ├── body: []
+ │ ├── close_tag: ∅
+ │ ├── is_void: true
+ │ └── source: "HTML"
+ │
+ └── @ ERBContentNode (location: (1:21)-(1:30))
+ ├── tag_opening: "<%=" (location: (1:21)-(1:21))
+ ├── content: " hello " (location: (1:21)-(1:28))
+ ├── tag_closing: "%>" (location: (1:28)-(1:30))
+ ├── parsed: true
+ └── valid: true
\ No newline at end of file
diff --git a/test/snapshots/parser/boolean_attributes_test/test_0009_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_1d3295149578acc142f855bb3b75b3ec-b26dbda6d8a652930695c93bd07179f4.txt b/test/snapshots/parser/boolean_attributes_test/test_0009_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_1d3295149578acc142f855bb3b75b3ec-b26dbda6d8a652930695c93bd07179f4.txt
new file mode 100644
index 000000000..3dce31a53
--- /dev/null
+++ b/test/snapshots/parser/boolean_attributes_test/test_0009_boolean_attribute_on_void_element_followed_by_ERB_tag_with_track_whitespace_1d3295149578acc142f855bb3b75b3ec-b26dbda6d8a652930695c93bd07179f4.txt
@@ -0,0 +1,75 @@
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (2 items)
+ ├── @ HTMLElementNode (location: (1:0)-(3:6))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:22))
+ │ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ │ ├── tag_closing: ">" (location: (1:21)-(1:22))
+ │ │ ├── children: (4 items)
+ │ │ │ ├── @ WhitespaceNode (location: (1:4)-(1:5))
+ │ │ │ │ └── value: " " (location: (1:4)-(1:5))
+ │ │ │ │
+ │ │ │ ├── @ HTMLAttributeNode (location: (1:5)-(1:14))
+ │ │ │ │ ├── name:
+ │ │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:5)-(1:7))
+ │ │ │ │ │ └── children: (1 item)
+ │ │ │ │ │ └── @ LiteralNode (location: (1:5)-(1:7))
+ │ │ │ │ │ └── content: "id"
+ │ │ │ │ │
+ │ │ │ │ │
+ │ │ │ │ ├── equals: "=" (location: (1:7)-(1:8))
+ │ │ │ │ └── value:
+ │ │ │ │ └── @ HTMLAttributeValueNode (location: (1:8)-(1:14))
+ │ │ │ │ ├── open_quote: """ (location: (1:8)-(1:9))
+ │ │ │ │ ├── children: (1 item)
+ │ │ │ │ │ └── @ LiteralNode (location: (1:9)-(1:13))
+ │ │ │ │ │ └── content: "test"
+ │ │ │ │ │
+ │ │ │ │ ├── close_quote: """ (location: (1:13)-(1:14))
+ │ │ │ │ └── quoted: true
+ │ │ │ │
+ │ │ │ │
+ │ │ │ ├── @ WhitespaceNode (location: (1:14)-(1:15))
+ │ │ │ │ └── value: " " (location: (1:14)-(1:15))
+ │ │ │ │
+ │ │ │ └── @ HTMLAttributeNode (location: (1:15)-(1:21))
+ │ │ │ ├── name:
+ │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:15)-(1:21))
+ │ │ │ │ └── children: (1 item)
+ │ │ │ │ └── @ LiteralNode (location: (1:15)-(1:21))
+ │ │ │ │ └── content: "hidden"
+ │ │ │ │
+ │ │ │ │
+ │ │ │ ├── equals: ∅
+ │ │ │ └── value: ∅
+ │ │ │
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "div" (location: (1:1)-(1:4))
+ │ ├── body: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (2:5)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ ERBContentNode (location: (2:2)-(2:13))
+ │ │ │ ├── tag_opening: "<%=" (location: (2:2)-(2:5))
+ │ │ │ ├── content: " "hi" " (location: (2:5)-(2:11))
+ │ │ │ ├── tag_closing: "%>" (location: (2:11)-(2:13))
+ │ │ │ ├── parsed: true
+ │ │ │ └── valid: true
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:13)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── close_tag:
+ │ │ └── @ HTMLCloseTagNode (location: (3:0)-(3:6))
+ │ │ ├── tag_opening: "" (location: (3:0)-(3:2))
+ │ │ ├── tag_name: "div" (location: (3:2)-(3:5))
+ │ │ ├── children: []
+ │ │ └── tag_closing: ">" (location: (3:5)-(3:6))
+ │ │
+ │ ├── is_void: false
+ │ └── source: "HTML"
+ │
+ └── @ HTMLTextNode (location: (3:6)-(4:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0033_erb_output_with_=%gt_close_tag_e5e99c2c84b3c9a106b8cf08b02ce9d6.txt b/test/snapshots/parser/erb_test/test_0033_erb_output_with_=%gt_close_tag_e5e99c2c84b3c9a106b8cf08b02ce9d6.txt
new file mode 100644
index 000000000..06e906b7b
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0033_erb_output_with_=%gt_close_tag_e5e99c2c84b3c9a106b8cf08b02ce9d6.txt
@@ -0,0 +1,8 @@
+@ DocumentNode (location: (1:0)-(1:15))
+└── children: (1 item)
+ └── @ ERBContentNode (location: (1:0)-(1:15))
+ ├── tag_opening: "<%=" (location: (1:0)-(1:3))
+ ├── content: " "hello" " (location: (1:3)-(1:12))
+ ├── tag_closing: "=%>" (location: (1:12)-(1:15))
+ ├── parsed: true
+ └── valid: true
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0034_erb_if_with_=%gt_close_tag_040322f44f30d4766dc7bfe98114d53f.txt b/test/snapshots/parser/erb_test/test_0034_erb_if_with_=%gt_close_tag_040322f44f30d4766dc7bfe98114d53f.txt
new file mode 100644
index 000000000..5ac9012b3
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0034_erb_if_with_=%gt_close_tag_040322f44f30d4766dc7bfe98114d53f.txt
@@ -0,0 +1,47 @@
+@ DocumentNode (location: (1:0)-(4:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(3:10))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if true " (location: (1:2)-(1:11))
+ │ ├── tag_closing: "=%>" (location: (1:11)-(1:14))
+ │ ├── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:14)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ HTMLElementNode (location: (2:2)-(2:16))
+ │ │ │ ├── open_tag:
+ │ │ │ │ └── @ HTMLOpenTagNode (location: (2:2)-(2:5))
+ │ │ │ │ ├── tag_opening: "<" (location: (2:2)-(2:3))
+ │ │ │ │ ├── tag_name: "p" (location: (2:3)-(2:4))
+ │ │ │ │ ├── tag_closing: ">" (location: (2:4)-(2:5))
+ │ │ │ │ ├── children: []
+ │ │ │ │ └── is_void: false
+ │ │ │ │
+ │ │ │ ├── tag_name: "p" (location: (2:3)-(2:4))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (2:5)-(2:12))
+ │ │ │ │ └── content: "Content"
+ │ │ │ │
+ │ │ │ ├── close_tag:
+ │ │ │ │ └── @ HTMLCloseTagNode (location: (2:12)-(2:16))
+ │ │ │ │ ├── tag_opening: "" (location: (2:12)-(2:14))
+ │ │ │ │ ├── tag_name: "p" (location: (2:14)-(2:15))
+ │ │ │ │ ├── children: []
+ │ │ │ │ └── tag_closing: ">" (location: (2:15)-(2:16))
+ │ │ │ │
+ │ │ │ ├── is_void: false
+ │ │ │ └── source: "HTML"
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:16)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── subsequent: ∅
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (3:0)-(3:10))
+ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ ├── content: " end " (location: (3:2)-(3:7))
+ │ └── tag_closing: "=%>" (location: (3:7)-(3:10))
+ │
+ │
+ └── @ HTMLTextNode (location: (3:10)-(4:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/erb_test/test_0035_erb_if-elsif-else_with_=%gt_close_tag_276e28a241d24b1f8016400a1c84fef1.txt b/test/snapshots/parser/erb_test/test_0035_erb_if-elsif-else_with_=%gt_close_tag_276e28a241d24b1f8016400a1c84fef1.txt
new file mode 100644
index 000000000..8c281e427
--- /dev/null
+++ b/test/snapshots/parser/erb_test/test_0035_erb_if-elsif-else_with_=%gt_close_tag_276e28a241d24b1f8016400a1c84fef1.txt
@@ -0,0 +1,121 @@
+@ DocumentNode (location: (1:0)-(8:0))
+└── children: (2 items)
+ ├── @ ERBIfNode (location: (1:0)-(7:10))
+ │ ├── tag_opening: "<%" (location: (1:0)-(1:2))
+ │ ├── content: " if condition " (location: (1:2)-(1:16))
+ │ ├── tag_closing: "=%>" (location: (1:16)-(1:19))
+ │ ├── statements: (3 items)
+ │ │ ├── @ HTMLTextNode (location: (1:19)-(2:2))
+ │ │ │ └── content: "\n "
+ │ │ │
+ │ │ ├── @ HTMLElementNode (location: (2:2)-(2:13))
+ │ │ │ ├── open_tag:
+ │ │ │ │ └── @ HTMLOpenTagNode (location: (2:2)-(2:5))
+ │ │ │ │ ├── tag_opening: "<" (location: (2:2)-(2:3))
+ │ │ │ │ ├── tag_name: "p" (location: (2:3)-(2:4))
+ │ │ │ │ ├── tag_closing: ">" (location: (2:4)-(2:5))
+ │ │ │ │ ├── children: []
+ │ │ │ │ └── is_void: false
+ │ │ │ │
+ │ │ │ ├── tag_name: "p" (location: (2:3)-(2:4))
+ │ │ │ ├── body: (1 item)
+ │ │ │ │ └── @ HTMLTextNode (location: (2:5)-(2:9))
+ │ │ │ │ └── content: "True"
+ │ │ │ │
+ │ │ │ ├── close_tag:
+ │ │ │ │ └── @ HTMLCloseTagNode (location: (2:9)-(2:13))
+ │ │ │ │ ├── tag_opening: "" (location: (2:9)-(2:11))
+ │ │ │ │ ├── tag_name: "p" (location: (2:11)-(2:12))
+ │ │ │ │ ├── children: []
+ │ │ │ │ └── tag_closing: ">" (location: (2:12)-(2:13))
+ │ │ │ │
+ │ │ │ ├── is_void: false
+ │ │ │ └── source: "HTML"
+ │ │ │
+ │ │ └── @ HTMLTextNode (location: (2:13)-(3:0))
+ │ │ └── content: "\n"
+ │ │
+ │ ├── subsequent:
+ │ │ └── @ ERBIfNode (location: (3:0)-(5:0))
+ │ │ ├── tag_opening: "<%" (location: (3:0)-(3:2))
+ │ │ ├── content: " elsif other " (location: (3:2)-(3:15))
+ │ │ ├── tag_closing: "=%>" (location: (3:15)-(3:18))
+ │ │ ├── statements: (3 items)
+ │ │ │ ├── @ HTMLTextNode (location: (3:18)-(4:2))
+ │ │ │ │ └── content: "\n "
+ │ │ │ │
+ │ │ │ ├── @ HTMLElementNode (location: (4:2)-(4:14))
+ │ │ │ │ ├── open_tag:
+ │ │ │ │ │ └── @ HTMLOpenTagNode (location: (4:2)-(4:5))
+ │ │ │ │ │ ├── tag_opening: "<" (location: (4:2)-(4:3))
+ │ │ │ │ │ ├── tag_name: "p" (location: (4:3)-(4:4))
+ │ │ │ │ │ ├── tag_closing: ">" (location: (4:4)-(4:5))
+ │ │ │ │ │ ├── children: []
+ │ │ │ │ │ └── is_void: false
+ │ │ │ │ │
+ │ │ │ │ ├── tag_name: "p" (location: (4:3)-(4:4))
+ │ │ │ │ ├── body: (1 item)
+ │ │ │ │ │ └── @ HTMLTextNode (location: (4:5)-(4:10))
+ │ │ │ │ │ └── content: "Other"
+ │ │ │ │ │
+ │ │ │ │ ├── close_tag:
+ │ │ │ │ │ └── @ HTMLCloseTagNode (location: (4:10)-(4:14))
+ │ │ │ │ │ ├── tag_opening: "" (location: (4:10)-(4:12))
+ │ │ │ │ │ ├── tag_name: "p" (location: (4:12)-(4:13))
+ │ │ │ │ │ ├── children: []
+ │ │ │ │ │ └── tag_closing: ">" (location: (4:13)-(4:14))
+ │ │ │ │ │
+ │ │ │ │ ├── is_void: false
+ │ │ │ │ └── source: "HTML"
+ │ │ │ │
+ │ │ │ └── @ HTMLTextNode (location: (4:14)-(5:0))
+ │ │ │ └── content: "\n"
+ │ │ │
+ │ │ ├── subsequent:
+ │ │ │ └── @ ERBElseNode (location: (5:0)-(7:0))
+ │ │ │ ├── tag_opening: "<%" (location: (5:0)-(5:2))
+ │ │ │ ├── content: " else " (location: (5:2)-(5:8))
+ │ │ │ ├── tag_closing: "=%>" (location: (5:8)-(5:11))
+ │ │ │ └── statements: (3 items)
+ │ │ │ ├── @ HTMLTextNode (location: (5:11)-(6:2))
+ │ │ │ │ └── content: "\n "
+ │ │ │ │
+ │ │ │ ├── @ HTMLElementNode (location: (6:2)-(6:14))
+ │ │ │ │ ├── open_tag:
+ │ │ │ │ │ └── @ HTMLOpenTagNode (location: (6:2)-(6:5))
+ │ │ │ │ │ ├── tag_opening: "<" (location: (6:2)-(6:3))
+ │ │ │ │ │ ├── tag_name: "p" (location: (6:3)-(6:4))
+ │ │ │ │ │ ├── tag_closing: ">" (location: (6:4)-(6:5))
+ │ │ │ │ │ ├── children: []
+ │ │ │ │ │ └── is_void: false
+ │ │ │ │ │
+ │ │ │ │ ├── tag_name: "p" (location: (6:3)-(6:4))
+ │ │ │ │ ├── body: (1 item)
+ │ │ │ │ │ └── @ HTMLTextNode (location: (6:5)-(6:10))
+ │ │ │ │ │ └── content: "False"
+ │ │ │ │ │
+ │ │ │ │ ├── close_tag:
+ │ │ │ │ │ └── @ HTMLCloseTagNode (location: (6:10)-(6:14))
+ │ │ │ │ │ ├── tag_opening: "" (location: (6:10)-(6:12))
+ │ │ │ │ │ ├── tag_name: "p" (location: (6:12)-(6:13))
+ │ │ │ │ │ ├── children: []
+ │ │ │ │ │ └── tag_closing: ">" (location: (6:13)-(6:14))
+ │ │ │ │ │
+ │ │ │ │ ├── is_void: false
+ │ │ │ │ └── source: "HTML"
+ │ │ │ │
+ │ │ │ └── @ HTMLTextNode (location: (6:14)-(7:0))
+ │ │ │ └── content: "\n"
+ │ │ │
+ │ │ │
+ │ │ └── end_node: ∅
+ │ │
+ │ └── end_node:
+ │ └── @ ERBEndNode (location: (7:0)-(7:10))
+ │ ├── tag_opening: "<%" (location: (7:0)-(7:2))
+ │ ├── content: " end " (location: (7:2)-(7:7))
+ │ └── tag_closing: "=%>" (location: (7:7)-(7:10))
+ │
+ │
+ └── @ HTMLTextNode (location: (7:10)-(8:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/test/snapshots/parser/script_style_test/test_0035_script_tag_with_escaped_quotes_in_strings_3217a5ed01ac8ce6c44f9e106f9b4017.txt b/test/snapshots/parser/script_style_test/test_0035_script_tag_with_escaped_quotes_in_strings_3217a5ed01ac8ce6c44f9e106f9b4017.txt
index 43d5aa8af..48469075a 100644
--- a/test/snapshots/parser/script_style_test/test_0035_script_tag_with_escaped_quotes_in_strings_3217a5ed01ac8ce6c44f9e106f9b4017.txt
+++ b/test/snapshots/parser/script_style_test/test_0035_script_tag_with_escaped_quotes_in_strings_3217a5ed01ac8ce6c44f9e106f9b4017.txt
@@ -1,11 +1,4 @@
@ DocumentNode (location: (1:0)-(1:59))
-├── errors: (1 error)
-│ └── @ UnexpectedError (location: (1:36)-(1:37))
-│ ├── message: "Unexpected token. Expected: `TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, TOKEN_NBSP, TOKEN_AT, or TOKE`, found: `TOKEN_BACKSLASH`."
-│ ├── description: "Unexpected token"
-│ ├── expected: "TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, TOKEN_NBSP, TOKEN_AT, or TOKEN_NEWLINE"
-│ └── found: "TOKEN_BACKSLASH"
-│
└── children: (3 items)
├── @ HTMLElementNode (location: (1:0)-(1:36))
│ ├── open_tag:
@@ -31,8 +24,8 @@
│ ├── is_void: false
│ └── source: "HTML"
│
- ├── @ HTMLTextNode (location: (1:37)-(1:50))
- │ └── content: "\" yesterday\";"
+ ├── @ HTMLTextNode (location: (1:36)-(1:50))
+ │ └── content: "\\\" yesterday\";"
│
└── @ HTMLCloseTagNode (location: (1:50)-(1:59))
├── errors: (1 error)
diff --git a/test/snapshots/parser/tags_test/test_0027_stray_closing_tag_with_whitespace_0ee4b96cac701f87a8b6cb0cf5ad48bc.txt b/test/snapshots/parser/tags_test/test_0027_stray_closing_tag_with_whitespace_0ee4b96cac701f87a8b6cb0cf5ad48bc.txt
index 2a55af7ab..1bebae8b0 100644
--- a/test/snapshots/parser/tags_test/test_0027_stray_closing_tag_with_whitespace_0ee4b96cac701f87a8b6cb0cf5ad48bc.txt
+++ b/test/snapshots/parser/tags_test/test_0027_stray_closing_tag_with_whitespace_0ee4b96cac701f87a8b6cb0cf5ad48bc.txt
@@ -1,9 +1,9 @@
@ DocumentNode (location: (1:0)-(1:24))
├── errors: (1 error)
│ └── @ UnexpectedError (location: (1:16)-(1:17))
-│ ├── message: "Unexpected token. Expected: `TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, TOKEN_NBSP, TOKEN_AT, or TOKE`, found: `TOKEN_LT`."
+│ ├── message: "Unexpected token. Expected: `TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, TOKEN_NBSP, TOKEN_AT, TOKEN_B`, found: `TOKEN_LT`."
│ ├── description: "Unexpected token"
-│ ├── expected: "TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, TOKEN_NBSP, TOKEN_AT, or TOKEN_NEWLINE"
+│ ├── expected: "TOKEN_ERB_START, TOKEN_HTML_DOCTYPE, TOKEN_HTML_COMMENT_START, TOKEN_IDENTIFIER, TOKEN_WHITESPACE, TOKEN_NBSP, TOKEN_AT, TOKEN_BACKSLASH, or TOKEN_NEWLINE"
│ └── found: "TOKEN_LT"
│
└── children: (2 items)
diff --git a/test/snapshots/parser/tags_test/test_0047_closing_tag_with_newline_before_>_045e6b8518e8214f621854f251d1f323.txt b/test/snapshots/parser/tags_test/test_0047_closing_tag_with_newline_before_gt_045e6b8518e8214f621854f251d1f323.txt
similarity index 100%
rename from test/snapshots/parser/tags_test/test_0047_closing_tag_with_newline_before_>_045e6b8518e8214f621854f251d1f323.txt
rename to test/snapshots/parser/tags_test/test_0047_closing_tag_with_newline_before_gt_045e6b8518e8214f621854f251d1f323.txt
diff --git a/test/snapshots/parser/tags_test/test_0048_closing_tag_with_whitespace_and_newline_before_>_c39bc3d537f50940f8301bcf5be6a5e9.txt b/test/snapshots/parser/tags_test/test_0048_closing_tag_with_whitespace_and_newline_before_gt_c39bc3d537f50940f8301bcf5be6a5e9.txt
similarity index 100%
rename from test/snapshots/parser/tags_test/test_0048_closing_tag_with_whitespace_and_newline_before_>_c39bc3d537f50940f8301bcf5be6a5e9.txt
rename to test/snapshots/parser/tags_test/test_0048_closing_tag_with_whitespace_and_newline_before_gt_c39bc3d537f50940f8301bcf5be6a5e9.txt
diff --git a/test/snapshots/parser/tags_test/test_0049_multiple_closing_tags_with_newlines_before_>_c9d0ca9f702b1a3786a1a8ba835fb475.txt b/test/snapshots/parser/tags_test/test_0049_multiple_closing_tags_with_newlines_before_gt_c9d0ca9f702b1a3786a1a8ba835fb475.txt
similarity index 100%
rename from test/snapshots/parser/tags_test/test_0049_multiple_closing_tags_with_newlines_before_>_c9d0ca9f702b1a3786a1a8ba835fb475.txt
rename to test/snapshots/parser/tags_test/test_0049_multiple_closing_tags_with_newlines_before_gt_c9d0ca9f702b1a3786a1a8ba835fb475.txt
diff --git a/test/snapshots/parser/tags_test/test_0051_self-closing_tag_with_closing_tag_having_newline_before_>_004b02b673d57d73eb07a649eb08e705.txt b/test/snapshots/parser/tags_test/test_0051_self-closing_tag_with_closing_tag_having_newline_before_gt_004b02b673d57d73eb07a649eb08e705.txt
similarity index 100%
rename from test/snapshots/parser/tags_test/test_0051_self-closing_tag_with_closing_tag_having_newline_before_>_004b02b673d57d73eb07a649eb08e705.txt
rename to test/snapshots/parser/tags_test/test_0051_self-closing_tag_with_closing_tag_having_newline_before_gt_004b02b673d57d73eb07a649eb08e705.txt
diff --git a/test/snapshots/parser/text_content_test/test_0006_text_content_that_exceeds_initial_buffer_T_size_(ca._4K)_998d9cbd9b8794fa030c888bfa09cad4.txt b/test/snapshots/parser/text_content_test/test_0006_text_content_that_exceeds_initial_hb_buffer_T_size_(ca._4K)_998d9cbd9b8794fa030c888bfa09cad4.txt
similarity index 100%
rename from test/snapshots/parser/text_content_test/test_0006_text_content_that_exceeds_initial_buffer_T_size_(ca._4K)_998d9cbd9b8794fa030c888bfa09cad4.txt
rename to test/snapshots/parser/text_content_test/test_0006_text_content_that_exceeds_initial_hb_buffer_T_size_(ca._4K)_998d9cbd9b8794fa030c888bfa09cad4.txt
diff --git a/test/snapshots/parser/text_content_test/test_0007_text_content_that_exceeds_initial_buffer_T_size_(ca._8K)_45e3ecb8edc7dbec7d68b72b5d083439.txt b/test/snapshots/parser/text_content_test/test_0007_text_content_that_exceeds_initial_hb_buffer_T_size_(ca._8K)_45e3ecb8edc7dbec7d68b72b5d083439.txt
similarity index 100%
rename from test/snapshots/parser/text_content_test/test_0007_text_content_that_exceeds_initial_buffer_T_size_(ca._8K)_45e3ecb8edc7dbec7d68b72b5d083439.txt
rename to test/snapshots/parser/text_content_test/test_0007_text_content_that_exceeds_initial_hb_buffer_T_size_(ca._8K)_45e3ecb8edc7dbec7d68b72b5d083439.txt
diff --git a/test/snapshots/parser/text_content_test/test_0034_backslash-prefixed_text_stays_literal_-_issue_635_816fd02d8670264d606d92812679f159.txt b/test/snapshots/parser/text_content_test/test_0034_backslash-prefixed_text_stays_literal_-_issue_635_816fd02d8670264d606d92812679f159.txt
new file mode 100644
index 000000000..e75b2ccb2
--- /dev/null
+++ b/test/snapshots/parser/text_content_test/test_0034_backslash-prefixed_text_stays_literal_-_issue_635_816fd02d8670264d606d92812679f159.txt
@@ -0,0 +1,25 @@
+@ DocumentNode (location: (1:0)-(1:22))
+└── children: (1 item)
+ └── @ HTMLElementNode (location: (1:0)-(1:22))
+ ├── open_tag:
+ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:3))
+ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ ├── tag_name: "p" (location: (1:1)-(1:2))
+ │ ├── tag_closing: ">" (location: (1:2)-(1:3))
+ │ ├── children: []
+ │ └── is_void: false
+ │
+ ├── tag_name: "p" (location: (1:1)-(1:2))
+ ├── body: (1 item)
+ │ └── @ HTMLTextNode (location: (1:3)-(1:18))
+ │ └── content: "\\Asome-regexp\\z"
+ │
+ ├── close_tag:
+ │ └── @ HTMLCloseTagNode (location: (1:18)-(1:22))
+ │ ├── tag_opening: "" (location: (1:18)-(1:20))
+ │ ├── tag_name: "p" (location: (1:20)-(1:21))
+ │ ├── children: []
+ │ └── tag_closing: ">" (location: (1:21)-(1:22))
+ │
+ ├── is_void: false
+ └── source: "HTML"
\ No newline at end of file
diff --git a/test/snapshots/parser/text_content_test/test_0035_backslash-prefixed_text_-_issue_633_fd1ab6af93689cddc537bac4cbdaefde.txt b/test/snapshots/parser/text_content_test/test_0035_backslash-prefixed_text_-_issue_633_fd1ab6af93689cddc537bac4cbdaefde.txt
new file mode 100644
index 000000000..e2d4e504d
--- /dev/null
+++ b/test/snapshots/parser/text_content_test/test_0035_backslash-prefixed_text_-_issue_633_fd1ab6af93689cddc537bac4cbdaefde.txt
@@ -0,0 +1,72 @@
+@ DocumentNode (location: (1:0)-(2:0))
+└── children: (2 items)
+ ├── @ HTMLElementNode (location: (1:0)-(1:119))
+ │ ├── open_tag:
+ │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:16))
+ │ │ ├── tag_opening: "<" (location: (1:0)-(1:1))
+ │ │ ├── tag_name: "p" (location: (1:1)-(1:2))
+ │ │ ├── tag_closing: ">" (location: (1:15)-(1:16))
+ │ │ ├── children: (1 item)
+ │ │ │ └── @ HTMLAttributeNode (location: (1:3)-(1:15))
+ │ │ │ ├── name:
+ │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:3)-(1:8))
+ │ │ │ │ └── children: (1 item)
+ │ │ │ │ └── @ LiteralNode (location: (1:3)-(1:8))
+ │ │ │ │ └── content: "class"
+ │ │ │ │
+ │ │ │ │
+ │ │ │ ├── equals: "=" (location: (1:8)-(1:9))
+ │ │ │ └── value:
+ │ │ │ └── @ HTMLAttributeValueNode (location: (1:9)-(1:15))
+ │ │ │ ├── open_quote: """ (location: (1:9)-(1:10))
+ │ │ │ ├── children: (1 item)
+ │ │ │ │ └── @ LiteralNode (location: (1:10)-(1:14))
+ │ │ │ │ └── content: "help"
+ │ │ │ │
+ │ │ │ ├── close_quote: """ (location: (1:14)-(1:15))
+ │ │ │ └── quoted: true
+ │ │ │
+ │ │ │
+ │ │ └── is_void: false
+ │ │
+ │ ├── tag_name: "p" (location: (1:1)-(1:2))
+ │ ├── body: (2 items)
+ │ │ ├── @ HTMLTextNode (location: (1:16)-(1:70))
+ │ │ │ └── content: "Regexp with captures, must consume whole string like: "
+ │ │ │
+ │ │ └── @ HTMLElementNode (location: (1:70)-(1:115))
+ │ │ ├── open_tag:
+ │ │ │ └── @ HTMLOpenTagNode (location: (1:70)-(1:75))
+ │ │ │ ├── tag_opening: "<" (location: (1:70)-(1:71))
+ │ │ │ ├── tag_name: "kbd" (location: (1:71)-(1:74))
+ │ │ │ ├── tag_closing: ">" (location: (1:74)-(1:75))
+ │ │ │ ├── children: []
+ │ │ │ └── is_void: false
+ │ │ │
+ │ │ ├── tag_name: "kbd" (location: (1:71)-(1:74))
+ │ │ ├── body: (1 item)
+ │ │ │ └── @ HTMLTextNode (location: (1:75)-(1:109))
+ │ │ │ └── content: "\\Ahttps?://github.com/+([^/]+).*\\z"
+ │ │ │
+ │ │ ├── close_tag:
+ │ │ │ └── @ HTMLCloseTagNode (location: (1:109)-(1:115))
+ │ │ │ ├── tag_opening: "" (location: (1:109)-(1:111))
+ │ │ │ ├── tag_name: "kbd" (location: (1:111)-(1:114))
+ │ │ │ ├── children: []
+ │ │ │ └── tag_closing: ">" (location: (1:114)-(1:115))
+ │ │ │
+ │ │ ├── is_void: false
+ │ │ └── source: "HTML"
+ │ │
+ │ ├── close_tag:
+ │ │ └── @ HTMLCloseTagNode (location: (1:115)-(1:119))
+ │ │ ├── tag_opening: "" (location: (1:115)-(1:117))
+ │ │ ├── tag_name: "p" (location: (1:117)-(1:118))
+ │ │ ├── children: []
+ │ │ └── tag_closing: ">" (location: (1:118)-(1:119))
+ │ │
+ │ ├── is_void: false
+ │ └── source: "HTML"
+ │
+ └── @ HTMLTextNode (location: (1:119)-(2:0))
+ └── content: "\n"
\ No newline at end of file
diff --git a/wasm/extension_helpers.cpp b/wasm/extension_helpers.cpp
index 490a81412..3118cf13e 100644
--- a/wasm/extension_helpers.cpp
+++ b/wasm/extension_helpers.cpp
@@ -7,12 +7,12 @@
#include "nodes.h"
extern "C" {
-#include "../src/include/array.h"
+#include "../src/include/util/hb_array.h"
#include "../src/include/ast_node.h"
#include "../src/include/ast_nodes.h"
#include "../src/include/pretty_print.h"
#include "../src/include/ast_pretty_print.h"
-#include "../src/include/buffer.h"
+#include "../src/include/util/hb_buffer.h"
#include "../src/include/herb.h"
#include "../src/include/token.h"
#include "../src/include/position.h"
@@ -27,44 +27,32 @@ val CreateString(const char* string) {
return string ? val(string) : val::null();
}
-val CreatePosition(position_T* position) {
- if (!position) {
- return val::null();
- }
-
+val CreatePosition(position_T position) {
val Object = val::global("Object");
val result = Object.new_();
- result.set("line", position->line);
- result.set("column", position->column);
+ result.set("line", position.line);
+ result.set("column", position.column);
return result;
}
-val CreateLocation(location_T* location) {
- if (!location) {
- return val::null();
- }
-
+val CreateLocation(location_T location) {
val Object = val::global("Object");
val result = Object.new_();
- result.set("start", CreatePosition(location->start));
- result.set("end", CreatePosition(location->end));
+ result.set("start", CreatePosition(location.start));
+ result.set("end", CreatePosition(location.end));
return result;
}
-val CreateRange(range_T* range) {
- if (!range) {
- return val::null();
- }
-
+val CreateRange(range_T range) {
val Array = val::global("Array");
val result = Array.new_();
- result.call("push", range->from);
- result.call("push", range->to);
+ result.call("push", range.from);
+ result.call("push", range.to);
return result;
}
@@ -90,7 +78,7 @@ val CreateToken(token_T* token) {
return result;
}
-val CreateLexResult(array_T* tokens, const std::string& source) {
+val CreateLexResult(hb_array_T* tokens, const std::string& source) {
val Object = val::global("Object");
val Array = val::global("Array");
@@ -100,8 +88,8 @@ val CreateLexResult(array_T* tokens, const std::string& source) {
val warningsArray = Array.new_();
if (tokens) {
- for (size_t i = 0; i < array_size(tokens); i++) {
- token_T* token = (token_T*)array_get(tokens, i);
+ for (size_t i = 0; i < hb_array_size(tokens); i++) {
+ token_T* token = (token_T*)hb_array_get(tokens, i);
if (token) {
tokensArray.call("push", CreateToken(token));
}
diff --git a/wasm/extension_helpers.h b/wasm/extension_helpers.h
index 923df6f3b..e4d2332f4 100644
--- a/wasm/extension_helpers.h
+++ b/wasm/extension_helpers.h
@@ -14,11 +14,11 @@ extern "C" {
}
emscripten::val CreateString(const char* string);
-emscripten::val CreatePosition(position_T* position);
-emscripten::val CreateLocation(location_T* location);
-emscripten::val CreateRange(range_T* range);
+emscripten::val CreatePosition(position_T position);
+emscripten::val CreateLocation(location_T location);
+emscripten::val CreateRange(range_T range);
emscripten::val CreateToken(token_T* token);
-emscripten::val CreateLexResult(array_T* tokens, const std::string& source);
+emscripten::val CreateLexResult(hb_array_T* tokens, const std::string& source);
emscripten::val CreateParseResult(AST_DOCUMENT_NODE_T *root, const std::string& source);
#endif
diff --git a/wasm/herb-wasm.cpp b/wasm/herb-wasm.cpp
index 91d270fdb..e1e5ce06e 100644
--- a/wasm/herb-wasm.cpp
+++ b/wasm/herb-wasm.cpp
@@ -8,11 +8,11 @@
extern "C" {
#include "../src/include/analyze.h"
-#include "../src/include/array.h"
+#include "../src/include/util/hb_array.h"
#include "../src/include/ast_node.h"
#include "../src/include/ast_nodes.h"
#include "../src/include/ast_pretty_print.h"
-#include "../src/include/buffer.h"
+#include "../src/include/util/hb_buffer.h"
#include "../src/include/extract.h"
#include "../src/include/herb.h"
#include "../src/include/location.h"
@@ -25,7 +25,7 @@ extern "C" {
using namespace emscripten;
val Herb_lex(const std::string& source) {
- array_T* tokens = herb_lex(source.c_str());
+ hb_array_T* tokens = herb_lex(source.c_str());
val result = CreateLexResult(tokens, source);
@@ -60,20 +60,22 @@ val Herb_parse(const std::string& source, val options) {
}
std::string Herb_extract_ruby(const std::string& source) {
- buffer_T output;
- buffer_init(&output);
+ hb_buffer_T output;
+ hb_buffer_init(&output, source.length());
+
herb_extract_ruby_to_buffer(source.c_str(), &output);
- std::string result(buffer_value(&output));
- buffer_free(&output);
+ std::string result(hb_buffer_value(&output));
+ free(output.value);
return result;
}
std::string Herb_extract_html(const std::string& source) {
- buffer_T output;
- buffer_init(&output);
+ hb_buffer_T output;
+ hb_buffer_init(&output, source.length());
+
herb_extract_html_to_buffer(source.c_str(), &output);
- std::string result(buffer_value(&output));
- buffer_free(&output);
+ std::string result(hb_buffer_value(&output));
+ free(output.value);
return result;
}
diff --git a/yarn.lock b/yarn.lock
index 67c73992f..bd70e0153 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -219,135 +219,135 @@
dependencies:
tslib "^2.4.0"
-"@esbuild/aix-ppc64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz#ee6b7163a13528e099ecf562b972f2bcebe0aa97"
- integrity sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==
-
-"@esbuild/android-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz#115fc76631e82dd06811bfaf2db0d4979c16e2cb"
- integrity sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==
-
-"@esbuild/android-arm@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.10.tgz#8d5811912da77f615398611e5bbc1333fe321aa9"
- integrity sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==
-
-"@esbuild/android-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.10.tgz#e3e96516b2d50d74105bb92594c473e30ddc16b1"
- integrity sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==
-
-"@esbuild/darwin-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz#6af6bb1d05887dac515de1b162b59dc71212ed76"
- integrity sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==
-
-"@esbuild/darwin-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz#99ae82347fbd336fc2d28ffd4f05694e6e5b723d"
- integrity sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==
-
-"@esbuild/freebsd-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz#0c6d5558a6322b0bdb17f7025c19bd7d2359437d"
- integrity sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==
-
-"@esbuild/freebsd-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz#8c35873fab8c0857a75300a3dcce4324ca0b9844"
- integrity sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==
-
-"@esbuild/linux-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz#3edc2f87b889a15b4cedaf65f498c2bed7b16b90"
- integrity sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==
-
-"@esbuild/linux-arm@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz#86501cfdfb3d110176d80c41b27ed4611471cde7"
- integrity sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==
-
-"@esbuild/linux-ia32@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz#e6589877876142537c6864680cd5d26a622b9d97"
- integrity sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==
-
-"@esbuild/linux-loong64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz#11119e18781f136d8083ea10eb6be73db7532de8"
- integrity sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==
-
-"@esbuild/linux-mips64el@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz#3052f5436b0c0c67a25658d5fc87f045e7def9e6"
- integrity sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==
-
-"@esbuild/linux-ppc64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz#2f098920ee5be2ce799f35e367b28709925a8744"
- integrity sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==
-
-"@esbuild/linux-riscv64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz#fa51d7fd0a22a62b51b4b94b405a3198cf7405dd"
- integrity sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==
-
-"@esbuild/linux-s390x@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz#a27642e36fc282748fdb38954bd3ef4f85791e8a"
- integrity sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==
-
-"@esbuild/linux-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz#9d9b09c0033d17529570ced6d813f98315dfe4e9"
- integrity sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==
-
-"@esbuild/netbsd-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz#25c09a659c97e8af19e3f2afd1c9190435802151"
- integrity sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==
-
-"@esbuild/netbsd-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz#7fa5f6ffc19be3a0f6f5fd32c90df3dc2506937a"
- integrity sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==
-
-"@esbuild/openbsd-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz#8faa6aa1afca0c6d024398321d6cb1c18e72a1c3"
- integrity sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==
-
-"@esbuild/openbsd-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz#a42979b016f29559a8453d32440d3c8cd420af5e"
- integrity sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==
-
-"@esbuild/openharmony-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz#fd87bfeadd7eeb3aa384bbba907459ffa3197cb1"
- integrity sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==
-
-"@esbuild/sunos-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz#3a18f590e36cb78ae7397976b760b2b8c74407f4"
- integrity sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==
-
-"@esbuild/win32-arm64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz#e71741a251e3fd971408827a529d2325551f530c"
- integrity sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==
-
-"@esbuild/win32-ia32@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz#c6f010b5d3b943d8901a0c87ea55f93b8b54bf94"
- integrity sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==
-
-"@esbuild/win32-x64@0.25.10":
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz#e4b3e255a1b4aea84f6e1d2ae0b73f826c3785bd"
- integrity sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==
+"@esbuild/aix-ppc64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49"
+ integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==
+
+"@esbuild/android-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03"
+ integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==
+
+"@esbuild/android-arm@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae"
+ integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==
+
+"@esbuild/android-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6"
+ integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==
+
+"@esbuild/darwin-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz#0a678c4ac4bf8717e67481e1a797e6c152f93c84"
+ integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==
+
+"@esbuild/darwin-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe"
+ integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==
+
+"@esbuild/freebsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a"
+ integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==
+
+"@esbuild/freebsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb"
+ integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==
+
+"@esbuild/linux-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5"
+ integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==
+
+"@esbuild/linux-arm@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f"
+ integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==
+
+"@esbuild/linux-ia32@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b"
+ integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==
+
+"@esbuild/linux-loong64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb"
+ integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==
+
+"@esbuild/linux-mips64el@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5"
+ integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==
+
+"@esbuild/linux-ppc64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74"
+ integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==
+
+"@esbuild/linux-riscv64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273"
+ integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==
+
+"@esbuild/linux-s390x@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263"
+ integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==
+
+"@esbuild/linux-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910"
+ integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==
+
+"@esbuild/netbsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077"
+ integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==
+
+"@esbuild/netbsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034"
+ integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==
+
+"@esbuild/openbsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad"
+ integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==
+
+"@esbuild/openbsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2"
+ integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==
+
+"@esbuild/openharmony-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1"
+ integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==
+
+"@esbuild/sunos-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244"
+ integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==
+
+"@esbuild/win32-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935"
+ integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==
+
+"@esbuild/win32-ia32@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343"
+ integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==
+
+"@esbuild/win32-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f"
+ integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
version "4.7.0"
@@ -3930,37 +3930,37 @@ esbuild-plugin-copy@^2.1.1:
fs-extra "^10.0.1"
globby "^11.0.3"
-esbuild@^0.25.0, esbuild@^0.25.10, esbuild@~0.25.0:
- version "0.25.10"
- resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.10.tgz#37f5aa5cd14500f141be121c01b096ca83ac34a9"
- integrity sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==
+esbuild@^0.25.0, esbuild@^0.25.11, esbuild@~0.25.0:
+ version "0.25.11"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.11.tgz#0f31b82f335652580f75ef6897bba81962d9ae3d"
+ integrity sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==
optionalDependencies:
- "@esbuild/aix-ppc64" "0.25.10"
- "@esbuild/android-arm" "0.25.10"
- "@esbuild/android-arm64" "0.25.10"
- "@esbuild/android-x64" "0.25.10"
- "@esbuild/darwin-arm64" "0.25.10"
- "@esbuild/darwin-x64" "0.25.10"
- "@esbuild/freebsd-arm64" "0.25.10"
- "@esbuild/freebsd-x64" "0.25.10"
- "@esbuild/linux-arm" "0.25.10"
- "@esbuild/linux-arm64" "0.25.10"
- "@esbuild/linux-ia32" "0.25.10"
- "@esbuild/linux-loong64" "0.25.10"
- "@esbuild/linux-mips64el" "0.25.10"
- "@esbuild/linux-ppc64" "0.25.10"
- "@esbuild/linux-riscv64" "0.25.10"
- "@esbuild/linux-s390x" "0.25.10"
- "@esbuild/linux-x64" "0.25.10"
- "@esbuild/netbsd-arm64" "0.25.10"
- "@esbuild/netbsd-x64" "0.25.10"
- "@esbuild/openbsd-arm64" "0.25.10"
- "@esbuild/openbsd-x64" "0.25.10"
- "@esbuild/openharmony-arm64" "0.25.10"
- "@esbuild/sunos-x64" "0.25.10"
- "@esbuild/win32-arm64" "0.25.10"
- "@esbuild/win32-ia32" "0.25.10"
- "@esbuild/win32-x64" "0.25.10"
+ "@esbuild/aix-ppc64" "0.25.11"
+ "@esbuild/android-arm" "0.25.11"
+ "@esbuild/android-arm64" "0.25.11"
+ "@esbuild/android-x64" "0.25.11"
+ "@esbuild/darwin-arm64" "0.25.11"
+ "@esbuild/darwin-x64" "0.25.11"
+ "@esbuild/freebsd-arm64" "0.25.11"
+ "@esbuild/freebsd-x64" "0.25.11"
+ "@esbuild/linux-arm" "0.25.11"
+ "@esbuild/linux-arm64" "0.25.11"
+ "@esbuild/linux-ia32" "0.25.11"
+ "@esbuild/linux-loong64" "0.25.11"
+ "@esbuild/linux-mips64el" "0.25.11"
+ "@esbuild/linux-ppc64" "0.25.11"
+ "@esbuild/linux-riscv64" "0.25.11"
+ "@esbuild/linux-s390x" "0.25.11"
+ "@esbuild/linux-x64" "0.25.11"
+ "@esbuild/netbsd-arm64" "0.25.11"
+ "@esbuild/netbsd-x64" "0.25.11"
+ "@esbuild/openbsd-arm64" "0.25.11"
+ "@esbuild/openbsd-x64" "0.25.11"
+ "@esbuild/openharmony-arm64" "0.25.11"
+ "@esbuild/sunos-x64" "0.25.11"
+ "@esbuild/win32-arm64" "0.25.11"
+ "@esbuild/win32-ia32" "0.25.11"
+ "@esbuild/win32-x64" "0.25.11"
escalade@^3.1.1, escalade@^3.2.0:
version "3.2.0"
@@ -7462,17 +7462,17 @@ pkg-types@^2.3.0:
exsolve "^1.0.7"
pathe "^2.0.3"
-playwright-core@1.55.0:
- version "1.55.0"
- resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.55.0.tgz#ec8a9f8ef118afb3e86e0f46f1393e3bea32adf4"
- integrity sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==
+playwright-core@1.55.1:
+ version "1.55.1"
+ resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.55.1.tgz#5d3bb1846bc4289d364ea1a9dcb33f14545802e9"
+ integrity sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==
-playwright@^1.51.0:
- version "1.55.0"
- resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.55.0.tgz#7aca7ac3ffd9e083a8ad8b2514d6f9ba401cc78b"
- integrity sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==
+playwright@^1.55.1:
+ version "1.55.1"
+ resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.55.1.tgz#8a9954e9e61ed1ab479212af9be336888f8b3f0e"
+ integrity sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==
dependencies:
- playwright-core "1.55.0"
+ playwright-core "1.55.1"
optionalDependencies:
fsevents "2.3.2"
@@ -9757,10 +9757,10 @@ vite-node@3.2.4:
pathe "^2.0.3"
vite "^5.0.0 || ^6.0.0 || ^7.0.0-0"
-"vite@^5.0.0 || ^6.0.0 || ^7.0.0-0", vite@^7.1.2, vite@^7.1.7:
- version "7.1.7"
- resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.7.tgz#ed3f9f06e21d6574fe1ad425f6b0912d027ffc13"
- integrity sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==
+"vite@^5.0.0 || ^6.0.0 || ^7.0.0-0", vite@^7.1.11, vite@^7.1.2:
+ version "7.1.11"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.11.tgz#4d006746112fee056df64985191e846ebfb6007e"
+ integrity sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==
dependencies:
esbuild "^0.25.0"
fdir "^6.5.0"