From a3f5a5d4dd1fa4e924fa12ef8a75000b87a763cb Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 20 Apr 2026 11:13:46 +0900 Subject: [PATCH] Config: Add `framework` and `template_engine` options --- .gitignore | 2 + config/options.yml | 14 ++++ .../packages/config/src/config-schema.ts | 17 +++++ .../packages/config/src/config-template.yml | 8 ++ javascript/packages/core/src/index.ts | 1 + lib/herb/configuration.rb | 30 ++++++++ lib/herb/defaults.yml | 3 + sig/herb/configuration.rbs | 14 ++++ .../packages/core/src/config.ts.erb | 43 +++++++++++ templates/rust/src/config.rs.erb | 50 ++++++++++++ test/configuration_test.rb | 76 +++++++++++++++++++ 11 files changed, 258 insertions(+) create mode 100644 config/options.yml create mode 100644 templates/javascript/packages/core/src/config.ts.erb create mode 100644 templates/rust/src/config.rs.erb diff --git a/.gitignore b/.gitignore index dac59c7ac..b13d81285 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,7 @@ javascript/packages/core/src/action-view-helpers.ts javascript/packages/core/src/errors.ts javascript/packages/core/src/html-entities.json javascript/packages/core/src/node-type-guards.ts +javascript/packages/core/src/config.ts javascript/packages/core/src/nodes.ts javascript/packages/core/src/visitor.ts javascript/packages/node/extension/error_helpers.cpp @@ -116,6 +117,7 @@ lib/herb/errors.rb lib/herb/visitor.rb rust/src/action_view_helpers.rs rust/src/ast/nodes.rs +rust/src/config.rs rust/src/errors.rs rust/src/nodes.rs rust/src/union_types.rs diff --git a/config/options.yml b/config/options.yml new file mode 100644 index 000000000..400365042 --- /dev/null +++ b/config/options.yml @@ -0,0 +1,14 @@ +framework: + default: ruby + values: + - ruby + - actionview + - hanami + - sinatra + +template_engine: + default: erubi + values: + - erubi + - erb + - herb diff --git a/javascript/packages/config/src/config-schema.ts b/javascript/packages/config/src/config-schema.ts index 1a41c516c..44252ee48 100644 --- a/javascript/packages/config/src/config-schema.ts +++ b/javascript/packages/config/src/config-schema.ts @@ -50,12 +50,29 @@ export const ValidatorsConfigSchema = z.object({ accessibility: z.boolean().optional().describe("Enable or disable the accessibility validator (default: true)"), }).strict().optional() +export const FrameworkSchema = z.enum(["ruby", "actionview", "hanami", "sinatra"]).optional() + .describe("Framework context (default: 'ruby')") + +export const TemplateEngineSchema = z.enum(["erubi", "erb", "herb"]).optional() + .describe("Template engine used for compilation (default: 'erubi')") + +export const ParserOptionsSchema = z.object({ + strict: z.boolean().optional().describe("Enable strict parsing mode (default: true)"), + render_nodes: z.boolean().optional().describe("Enable render node detection"), + strict_locals: z.boolean().optional().describe("Enable strict locals detection"), +}).strict().optional() + export const EngineConfigSchema = z.object({ + optimize: z.boolean().optional().describe("Enable compile-time optimizations (default: false)"), + debug: z.boolean().optional().describe("Enable debug mode (default: false)"), + parser_options: ParserOptionsSchema.describe("Parser options passed through to Herb.parse"), validators: ValidatorsConfigSchema.describe("Per-validator enable/disable configuration"), }).strict().optional() export const HerbConfigSchema = z.object({ version: z.string().describe("Configuration file version"), + framework: FrameworkSchema, + template_engine: TemplateEngineSchema, files: FilesConfigSchema.describe("Top-level file configuration"), engine: EngineConfigSchema.describe("Engine configuration"), linter: LinterConfigSchema, diff --git a/javascript/packages/config/src/config-template.yml b/javascript/packages/config/src/config-template.yml index d6ecd7506..82f0dd5b4 100644 --- a/javascript/packages/config/src/config-template.yml +++ b/javascript/packages/config/src/config-template.yml @@ -13,6 +13,14 @@ version: 0.9.7 +# # Framework and template engine configuration +# +# # Options: ruby | actionview | hanami | sinatra (default: ruby) +# framework: ruby +# +# # Options: erubi | erb | herb (default: erubi) +# template_engine: erubi + # files: # # Additional patterns beyond the defaults (**.html, **.rhtml, **.html.erb, etc.) # include: diff --git a/javascript/packages/core/src/index.ts b/javascript/packages/core/src/index.ts index 9addb78e6..0d65770b3 100644 --- a/javascript/packages/core/src/index.ts +++ b/javascript/packages/core/src/index.ts @@ -1,4 +1,5 @@ export * from "./action-view-helpers.js" +export * from "./config.js" export * from "./ast-utils.js" export * from "./html-constants.js" export * from "./html-character-references.js" diff --git a/lib/herb/configuration.rb b/lib/herb/configuration.rb index 6113fd4d7..4fbb64beb 100644 --- a/lib/herb/configuration.rb +++ b/lib/herb/configuration.rb @@ -5,6 +5,12 @@ module Herb class Configuration + OPTIONS_PATH = File.expand_path("../../config/options.yml", __dir__ || __FILE__).freeze #: String + OPTIONS = YAML.safe_load_file(OPTIONS_PATH).freeze #: Hash[String, untyped] + + VALID_FRAMEWORKS = OPTIONS["framework"]["values"].freeze #: Array[String] + VALID_TEMPLATE_ENGINES = OPTIONS["template_engine"]["values"].freeze #: Array[String] + CONFIG_FILENAMES = [".herb.yml"].freeze PROJECT_INDICATORS = [ @@ -42,6 +48,30 @@ def version @config["version"] end + #: () -> String + def framework + value = @config["framework"] || "ruby" + + unless VALID_FRAMEWORKS.include?(value) + warn "[Herb] Unknown framework: #{value.inspect}. Valid values: #{VALID_FRAMEWORKS.join(", ")}. Defaulting to 'ruby'." + return "ruby" + end + + value + end + + #: () -> String + def template_engine + value = @config["template_engine"] || "erubi" + + unless VALID_TEMPLATE_ENGINES.include?(value) + warn "[Herb] Unknown template_engine: #{value.inspect}. Valid values: #{VALID_TEMPLATE_ENGINES.join(", ")}. Defaulting to 'erubi'." + return "erubi" + end + + value + end + def files @config["files"] || {} end diff --git a/lib/herb/defaults.yml b/lib/herb/defaults.yml index 3c08b6e21..3aa1938a9 100644 --- a/lib/herb/defaults.yml +++ b/lib/herb/defaults.yml @@ -1,3 +1,6 @@ +framework: ruby +template_engine: erubi + files: include: - "**/*.herb" diff --git a/sig/herb/configuration.rbs b/sig/herb/configuration.rbs index cb7c27f3c..9b37706c4 100644 --- a/sig/herb/configuration.rbs +++ b/sig/herb/configuration.rbs @@ -2,6 +2,14 @@ module Herb class Configuration + OPTIONS_PATH: String + + OPTIONS: Hash[String, untyped] + + VALID_FRAMEWORKS: Array[String] + + VALID_TEMPLATE_ENGINES: Array[String] + CONFIG_FILENAMES: untyped PROJECT_INDICATORS: untyped @@ -24,6 +32,12 @@ module Herb def version: () -> untyped + # : () -> String + def framework: () -> String + + # : () -> String + def template_engine: () -> String + def files: () -> untyped def file_include_patterns: () -> untyped diff --git a/templates/javascript/packages/core/src/config.ts.erb b/templates/javascript/packages/core/src/config.ts.erb new file mode 100644 index 000000000..65aad7522 --- /dev/null +++ b/templates/javascript/packages/core/src/config.ts.erb @@ -0,0 +1,43 @@ +<% + options = YAML.safe_load_file("config/options.yml") + + def camel_case(string) + parts = string.split("_") + parts[0] + parts[1..].map(&:capitalize).join + end + + def pascal_case(string) + string.split("_").map(&:capitalize).join + end +-%> + +<% options.each do |key, config| -%> +export type <%= pascal_case(key) %> = <%= config["values"].map { |v| "\"#{v}\"" }.join(" | ") %> +<% end -%> + +<% options.each do |key, config| -%> +export const VALID_<%= key.upcase %>S: readonly <%= pascal_case(key) %>[] = [<%= config["values"].map { |v| "\"#{v}\"" }.join(", ") %>] as const +<% end -%> + +<% options.each do |key, config| -%> +export const DEFAULT_<%= key.upcase %>: <%= pascal_case(key) %> = "<%= config["default"] %>" +<% end -%> + +export interface HerbConfig { +<% options.each do |key, _config| -%> + <%= camel_case(key) %>: <%= pascal_case(key) %> +<% end -%> +} + +export const DEFAULT_CONFIG: HerbConfig = { +<% options.each do |key, _config| -%> + <%= camel_case(key) %>: DEFAULT_<%= key.upcase %>, +<% end -%> +} + +<% options.each do |key, _config| -%> +export function isValid<%= pascal_case(key) %>(value: string): value is <%= pascal_case(key) %> { + return (VALID_<%= key.upcase %>S as readonly string[]).includes(value) +} + +<% end -%> diff --git a/templates/rust/src/config.rs.erb b/templates/rust/src/config.rs.erb new file mode 100644 index 000000000..2a51ff3aa --- /dev/null +++ b/templates/rust/src/config.rs.erb @@ -0,0 +1,50 @@ +<% + options = YAML.safe_load_file("config/options.yml") + + def pascal_case(string) + string.split("_").map(&:capitalize).join + end + + def title_case(string) + string.split("_").map(&:capitalize).join("") + end +-%> + +use std::fmt; + +<% options.each do |key, config| -%> +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum <%= pascal_case(key) %> { +<% config["values"].each do |value| -%> + <%= title_case(value) %>, +<% end -%> +} + +impl Default for <%= pascal_case(key) %> { + fn default() -> Self { + <%= pascal_case(key) %>::<%= title_case(config["default"]) %> + } +} + +impl fmt::Display for <%= pascal_case(key) %> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + <% config["values"].each do |value| -%> + <%= pascal_case(key) %>::<%= title_case(value) %> => write!(f, "<%= value %>"), + <% end -%> + } + } +} + +impl <%= pascal_case(key) %> { + pub fn from_str(string: &str) -> Option { + match string { + <% config["values"].each do |value| -%> + "<%= value %>" => Some(<%= pascal_case(key) %>::<%= title_case(value) %>), + <% end -%> + _ => None, + } + } +} + +<% end -%> diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 2a4bd7b0f..b209ca930 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -690,4 +690,80 @@ def write_config(content, filename = ".herb.yml") assert_equal false, engine.debug end + + test "framework defaults to ruby" do + config = Herb::Configuration.load(@temp_dir) + + assert_equal "ruby", config.framework + end + + test "framework reads from config file" do + write_config(<<~YAML) + framework: actionview + YAML + + config = Herb::Configuration.load(@temp_dir) + + assert_equal "actionview", config.framework + end + + test "framework warns on invalid value and defaults to ruby" do + write_config(<<~YAML) + framework: invalid + YAML + + config = Herb::Configuration.load(@temp_dir) + + assert_output(nil, /Unknown framework/) do + assert_equal "ruby", config.framework + end + end + + test "framework accepts all valid values" do + Herb::Configuration::VALID_FRAMEWORKS.each do |framework| + write_config("framework: #{framework}") + + config = Herb::Configuration.load(@temp_dir) + + assert_equal framework, config.framework + end + end + + test "template_engine defaults to erubi" do + config = Herb::Configuration.load(@temp_dir) + + assert_equal "erubi", config.template_engine + end + + test "template_engine reads from config file" do + write_config(<<~YAML) + template_engine: herb + YAML + + config = Herb::Configuration.load(@temp_dir) + + assert_equal "herb", config.template_engine + end + + test "template_engine warns on invalid value and defaults to erubi" do + write_config(<<~YAML) + template_engine: invalid + YAML + + config = Herb::Configuration.load(@temp_dir) + + assert_output(nil, /Unknown template_engine/) do + assert_equal "erubi", config.template_engine + end + end + + test "template_engine accepts all valid values" do + Herb::Configuration::VALID_TEMPLATE_ENGINES.each do |engine| + write_config("template_engine: #{engine}") + + config = Herb::Configuration.load(@temp_dir) + + assert_equal engine, config.template_engine + end + end end