ma
marcoroth
GitHub
herb
Workspace
GitHub
CI Pipeline Executions
Filtered
Runs
Demo
Insights
Compare tasks
Analytics
Sign in
Toggle sidebar
Overview
⌘K
herb
Loading workspace stats
Loading workspace insights...
Statistics interval
7 days
30 days
Latest CI Pipeline Executions
Status
Fix filter
Filter
Fuzzy
Filter range
Sort by
Sort by
Start time
Sort ascending
Sort descending
Succeeded
main
d36e19fb Engine: Introduce `SlotSubtree` to render the markup a slot covers (#2283) This pull request introduces `Herb::Engine::SlotSubtree`, which compiles the markup behind one slot, for the two cases a payload of values cannot carry. `SlotIndex#apply` reports a deferred entry when the page is asked for something values cannot express, which is a conditional taking a branch that never rendered, and a collection gaining an item there is no row to copy. Both name a slot, and nothing on the server answered one. ```ruby subtree = Herb::Engine::SlotSubtree.new(source, filename: "app/views/posts/index.html.erb") view.instance_eval(subtree.source_for(0)) #=> "<li>one</li><li>two</li>" ``` `SubtreeCompiler` renders the node at a `node_path` and `SlotVisitor` records the `node_path` of every slot, so a slot index is all that is needed to reach its markup. The two agreeing on what a `node_path` means is what makes this a lookup. #### An attribute is refused A path indexes an element's body and does not descend into its open tag, so an attribute slot's path names the element holding it. Compiling that would hand back `<div class="card">x</div>` where the caller asked for `card`. Those slots are values, and values is how they come back. #### Asking for only what changed `subtree_slots` says which slots a change needs markup for, so a request naming what changed can be answered without asking the page. ```ruby dependencies.subtree_slots("app/views/posts/index.html.erb", ["@admin"]) #=> [{ file: ".../index.html.erb", version: "a1b2c3d4", index: 0, mode: :structural }] ``` Only structural slots appear, because that is the whole of what values cannot say. Changing something a branch merely displays asks for nothing. #### What this does not do It does not make the render cheaper. `SubtreeCompiler` runs the whole template and keeps one node's output, which its own documentation says is the answer that is correct without knowing which expressions the target depends on: > Pruning the work that only fed discarded output is a separate question, and answering it needs to know which expressions the target actually depends on. Running everything is the answer that is correct without that analysis. That analysis now exists, so the question can be asked. It is not asked here, because answering it means changing what the compiler emits and taking on the side effects that come with skipping work.
by Marco Roth
M
Succeeded
main
d7c8a741 Client: Rename a collection's "row" to an "item" and fold content anchor (#2278) This pull request settles the vocabulary of the slot marker format on one set of words, and reduces the two attributes that anchor a slot to an element down to one. Nothing outside this stack reads the format yet, which is the only reason this is cheap. Every one of these changes is breaking, and each gets more expensive the longer it waits. `row` is table vocabulary, and a collection repeats `<li>`, cards and `<option>` as readily as it repeats `<tr>`. The slot type beside it is already `collection`, and a collection has items, which is also what Rails calls them when `render collection:` renders one partial per item. ```html <!--herb-item:0:42--><li data-herb-slot="1:child">Marco</li><!--/herb-item:0--> ``` ```ruby { 0 => { items: { "42" => { 1 => "Marco" } } } } ``` ```typescript slots.itemsFor(file, 0) slots.slotInItem(file, 0, "42", 2) slots.updateItem(collection, "42", html) ``` Also, an element's content slot had an attribute of its own while every other slot on that element was in `data-herb-slot`, and the index called what the first one produced a `content` anchor, so the format and the index disagreed about its name. The anchor kind is a property of the slot's type and does not need an attribute to carry it. `child` means the value goes inside the element, and every other type on that attribute means the element itself: ```diff -<li id="1" data-herb-slot="1:attribute:id" data-herb-child="2">Marco</li> +<li id="1" data-herb-slot="1:attribute:id 2:child">Marco</li> ``` The list is space-separated now, which is what HTML uses for token lists, and it means a slot can be found with a selector instead of by parsing the attribute: ```javascript document.querySelectorAll('[data-herb-slot~="2:child"]') ``` `~=` matches one token of a whitespace-separated list, so the comma form could not be queried this way. A content-only element grows three characters, an element carrying both roles shrinks by a whole attribute. Some values of `SlotOperation` named what kind of thing changed and two named what happened to one item, and rewriting an item's markup fell through to `markup` with a key set: ```diff -{ operation: "markup", key: "42" } +{ operation: "item-updated", key: "42" } ``` `markup` keeps its meaning for a whole slot rewritten at once, which is the case that carries no key. Nothing read the operation except the Dev Tools flash, which looks it up in a colour map, so this costs one entry there. No test covered that path before, and one does now. Slots are experimental and the marker format is not stable yet.
by Marco Roth
M
Succeeded
main
5c645f51 CI: Remove `labeled` from `javascript.yml` workflow
by Marco Roth
M
Succeeded
main
7bc7e9f5 Parser: Introduce `XMLProcessingInstructionNode` (#2266) This pull request introduces `XMLProcessingInstructionNode` so that XML processing instructions get their own node and survive a round trip through the parser, the printers, and the engine. Herb only recognized the XML declaration, `<?xml version="1.0"?>`. Every other processing instruction fell through to text content, so `<?marker name="placeholder">` came back as an `HTMLTextNode` with no way to tell it apart from the prose around it. The `<?xml` check was also a plain prefix match, which meant `<?xml-stylesheet type="text/xsl" href="style.xsl"?>` lexed as an XML declaration with a `-stylesheet ...` literal hanging off it. ```xml <?marker name="placeholder"> ``` ```js @ DocumentNode (location: (1:0)-(1:28)) └── children: (1 item) └── @ XMLProcessingInstructionNode (location: (1:0)-(1:28)) ├── tag_opening: "<?" (location: (1:0)-(1:2)) ├── target: "marker" (location: (1:2)-(1:8)) ├── children: (1 item) │ └── @ LiteralNode (location: (1:8)-(1:27)) │ └── content: " name=\"placeholder\"" │ └── tag_closing: ">" (location: (1:27)-(1:28)) ``` ERB inside an instruction is interpolated into `children`, the same way it already works for comments and doctypes. ```xml <?marker name="<%= placeholder_name %>"> ``` ```js @ DocumentNode (location: (1:0)-(1:40)) └── children: (1 item) └── @ XMLProcessingInstructionNode (location: (1:0)-(1:40)) ├── tag_opening: "<?" (location: (1:0)-(1:2)) ├── target: "marker" (location: (1:2)-(1:8)) ├── children: (3 items) │ ├── @ LiteralNode (location: (1:8)-(1:15)) │ │ └── content: " name=\"" │ │ │ ├── @ ERBContentNode (location: (1:15)-(1:38)) │ │ ├── tag_opening: "<%=" (location: (1:15)-(1:18)) │ │ ├── content: " placeholder_name " (location: (1:18)-(1:36)) │ │ ├── tag_closing: "%>" (location: (1:36)-(1:38)) │ │ ├── parsed: true │ │ └── valid: true │ │ │ └── @ LiteralNode (location: (1:38)-(1:39)) │ └── content: "\"" │ └── tag_closing: ">" (location: (1:39)-(1:40)) ``` Both `?>` and `>` terminate an instruction, so `<?target content>` and `<?target content?>` both parse. Resolves #2259
by Marco Roth
M
Failed
main
7bc7e9f5 Parser: Introduce `XMLProcessingInstructionNode` (#2266) This pull request introduces `XMLProcessingInstructionNode` so that XML processing instructions get their own node and survive a round trip through the parser, the printers, and the engine. Herb only recognized the XML declaration, `<?xml version="1.0"?>`. Every other processing instruction fell through to text content, so `<?marker name="placeholder">` came back as an `HTMLTextNode` with no way to tell it apart from the prose around it. The `<?xml` check was also a plain prefix match, which meant `<?xml-stylesheet type="text/xsl" href="style.xsl"?>` lexed as an XML declaration with a `-stylesheet ...` literal hanging off it. ```xml <?marker name="placeholder"> ``` ```js @ DocumentNode (location: (1:0)-(1:28)) └── children: (1 item) └── @ XMLProcessingInstructionNode (location: (1:0)-(1:28)) ├── tag_opening: "<?" (location: (1:0)-(1:2)) ├── target: "marker" (location: (1:2)-(1:8)) ├── children: (1 item) │ └── @ LiteralNode (location: (1:8)-(1:27)) │ └── content: " name=\"placeholder\"" │ └── tag_closing: ">" (location: (1:27)-(1:28)) ``` ERB inside an instruction is interpolated into `children`, the same way it already works for comments and doctypes. ```xml <?marker name="<%= placeholder_name %>"> ``` ```js @ DocumentNode (location: (1:0)-(1:40)) └── children: (1 item) └── @ XMLProcessingInstructionNode (location: (1:0)-(1:40)) ├── tag_opening: "<?" (location: (1:0)-(1:2)) ├── target: "marker" (location: (1:2)-(1:8)) ├── children: (3 items) │ ├── @ LiteralNode (location: (1:8)-(1:15)) │ │ └── content: " name=\"" │ │ │ ├── @ ERBContentNode (location: (1:15)-(1:38)) │ │ ├── tag_opening: "<%=" (location: (1:15)-(1:18)) │ │ ├── content: " placeholder_name " (location: (1:18)-(1:36)) │ │ ├── tag_closing: "%>" (location: (1:36)-(1:38)) │ │ ├── parsed: true │ │ └── valid: true │ │ │ └── @ LiteralNode (location: (1:38)-(1:39)) │ └── content: "\"" │ └── tag_closing: ">" (location: (1:39)-(1:40)) ``` Both `?>` and `>` terminate an instruction, so `<?target content>` and `<?target content?>` both parse. Resolves #2259
by Marco Roth
M
Succeeded
main
f433e156 Add `client-runtime` label Signed-off-by: Marco Roth <marco.roth@intergga.ch>
by Marco Roth
M
Succeeded
main
4d7fd5f1 Linter: Improve locations for `erb-strict-locals-comment-syntax` (#2267) This pull request improves offense locations reported by the `erb-strict-locals-comment-syntax` rule so diagnostics highlight the specific invalid syntax instead of the entire ERB node. The rule now uses parser-provided error locations when available and derives precise content ranges for malformed declarations, missing whitespace, unmatched parentheses, extra commas, duplicate declarations, and incorrect ERB opening tags. Follow up on https://github.com/marcoroth/herb/pull/2252
by Marco Roth
M
Succeeded
main
83f3b272 Linter: Respect `framework` when parsing Action View helpers (#2265) This pull request stops the linter from reading Action View helpers into a template that isn't rendered by Action View. On a project configured as `framework: ruby`, this template: ```erb <tr id="<%= tag.ubid %>"></tr> ``` produced two offenses, neither of which describes anything wrong with the code: ``` Unknown HTML tag `<ubid>`. This is not a standard HTML element. Attribute `id` must not be empty. Either provide a meaningful value or remove the attribute entirely. ``` Both come from the same cause. A lot of framework-agnostic rules ask for the `action_view_helpers` parser option so they can see a helper as the element it renders, which is what lets `html-img-require-alt` catch an `image_tag` without an `alt`. The option was requested unconditionally, so the Action View tag proxy was applied everywhere. A rule asking for `action_view_helpers` is saying it wants to see helpers as the elements they render. Whether those helpers exist depends on the project, not the rule configuration. So now, `Linter#parserOptionsFor` answers that question once for every rule. Rules keep their Action View awareness on Action View projects, and see the ERB they were actually given anywhere else. The same fix covers the plainer version of the bug, where `<%= image_tag "logo.png" %>` was reported by `html-img-require-alt` in a project with no Action View at all. The template from the issue still reports under `framework: actionview`, and deliberately so. In Rails `tag.ubid` really is the tag proxy and really does render `<ubid></ubid>`, so Herb cannot tell that apart from a record that happens to be called `tag`. Resolves #2258
by Marco Roth
M
Succeeded
main
74f02b3f Language Service: Respect configured `framework` in Action View features (#2264) This pull request updates every provider that offers Action View knowledge so it only does so for projects that render through Action View. A Sinatra, Hanami or plain ERB project got the full Rails treatment from the editor. Hovering `<%= tag.div class: "x" %>` returned an Action View helper signature, its documentation, and an "HTML equivalent" preview. Hovering `<%= render "posts/card" %>` explained how Rails derives a partial from `to_partial_path`, go-to-definition navigated to the partial, find-references listed its call sites, `<%= link` completed to `link_to`, and the editor offered to convert markup into a tag helper or extract it into a partial with a strict locals header. None of that exists outside Action View. | Entry point | Behavior without Action View | |---------------------------------------------|---------------------------------------------------------------| | `HoverProvider#getHover` | Falls through to the character reference hover | | `DefinitionProvider#getHover` | Returns nothing | | `DefinitionProvider#getDefinition` | Returns no locations | | `ReferencesProvider#getReferences` | Returns no call sites | | `CompletionProvider#getERBCompletions` | Offers no helper, `tag.`, `content_tag` or render completions | | `RewriteCodeActionProvider#getCodeActions` | Offers no conversions | | `ExtractCodeActionProvider#getCodeActions` | Offers no extraction | | `DocumentSymbolProvider#getDocumentSymbols` | Parses without `action_view_helpers` | `CommentProvider`, `FoldingRangeProvider`, `SelectionRangeProvider`, `InlayHintProvider` and `DocumentHighlightProvider` carry no Action View knowledge, so they are untouched. Resolves #2221
by Marco Roth
M
Succeeded
main
6ca2914f Engine: Compute relative file paths lazily (#2262) Compute `VisitorContext#relative_file_path` on demand instead of eagerly during context construction. `Herb::Engine` creates a context for every template, but the relative path is primarily consumed by diagnostics, overlays, debug output, and visitors that explicitly request it. Valid templates compiled without those features previously still paid for `Pathname#absolute?`, path joining, `Pathname#relative_path_from`, and `#to_s`. The public behavior is unchanged: `relative_file_path`, context hash access, merging, inspection, and serialization still return the same value. A regression test verifies that derivation does not happen during initialization. Split out of #1872. ## Benchmark Measured `Herb::Engine` compilation over [`marcoroth/herb-corpus`](https://github.com/marcoroth/herb-corpus) at `5560d823`, using Ruby 4.0.2. The benchmark compiled the 36,046 corpus templates accepted by both variants with a filename and project path, `escape: true`, no visitors, non-strict parsing, and Ruby validation disabled. Results are medians of three full-corpus runs: | metric | `main` (`cc3eb8bc`) | this branch | delta | |-------------------|:--------------------|:------------|:------------------------| | allocated objects | 88,907,732 | 79,115,256 | **-9,792,476 (-11.0%)** | | wall time | 9.21s | 7.94s | **-13.8%** |
by Joel Hawksley
J
Succeeded
main
cc3eb8bc Dev Tools: Fold dev server client into dev tools (#2255) This pull request moves `@herb-tools/client` into `@herb-tools/dev-tools` and replaces the package's exports with one class, because the two were never used apart and neither of them waited to be asked before running. `@herb-tools/client` was the browser half of the dev server. A connection, live DOM patches, a toast, a connection dot. `@herb-tools/dev-tools` was the overlay drawn over the same page. The overlay imported the client for a type and nothing else, the only application consuming either imported both one line after the other, and the panel already draws the connection's state, its dot and its retry button. They were one developer experience packaged as two. This also frees the `@herb-tools/client` name which we are going to use for the browser-runtime for the reactivity. The package exported an overlay, an error overlay, a client, a connection, a patch function and three init helpers, and it decided on its own when to run. Importing it sniffed the document for `herb-debug-mode`, `data-herb-debug-erb` and the validation templates, and called `initHerbDevTools()` on `DOMContentLoaded` when it found one, so the import was the switch. `HerbDevTools` is the only export now, and nothing happens until it is started. ```ts import { HerbDevTools } from "@herb-tools/dev-tools" HerbDevTools.start() ```
by Marco Roth
M
Succeeded
main
02864bcf Linter: Don't report duplicate strict locals diagnostics (#2252) A strict locals declaration in a non-partial file was reported twice. `erb-strict-locals-comment-syntax` carried its own partial-only check, and `actionview-strict-locals-partial-only` reports the same thing for Action View projects, so a single `<%# locals: (user:) %>` in `app/views/users/show.html.erb` produced two offenses that say the same thing in different words. `erb-strict-locals-comment-syntax` is about the syntax of the declaration, so the partial-only check is dropped from it and the Rails-specific diagnostic stays in `actionview-strict-locals-partial-only`, where it is already framework-gated and autocorrectable. The same overlap existed one level down. `<% # locals: (user:) %>` was flagged by both `erb-comment-syntax`, for the generic "use `<%#` instead of `<% #`" reason, and by `erb-strict-locals-comment-syntax`, which has a dedicated message explaining that only ERB comment syntax is recognized for strict locals. `erb-comment-syntax` now skips content that looks like a locals declaration and leaves it to the more specific rule. The two helpers that recognize such a declaration, `extractRubyCommentContent` and `looksLikeLocalsDeclaration`, move to a shared `strict-locals-utils.ts` so both rules use the same definition. Resolves #2236 --------- Co-authored-by: Marco Roth <marco.roth@intergga.ch>
by AG0708
A
Succeeded
main
1de36b03 JavaScript: Bump vscode-languageserver-types from 3.17.5 to 3.18.0 (#2245) Bumps [vscode-languageserver-types](https://github.com/Microsoft/vscode-languageserver-node/tree/HEAD/types) from 3.17.5 to 3.18.0. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
by dependabot...
d
Succeeded
main
037a938c JavaScript: Bump postcss-import from 16.1.1 to 16.2.0 (#2247) Bumps [postcss-import](https://github.com/postcss/postcss-import) from 16.1.1 to 16.2.0. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
by dependabot...
d
Succeeded
main
c970b298 Engine: Use `String#match?` for the remaining compiler regexp guards (#2250) Follow up on #2235, which switched the boolean guards in `trailing_whitespace?`, `leading_whitespace?` and `at_line_start?` from `=~` to `String#match?`. That pull request deliberately left the other `=~` calls alone, on the grounds that they consume `Regexp.last_match`. That is true for `extract_leading_space`, but four more call sites are also pure boolean guards that never read `$~`, in `extract_and_remove_leading_space!` and `remove_trailing_whitespace_from_last_token!`. After this change the only remaining `=~` in the compiler is the one in `extract_leading_space`, which genuinely consumes `Regexp.last_match(1)`. #### Benchmark Compiling `marcoroth/herb-corpus` of 36,284 templates with `Herb::Engine` on Ruby 4.0.2, counting `GC.stat(:total_allocated_objects)`: | | objects allocated | |---|---| | `=~` | 84,704,561 | | `match?` | 84,535,243 | That is about **−0.20%**, roughly 169,000 fewer objects per full-corpus compile. Repeating both sides put the delta at 169,318 and 169,235, so the measurement is stable well inside the effect. Noticeably smaller than the −0.7% in #2235, which is expected. These two methods only run on explicit trim markers and line-start handling rather than once per token, so they fire far less often. /cc @joelhawksley
by Marco Roth
M
Succeeded
main
e40897bc Bump reline from 0.6.3 to 0.7.0 (#2244) Bumps [reline](https://github.com/ruby/reline) from 0.6.3 to 0.7.0. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
by dependabot...
d
Succeeded
main
7aa3ae69 Ruby: Bump sorbet from 0.6.13405 to 0.6.13425 (#2246) Bumps [sorbet](https://github.com/sorbet/sorbet) from 0.6.13405 to 0.6.13425. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
by dependabot...
d
Succeeded
main
5917ffe1 Bump rolldown from 1.2.3 to 1.2.4 (#2249) Bumps [rolldown](https://github.com/rolldown/rolldown/tree/HEAD/packages/rolldown) from 1.2.3 to 1.2.4. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
by dependabot...
d
Succeeded
main
7f8ee206 Bump rbs from 4.1.2 to 4.1.3 (#2243) Bumps [rbs](https://github.com/ruby/rbs) from 4.1.2 to 4.1.3. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
by dependabot...
d
Succeeded
main
396700eb JavaScript: Bump oxlint from 1.77.0 to 1.78.0 (#2248) Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) from 1.77.0 to 1.78.0. Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
by dependabot...
d
Previous
Next