Skip to content

Commit 37a1bea

Browse files
authored
Config: Add framework and template_engine options (#1658)
This pull request adds `framework` and `template_engine` configuration options to the `.herb.yml`. These top-level settings tell Herb what environment it's running in and what template engine is being used for compilation. This enables context-aware behavior across the toolchain. Configuration in `.herb.yml`: ```yml framework: actionview # ruby | actionview | hanami | sinatra (default: ruby) template_engine: herb # erubi | erb | herb (default: erubi) ``` Valid values are defined once in `config/options.yml` and code-generated into the relevant configuration files in all bindings. This allows us in the future to have more specific features tailored for each target framework and engine. This pull request is meant to lay the groundwork for: - Framework-aware linter rules, so that we can automatically enable/disable rules based on the framework (see #480) - Framework-aware compile-time optimizations. Knowing the framework context determines which helpers are available for optimization (like #1613) - Template engine compatibility, surfacing invalid syntax or other target engine's conventions across the toolchain (for example no ERB block support)
1 parent 864e6f4 commit 37a1bea

11 files changed

Lines changed: 258 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ javascript/packages/core/src/action-view-helpers.ts
104104
javascript/packages/core/src/errors.ts
105105
javascript/packages/core/src/html-entities.json
106106
javascript/packages/core/src/node-type-guards.ts
107+
javascript/packages/core/src/config.ts
107108
javascript/packages/core/src/nodes.ts
108109
javascript/packages/core/src/visitor.ts
109110
javascript/packages/node/extension/error_helpers.cpp
@@ -116,6 +117,7 @@ lib/herb/errors.rb
116117
lib/herb/visitor.rb
117118
rust/src/action_view_helpers.rs
118119
rust/src/ast/nodes.rs
120+
rust/src/config.rs
119121
rust/src/errors.rs
120122
rust/src/nodes.rs
121123
rust/src/union_types.rs

config/options.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
framework:
2+
default: ruby
3+
values:
4+
- ruby
5+
- actionview
6+
- hanami
7+
- sinatra
8+
9+
template_engine:
10+
default: erubi
11+
values:
12+
- erubi
13+
- erb
14+
- herb

javascript/packages/config/src/config-schema.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,29 @@ export const ValidatorsConfigSchema = z.object({
5050
accessibility: z.boolean().optional().describe("Enable or disable the accessibility validator (default: true)"),
5151
}).strict().optional()
5252

53+
export const FrameworkSchema = z.enum(["ruby", "actionview", "hanami", "sinatra"]).optional()
54+
.describe("Framework context (default: 'ruby')")
55+
56+
export const TemplateEngineSchema = z.enum(["erubi", "erb", "herb"]).optional()
57+
.describe("Template engine used for compilation (default: 'erubi')")
58+
59+
export const ParserOptionsSchema = z.object({
60+
strict: z.boolean().optional().describe("Enable strict parsing mode (default: true)"),
61+
render_nodes: z.boolean().optional().describe("Enable render node detection"),
62+
strict_locals: z.boolean().optional().describe("Enable strict locals detection"),
63+
}).strict().optional()
64+
5365
export const EngineConfigSchema = z.object({
66+
optimize: z.boolean().optional().describe("Enable compile-time optimizations (default: false)"),
67+
debug: z.boolean().optional().describe("Enable debug mode (default: false)"),
68+
parser_options: ParserOptionsSchema.describe("Parser options passed through to Herb.parse"),
5469
validators: ValidatorsConfigSchema.describe("Per-validator enable/disable configuration"),
5570
}).strict().optional()
5671

5772
export const HerbConfigSchema = z.object({
5873
version: z.string().describe("Configuration file version"),
74+
framework: FrameworkSchema,
75+
template_engine: TemplateEngineSchema,
5976
files: FilesConfigSchema.describe("Top-level file configuration"),
6077
engine: EngineConfigSchema.describe("Engine configuration"),
6178
linter: LinterConfigSchema,

javascript/packages/config/src/config-template.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313

1414
version: 0.9.7
1515

16+
# # Framework and template engine configuration
17+
#
18+
# # Options: ruby | actionview | hanami | sinatra (default: ruby)
19+
# framework: ruby
20+
#
21+
# # Options: erubi | erb | herb (default: erubi)
22+
# template_engine: erubi
23+
1624
# files:
1725
# # Additional patterns beyond the defaults (**.html, **.rhtml, **.html.erb, etc.)
1826
# include:

javascript/packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./action-view-helpers.js"
2+
export * from "./config.js"
23
export * from "./ast-utils.js"
34
export * from "./html-constants.js"
45
export * from "./html-character-references.js"

lib/herb/configuration.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
module Herb
77
class Configuration
8+
OPTIONS_PATH = File.expand_path("../../config/options.yml", __dir__ || __FILE__).freeze #: String
9+
OPTIONS = YAML.safe_load_file(OPTIONS_PATH).freeze #: Hash[String, untyped]
10+
11+
VALID_FRAMEWORKS = OPTIONS["framework"]["values"].freeze #: Array[String]
12+
VALID_TEMPLATE_ENGINES = OPTIONS["template_engine"]["values"].freeze #: Array[String]
13+
814
CONFIG_FILENAMES = [".herb.yml"].freeze
915

1016
PROJECT_INDICATORS = [
@@ -42,6 +48,30 @@ def version
4248
@config["version"]
4349
end
4450

51+
#: () -> String
52+
def framework
53+
value = @config["framework"] || "ruby"
54+
55+
unless VALID_FRAMEWORKS.include?(value)
56+
warn "[Herb] Unknown framework: #{value.inspect}. Valid values: #{VALID_FRAMEWORKS.join(", ")}. Defaulting to 'ruby'."
57+
return "ruby"
58+
end
59+
60+
value
61+
end
62+
63+
#: () -> String
64+
def template_engine
65+
value = @config["template_engine"] || "erubi"
66+
67+
unless VALID_TEMPLATE_ENGINES.include?(value)
68+
warn "[Herb] Unknown template_engine: #{value.inspect}. Valid values: #{VALID_TEMPLATE_ENGINES.join(", ")}. Defaulting to 'erubi'."
69+
return "erubi"
70+
end
71+
72+
value
73+
end
74+
4575
def files
4676
@config["files"] || {}
4777
end

lib/herb/defaults.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
framework: ruby
2+
template_engine: erubi
3+
14
files:
25
include:
36
- "**/*.herb"

sig/herb/configuration.rbs

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<%
2+
options = YAML.safe_load_file("config/options.yml")
3+
4+
def camel_case(string)
5+
parts = string.split("_")
6+
parts[0] + parts[1..].map(&:capitalize).join
7+
end
8+
9+
def pascal_case(string)
10+
string.split("_").map(&:capitalize).join
11+
end
12+
-%>
13+
14+
<% options.each do |key, config| -%>
15+
export type <%= pascal_case(key) %> = <%= config["values"].map { |v| "\"#{v}\"" }.join(" | ") %>
16+
<% end -%>
17+
18+
<% options.each do |key, config| -%>
19+
export const VALID_<%= key.upcase %>S: readonly <%= pascal_case(key) %>[] = [<%= config["values"].map { |v| "\"#{v}\"" }.join(", ") %>] as const
20+
<% end -%>
21+
22+
<% options.each do |key, config| -%>
23+
export const DEFAULT_<%= key.upcase %>: <%= pascal_case(key) %> = "<%= config["default"] %>"
24+
<% end -%>
25+
26+
export interface HerbConfig {
27+
<% options.each do |key, _config| -%>
28+
<%= camel_case(key) %>: <%= pascal_case(key) %>
29+
<% end -%>
30+
}
31+
32+
export const DEFAULT_CONFIG: HerbConfig = {
33+
<% options.each do |key, _config| -%>
34+
<%= camel_case(key) %>: DEFAULT_<%= key.upcase %>,
35+
<% end -%>
36+
}
37+
38+
<% options.each do |key, _config| -%>
39+
export function isValid<%= pascal_case(key) %>(value: string): value is <%= pascal_case(key) %> {
40+
return (VALID_<%= key.upcase %>S as readonly string[]).includes(value)
41+
}
42+
43+
<% end -%>

templates/rust/src/config.rs.erb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<%
2+
options = YAML.safe_load_file("config/options.yml")
3+
4+
def pascal_case(string)
5+
string.split("_").map(&:capitalize).join
6+
end
7+
8+
def title_case(string)
9+
string.split("_").map(&:capitalize).join("")
10+
end
11+
-%>
12+
13+
use std::fmt;
14+
15+
<% options.each do |key, config| -%>
16+
#[derive(Debug, Clone, PartialEq, Eq)]
17+
pub enum <%= pascal_case(key) %> {
18+
<% config["values"].each do |value| -%>
19+
<%= title_case(value) %>,
20+
<% end -%>
21+
}
22+
23+
impl Default for <%= pascal_case(key) %> {
24+
fn default() -> Self {
25+
<%= pascal_case(key) %>::<%= title_case(config["default"]) %>
26+
}
27+
}
28+
29+
impl fmt::Display for <%= pascal_case(key) %> {
30+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31+
match self {
32+
<% config["values"].each do |value| -%>
33+
<%= pascal_case(key) %>::<%= title_case(value) %> => write!(f, "<%= value %>"),
34+
<% end -%>
35+
}
36+
}
37+
}
38+
39+
impl <%= pascal_case(key) %> {
40+
pub fn from_str(string: &str) -> Option<Self> {
41+
match string {
42+
<% config["values"].each do |value| -%>
43+
"<%= value %>" => Some(<%= pascal_case(key) %>::<%= title_case(value) %>),
44+
<% end -%>
45+
_ => None,
46+
}
47+
}
48+
}
49+
50+
<% end -%>

0 commit comments

Comments
 (0)