Skip to content

Commit a248389

Browse files
authored
Rust: Add Herb Rust FFI Bindings (#767)
This pull request adds Rust bindings for Herb using FFI (Foreign Function Interface) via `bindgen`, allowing Rust applications to parse and analyze ERB templates with the same implementation as the native C, Ruby C-Extension, C++ Emscripten WASM, Java JNI, and the C++ Node.js NAPI bindings. Building the Rust bindings is requires a Rust toolchain, Bundler, Prism, and a C compiler: ```bash cd rust make all ``` Once built, you can use the `herb-rust` CLI tool: #### **`herb-rust version`** ``` herb rust v0.7.5, libprism v1.6.0, libherb v0.7.5 (Rust FFI) ``` #### **`herb-rust lex examples/test.html.erb`** ```js #<Herb::Token type="TOKEN_HTML_TAG_START" value="<" range=[0, 1] start=(1:0) end=(1:1)> #<Herb::Token type="TOKEN_IDENTIFIER" value="h1" range=[1, 3] start=(1:1) end=(1:3)> #<Herb::Token type="TOKEN_WHITESPACE" value=" " range=[3, 4] start=(1:3) end=(1:4)> #<Herb::Token type="TOKEN_IDENTIFIER" value="class" range=[4, 9] start=(1:4) end=(1:9)> #<Herb::Token type="TOKEN_EQUALS" value="=" range=[9, 10] start=(1:9) end=(1:10)> #<Herb::Token type="TOKEN_QUOTE" value="\"" range=[10, 11] start=(1:10) end=(1:11)> #<Herb::Token type="TOKEN_IDENTIFIER" value="title" range=[11, 16] start=(1:11) end=(1:16)> #<Herb::Token type="TOKEN_QUOTE" value="\"" range=[16, 17] start=(1:16) end=(1:17)> #<Herb::Token type="TOKEN_HTML_TAG_END" value=">" range=[17, 18] start=(1:17) end=(1:18)> #<Herb::Token type="TOKEN_ERB_START" value="<%=" range=[18, 21] start=(1:18) end=(1:21)> #<Herb::Token type="TOKEN_ERB_CONTENT" value=" content " range=[21, 30] start=(1:21) end=(1:30)> #<Herb::Token type="TOKEN_ERB_END" value="%>" range=[30, 32] start=(1:30) end=(1:32)> #<Herb::Token type="TOKEN_HTML_TAG_START_CLOSE" value="</" range=[32, 34] start=(1:32) end=(1:34)> #<Herb::Token type="TOKEN_IDENTIFIER" value="h1" range=[34, 36] start=(1:34) end=(1:36)> #<Herb::Token type="TOKEN_HTML_TAG_END" value=">" range=[36, 37] start=(1:36) end=(1:37)> #<Herb::Token type="TOKEN_NEWLINE" value="\n" range=[37, 38] start=(1:37) end=(2:0)> #<Herb::Token type="TOKEN_EOF" value="<EOF>" range=[38, 38] start=(2:0) end=(2:0)> ``` #### **`herb-rust parse examples/test.html.erb`** ```js @ DocumentNode (location: (1:0)-(2:0)) └── children: (2 items) ├── @ HTMLElementNode (location: (1:0)-(1:37)) │ ├── open_tag: │ │ └── @ HTMLOpenTagNode (location: (1:0)-(1:18)) │ │ ├── tag_opening: "<" (location: (1:0)-(1:1)) │ │ ├── tag_name: "h1" (location: (1:1)-(1:3)) │ │ ├── tag_closing: ">" (location: (1:17)-(1:18)) │ │ ├── children: (1 item) │ │ │ └── @ HTMLAttributeNode (location: (1:4)-(1:17)) │ │ │ ├── name: │ │ │ │ └── @ HTMLAttributeNameNode (location: (1:4)-(1:9)) │ │ │ │ └── children: (1 item) │ │ │ │ └── @ LiteralNode (location: (1:4)-(1:9)) │ │ │ │ └── content: "class" │ │ │ │ │ │ │ ├── equals: "=" (location: (1:9)-(1:10)) │ │ │ └── value: │ │ │ └── @ HTMLAttributeValueNode (location: (1:10)-(1:17)) │ │ │ ├── open_quote: "\"" (location: (1:10)-(1:11)) │ │ │ ├── children: (1 item) │ │ │ │ └── @ LiteralNode (location: (1:11)-(1:16)) │ │ │ │ └── content: "title" │ │ │ ├── close_quote: "\"" (location: (1:16)-(1:17)) │ │ │ └── quoted: true │ │ └── is_void: false │ │ │ ├── tag_name: "h1" (location: (1:1)-(1:3)) │ ├── body: (1 item) │ │ └── @ ERBContentNode (location: (1:18)-(1:32)) │ │ ├── tag_opening: "<%=" (location: (1:18)-(1:21)) │ │ ├── content: " content " (location: (1:21)-(1:30)) │ │ ├── tag_closing: "%>" (location: (1:30)-(1:32)) │ │ ├── parsed: false │ │ └── valid: false │ ├── close_tag: │ │ └── @ HTMLCloseTagNode (location: (1:32)-(1:37)) │ │ ├── tag_opening: "</" (location: (1:32)-(1:34)) │ │ ├── tag_name: "h1" (location: (1:34)-(1:36)) │ │ ├── children: [] │ │ └── tag_closing: ">" (location: (1:36)-(1:37)) │ │ │ ├── is_void: false │ └── source: "HTML" │ └── @ HTMLTextNode (location: (1:37)-(2:0)) └── content: "\n" ``` Resolves #647
1 parent 57d8b51 commit a248389

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2764
-2
lines changed

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export PATH="$PWD/javascript/packages/language-server/bin:$PATH"
55
export PATH="$PWD/javascript/packages/highlighter/bin:$PATH"
66
export PATH="$PWD/javascript/packages/stimulus-lint/bin:$PATH"
77
export PATH="$PWD/java/bin:$PATH"
8+
export PATH="$PWD/rust/bin:$PATH"

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ templates/**/*.h.erb linguist-language=C
55
templates/**/*.java.erb linguist-language=Java
66
templates/**/*.js.erb linguist-language=JavaScript
77
templates/**/*.rb.erb linguist-language=Ruby
8+
templates/**/*.rs.erb linguist-language=Rust
89
templates/**/*.ts.erb linguist-language=TypeScript
910

1011
# Template-generated RBS files

.github/labeler.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ rust:
2222
- changed-files:
2323
- any-glob-to-any-file:
2424
- '**/*.rs'
25+
- '**/*.rs.erb'
2526
- '**/Cargo.toml'
2627
- '**/Cargo.lock'
2728

.github/workflows/rust.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Rust
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
permissions:
10+
actions: read
11+
contents: read
12+
13+
jobs:
14+
build:
15+
name: Build
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 10
18+
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Rust
24+
uses: actions-rust-lang/setup-rust-toolchain@v1
25+
with:
26+
toolchain: stable
27+
components: clippy
28+
29+
- name: Set up Rust Nightly (for rustfmt)
30+
uses: actions-rust-lang/setup-rust-toolchain@v1
31+
with:
32+
toolchain: nightly
33+
components: rustfmt
34+
35+
- name: Rust Cache
36+
uses: Swatinem/rust-cache@v2
37+
with:
38+
workspaces: rust
39+
40+
- name: Set up Ruby
41+
uses: ruby/setup-ruby@v1
42+
with:
43+
bundler-cache: true
44+
45+
- name: bundle install
46+
run: bundle install
47+
48+
- name: Render Templates
49+
run: bundle exec rake templates
50+
51+
- name: Compile Herb
52+
run: bundle exec rake make
53+
54+
- name: Check Rust formatting
55+
run: make format-check
56+
working-directory: rust
57+
58+
- name: Clippy
59+
run: make lint
60+
working-directory: rust
61+
62+
- name: Build Rust
63+
run: make build
64+
working-directory: rust
65+
66+
- name: Run Rust tests
67+
run: make test
68+
working-directory: rust

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ javascript/packages/node/extension/nodes.h
108108
lib/herb/ast/nodes.rb
109109
lib/herb/errors.rb
110110
lib/herb/visitor.rb
111+
rust/src/ast/nodes.rs
112+
rust/src/errors.rs
113+
rust/src/nodes.rs
111114
sig/serialized_ast_errors.rbs
112115
sig/serialized_ast_nodes.rbs
113116
src/ast_nodes.c
@@ -126,6 +129,10 @@ wasm/nodes.h
126129
java/target/
127130
java/.java_compiled
128131

132+
# Rust Build Artifacts
133+
rust/target/
134+
rust/Cargo.lock
135+
129136
# NX Monorepo
130137
.nx/
131138
.nx/cache

.rubocop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Metrics/ClassLength:
9393
Metrics/ModuleLength:
9494
Exclude:
9595
- test/**/*.rb
96+
- templates/**/*.rb
9697

9798
Metrics/BlockLength:
9899
Max: 30
@@ -118,6 +119,7 @@ Metrics/PerceivedComplexity:
118119
- lib/herb/project.rb
119120
- lib/herb/engine.rb
120121
- lib/herb/engine/**/*.rb
122+
- templates/template.rb
121123
- test/**/*.rb
122124
- bin/**/*
123125

docs/.vitepress/config/theme.mts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ const defaultSidebar = [
9898
{ text: "Reference", link: "/bindings/java/reference" },
9999
],
100100
},
101+
{
102+
text: "Rust",
103+
collapsed: false,
104+
items: [
105+
{ text: "Installation", link: "/bindings/rust/" },
106+
{ text: "Reference", link: "/bindings/rust/reference" },
107+
],
108+
},
101109
{ text: "WebAssembly", link: "/projects/webassembly" },
102110
],
103111
},

docs/docs/bindings/java/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Herb provides official Java bindings through JNI (Java Native Interface) to the
1010
> Herb also has bindings for:
1111
> - [Ruby](/bindings/ruby/)
1212
> - [JavaScript/Node.js](/bindings/javascript/)
13+
> - [Rust](/bindings/rust/)
1314
1415
## Installation
1516

docs/docs/bindings/javascript/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Herb supports both **browser** and **Node.js** environments with separate packag
88
> Herb also has bindings for:
99
> - [Ruby](/bindings/ruby/)
1010
> - [Java](/bindings/java/)
11+
> - [Rust](/bindings/rust/)
1112
1213
## Installation
1314

docs/docs/bindings/ruby/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Herb is bundled and packaged up as a precompiled RubyGem and available to be ins
1010
> Herb also has bindings for:
1111
> - [JavaScript/Node.js](/bindings/javascript/)
1212
> - [Java](/bindings/java/)
13+
> - [Rust](/bindings/rust/)
1314
1415
## Installation
1516

0 commit comments

Comments
 (0)