diff --git a/.agent/team/frontend-dev-styles.adoc b/.agent/team/frontend-dev-styles.adoc
new file mode 100644
index 0000000..d1ffb76
--- /dev/null
+++ b/.agent/team/frontend-dev-styles.adoc
@@ -0,0 +1,408 @@
+= ReleaseHx Frontend Development Styles & Conventions
+:toc: macro
+:toclevels: 2
+
+Project-specific guidance for frontend work on ReleaseHx 0.2.0 template overhaul (Issue #38).
+
+toc::[]
+
+== Context & Starting Point
+
+See `.agent/team/template-overhaul-plan.adoc` for full implementation strategy and phased approach.
+
+All work is tracked in `.agent/team/template-overhaul-plan.imyml.yml` with specific task details.
+
+== Quick Start for Frontend Developers
+
+**For Phase 1 (HTML Templates):**
+
+. Read `.agent/team/template-overhaul-plan.adoc` sections on "Tier 1: Semantic HTML Foundation" and "Phase 1: HTML Templates Core"
+. Review current templates in `lib/releasehx/rhyml/templates/` to understand patterns
+. Check `artifacts/` in releasehx-demo repo to see current output
+. Look at specific task in IMYML file for detailed requirements
+. Review "HTML Component Patterns" section below for ReleaseHx conventions
+. Start implementing with Bootstrap 5 and semantic HTML
+
+## Technologies & Frameworks
+
+=== Templating
+
+* **Liquid**: Jekyll variant with custom filters
+* Available filters in ReleaseHx: `md_to_asciidoc`, `render`, `indent`, `pasterize`, `demarkupify`, `inspect_yaml`
+* Template location: `lib/releasehx/rhyml/templates/`
+
+=== CSS Framework
+
+* **Bootstrap 5**: Loaded via CDN in current templates
+* Bootstrap CDN: `https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css`
+* Font Awesome 4.7: `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css`
+* No JavaScript required (pure HTML/CSS solution)
+
+=== Output Targets
+
+* **HTML**: Standalone (with embedded CSS), SSG partial (with external CSS), or minimal
+* **AsciiDoc**: Using role attributes `[.classname]` for styling hooks
+* **Markdown**: Graceful text-based fallback with HTML class syntax where supported
+* **PDF**: Generated from HTML via Asciidoctor or other converters
+
+== HTML Component Patterns
+
+=== Release History Structure
+
+[source,html]
+----
+
+ {%- if framework == 'bootstrap' %}
+ {%- case framework_version %}
+ {%- when '5.3.0' %}
+
+
+ {%- when '4.6.2' %}
+
+ {%- else %}
+
+ {%- endcase %}
+ {%- endif %}
+
+
diff --git a/lib/releasehx/sgyml/helpers.rb b/lib/releasehx/sgyml/helpers.rb
index 1e77221..5a65779 100644
--- a/lib/releasehx/sgyml/helpers.rb
+++ b/lib/releasehx/sgyml/helpers.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_relative '../../schemagraphy/templating'
-
module ReleaseHx
module SgymlHelpers
# Precompiles a schema into a set of templates, using the provided data and schema.
diff --git a/lib/releasehx/transforms/adf_to_markdown.rb b/lib/releasehx/transforms/adf_to_markdown.rb
index 15f5518..52abf33 100644
--- a/lib/releasehx/transforms/adf_to_markdown.rb
+++ b/lib/releasehx/transforms/adf_to_markdown.rb
@@ -135,7 +135,7 @@ def self.convert_paragraph node, excluded
end
# Converts a list (bullet or ordered)
- def self.convert_list node, excluded, depth, unordered: true
+ def self.convert_list node, excluded, depth, unordered: true # rubocop:disable Lint/UnusedMethodArgument
content = node['content'] || []
items = content.map { |item| convert_node(item, excluded, depth + 1) }
"#{items.join}\n"
diff --git a/lib/releasehx/version.rb b/lib/releasehx/version.rb
index c45487d..e8cbcf9 100644
--- a/lib/releasehx/version.rb
+++ b/lib/releasehx/version.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require_relative '../sourcerer'
-
module ReleaseHx
VERSION = ReleaseHx::ATTRIBUTES[:globals]['this_prod_vrsn']
end
diff --git a/lib/schemagraphy.rb b/lib/schemagraphy.rb
deleted file mode 100644
index 7032b83..0000000
--- a/lib/schemagraphy.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'schemagraphy/loader'
-require_relative 'schemagraphy/tag_utils'
-require_relative 'schemagraphy/schema_utils'
-require_relative 'schemagraphy/templating'
-require_relative 'schemagraphy/regexp_utils'
-require_relative 'schemagraphy/cfgyml/doc_builder'
-require_relative 'schemagraphy/data_query/json_pointer'
-require_relative 'schemagraphy/cfgyml/path_reference'
-
-# SchemaGraphy is a component for working with schema-driven data structures and extending YAML with robust typing and dynamic directives.
-# It provides utilities for loading, validating, and transforming data based on
-# a schema definition, with a focus on templating and safe expression evaluation.
-# This module is under early development and will be spun off as its own gem after ReleaseHx is generally available.
-module SchemaGraphy
-end
diff --git a/lib/schemagraphy/README.adoc b/lib/schemagraphy/README.adoc
deleted file mode 100644
index d3d37a4..0000000
--- a/lib/schemagraphy/README.adoc
+++ /dev/null
@@ -1,3 +0,0 @@
-= SchemaGraphy
-
-include::../../README.adoc[tag=schemagraphy,leveloffset=-2]
\ No newline at end of file
diff --git a/lib/schemagraphy/attribute_resolver.rb b/lib/schemagraphy/attribute_resolver.rb
deleted file mode 100644
index a0e3620..0000000
--- a/lib/schemagraphy/attribute_resolver.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module SchemaGraphy
- # The AttributeResolver module provides methods for resolving AsciiDoc attribute references
- # within a schema hash. It is used to substitute placeholders like `\{attribute_name}`
- # with actual values.
- module AttributeResolver
- # Recursively walk a schema Hash and resolve `\{attribute_name}` references
- # in 'dflt' values.
- #
- # @param schema [Hash] The schema or definition hash to process.
- # @param attrs [Hash] The key-value pairs from AsciiDoc attributes to use for resolution.
- # @return [Hash] The schema with resolved attributes.
- def self.resolve_attributes! schema, attrs
- case schema
- when Hash
- schema.transform_values! do |value|
- if value.is_a?(Hash)
- if value.key?('dflt') && value['dflt'].is_a?(String)
- value['dflt'] = resolve_attribute_reference(value['dflt'], attrs)
- end
- resolve_attributes!(value, attrs)
- else
- value
- end
- end
- end
- schema
- end
-
- # Replace `\{attribute_name}` patterns with corresponding values from the attrs hash.
- #
- # @param value [String] The string to process.
- # @param attrs [Hash] The attributes to use for resolution.
- # @return [String] The processed string with attribute references replaced.
- def self.resolve_attribute_reference value, attrs
- # Handle \{attribute_name} references
- if value.match?(/\{[^}]+\}/)
- value.gsub(/\{([^}]+)\}/) do |match|
- attr_name = ::Regexp.last_match(1)
- attrs[attr_name] || match # Keep original if no matching attribute
- end
- else
- value
- end
- end
- end
-end
diff --git a/lib/schemagraphy/cfgyml/definition.rb b/lib/schemagraphy/cfgyml/definition.rb
deleted file mode 100644
index c57cef1..0000000
--- a/lib/schemagraphy/cfgyml/definition.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../loader'
-require_relative '../schema_utils'
-
-module SchemaGraphy
- # A module for handling CFGYML, a schema-driven configuration system.
- module CFGYML
- # Represents a configuration definition loaded from a schema file.
- # It provides methods for accessing defaults and rendering documentation.
- class Definition
- # @return [Hash] The loaded schema hash.
- attr_reader :schema
-
- # @return [Hash] The attributes used for resolving placeholders in the schema.
- attr_reader :attributes
-
- # @param schema_path [String] The path to the schema YAML file.
- # @param attrs [Hash] A hash of attributes for placeholder resolution.
- def initialize schema_path, attrs = {}
- @schema = Loader.load_yaml_with_attributes(schema_path, attrs)
- @attributes = attrs
- end
-
- # Extract default values from the loaded schema.
- # @return [Hash] A hash of default values.
- def defaults
- SchemaUtils.crawl_defaults(@schema)
- end
-
- # Get the search paths for templates.
- # @return [Array] An array of template paths.
- def template_paths
- @template_paths ||= [
- File.join(File.dirname(__FILE__), '..', 'templates', 'cfgyml'),
- *additional_template_paths
- ]
- end
-
- # Render a configuration reference or sample in the specified format.
- #
- # @param format [Symbol] The output format (`:adoc` or `:yaml`).
- # @return [String] The rendered output.
- # @raise [ArgumentError] if the format is unsupported.
- def render_reference format = :adoc
- template = case format
- when :adoc
- 'config-reference.adoc.liquid'
- when :yaml
- 'sample-config.yaml.liquid'
- else
- raise ArgumentError, "Unsupported format: #{format}"
- end
-
- render_template(template)
- end
-
- private
-
- # Render a template using the Liquid engine.
- def render_template template_name
- template_path = find_template(template_name)
- raise "Template not found: #{template_name}" unless template_path
-
- require 'liquid'
- template_content = File.read(template_path)
- template = Liquid::Template.parse(template_content)
-
- template.render(
- 'config_def' => @schema,
- 'attrs' => @attributes)
- end
-
- # Find a template file in the configured template paths.
- def find_template name
- template_paths.each do |path|
- file = File.join(path, name)
- return file if File.exist?(file)
- end
- nil
- end
-
- # Provides an extension point for subclasses to add more template paths.
- def additional_template_paths
- # Can be overridden by subclasses
- []
- end
- end
- end
-end
diff --git a/lib/schemagraphy/cfgyml/doc_builder.rb b/lib/schemagraphy/cfgyml/doc_builder.rb
deleted file mode 100644
index da7c758..0000000
--- a/lib/schemagraphy/cfgyml/doc_builder.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require 'json'
-
-module SchemaGraphy
- module CFGYML
- # Builds documentation-friendly CFGYML references for machine consumption.
- module DocBuilder
- module_function
-
- def call schema, options = {}
- pretty = options.fetch(:pretty, true)
- data = reference_hash(schema)
- pretty ? JSON.pretty_generate(data) : JSON.generate(data)
- end
-
- def reference_hash schema
- {
- 'format' => 'releasehx-config-reference',
- 'version' => 1,
- 'properties' => build_properties(schema['properties'], [])
- }
- end
-
- def build_properties properties, path
- return {} unless properties.is_a?(Hash)
-
- properties.each_with_object({}) do |(key, definition), acc|
- next unless definition.is_a?(Hash)
-
- current_path = path + [key]
- entry = build_entry(current_path, definition)
- children = build_properties(definition['properties'], current_path)
- entry['properties'] = children unless children.empty?
- acc[key] = entry
- end
- end
-
- def build_entry path, definition
- entry = {
- 'path' => path.join('.'),
- 'desc' => definition['desc'],
- 'docs' => definition['docs'],
- 'type' => definition['type'],
- 'templating' => definition['templating'],
- 'default' => definition.key?('dflt') ? definition['dflt'] : nil
- }
- entry.compact
- end
- end
- end
-end
diff --git a/lib/schemagraphy/cfgyml/path_reference.rb b/lib/schemagraphy/cfgyml/path_reference.rb
deleted file mode 100644
index 19ae9e1..0000000
--- a/lib/schemagraphy/cfgyml/path_reference.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'json'
-
-module SchemaGraphy
- module CFGYML
- # Loads and queries a JSON config reference using JSON Pointer.
- class PathReference
- def initialize data
- @data = data
- end
-
- def self.load path
- new(JSON.parse(File.read(path)))
- end
-
- def get pointer
- SchemaGraphy::DataQuery::JSONPointer.resolve(@data, pointer)
- end
- end
-
- Reference = PathReference
- end
-end
diff --git a/lib/schemagraphy/data_query/json_pointer.rb b/lib/schemagraphy/data_query/json_pointer.rb
deleted file mode 100644
index fbccaf8..0000000
--- a/lib/schemagraphy/data_query/json_pointer.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module SchemaGraphy
- module DataQuery
- # Resolves JSON Pointer queries against a Hash or Array.
- module JSONPointer
- module_function
-
- def resolve data, pointer
- return data if pointer.nil? || pointer == ''
- raise ArgumentError, "Invalid JSON Pointer: #{pointer}" unless pointer.start_with?('/')
-
- tokens = pointer.split('/')[1..]
- tokens.reduce(data) do |current, token|
- key = unescape(token)
- resolve_token(current, key, pointer)
- end
- end
-
- def resolve_token current, key, pointer
- case current
- when Array
- index = Integer(key, 10)
- current.fetch(index)
- when Hash
- return current.fetch(key) if current.key?(key)
- return current.fetch(key.to_sym) if current.key?(key.to_sym)
-
- raise KeyError, "JSON Pointer not found: #{pointer}"
- else
- raise KeyError, "JSON Pointer not found: #{pointer}"
- end
- rescue ArgumentError, IndexError, KeyError
- raise KeyError, "JSON Pointer not found: #{pointer}"
- end
-
- def unescape token
- token.gsub('~1', '/').gsub('~0', '~')
- end
- end
- end
-end
diff --git a/lib/schemagraphy/loader.rb b/lib/schemagraphy/loader.rb
deleted file mode 100644
index 8eb6ebd..0000000
--- a/lib/schemagraphy/loader.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'yaml'
-require 'psych'
-require_relative 'attribute_resolver'
-
-module SchemaGraphy
- # The Loader class provides methods for loading YAML files while preserving
- # custom tags and resolving attribute references.
- class Loader
- # Load a YAML file and resolve AsciiDoc attribute references like `\{attribute_name}`.
- #
- # @param path [String] The path to the YAML file.
- # @param attrs [Hash] The AsciiDoc attributes to use for resolution.
- # @return [Hash] The loaded YAML data with attributes resolved.
- def self.load_yaml_with_attributes path, attrs = {}
- raw_data = load_yaml_with_tags(path)
- AttributeResolver.resolve_attributes!(raw_data, attrs)
- raw_data
- end
-
- # Load a YAML file, preserving any custom tags (e.g., `!foo`).
- # Custom tags are attached to the data structure.
- #
- # @param path [String] The path to the YAML file.
- # @return [Hash] The loaded YAML data with custom tags attached.
- def self.load_yaml_with_tags path
- return {} if File.empty?(path)
-
- data = Psych.load_file(path, aliases: true, permitted_classes: [Date, Time])
- ast = Psych.parse_file(path)
- attach_tags(ast.root, data)
- data
- end
-
- # Recursively attach YAML tags to the loaded data structure for template processing.
- #
- # @param node [Psych::Nodes::Node] The current AST node.
- # @param data [Object] The data corresponding to the current node.
- # @api private
- def self.attach_tags node, data
- return unless node.is_a?(Psych::Nodes::Mapping)
-
- node.children.each_slice(2) do |key_node, val_node|
- key = key_node.value
-
- if val_node.respond_to?(:tag) && val_node.tag && data[key].is_a?(String)
- normalized_tag = val_node.tag.sub(/^!+/, '').sub(/^.*:/, '')
- data[key] = {
- 'value' => data[key],
- '__tag__' => normalized_tag
- }
- elsif data[key].is_a?(Hash)
- attach_tags(val_node, data[key])
- end
- end
- end
- end
-end
diff --git a/lib/schemagraphy/regexp_utils.rb b/lib/schemagraphy/regexp_utils.rb
deleted file mode 100644
index c58a339..0000000
--- a/lib/schemagraphy/regexp_utils.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-# frozen_string_literal: true
-
-require 'to_regexp'
-
-module SchemaGraphy
- # A utility module for robustly parsing and using regular expressions.
- # It handles various formats, including literals and plain strings,
- # and provides helpers for extracting captured content.
- module RegexpUtils
- module_function
-
- # Parse a regex pattern string using the `to_regexp` gem for robust parsing.
- # Handles `/pattern/flags`, `%r{pattern}flags`, and plain text formats.
- #
- # @example
- # parse_pattern("/^hello.*$/im")
- # # => { pattern: "^hello.*$", flags: "im", regexp: /^hello.*$/im, options: 6 }
- #
- # @example
- # parse_pattern("hello world")
- # # => { pattern: "hello world", flags: "", regexp: /hello world/, options: 0 }
- #
- # @example
- # parse_pattern("hello world", "i")
- # # => { pattern: "hello world", flags: "i", regexp: /hello world/i, options: 1 }
- #
- # @param input [String] The input string, e.g., "/pattern/flags" or "plain pattern".
- # @param default_flags [String] Default flags to apply if none are specified (default: "").
- # @return [Hash, nil] A hash with `:pattern`, `:flags`, `:regexp`, and `:options`, or `nil`.
- def parse_pattern input, default_flags = ''
- return nil if input.nil? || input.to_s.strip.empty?
-
- input_str = input.to_s.strip
-
- # Remove surrounding quotes that might come from YAML parsing
- clean_input = input_str.gsub(/^["']|["']$/, '')
-
- # Manual parsing for /pattern/flags format (common in YAML configs)
- if clean_input =~ %r{^/(.+)/([a-z]*)$}
- pattern_str = Regexp.last_match(1)
- flags_str = Regexp.last_match(2)
- options = flags_to_options(flags_str)
-
- begin
- regexp_obj = Regexp.new(pattern_str, options)
-
- return {
- pattern: pattern_str,
- flags: flags_str,
- regexp: regexp_obj,
- options: options
- }
- rescue RegexpError => e
- raise RegexpError, "Invalid regex pattern '#{input}': #{e.message}"
- end
- end
-
- # Heuristic to detect if it's a Regexp literal
- is_literal = clean_input.start_with?('%r{')
-
- if is_literal
- # Try to parse as regex literal using to_regexp
- begin
- regexp_obj = clean_input.to_regexp(detect: true)
-
- # Extract pattern and flags from the compiled regexp
- pattern_str = regexp_obj.source
- flags_str = extract_flags_from_regexp(regexp_obj)
-
- {
- pattern: pattern_str,
- flags: flags_str,
- regexp: regexp_obj,
- options: regexp_obj.options
- }
- rescue RegexpError => e
- # Malformed literal is an error
- raise RegexpError, "Invalid regex literal '#{input}': #{e.message}"
- end
- else
- # Treat as plain pattern string with default flags
- flags_str = default_flags.to_s
- options = flags_to_options(flags_str)
-
- begin
- regexp_obj = Regexp.new(clean_input, options)
-
- {
- pattern: clean_input,
- flags: flags_str,
- regexp: regexp_obj,
- options: options
- }
- rescue RegexpError => e
- raise RegexpError, "Invalid regex pattern '#{input}': #{e.message}"
- end
- end
- end
-
- # @note Not yet implemented.
- # Future enhancement to parse structured pattern definitions from a Hash.
- # @param pattern_hash [Hash] A hash with 'pattern' and 'flags' keys.
- # @raise [NotImplementedError] Always raises this error.
- def parse_structured_pattern pattern_hash
- # TODO: Implement structured pattern parsing
- # pattern_hash should have 'pattern' and 'flags' keys
- # flags can be string or array
- raise NotImplementedError, 'Structured pattern parsing not yet implemented'
- end
-
- # @note Not yet implemented.
- # Future enhancement to parse custom YAML tags for regular expressions.
- # @param tagged_input [String] The input string with a YAML tag.
- # @param tag_type [Symbol] The type of tag, e.g., `:literal` or `:pattern`.
- # @raise [NotImplementedError] Always raises this error.
- def parse_tagged_pattern tagged_input, tag_type
- # TODO: Implement custom YAML tag parsing
- # tag_type would be :literal or :pattern
- raise NotImplementedError, 'Tagged pattern parsing not yet implemented'
- end
-
- # Convert a flags string (ex: "im") to a Regexp options integer.
- #
- # @param flags [String] String containing regex flags.
- # @return [Integer] Regexp options integer.
- def flags_to_options flags
- options = 0
- flags = flags.to_s
-
- options |= Regexp::IGNORECASE if flags.include?('i')
- options |= Regexp::MULTILINE if flags.include?('m')
- options |= Regexp::EXTENDED if flags.include?('x')
-
- # NOTE: 'g' (global) and 'o' (once) are not standard Ruby flags
- # encoding flags ('n', 'e', 's', 'u') are handled by to_regexp
-
- options
- end
-
- # Extract a flags string from a compiled Regexp object.
- #
- # @param regexp [Regexp] A compiled regexp object.
- # @return [String] String representation of the flags (e.g., "im").
- def extract_flags_from_regexp regexp
- flags = ''
- flags += 'i' if regexp.options.anybits?(Regexp::IGNORECASE)
- flags += 'm' if regexp.options.anybits?(Regexp::MULTILINE)
- flags += 'x' if regexp.options.anybits?(Regexp::EXTENDED)
- flags
- end
-
- # Create a Regexp object from a pattern string and explicit flags.
- #
- # @param pattern [String] The regex pattern (without delimiters).
- # @param flags [String] The flags string (ex: "im").
- # @return [Regexp] The compiled Regexp object.
- def create_regexp pattern, flags = ''
- options = flags_to_options(flags)
- Regexp.new(pattern, options)
- end
-
- # Extract content using named or positional capture groups.
- #
- # @param text [String] The text to match against.
- # @param pattern_info [Hash] The hash result from `parse_pattern`.
- # @param capture_name [String] The name of the capture group to extract (optional).
- # @return [String, nil] The extracted text, or `nil` if no match is found.
- def extract_capture text, pattern_info, capture_name = nil
- return nil unless text && pattern_info
-
- regexp = pattern_info[:regexp]
- match = text.match(regexp)
-
- return nil unless match
-
- if capture_name && match.names.include?(capture_name.to_s)
- # Extract named capture group
- match[capture_name.to_s]
- elsif match.captures.any?
- # Extract first capture group
- match[1]
- else
- # Return the entire match
- match[0]
- end
- end
-
- # Extract all named capture groups as a hash or positional captures as an array.
- #
- # @param text [String] The text to match against.
- # @param pattern_info [Hash] The hash result from `parse_pattern`.
- # @return [Hash, Array, nil] A hash of named captures, an array of positional captures, or `nil`.
- def extract_all_captures text, pattern_info
- return nil unless text && pattern_info
-
- regexp = pattern_info[:regexp]
- match = text.match(regexp)
-
- return nil unless match
-
- if match.names.any?
- # Return hash of named captures
- match.names.each_with_object({}) do |name, captures|
- captures[name] = match[name]
- end
- else
- # Return array of positional captures
- match.captures
- end
- end
-
- # A convenience method that combines parsing and a single extraction.
- #
- # @param text [String] The text to match against.
- # @param pattern_input [String] The pattern string (with or without /flags/).
- # @param capture_name [String] Name of the capture group to extract (optional).
- # @param default_flags [String] Default flags if the pattern has no flags.
- # @return [String, nil] The extracted text, or `nil` if no match is found.
- def parse_and_extract text, pattern_input, capture_name = nil, default_flags = ''
- pattern_info = parse_pattern(pattern_input, default_flags)
- extract_capture(text, pattern_info, capture_name)
- end
-
- # A convenience method that combines parsing and extraction of all captures.
- #
- # @param text [String] The text to match against.
- # @param pattern_input [String] The pattern string (with or without /flags/).
- # @param default_flags [String] Default flags if the pattern has no flags.
- # @return [Hash, Array, nil] All captured content, or `nil` if no match is found.
- def parse_and_extract_all text, pattern_input, default_flags = ''
- pattern_info = parse_pattern(pattern_input, default_flags)
- extract_all_captures(text, pattern_info)
- end
- end
-end
diff --git a/lib/schemagraphy/safe_expression.rb b/lib/schemagraphy/safe_expression.rb
deleted file mode 100644
index 6424ff1..0000000
--- a/lib/schemagraphy/safe_expression.rb
+++ /dev/null
@@ -1,189 +0,0 @@
-# frozen_string_literal: true
-
-require 'prism'
-require 'timeout'
-
-module SchemaGraphy
- # Provides a simple, deny-by-exception sandbox for mapping expressions.
- # It validates code by walking the Abstract Syntax Tree (AST) and blocking
- # known dangerous operations, rather than attempting to allowlist safe ones.
- class AstGate
- # A list of dangerous bareword methods that are blocked.
- BLOCKED_BAREWORDS = %w[
- eval instance_eval class_eval module_eval binding
- require require_relative load autoload
- system exec spawn fork backtick `
- open ObjectSpace GC Thread Process at_exit
- ].freeze
-
- # A list of AST node types that are explicitly disallowed.
- DISALLOWED_NODES = %i[
- # Definitions and meta-programming
- def_node class_node module_node define_node alias_node undef_node
- # Globals and constants paths
- global_variable_read_node constant_path_node
- # Shell and backticks
- x_string_node interpolated_x_string_node
- ].freeze
-
- # A list of constants that are considered dangerous and are blocked.
- DANGEROUS_CONSTANTS = %w[
- Kernel Object Module Class File FileUtils IO Dir Process Open3 PTY Thread
- SystemSignal Signal Gem Net HTTP TCPSocket UDPSocket Socket ObjectSpace GC
- ].freeze
-
- # Validates the given code by parsing it and walking the AST.
- #
- # @param code [String] The Ruby code to validate.
- # @param context_keys [Array] A list of keys available in the execution context.
- # @raise [SyntaxError] if the code has syntax errors.
- # @raise [SecurityError] if the code contains disallowed operations.
- def self.validate! code, context_keys: []
- result = Prism.parse(code)
- raise SyntaxError, result.errors.map(&:message).join(', ') if result.errors.any?
-
- walk(result.value, context_keys: context_keys)
- end
-
- # @api private
- # Recursively walks the AST, checking for disallowed nodes and operations.
- #
- # @param node [Prism::Node] The current AST node.
- # @param context_keys [Array] A list of keys available in the execution context.
- # @raise [SecurityError] if a disallowed operation is found.
- def self.walk node, context_keys: []
- return unless node.is_a?(Prism::Node)
-
- type = node.type
- raise SecurityError, "node not allowed: #{type}" if DISALLOWED_NODES.include?(type)
-
- case node
- when Prism::CallNode
- # Block dangerous barewords (system, eval, etc.)
- if node.receiver.nil? && BLOCKED_BAREWORDS.include?(node.name.to_s)
- raise SecurityError, "method not allowed: #{node.name}"
- end
- # Block dangerous constants and constant paths
- if node.receiver.is_a?(Prism::ConstantReadNode) && DANGEROUS_CONSTANTS.include?(node.receiver.name.to_s)
- raise SecurityError, "unsafe constant: #{node.receiver.name}"
- end
- raise SecurityError, 'unsafe constant path' if node.receiver.is_a?(Prism::ConstantPathNode)
-
- when Prism::ConstantReadNode
- # Allow only core Ruby constants defined in SafeTransform
- const_name = node.name.to_s
- unless SafeTransform::CORE_CONSTANTS.key?(const_name.to_sym)
- raise SecurityError, "constant not allowed: #{const_name}"
- end
- when Prism::ConstantPathNode, Prism::GlobalVariableReadNode
- raise SecurityError, 'constant paths and global variables are not allowed'
- when Prism::DefNode, Prism::ClassNode, Prism::ModuleNode
- raise SecurityError, 'method, class, and module definitions are not allowed'
- when Prism::BackReferenceReadNode, Prism::XStringNode, Prism::InterpolatedXStringNode
- raise SecurityError, 'shell commands and backticks are not allowed'
- end
-
- node.child_nodes.each { |child| walk(child, context_keys: context_keys) if child }
- end
- end
-
- # Provides a sandboxed environment for executing Ruby code.
- # Inherits from `BasicObject` for a minimal namespace and uses `instance_eval`
- # to run code within its own context. All code is validated by {AstGate} before execution.
- class SafeTransform < BasicObject
- # A minimal set of core Ruby constants exposed to the sandboxed environment.
- CORE_CONSTANTS = {
- Array: ::Array,
- Hash: ::Hash,
- String: ::String,
- Integer: ::Integer,
- Float: ::Float,
- TrueClass: ::TrueClass,
- FalseClass: ::FalseClass,
- NilClass: ::NilClass,
- Symbol: ::Symbol,
- Numeric: ::Numeric,
- Regexp: ::Regexp
- }.freeze
-
- CORE_CONSTANTS.each do |name, ref|
- const_set(name, ref) unless const_defined?(name, false)
- end
-
- # @param context [Hash] A hash of data to be made available in the sandbox.
- def initialize context = {}
- @context = context
- end
-
- # Executes the given code within the sandboxed environment.
- #
- # @param code [String] The Ruby code to execute.
- # @return [Object] The result of the executed code.
- # @raise [Timeout::Error] if the execution time exceeds the limit.
- # @raise [SecurityError] if the code contains disallowed operations.
- def transform code
- ::Timeout.timeout(0.25) do
- AstGate.validate!(code, context_keys: @context.keys)
- instance_eval(code)
- end
- rescue ::Timeout::Error
- ::Kernel.raise ::StandardError, 'transform timed out'
- end
-
- # Adds a key-value pair to the execution context.
- #
- # @param key [String, Symbol] The key to add.
- # @param value [Object] The value to associate with the key.
- def add_context key, value
- @context[key.to_s] = value
- end
-
- # Safely traverses a nested object using a dot-separated path.
- #
- # @param obj [Object] The object to traverse.
- # @param path [String] The dot-separated path (e.g., "a.b.c").
- # @return [Object, nil] The value at the specified path, or `nil`.
- def dig_path obj, path
- keys = path.to_s.split('.')
- keys.reduce(obj) { |memo, key| memo.respond_to?(:[]) ? memo[key] : nil }
- end
-
- def to_s
- '#'
- end
-
- private
-
- # Handles access to variables in the context.
- def method_missing(name, *args, &block)
- key = name.to_s
- if @context.key?(key) && args.empty? && block.nil?
- @context[key]
- else
- ::Kernel.raise ::NoMethodError, "undefined method `#{name}` for #{self}"
- end
- end
-
- def respond_to_missing? name, include_private = false
- @context.key?(name.to_s) || super
- end
-
- # Disable methods that could be used to break out of the sandbox.
-
- def instance_exec(*_args)
- ::Kernel.raise ::NoMethodError, 'disabled'
- end
-
- def method(*_args)
- ::Kernel.raise ::NoMethodError, 'disabled'
- end
-
- def singleton_class(*_args)
- ::Kernel.raise ::NoMethodError, 'disabled'
- end
-
- def define_singleton_method(*_args)
- ::Kernel.raise ::NoMethodError, 'disabled'
- end
- end
-end
diff --git a/lib/schemagraphy/schema_utils.rb b/lib/schemagraphy/schema_utils.rb
deleted file mode 100644
index a8d6471..0000000
--- a/lib/schemagraphy/schema_utils.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-# frozen_string_literal: true
-
-module SchemaGraphy
- # A utility module for introspecting schema definitions.
- # Provides methods for retrieving metadata, default values, and type information
- # from a schema hash using a dot-separated path syntax.
- module SchemaUtils
- module_function
-
- # Retrieve a nested property definition from a schema using a dot-separated path.
- #
- # @example Schema Structure
- # schema = {
- # "$schema": {
- # "properties": {
- # "property1": {
- # "properties": {
- # "subproperty1": {
- # "default": "value1",
- # "type": "String"
- # }
- # }
- # }
- # }
- # }
- # }
- # crawl_properties(schema, "property1.subproperty1")
- # # => { "default" => "value1", "type" => "String" }
- #
- # @param schema [Hash] The schema hash to crawl.
- # @param path [String] The dot-separated path to the property.
- # @return [Hash, nil] The property definition hash, or `nil` if not found.
- def crawl_properties schema, path
- path_components = path.split('.')
- current = schema['$schema'] || schema
-
- path_components.each do |component|
- return nil unless current.is_a?(Hash)
- return nil unless current['properties']&.key?(component)
-
- current = current['properties'][component]
- end
-
- current
- end
-
- # Get the default value for a property from the schema.
- #
- # @param schema [Hash] The schema hash.
- # @param path [String] The dot-separated path to the property.
- # @return [Object, nil] The default value, or `nil` if not defined.
- def default_for schema, path
- property = crawl_properties(schema, path)
- return nil unless property.is_a?(Hash)
-
- property['default'] || property['dflt']
- end
-
- # Get the type for a property from the schema.
- #
- # @param schema [Hash] The schema hash.
- # @param path [String] The dot-separated path to the property.
- # @return [String, nil] The property type, or `nil` if not defined.
- def type_for schema, path
- property = crawl_properties(schema, path)
- return nil unless property.is_a?(Hash)
-
- property['type']
- end
-
- # Get the templating configuration for a property from the schema.
- #
- # @param schema [Hash] The schema hash.
- # @param path [String] The dot-separated path to the property.
- # @return [Hash] The templating configuration hash.
- def templating_config_for schema, path
- property = crawl_properties(schema, path)
- return {} unless property.is_a?(Hash)
-
- return property['templating'] if property['templating']
-
- if property['type'].to_s.downcase == 'liquid'
- { 'default' => 'liquid', 'delay' => true }
- elsif property['type'].to_s.downcase == 'erb'
- { 'default' => 'erb', 'delay' => true }
- else
- {}
- end
- end
-
- # Check if a property is a templated field.
- #
- # @param schema [Hash] The schema hash.
- # @param path [String] The dot-separated path to the property.
- # @return [Boolean] `true` if the field has templating configured, `false` otherwise.
- def templated_field? schema, path
- property = crawl_properties(schema, path)
- return false unless property.is_a?(Hash)
-
- property.key?('templating') && property['templating'].is_a?(Hash)
- end
-
- # Crawl the schema to find the metadata for a given path.
- #
- # @param schema [Hash] The schema hash.
- # @param path [String, nil] The dot-separated path.
- # @return [Hash] The metadata hash.
- def self.crawl_meta schema, path = nil
- parts = path ? path.split('.') : []
- node = schema['$schema'] || schema
- meta = {}
-
- parts.each do |part|
- node = node['properties'][part] if node['properties']&.key?(part)
- break unless node.is_a?(Hash)
-
- # Only update meta if this level has it
- meta = node if node['templating']
- end
-
- meta['$meta'] || meta['sgyml'] || meta['templating'] || {}
- end
- end
-end
diff --git a/lib/schemagraphy/tag_utils.rb b/lib/schemagraphy/tag_utils.rb
deleted file mode 100644
index 3c0757f..0000000
--- a/lib/schemagraphy/tag_utils.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module SchemaGraphy
- # A utility module for working with the custom tag data structure.
- # The structure is a hash with 'value' and '__tag__' keys.
- module TagUtils
- # Extracts the original value from a tagged data structure.
- #
- # @param value [Object] The tagged value (a Hash) or any other value.
- # @return [Object] The original value, or the value itself if not tagged.
- def self.detag value
- value.is_a?(Hash) && value.key?('value') ? value['value'] : value
- end
-
- # Retrieves the tag from a tagged data structure.
- #
- # @param value [Object] The tagged value (a Hash) or any other value.
- # @return [String, nil] The tag string, or `nil` if not tagged.
- def self.tag_of value
- value.is_a?(Hash) ? value['__tag__'] : nil
- end
-
- # Checks if a value has a specific tag.
- #
- # @param value [Object] The tagged value to check.
- # @param tag [String, Symbol] The tag to check for.
- # @return [Boolean] `true` if the value has the specified tag, `false` otherwise.
- def self.tag? value, tag
- tag_of(value)&.to_s == tag.to_s
- end
- end
-end
diff --git a/lib/schemagraphy/templates/cfgyml/config-property.adoc.liquid b/lib/schemagraphy/templates/cfgyml/config-property.adoc.liquid
deleted file mode 100644
index 8f84afd..0000000
--- a/lib/schemagraphy/templates/cfgyml/config-property.adoc.liquid
+++ /dev/null
@@ -1,57 +0,0 @@
-{%- assign pname = include.property[0] %}
-{%- assign pbody = include.property[1] %}
-{%- assign pid = include.ppty_path | replace: "__", "_" | replace: "<", "_" | replace: ">", "_" | replace: "$", "DOLLARSIGN_" | prepend: "conf_ppty_" %}
-{%- assign ppath = include.ppty_path | replace: "__", "." | replace: "DOLLARSIGN_", "$" %}
-{%- case include.tier %}
-{%- when 1 %}
-{%- assign dlchars = "::" %}
-{%- when 2 %}
-{%- assign dlchars = ":::" %}
-{%- when 3 %}
-{%- assign dlchars = "::::" %}
-{%- else %}
-{%- assign dlchars = ";;" %}
-{%- endcase %}
-
-[[{{ pid }},config.{{ ppath }}]]
-{{ ppath }}{{ dlchars }}
-+
---
-{{ pbody.desc }}
-{%- if pbody.docs %}
-{{ pbody.docs }}
-{%- endif %}
-
-[horizontal]
-{%- if pbody.type %}
-type;; {{ pbody.type }}
-{%- endif %}
-{%- if pbody.templating %}
-templating;; {% if pbody.templating.default %}{{ pbody.templating.default }}{% if pbody.templating.delay %}, {% endif %}{% endif %}{% if pbody.templating.delay %} delayed{% else %} immediate{% endif %} rendering
-{%- endif %}
-{%- if pbody.dflt != nil %}
-default;;
-+
-{%- if pbody.dflt.first %}
-{%- if pbody.dflt.size > 0 %}
-....
-{%- for item in pbody.dflt %}
-- {{ item }}
-{%- endfor %}
-....
-{%- else %}
-[_empty array_]
-{%- endif %}
-{%- else %}
-{%- if pbody.dflt contains '
-' or pbody.dflt.size > 20 %}
-....
-{{ pbody.dflt | trim }}
-....
-{%- else %}
-`+++{{ pbody.dflt | trim }}+++`
-{%- endif %}
-{%- endif %}
-{%- endif %}
-path;; `xref:{{ pid }}[{{ ppath | prepend: "config." }}]`
---
\ No newline at end of file
diff --git a/lib/schemagraphy/templates/cfgyml/config-reference.adoc.liquid b/lib/schemagraphy/templates/cfgyml/config-reference.adoc.liquid
deleted file mode 100644
index f60130a..0000000
--- a/lib/schemagraphy/templates/cfgyml/config-reference.adoc.liquid
+++ /dev/null
@@ -1,33 +0,0 @@
-{%- for property in config_def.properties -%}
-{%- assign ppty_key1 = property[0] -%}
-{%- assign ppty_path_t1 = ppty_key1 -%}
-{%- include cfgyml/config-property.adoc.liquid
- test_string="test string"
- property=property
- tier=1
- ppty_path=ppty_path_t1 -%}
-
-{%- assign props_t2 = property[1].properties -%}
-{%- if props_t2 -%}
-{%- for propty in props_t2 -%}
-{%- assign ppty_key2 = propty[0] -%}
-{%- assign ppty_path_t2 = ppty_path_t1 | append: '__' | append: ppty_key2 -%}
-{%- include cfgyml/config-property.adoc.liquid
- property=propty
- tier=2
- ppty_path=ppty_path_t2 -%}
-
-{%- assign props_t3 = propty[1].properties -%}
-{%- if props_t2 -%}
-{%- for ppty in props_t3 -%}
-{%- assign ppty_key3 = ppty[0] -%}
-{%- assign ppty_path_t3 = ppty_path_t2 | append: '__' | append: ppty_key3 -%}
-{%- include cfgyml/config-property.adoc.liquid
- property=ppty
- tier=3
- ppty_path=ppty_path_t3 -%}
-{%- endfor -%}
-{%- endif -%}
-{%- endfor -%}
-{%- endif -%}
-{%- endfor %}
diff --git a/lib/schemagraphy/templates/cfgyml/sample-config.yaml.liquid b/lib/schemagraphy/templates/cfgyml/sample-config.yaml.liquid
deleted file mode 100644
index c70421a..0000000
--- a/lib/schemagraphy/templates/cfgyml/sample-config.yaml.liquid
+++ /dev/null
@@ -1,46 +0,0 @@
-# Sample configuration file generated from the configuration definition.
-# All values are upstream (gem) defaults.
-# Properties without defaults are commented out.
-{%- for property_t1 in config_def.properties %}
-{%- assign key1 = property_t1[0] %}
-{%- assign val1 = property_t1[1] %}
-{%- assign dflt1 = val1.dflt %}
-{%- assign key1_first_char = key1 | slice: 0, 1 %}
-{%- if key1_first_char == '<' or dflt1 == nil and val1.properties == nil %}
-{%- assign nulled1 = true %}
-{%- else %}
-{%- assign nulled1 = false %}
-{%- endif %}
-{%- include cfgyml/sample-property.yaml.liquid property=property_t1 tier=1 nulled=nulled1 %}
-{%- assign sub_properties_t2 = val1.properties %}
-{%- if sub_properties_t2 %}
-{%- for property_t2 in sub_properties_t2 %}
-{%- assign key2 = property_t2[0] %}
-{%- assign val2 = property_t2[1] %}
-{%- assign dflt2 = val2.dflt %}
-{%- assign key2_first_char = key2 | slice: 0, 1 %}
-{%- if nulled1 or (key2_first_char == '<' or (dflt2 == nil and val2.properties == nil)) %}
-{%- assign nulled2 = true %}
-{%- else %}
-{%- assign nulled2 = false %}
-{%- endif %}
-{%- include cfgyml/sample-property.yaml.liquid property=property_t2 tier=2 nulled=nulled2 %}
-
-{%- assign sub_properties_t3 = val2.properties %}
-{%- if sub_properties_t3 %}
-{%- for property_t3 in sub_properties_t3 %}
-{%- assign key3 = property_t3[0] %}
-{%- assign val3 = property_t3[1] %}
-{%- assign dflt3 = val3.dflt %}
-{%- assign key3_first_char = key3 | slice: 0, 1 %}
-{%- if nulled2 or (key3_first_char == '<' or (dflt3 == nil and val3.properties == nil)) %}
-{%- assign nulled3 = true %}
-{%- else %}
-{%- assign nulled3 = false %}
-{%- endif %}
-{%- include cfgyml/sample-property.yaml.liquid property=property_t3 tier=3 nulled=nulled3 %}
-{%- endfor %}
-{%- endif %}
-{%- endfor %}
-{%- endif %}
-{%- endfor %}
diff --git a/lib/schemagraphy/templates/cfgyml/sample-property.yaml.liquid b/lib/schemagraphy/templates/cfgyml/sample-property.yaml.liquid
deleted file mode 100644
index b17026e..0000000
--- a/lib/schemagraphy/templates/cfgyml/sample-property.yaml.liquid
+++ /dev/null
@@ -1,82 +0,0 @@
-{%- assign key = include.property[0] %}
-{%- assign val = include.property[1] %}
-{%- assign dflt = val.dflt %}
-{%- assign tier = include.tier | plus: 0 %}
-{%- assign nulled = include.nulled %}
-{%- assign indent = '' %}
-{%- if tier > 1 %}
-{%- for i in (2..tier) %}{% assign indent = indent | append: ' ' %}{% endfor %}
-{%- endif %}
-{%- if nulled %}
- {%- assign pfx = '# ' %}
-{%- else %}
- {%- assign pfx = '' %}
-{%- endif %}
-{%- assign cmmt = val.cmmt | default: val.desc | default: '' | strip | split: "
-" | first %}
-{%- if cmmt and cmmt != "" %}
- {%- assign cmmt = cmmt | demarkupify | truncatewords: 20 | prepend: '# ' %}
-{%- endif %}
-{%- assign dflt_kind = dflt | sgyml_type | split: ":" | first %}
-{%- assign dflt_class = dflt | sgyml_type | split: ":" | last %}
-{%- assign disp = nil %}
-{%- if dflt contains "
-" or val.type == "Multiline" %}
- {%- assign disp = "multiline" %}
-{%- elsif val.type == "String" %}
- {%- if dflt.size > 40 %}
- {%- assign disp = "multiline" %}
- {%- elsif dflt contains ":" or dflt contains "{" %}
- {%- assign disp = "quoted" %}
- {%- else %}
- {%- assign disp = "unquoted" %}
- {% endif %}
-{%- elsif val.templating or val.type == "Template" or val.type == "Liquid" or val.type == "RegExp" %}
- {%- assign disp = "multiline" %}
-{%- elsif val.type == "Array" or val.type == "ArrayList" or dflt_class == "ArrayList" %}
- {%- if dflt.size < 4 and (val.type == "ArrayList" or dflt_class == "ArrayList") %}
- {%- assign disp = "sequence-flow" %}
- {%- else %}
- {%- assign disp = "sequence-block" %}
- {%- endif %}
-{%- elsif val.properties or val.type == "Map" %}
- {%- assign disp = "mapping-block" %}
-{%- elsif !val.type %}
- {%- assign disp = "unquoted" %}
-{%- elsif val.type == "String" and dflt == "" %}
- {%- assign disp = "blank-string" %}
-{%- else %}
- {%- assign disp = "unquoted" %}
-{%- endif %}
-{%- case disp %}
-{%- when "multiline" %}
-{{ pfx }}{{ indent }}{{ key }}: | {{ cmmt }}
-{%- assign ml_indent = indent | append: ' ' %}
-{%- if nulled %}
-{%- assign dflt_lines = dflt | split: "
-" -%}
-{%- for line in dflt_lines %}
-{{ ml_indent }}# {{ line }}
-{%- endfor %}
-{%- else %}
-{%- assign ml_indent_size = indent.size | plus: 2 %}
-{{ dflt | indent: ml_indent_size, true }}
-{%- endif %}
-{%- when "mapping-block" %}
-{{ pfx }}{{ indent }}{{ key }}: {{ cmmt }}
-{%- when "sequence-block" %}
-{{ pfx }}{{ indent }}{{ key }}: {{ cmmt }}
-{%- for item in dflt %}
-{{ pfx }}{{ indent }} - {{ item }}
-{%- endfor %}
-{%- when "sequence-flow" %}
-{{ pfx }}{{ indent }}{{ key }}: [{{ dflt | join: ', ' }}] {{ cmmt }}
-{%- when "blank-string" %}
-{{ pfx }}{{ indent }}{{ key }}: "" {{ cmmt }}
-{%- when "quoted" %}
-{{ pfx }}{{ indent }}{{ key }}: "{{ dflt }}" {{ cmmt }}
-{%- when "unquoted" %}
-{{ pfx }}{{ indent }}{{ key }}: {{ dflt }} {{ cmmt }}
-{%- else %}
-{{ pfx }}{{ indent }}# UNIDENTIFIED
-{%- endcase %}
\ No newline at end of file
diff --git a/lib/schemagraphy/templating.rb b/lib/schemagraphy/templating.rb
deleted file mode 100644
index 39a3210..0000000
--- a/lib/schemagraphy/templating.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-# frozen_string_literal: true
-
-require 'tilt'
-require_relative '../sourcerer/templating'
-
-# frozen_string_literal: true
-
-module SchemaGraphy
- # A module for handling templated fields within a data structure based on a schema or definition.
- # It provides methods for pre-compiling and rendering fields using various template engines.
- module Templating
- extend Sourcerer::Templating
-
- # Renders a field if it is a template.
- #
- # @param field [Object] The field to render.
- # @param context [Hash] The context to use for rendering.
- # @return [Object] The rendered field, or the original field if it's not a template.
- def self.resolve_field field, context = {}
- render_field_if_template(field, context)
- end
-
- # Recursively pre-compiles templated fields in a data structure based on a schema.
- #
- # @param data [Hash] The data to process.
- # @param schema [Hash] The schema defining which fields are templated.
- # @param base_path [String] The base path for the current data level.
- # @param scope [Hash] The scope to use for compilation.
- def self.precompile_from_schema! data, schema, base_path = '', scope: {}
- return unless data.is_a?(Hash)
-
- data.each do |key, value|
- path = [base_path, key].reject(&:empty?).join('.')
-
- precompile_from_schema!(value, schema, path, scope: scope) if value.is_a?(Hash)
-
- next unless SchemaGraphy::SchemaUtils.templated_field?(schema, path)
-
- compile_templated_fields!(
- data: data,
- schema: schema,
- fields: [{ key: key, path: path }],
- scope: scope)
- end
- end
-
- # An alias for the `Sourcerer::Templating::TemplatedField` class.
- TemplatedField = Sourcerer::Templating::TemplatedField
-
- # Compiles templated fields in the data.
- #
- # @param data [Hash] The data containing the fields to compile.
- # @param schema [Hash] The schema definition.
- # @param fields [Array] An array of fields to compile, each with a `:key` and `:path`.
- # @param scope [Hash] The scope to use for compilation.
- def self.compile_templated_fields! data:, schema:, fields:, scope: {}
- fields.each do |entry|
- key = entry[:key]
- path = entry[:path]
- val = data[key]
-
- next unless val.is_a?(String) || (val.is_a?(Hash) && val['__tag__'] && val['value'])
-
- raw = val.is_a?(Hash) ? val['value'] : val
- tagged = val.is_a?(Hash)
- config = SchemaGraphy::SchemaUtils.templating_config_for(schema, path)
- engine = tagged ? val['__tag__'] : (config['default'] || 'liquid')
-
- compiled = Sourcerer::Templating::Engines.compile(raw, engine)
-
- data[key] = if config['delay']
- Sourcerer::Templating::TemplatedField.new(raw, compiled, engine, tagged, inferred: !tagged)
- else
- Sourcerer::Templating::Engines.render(compiled, engine, scope)
- end
- end
- end
-
- # Recursively renders all pre-compiled templated fields in a data structure.
- #
- # @param data [Hash, Array] The data structure to process.
- # @param context [Hash] The context to use for rendering.
- def self.render_all_templated_fields! data, context = {}
- return unless data.is_a?(Hash)
-
- data.each do |key, value|
- case value
- when TemplatedField
- data[key] = value.render(context)
- when Hash
- render_all_templated_fields!(value, context)
- when Array
- value.each_with_index do |item, idx|
- if item.is_a?(TemplatedField)
- value[idx] = item.render(context)
- elsif item.is_a?(Hash)
- render_all_templated_fields!(item, context)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/sourcerer.rb b/lib/sourcerer.rb
deleted file mode 100644
index d10a80f..0000000
--- a/lib/sourcerer.rb
+++ /dev/null
@@ -1,322 +0,0 @@
-# frozen_string_literal: true
-
-# This module is a pre-alpha version of what I will eventually spin off
-# as AsciiSourcery, for single-sourcing documentation AND product data
-# in AsciiDoc and YAML files
-# It is pretty messy for now as I play around with various ways it might
-# get used, including as a build-time generator of artifacts to be used
-# in both the app and the docs
-
-require 'asciidoctor'
-require 'fileutils'
-require 'yaml'
-require_relative 'sourcerer/builder'
-require_relative 'sourcerer/plaintext_converter'
-require_relative 'sourcerer/templating'
-require_relative 'sourcerer/jekyll'
-require_relative 'schemagraphy'
-
-# A tool for single-sourcing documentation and data from AsciiDoc and YAML files.
-# It provides methods for extracting data, rendering templates, and generating various outputs.
-module Sourcerer
- # Loads AsciiDoc attributes from a document header as a Hash.
- #
- # @param path [String] The path to the AsciiDoc file.
- # @return [Hash] A hash of the document attributes.
- def self.load_attributes path
- doc = Asciidoctor.load_file(path, safe: :unsafe)
- doc.attributes
- end
-
- # Loads a snippet from an AsciiDoc file using an `include::` directive.
- #
- # @param path_to_main_adoc [String] The path to the main AsciiDoc file.
- # @param tag [String] A single tag to include.
- # @param tags [Array] An array of tags to include.
- # @param leveloffset [Integer] The level offset for the include.
- # @return [String] The content of the included snippet.
- def self.load_include path_to_main_adoc, tag: nil, tags: [], leveloffset: nil
- opts = []
- opts << "tag=#{tag}" if tag
- opts << "tags=#{tags.join(',')}" if tags.any?
- opts << "leveloffset=#{leveloffset}" if leveloffset
-
- snippet_doc = <<~ADOC
- include::#{path_to_main_adoc}[#{opts.join(', ')}]
- ADOC
-
- doc = Asciidoctor.load(
- snippet_doc,
- safe: :unsafe,
- base_dir: File.expand_path('.'),
- header_footer: false,
- attributes: { 'source-highlighter' => nil }) # disable extras
-
- # Get raw text from all top-level blocks
- doc.blocks.map(&:content).join("\n")
- end
-
- # Extracts tagged content from a file.
- #
- # @param path_to_tagged_adoc [String] The path to the file with tagged content.
- # @param tag [String] A single tag to extract.
- # @param tags [Array] An array of tags to extract.
- # @param comment_prefix [String] The prefix for comment lines.
- # @param comment_suffix [String] The suffix for comment lines.
- # @param skip_comments [Boolean] Whether to skip comment lines in the output.
- # @return [String] The extracted content.
- # rubocop:disable Lint/UnusedMethodArgument
- def self.extract_tagged_content path_to_tagged_adoc, tag: nil, tags: [], comment_prefix: '// ', comment_suffix: '',
- skip_comments: false
- # rubocop:enable Lint/UnusedMethodArgument
- # NOTE: comment_suffix parameter is currently unused but kept for future functionality
- raise ArgumentError, 'tag and tags cannot coexist' if tag && !tags.empty?
-
- tags = [tag] if tag
- raise ArgumentError, 'at least one tag must be specified' if tags.empty?
- raise ArgumentError, 'tags must all be strings' unless tags.is_a?(Array) && tags.all? { |t| t.is_a?(String) }
-
- tagged_content = []
- open_tags = {}
- tag_comment_prefix = comment_prefix.strip || '//'
- tag_pattern = /^#{Regexp.escape(tag_comment_prefix)}\s*tag::([\w-]+)\[\]/
- end_pattern = /^#{Regexp.escape(tag_comment_prefix)}\s*end::([\w-]+)\[\]/
- comment_line_init_pattern = /^#{Regexp.escape(tag_comment_prefix)}+/
- collecting = false
- File.open(path_to_tagged_adoc, 'r') do |file|
- file.each_line do |line|
- # check for tag:: line
- if line =~ tag_pattern
- tag_name = Regexp.last_match(1)
- if tags.include?(tag_name)
- collecting = true
- open_tags[tag_name] = true
- end
- elsif line =~ end_pattern
- tag_name = Regexp.last_match(1)
- if open_tags[tag_name]
- open_tags.delete(tag_name)
- collecting = false if open_tags.empty?
- end
- elsif collecting
- tagged_content << line unless skip_comments && line =~ comment_line_init_pattern
- end
- end
- tagged_content = if tagged_content.empty?
- ''
- else
- # return a string of concatenated lines
- tagged_content.join
- end
- end
-
- tagged_content
- end
-
- # Generates a manpage from an AsciiDoc source file.
- #
- # @param source_adoc [String] The path to the source AsciiDoc file.
- # @param target_manpage [String] The path to the target manpage file.
- def self.generate_manpage source_adoc, target_manpage
- FileUtils.mkdir_p File.dirname(target_manpage)
- Asciidoctor.convert_file(
- source_adoc,
- backend: 'manpage',
- safe: :unsafe,
- standalone: true,
- to_file: target_manpage)
- end
-
- # Renders a set of templates based on a configuration.
- #
- # @param templates_config [Array] An array of template configurations.
- def self.render_templates templates_config
- render_outputs(templates_config)
- end
-
- # Renders templates or converter outputs based on a configuration.
- #
- # @param render_config [Array] A list of render configurations.
- def self.render_outputs render_config
- return if render_config.nil? || render_config.empty?
-
- render_config.each do |render_entry|
- if render_entry[:converter]
- render_with_converter(render_entry)
- next
- end
-
- data_obj = render_entry[:key] || 'data'
- attrs_source = render_entry[:attrs]
- engine = render_entry[:engine] || 'liquid'
-
- render_template(
- render_entry[:template],
- render_entry[:data],
- render_entry[:out],
- data_object: data_obj,
- attrs_source: attrs_source,
- engine: engine)
- end
- end
-
- # Renders a single template with data.
- #
- # @param template_file [String] The path to the template file.
- # @param data_file [String] The path to the data file (YAML).
- # @param out_file [String] The path to the output file.
- # @param data_object [String] The name of the data object in the template.
- # @param includes_load_paths [Array] Paths for Liquid includes.
- # @param attrs_source [String] The path to an AsciiDoc file for attributes.
- # @param engine [String] The template engine to use.
- def self.render_template template_file, data_file, out_file, data_object: 'data', includes_load_paths: [],
- attrs_source: nil, engine: 'liquid'
- data = load_render_data(data_file, attrs_source)
- out_file = File.expand_path(out_file)
- FileUtils.mkdir_p(File.dirname(out_file))
-
- template_path = File.expand_path(template_file)
- template_content = File.read(template_path)
-
- # Prepare context
- context = {
- data_object => data,
- 'include' => { data_object => data } # for compatibility with {% include ... %} expecting include.var
- }
-
- rendered = case engine.to_s
- when 'erb' then render_erb(template_content, context)
- when 'liquid' then render_liquid(template_file, template_content, context, includes_load_paths)
- else raise ArgumentError, "Unsupported template engine: #{engine}"
- end
-
- File.write(out_file, rendered)
- end
-
- def self.render_with_converter render_entry
- data_file = render_entry[:data]
- out_file = render_entry[:out]
- raise ArgumentError, 'render entry missing :data' unless data_file
- raise ArgumentError, 'render entry missing :out' unless out_file
-
- data = load_render_data(data_file, render_entry[:attrs])
- converter = resolve_converter(render_entry[:converter])
- rendered = converter.call(data, render_entry)
- raise ArgumentError, 'converter returned non-string output' unless rendered.is_a?(String)
-
- out_file = File.expand_path(out_file)
- FileUtils.mkdir_p(File.dirname(out_file))
- File.write(out_file, rendered)
- end
-
- def self.load_render_data data_file, attrs_source
- if attrs_source
- attrs = load_attributes(attrs_source)
- SchemaGraphy::Loader.load_yaml_with_attributes(data_file, attrs)
- else
- SchemaGraphy::Loader.load_yaml_with_tags(data_file)
- end
- end
-
- def self.resolve_converter converter
- return converter if converter.respond_to?(:call)
- return Object.const_get(converter) if converter.is_a?(String)
-
- raise ArgumentError, "Unsupported converter: #{converter.inspect}"
- end
-
- def self.render_erb template_content, context
- require 'erb'
- ERB.new(template_content, trim_mode: '-').result_with_hash(context)
- end
-
- def self.render_liquid template_file, template_content, context, includes_load_paths
- require_relative 'sourcerer/jekyll'
- require_relative 'sourcerer/jekyll/liquid/filters'
- require_relative 'sourcerer/jekyll/liquid/tags'
- require 'liquid' unless defined?(Liquid::Template)
- Sourcerer::Jekyll.initialize_liquid_runtime
-
- # Determine includes root; add template directory to search paths
- fallback_templates_dir = File.expand_path('.', Dir.pwd)
- template_dir = File.dirname(File.expand_path(template_file))
- # For templates that use includes like cfgyml/config-property.adoc.liquid,
- # we need the parent directory of the template's directory as well
- template_parent_dir = File.dirname(template_dir)
-
- paths = if includes_load_paths.any?
- includes_load_paths
- else
- [template_parent_dir, template_dir, fallback_templates_dir]
- end
-
- # Create a fake Jekyll site
- site = Sourcerer::Jekyll::Bootstrapper.fake_site(
- includes_load_paths: paths,
- plugin_dirs: [])
-
- # Setup file system for includes with multiple paths
- file_system = Sourcerer::Jekyll::Liquid::FileSystem.new(paths)
-
- template = Liquid::Template.parse(template_content)
- options = {
- registers: {
- site: site,
- file_system: file_system
- }
- }
- template.render(context, options)
- end
-
- # Extracts commands from listing and literal blocks with a specific role.
- #
- # @param file_path [String] The path to the AsciiDoc file.
- # @param role [String] The role to look for.
- # @return [Array] An array of command groups.
- def self.extract_commands file_path, role: 'testable'
- doc = Asciidoctor.load_file(file_path, safe: :unsafe)
- command_groups = []
- current_group = []
-
- blocks = doc.find_by(context: :listing) + doc.find_by(context: :literal)
-
- blocks.each do |block|
- next unless block.has_role?(role)
-
- commands = process_block_content(block.content)
- if block.has_role?('testable-newshell')
- command_groups << current_group.join("\n") unless current_group.empty?
- command_groups << commands.join("\n") unless commands.empty?
- current_group = []
- else
- current_group.concat(commands)
- end
- end
-
- command_groups << current_group.join("\n") unless current_group.empty?
- command_groups
- end
-
- # @api private
- # Processes the content of a block to extract commands.
- # It handles line continuations and skips comments.
- # @param content [String] The content of the block.
- # @return [Array] An array of commands.
- def self.process_block_content content
- processed_commands = []
- current_command = ''
- content.each_line do |line|
- stripped_line = line.strip
- next if stripped_line.start_with?('#') # Skip comments
-
- if stripped_line.end_with?('\\')
- current_command += "#{stripped_line.chomp('\\')} "
- else
- current_command += stripped_line
- processed_commands << current_command unless current_command.empty?
- current_command = ''
- end
- end
- processed_commands
- end
-end
diff --git a/lib/sourcerer/README.adoc b/lib/sourcerer/README.adoc
deleted file mode 100644
index fdfdb20..0000000
--- a/lib/sourcerer/README.adoc
+++ /dev/null
@@ -1,4 +0,0 @@
-[[sourcerer]]
-= Sourcerer
-
-include::../../README.adoc[tag=sourcerer]
\ No newline at end of file
diff --git a/lib/sourcerer/builder.rb b/lib/sourcerer/builder.rb
deleted file mode 100644
index a21b3b8..0000000
--- a/lib/sourcerer/builder.rb
+++ /dev/null
@@ -1,120 +0,0 @@
-# lib/sourcerer/builder.rb
-# frozen_string_literal: true
-
-require 'asciidoctor'
-require 'fileutils'
-require_relative '../sourcerer'
-
-module Sourcerer
- # A build-time code generator that creates assets such as new data, documentation, and even Ruby files from
- # data extracted from AsciiDoc files, such as attributes and tagged regions.
- module Builder
- # Generates a Ruby file at build-time to be used during the build process.
- #
- # @param generated [Hash] A hash with `:path` and `:module` for the generated file.
- # @param attributes [Array] A list of attribute sources to build.
- # @param snippets [Array] A list of snippets to build.
- # @param regions [Array] A list of tagged regions to build.
- # @param templates [Array] A list of templates to build (currently unused).
- # @param render [Array] A list of render entries (currently unused).
- # rubocop:disable Lint/UnusedMethodArgument
- def self.generate_prebuild generated: {}, attributes: [], snippets: [], regions: [], templates: [], render: []
- # rubocop:enable Lint/UnusedMethodArgument
- # NOTE: templates/render parameters are accepted from config but handled separately by Sourcerer.render_outputs
- attr_result = build_attributes(attributes)
- snippet_lookup = build_outputs(snippets, type: :snippet)
- region_lookup = build_outputs(regions, type: :region)
-
- File.write(generated[:path].to_s, <<~RUBY)
- # frozen_string_literal: true
- # Auto-generated by Sourcerer::Builder
-
- module #{generated[:module]}
- ATTRIBUTES = #{attr_result.inspect}
-
- SNIPPET_LOOKUP = #{snippet_lookup.inspect}
-
- REGION_LOOKUP = #{region_lookup.inspect}
-
- def self.read_built_snippet name
- fname = SNIPPET_LOOKUP[name.to_s] || name.to_s
- path = File.expand_path("../../../build/snippets/\#{fname}", __FILE__)
- raise "Snippet not found: \#{name}" unless File.exist?(path)
- File.read(path)
- end
- end
- RUBY
- end
-
- # @api private
- # Builds a hash of attributes from the given sources.
- # @param attributes [Array] The attribute sources.
- # @return [Hash] The built attributes.
- def self.build_attributes attributes
- attributes.each_with_object({}) do |entry, acc|
- source = entry[:source]
- name = entry[:name] || File.basename(source, '.adoc').to_sym
- acc[name.to_sym] = Sourcerer.load_attributes(source)
- end
- end
-
- # @api private
- # Builds output files from snippets or regions and returns a lookup hash.
- # @param entries [Array] The entries to build.
- # @param type [Symbol] The type of output (`:snippet` or `:region`).
- # @return [Hash] A lookup hash mapping names to output filenames.
- def self.build_outputs entries, type:
- lookup = {}
- names = []
- outnames = []
-
- entries.each do |entry|
- source = entry[:source] or raise ArgumentError, "#{type} entry is missing :source"
- tag = entry[:tag]
- tags = entry[:tags]
-
- raise ArgumentError, 'use only one of :tag or :tags' if tag && tags
- raise ArgumentError, "#{type} must include a :tag or :tags" unless tag || tags
-
- name = entry[:name] || tag || File.basename(source, '.adoc')
- outname = entry[:out] || default_output_name(name, type)
-
- raise ArgumentError, "name value must be unique; #{name} already used" if names.include? name
- raise ArgumentError, "out value must be unique; #{outname} already used" if outnames.include? outname
-
- names << name
- outnames << outname
-
- tags = [tag] if tag
-
- text =
- case type
- when :snippet then Sourcerer.load_include(source, tags: tags)
- when :region then Sourcerer.extract_tagged_content(source, tags: tags)
- else raise ArgumentError, "Unsupported type: #{type}"
- end
-
- lookup[name.to_s] = outname
-
- outpath = File.join("build/#{type}s", outname)
- FileUtils.mkdir_p File.dirname(outpath)
- File.write(outpath, text)
- end
-
- lookup
- end
-
- # @api private
- # Determines the default output filename for a given name and type.
- # @param name [String] The name of the output.
- # @param type [Symbol] The type of output.
- # @return [String] The default filename.
- def self.default_output_name name, type
- case type
- when :snippet then "#{name}.txt"
- when :region then "#{name}.adoc"
- else raise ArgumentError, "Unknown type: #{type}"
- end
- end
- end
-end
diff --git a/lib/sourcerer/jekyll.rb b/lib/sourcerer/jekyll.rb
deleted file mode 100644
index f193487..0000000
--- a/lib/sourcerer/jekyll.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'jekyll/bootstrapper'
-require_relative 'jekyll/monkeypatches'
-require_relative 'jekyll/liquid/file_system'
-require_relative 'jekyll/liquid/filters'
-require_relative 'jekyll/liquid/tags'
-require 'jekyll-asciidoc'
-
-module Sourcerer
- # This module encapsulates the logic for initializing a Jekyll-like Liquid
- # templating environment. It loads necessary plugins, applies monkeypatches,
- # and registers custom Liquid filters and tags.
- module Jekyll
- # Initializes the Liquid templating runtime by loading plugins,
- # applying patches, and registering custom filters.
- def self.initialize_liquid_runtime
- Bootstrapper.load_plugins
- Monkeypatches.patch_jekyll
- # Ensure Sourcerer filters are registered
- ::Liquid::Template.register_filter(::Sourcerer::Jekyll::Liquid::Filters)
- # Ensure jekyll-asciidoc filters are registered
- # ::Liquid::Template.register_filter(Jekyll::AsciiDoc::Filters)
- end
- end
-end
diff --git a/lib/sourcerer/jekyll/bootstrapper.rb b/lib/sourcerer/jekyll/bootstrapper.rb
deleted file mode 100644
index 0a7fede..0000000
--- a/lib/sourcerer/jekyll/bootstrapper.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-require 'jekyll'
-require 'jekyll-asciidoc'
-
-module Sourcerer
- module Jekyll
- # This module provides methods for programmatically setting up a Jekyll site
- # environment, which is useful for loading plugins or creating a mock site for rendering.
- module Bootstrapper
- # Loads Jekyll plugins from specified directories.
- #
- # @param plugin_dirs [Array] A list of directories to search for plugins.
- # @return [Jekyll::Site] The initialized Jekyll site object.
- def self.load_plugins plugin_dirs: []
- config = ::Jekyll.configuration(
- {
- 'source' => Dir.pwd,
- 'destination' => File.join(Dir.pwd, '_site'),
- 'quiet' => true,
- 'skip_config_files' => true,
- 'plugins_dir' => plugin_dirs.map { |d| File.expand_path(d) },
- 'disable_disk_cache' => true
- })
-
- site = ::Jekyll::Site.new(config)
- site.plugin_manager.conscientious_require
-
- ::Jekyll::Hooks.trigger :site, :after_init, site
-
- site
- end
-
- # Creates an ephemeral Jekyll site instance for rendering purposes.
- # This is useful for leveraging Jekyll's templating outside of a full site build.
- #
- # @param includes_load_paths [Array] Paths to load includes from.
- # @param plugin_dirs [Array] Paths to load plugins from.
- # @return [Jekyll::Site] The initialized fake Jekyll site object.
- # rubocop:disable Lint/UnusedMethodArgument
- def self.fake_site includes_load_paths: [], plugin_dirs: []
- # NOTE: plugin_dirs parameter is accepted but not yet implemented; reserved for future plugin loading
- ::Jekyll.logger.log_level = :error if ::Jekyll.logger.respond_to?(:log_level=)
-
- config = ::Jekyll.configuration(
- 'source' => Dir.pwd,
- 'includes_dir' => includes_load_paths.first,
- 'includes_load_paths' => includes_load_paths,
- 'destination' => File.join(Dir.pwd, '_site'),
- 'quiet' => true,
- 'skip_config_files' => true,
- 'disable_disk_cache' => true)
-
- site = ::Jekyll::Site.new(config)
-
- include_paths = site.includes_load_paths || []
- site.inclusions ||= {}
-
- include_paths.each do |dir|
- Dir[File.join(dir, '**/*')].each do |file|
- next unless File.file?(file)
-
- relative_path = file.sub("#{dir}/", '')
- site.inclusions[relative_path] = File.read(file)
- end
- end
-
- site.instance_variable_set(:@liquid_renderer, ::Jekyll::LiquidRenderer.new(site))
-
- plugin_manager = ::Jekyll::PluginManager.new(site)
- plugin_manager.conscientious_require
-
- site
- end
- # rubocop:enable Lint/UnusedMethodArgument
- end
- end
-end
diff --git a/lib/sourcerer/jekyll/liquid/file_system.rb b/lib/sourcerer/jekyll/liquid/file_system.rb
deleted file mode 100644
index f5994e7..0000000
--- a/lib/sourcerer/jekyll/liquid/file_system.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-require 'liquid'
-
-module Sourcerer
- module Jekyll
- module Liquid
- # A custom Liquid file system that extends `Liquid::LocalFileSystem` to support
- # multiple root paths for template lookups. This allows templates to be
- # resolved from a prioritized list of directories.
- class FileSystem < ::Liquid::LocalFileSystem
- # Initializes the file system with one or more root paths.
- #
- # @param roots_or_root [String, Array] A single root path or an array of root paths.
- # rubocop:disable Lint/MissingSuper
- # Intentional: Custom implementation that doesn't need parent's initialization
- def initialize roots_or_root
- if roots_or_root.is_a?(Array)
- @roots = roots_or_root.map { |root| File.expand_path(root) }
- @multi_root = true
- else
- @root = File.expand_path(roots_or_root)
- @multi_root = false
- end
- end
- # rubocop:enable Lint/MissingSuper
-
- # Finds the full path of a template, searching through multiple roots if configured.
- #
- # @param template_path [String] The path to the template.
- # @return [String] The full, validated path to the template.
- # @raise [Liquid::FileSystemError] if the template is not found.
- def full_path template_path
- if @multi_root
- @roots.each do |root|
- full = File.expand_path(File.join(root, template_path))
- return full if File.exist?(full) && full.start_with?(root)
- end
- raise ::Liquid::FileSystemError, "Template not found: '#{template_path}' in paths: #{@roots}"
- else
- full = File.expand_path(File.join(@root, template_path))
- validate_path(full)
- end
- end
-
- # Reads the content of a template file.
- #
- # @param template_path [String] The path to the template.
- # @return [String] The content of the template file.
- def read_template_file template_path
- path = full_path(template_path)
- File.read(path)
- end
-
- private
-
- # Validates that the resolved path is within the allowed root(s).
- def validate_path path
- if @multi_root
- # Check if path starts with any of the allowed roots
- unless @roots.any? { |root| path.start_with?(root) }
- raise ::Liquid::FileSystemError, "Illegal template path '#{path}'"
- end
-
- else
- raise ::Liquid::FileSystemError, "Illegal template path '#{path}'" unless path.start_with?(@root)
-
- end
- path
- end
- end
- end
- end
-end
diff --git a/lib/sourcerer/jekyll/liquid/filters.rb b/lib/sourcerer/jekyll/liquid/filters.rb
deleted file mode 100644
index 5c2919c..0000000
--- a/lib/sourcerer/jekyll/liquid/filters.rb
+++ /dev/null
@@ -1,215 +0,0 @@
-# frozen_string_literal: true
-
-require 'kramdown-asciidoc'
-require 'base64'
-require 'cgi'
-
-module Sourcerer
- module Jekyll
- module Liquid
- # This module provides a set of custom filters for use in Liquid templates.
- module Filters
- # Renders a Liquid template string with a given scope.
- # @param input [String, Object] The Liquid template string or a pre-parsed template object.
- # @param vars [Hash] A hash of variables to use as the scope.
- # @return [String] The rendered output.
- def render input, vars = nil
- scope = if vars.is_a?(Hash)
- vars.transform_keys(&:to_s)
- else
- {}
- end
-
- template =
- if input.respond_to?(:render) && input.respond_to?(:templated?) && input.templated?
- input
- else
- ::Liquid::Template.parse(input.to_s)
- end
-
- template.render(scope)
- end
-
- # Converts a string into a slug.
- # @param input [String] The string to convert.
- # @param format [String] The desired format (`kebab`, `snake`, `camel`, `pascal`).
- # @return [String] The sluggerized string.
- def sluggerize input, format = 'kebab'
- return input unless input.is_a? String
-
- case format
- when 'kebab' then input.downcase.gsub(/[\s\-_]/, '-')
- when 'snake' then input.downcase.gsub(/[\s\-_]/, '_')
- when 'camel' then input.downcase.gsub(/[\s\-_]/, '_').camelize(:lower)
- when 'pascal' then input.downcase.gsub(/[\s\-_]/, '_').camelize(:upper)
- else input
- end
- end
-
- # Replaces double newlines with a newline and a plus sign.
- # @param input [String] The input string.
- # @return [String] The processed string.
- def plusify input
- input.gsub(/\n\n+/, "\n+\n")
- end
-
- # Converts a Markdown string to AsciiDoc.
- # @param input [String] The Markdown string.
- # @param wrap [String] The wrapping option for the converter.
- # @return [String] The converted AsciiDoc string.
- def md_to_adoc input, wrap = 'ventilate'
- options = {}
- options[:wrap] = wrap.to_sym if wrap
- Kramdoc.convert(input, options)
- end
-
- # Indents a string by a given number of spaces.
- # @param input [String] The string to indent.
- # @param spaces [Integer] The number of spaces for indentation.
- # @param line1 [Boolean] Whether to indent the first line.
- # @return [String] The indented string.
- def indent input, spaces = 2, line1: false
- indent = ' ' * spaces
- lines = input.split("\n")
- indented = if line1
- lines.map { |line| indent + line }
- else
- lines.map.with_index { |line, i| i.zero? ? line : indent + line }
- end
- indented.join("\n")
- end
-
- # Checks the type of a value in the context of SG-YML.
- # @param input [Object] The value to check.
- # @return [String] A string representing the type.
- def sgyml_type_check input
- if input.nil?
- 'Null:nil'
- elsif input.is_a? Array
- # if all items in Array are (integer, float, string, boolean)
- if input.all? do |item|
- item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(String) ||
- item.is_a?(TrueClass) || item.is_a?(FalseClass)
- end
- 'Compound:ArrayList'
- elsif input.all? { |item| item.is_a?(Hash) && (item.keys.length >= 2) }
- 'Compound:ArrayTable'
- else
- 'Compound:Array'
- end
- elsif input.is_a? Hash
- if input.values.all? { |value| value.is_a?(Hash) && (value.keys.length >= 2) }
- 'Compound:MapTable'
- else
- 'Compound:Map'
- end
- elsif input.is_a? String
- 'Scalar:String'
- elsif input.is_a? Integer
- 'Scalar:Number'
- elsif input.is_a? Time
- 'Scalar:DateTime'
- elsif input.is_a? Float
- 'Scalar:Float'
- elsif input.is_a?(TrueClass) || input.is_a?(FalseClass)
- 'Scalar:Boolean'
- else
- 'unknown:unknown'
- end
- end
-
- # Returns the Ruby class name of a value.
- # @param input [Object] The value.
- # @return [String] The class name.
- def ruby_class input
- input.class.name
- end
-
- # Removes markup from a string.
- # @param input [String] The string to demarkupify.
- # @return [String] The demarkupified string.
- def demarkupify input
- return input unless input.is_a? String
-
- input = input.gsub(/`"|"`/, '"')
- input = input.gsub(/'`|`'/, "'")
- input = input.gsub(/[*_`]/, '')
- # change curly quotes to striaght quotes
- input = input.gsub(/[“”]/, '"')
- input.gsub(/[‘’]/, "'")
- end
-
- # Dumps a value to YAML format.
- # @param input [Object] The value to dump.
- # @return [String] The YAML representation.
- def inspect_yaml input
- require 'yaml'
- YAML.dump(input)
- end
-
- # Base64 encodes a string.
- # @param input [String] The string to encode.
- # @return [String] The Base64-encoded string.
- def base64 input
- return input unless input.is_a? String
-
- Base64.strict_encode64(input)
- end
-
- # Decodes a Base64-encoded string.
- # @param input [String] The string to decode.
- # @return [String] The decoded string.
- def base64_decode input
- return input unless input.is_a? String
-
- Base64.strict_decode64(input)
- rescue ArgumentError
- # Return original input if decoding fails
- input
- end
-
- # URL-encodes a string.
- # @param input [String] The string to encode.
- # @return [String] The URL-encoded string.
- def url_encode input
- return input unless input.is_a? String
-
- CGI.escape(input)
- end
-
- # Decodes a URL-encoded string.
- # @param input [String] The string to decode.
- # @return [String] The decoded string.
- def url_decode input
- return input unless input.is_a? String
-
- CGI.unescape(input)
- rescue ArgumentError
- # Return original input if decoding fails
- input
- end
-
- # HTML-escapes a string.
- # @param input [String] The string to escape.
- # @return [String] The HTML-escaped string.
- def html_escape input
- return input unless input.is_a? String
-
- CGI.escapeHTML(input)
- end
-
- # Unescapes an HTML-escaped string.
- # @param input [String] The string to unescape.
- # @return [String] The unescaped string.
- def html_unescape input
- return input unless input.is_a? String
-
- CGI.unescapeHTML(input)
- end
- end
- end
- end
-end
-
-# Register the filters automatically
-Liquid::Template.register_filter(Sourcerer::Jekyll::Liquid::Filters)
diff --git a/lib/sourcerer/jekyll/liquid/tags.rb b/lib/sourcerer/jekyll/liquid/tags.rb
deleted file mode 100644
index e95c709..0000000
--- a/lib/sourcerer/jekyll/liquid/tags.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-module Sourcerer
- module Jekyll
- # This module contains custom Liquid tags for the Sourcerer templating environment.
- module Tags
- # A Liquid tag for embedding and rendering a file within a template.
- # It searches for the file in the configured include paths.
- class EmbedTag < ::Liquid::Tag
- # @param tag_name [String] The name of the tag ('embed').
- # @param markup [String] The name of the partial to embed.
- # @param tokens [Array] The list of tokens.
- def initialize tag_name, markup, tokens
- super
- @partial_name = markup.strip
- end
-
- # Renders the embedded file.
- #
- # @param context [Liquid::Context] The Liquid context.
- # @return [String] The rendered content of the embedded file.
- # @raise [IOError] if the embed file is not found.
- def render context
- includes_paths = context.registers[:includes_load_paths] || []
-
- found_path = includes_paths.find do |base|
- candidate = File.expand_path(@partial_name, base)
- File.exist?(candidate)
- end
-
- raise "Embed file not found: #{@partial_name}" unless found_path
-
- full_path = File.expand_path(@partial_name, found_path)
- source = File.read(full_path)
-
- partial = ::Liquid::Template.parse(source)
- partial.render!(context)
- end
- end
- end
- end
-end
-
-Liquid::Template.register_tag('embed', Sourcerer::Jekyll::Tags::EmbedTag)
diff --git a/lib/sourcerer/jekyll/monkeypatches.rb b/lib/sourcerer/jekyll/monkeypatches.rb
deleted file mode 100644
index 3548638..0000000
--- a/lib/sourcerer/jekyll/monkeypatches.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-module Sourcerer
- module Jekyll
- # This module contains monkeypatches for Jekyll to modify or extend its behavior.
- module Monkeypatches
- # Patches Jekyll's `OptimizedIncludeTag` to modify its behavior.
- # The patch enhances include path resolution and context handling to better
- # suit the needs of Sourcerer's templating environment.
- def self.patch_jekyll
- return unless defined?(::Jekyll::Tags::OptimizedIncludeTag)
-
- ::Jekyll::Tags::OptimizedIncludeTag.class_eval do
- define_method :render do |context|
- site = context.registers[:site]
- file = render_variable(context) || @file
-
- context.stack do
- context['include'] = parse_params(context) if @params
-
- source = site.inclusions[file]
-
- unless source
-
- # Debug lines before attempting path resolution
-
- # Safe resolution
- paths = context.registers[:includes_load_paths] || []
- path = paths
- .map { |dir| File.join(dir, file) }
- .find { |p| File.file?(p) }
-
- raise IOError, "Include file not found: #{file}" unless path
-
- source = File.read(path)
- end
-
- partial = ::Liquid::Template.parse(source)
- partial.registers[:site] = context.registers[:site]
- partial.assigns['include'] = context['include']
-
- ::Liquid::Template.register_filter(::Jekyll::Filters)
- ::Liquid::Template.register_filter(::Sourcerer::Jekyll::Liquid::Filters)
-
- # Use an isolated context so we can inspect and copy assigns
- subcontext = ::Liquid::Context.new(
- [{ 'include' => context['include'] }],
- {}, # Environments
- context.registers,
- rethrow_errors: true)
-
- rendered = partial.render!(subcontext)
-
- # Copy assigns from subcontext to parent context
- subcontext.environments.each do |env|
- env.each do |k, v|
- # Avoid clobbering outer include if reentrant
- next if k == 'include'
-
- context.environments.first[k] = v
- end
- end
-
- rendered
- end
- end
- end
-
- ::Liquid::Template.tags['include'] = ::Jekyll::Tags::OptimizedIncludeTag
- end
- end
- end
-end
diff --git a/lib/sourcerer/plaintext_converter.rb b/lib/sourcerer/plaintext_converter.rb
deleted file mode 100644
index bf55751..0000000
--- a/lib/sourcerer/plaintext_converter.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# This module will likely spin off into a gem
-
-# frozen_string_literal: true
-
-require 'asciidoctor'
-
-module Sourcerer
- # A custom Asciidoctor converter that outputs plain text.
- # It is registered for the "plaintext" backend and can be used to extract
- # the raw text content or attributes from an AsciiDoc document.
- class PlainTextConverter < Asciidoctor::Converter::Base
- # Identify ourselves as a converter for the "plaintext" backend
- register_for 'plaintext'
-
- # The main entry point for the converter.
- # It is called by Asciidoctor to convert a node.
- #
- # @param node [Asciidoctor::AbstractNode] The node to convert.
- # @param _transform [String] The transform to apply (unused).
- # @param _opts [Hash] Options for the conversion (unused).
- # @return [String] The converted plain text output.
- def convert node, _transform = nil, _opts = {}
- if respond_to?("convert_#{node.node_name}", true)
- send("convert_#{node.node_name}", node)
- elsif node.respond_to?(:content)
- node.content.to_s
- elsif node.respond_to?(:text)
- node.text.to_s
- else
- ''
- end
- end
-
- private
-
- # Converts the document node.
- def convert_document node
- emit_attrs = node.attr('sourcerer_mode') == 'emit_attrs'
-
- if emit_attrs
- # only emit attribute lines directly, nothing else
- attrs = node.attributes.select do |k, v|
- k.is_a?(String) && !v.nil? && !k.start_with?('backend-', 'safe-mode', 'doctype', 'sourcerer_mode')
- end
-
- formatted_attrs = attrs.map { |k, v| ":#{k}: #{v}" }
- formatted_attrs.join("\n") # NO EXTRA SPACES OR LINES
- else
- node.blocks.map { |block| convert block }.join("\n")
- end
- end
-
- # Converts a section node.
- def convert_section node
- title = node.title? ? node.title : ''
- body = node.blocks.map { |block| convert block }.join("\n")
- [title, body].reject(&:empty?).join("\n")
- end
-
- # Converts a paragraph node.
- def convert_paragraph node
- node.lines.join("\n")
- end
-
- # Converts a listing node.
- def convert_listing node
- node.content
- end
-
- # Converts a literal node.
- def convert_literal node
- node.content
- end
- end
-end
diff --git a/lib/sourcerer/templating.rb b/lib/sourcerer/templating.rb
deleted file mode 100644
index 7b6e0fd..0000000
--- a/lib/sourcerer/templating.rb
+++ /dev/null
@@ -1,190 +0,0 @@
-# frozen_string_literal: true
-
-require 'liquid'
-
-module Sourcerer
- # This module provides the core templating functionality for Sourcerer.
- # It includes modules for template engines, and classes for representing
- # templated fields and their context.
- module Templating
- # This module handles the compilation and rendering of templates.
- module Engines
- module_function
-
- # A hash of supported template engines.
- SUPPORTED_ENGINES = {
- 'liquid' => 'liquid',
- 'erb' => 'erb'
- }.freeze
-
- # Compiles a template string using the specified engine.
- #
- # @param str [String] The template string to compile.
- # @param engine [String] The name of the template engine to use.
- # @return [Object] The compiled template object.
- # @raise [ArgumentError] if the engine is not supported.
- def compile str, engine
- case engine.to_s
- when 'liquid'
- Liquid::Template.parse(str)
- when 'erb'
- require 'erb'
- ERB.new(str)
- else
- raise ArgumentError, "Unsupported engine: #{engine}"
- end
- end
-
- # Renders a compiled template with the given variables.
- #
- # @param compiled [Object] The compiled template object.
- # @param engine [String] The name of the template engine.
- # @param vars [Hash] A hash of variables to use for rendering.
- # @return [String] The rendered output.
- def render compiled, engine, vars = {}
- case engine.to_s
- when 'liquid'
- compiled.render(vars)
- when 'erb'
- compiled.result_with_hash(vars)
- else
- compiled.to_s
- end
- end
- end
-
- # Represents a field that will be rendered by a template engine.
- class TemplatedField
- # @return [String] The raw, un-rendered template string.
- attr_reader :raw
- # @return [Object] The compiled template object.
- attr_reader :compiled
- # @return [String] The name of the template engine.
- attr_reader :engine
- # @return [Boolean] Whether the template was explicitly tagged.
- attr_reader :tagged
- # @return [Boolean] Whether the template engine was inferred.
- attr_reader :inferred
-
- # @param raw [String] The raw template string.
- # @param compiled [Object] The compiled template object.
- # @param engine [String] The name of the template engine.
- # @param tagged [Boolean] Whether the template was explicitly tagged.
- # @param inferred [Boolean] Whether the template engine was inferred.
- def initialize raw, compiled, engine, tagged, inferred
- @raw = raw
- @compiled = compiled
- @engine = engine
- @tagged = tagged
- @inferred = inferred
- end
-
- # @return [true] Always returns true to indicate this is a templated field.
- def templated?
- true
- end
-
- # @return [Boolean] True if the field is deferred (not yet compiled).
- def deferred?
- compiled.nil?
- end
-
- # @return [self] Returns self for Liquid compatibility.
- def to_liquid
- self
- end
-
- # Renders the template with the given context.
- # @param context [Hash, Liquid::Context] The context for rendering.
- # @return [String] The rendered output.
- def render context = {}
- scope = context.respond_to?(:environments) ? context.environments.first : context
- Engines.render(compiled, engine, scope)
- end
-
- # Renders the template with an empty context.
- # @return [String] The rendered output.
- def to_s
- render({})
- end
- end
-
- # Holds contextual information for templating.
- class Context
- # @return [Symbol] The rendering stage (e.g., `:load`).
- attr_reader :stage
- # @return [Boolean] Whether to use strict rendering.
- attr_reader :strict
- # @return [Hash] A hash of scopes for rendering.
- attr_reader :scopes
-
- # @param stage [Symbol] The rendering stage.
- # @param strict [Boolean] Whether to use strict rendering.
- # @param scopes [Hash] A hash of scopes.
- def initialize stage: :load, strict: false, scopes: {}
- @stage = stage.to_sym
- @strict = strict
- @scopes = scopes.transform_keys(&:to_sym)
- end
-
- # Creates a new Context object from a schema fragment.
- # @param schema_fragment [Hash] The schema fragment containing templating info.
- # @return [Context] The new Context object.
- def self.from_schema schema_fragment
- render_conf = schema_fragment['templating'] || {}
-
- stage = (render_conf['stage'] || :load).to_sym
- strict = render_conf['strict'] == true
- scopes = (render_conf['scopes'] || {}).transform_keys(&:to_sym)
-
- new(stage: stage, strict: strict, scopes: scopes)
- end
-
- # Merges all scopes into a single hash.
- # @return [Hash] The merged scope.
- def merged_scope
- scopes.values.reduce({}) { |acc, s| acc.merge(s) }
- end
- end
-
- # Compiles templated fields in a data structure.
- # @param data [Hash] The data to process.
- # @param schema [Hash] The schema defining the fields.
- # @param fields [Array] The fields to compile.
- # @param scope [Hash] The scope for rendering.
- def self.compile_templated_fields! data:, schema:, fields:, scope: {}
- fields.each do |field_entry|
- key = field_entry[:key]
- path = field_entry[:path]
- val = data[key]
-
- next unless val.is_a?(String) || (val.is_a?(Hash) && val['__tag__'] && val['value'])
-
- raw = val.is_a?(Hash) ? val['value'] : val
- tagged = val.is_a?(Hash)
- config = SchemaGraphy::SchemaUtils.templating_config_for(schema, path)
- engine = tagged ? val['__tag__'] : (config['default'] || 'liquid')
-
- compiled = Engines.compile(raw, engine)
-
- data[key] = if config['delay']
- TemplatedField.new(raw, compiled, engine, tagged, inferred: !tagged)
- else
- Engines.render(compiled, engine, scope)
- end
- end
- end
-
- # Renders a field if it is a template.
- # @param val [Object] The value to render.
- # @param context [Hash] The context for rendering.
- # @return [Object] The rendered value, or the original value if not a template.
- def self.render_field_if_template val, context = {}
- if val.respond_to?(:templated?) && val.templated?
- val.render(context)
- else
- val
- end
- end
- end
-end
diff --git a/releasehx.gemspec b/releasehx.gemspec
index 266e34f..b0536a5 100644
--- a/releasehx.gemspec
+++ b/releasehx.gemspec
@@ -55,7 +55,9 @@ Gem::Specification.new do |spec|
spec.add_dependency 'tilt', '~> 2.3'
spec.add_dependency 'yaml', '~> 0.4'
+ spec.add_dependency 'asciidoctor-html5s', '~> 0.5'
spec.add_dependency 'asciidoctor-pdf', '~> 2.3'
+ spec.add_dependency 'asciisourcerer', '~> 0.4'
spec.add_dependency 'commonmarker', '~> 0.23'
spec.add_dependency 'jekyll', '~> 4.4'
spec.add_dependency 'jekyll-asciidoc', '~> 3.0.0'
@@ -64,5 +66,6 @@ Gem::Specification.new do |spec|
spec.add_dependency 'liquid', '~> 4.0'
spec.add_dependency 'mcp', '~> 0.4'
spec.add_dependency 'prism', '~> 1.5'
+ spec.add_dependency 'schemagraphy', '~> 0.1'
spec.add_dependency 'to_regexp', '= 0.2.1'
end
diff --git a/scripts/build-docker.sh b/scripts/build-docker.sh
index c93165c..6d79fb1 100755
--- a/scripts/build-docker.sh
+++ b/scripts/build-docker.sh
@@ -36,7 +36,7 @@ gem_file=$(find pkg -name "${PROJECT_NAME}-*.gem" -type f 2>/dev/null | head -n
if [ -z "$gem_file" ]; then
echo -e "${RED}❌ Error: No gem found in pkg/ directory${NC}"
- echo -e "${YELLOW}Build the gem first with: bundle exec rake build${NC}"
+ echo -e "${YELLOW}Build the gem first with: bundle exec rake build:gem${NC}"
exit 1
fi
diff --git a/scripts/build_docs.rb b/scripts/build_docs.rb
index 92c3fa6..7a22ac7 100644
--- a/scripts/build_docs.rb
+++ b/scripts/build_docs.rb
@@ -64,68 +64,66 @@ def self.add_config_reference_front_matter
end
def self.generate_module_docs version
- puts 'Generating modular API docs with YARD...'
+ puts 'Generating API docs with YARD...'
- modules = discover_modules
+ mod = build_releasehx_module
- modules.each do |mod|
- puts "--> Generating docs for #{mod[:name]}..."
+ puts "--> Generating docs for #{mod[:name]}..."
- unless File.exist?(mod[:readme])
- puts " Warning: README not found at #{mod[:readme]}, skipping module"
- next
- end
+ unless File.exist?(mod[:readme])
+ puts " Warning: README not found at #{mod[:readme]}, skipping"
+ return
+ end
- if mod[:files].empty?
- puts " Warning: No Ruby files found for #{mod[:name]}, skipping module"
- next
- end
+ if mod[:files].empty?
+ puts " Warning: No Ruby files found for #{mod[:name]}, skipping"
+ return
+ end
- readme_dir = File.dirname(mod[:readme])
- readme_content = File.read(mod[:readme])
- processed_readme_html = Asciidoctor.convert(
- readme_content, safe: :unsafe, base_dir: readme_dir, header_footer: false)
- temp_readme_path = "build/docs/#{mod[:name].downcase}_readme.html"
- File.write(temp_readme_path, processed_readme_html)
+ readme_dir = File.dirname(mod[:readme])
+ readme_content = File.read(mod[:readme])
+ processed_readme_html = Asciidoctor.convert(
+ readme_content, safe: :unsafe, base_dir: readme_dir, header_footer: false)
+ temp_readme_path = "build/docs/#{mod[:name].downcase}_readme.html"
+ File.write(temp_readme_path, processed_readme_html)
- output_dir = "build/docs/docs/api/#{mod[:name].downcase}"
- FileUtils.mkdir_p output_dir
- file_list = mod[:files].join(' ')
+ output_dir = "build/docs/docs/api/#{mod[:name].downcase}"
+ FileUtils.mkdir_p output_dir
+ file_list = mod[:files].join(' ')
- # Use custom YARD templates from docs/yard/templates
- template_dir = 'docs/yard/templates'
- custom_css = 'docs/yard/assets/css/custom.css'
+ # Use custom YARD templates from docs/yard/templates
+ template_dir = 'docs/yard/templates'
+ custom_css = 'docs/yard/assets/css/custom.css'
- yard_cmd = "yard doc --output-dir #{output_dir} --readme #{temp_readme_path} " \
- "--title \"#{mod[:name]} API (v#{version})\" --markup html " \
- "--template-path #{template_dir}"
+ yard_cmd = "yard doc --output-dir #{output_dir} --readme #{temp_readme_path} " \
+ "--title \"#{mod[:name]} API (v#{version})\" --markup html " \
+ "--template-path #{template_dir}"
- # Add custom CSS if it exists
- yard_cmd += " --asset #{custom_css}:css/" if File.exist?(custom_css)
+ # Add custom CSS if it exists
+ yard_cmd += " --asset #{custom_css}:css/" if File.exist?(custom_css)
- yard_cmd += " #{file_list}"
+ yard_cmd += " #{file_list}"
- puts " Warning: YARD generation failed for #{mod[:name]}" unless system(yard_cmd)
+ puts " Warning: YARD generation failed for #{mod[:name]}" unless system(yard_cmd)
- # Post-process HTML files to add custom CSS with correct relative paths
- next unless File.exist?(custom_css)
+ # Post-process HTML files to add custom CSS with correct relative paths
+ return unless File.exist?(custom_css)
- Dir.glob("#{output_dir}/**/*.html").each do |html_file|
- add_custom_css_to_html(html_file, output_dir)
- end
+ Dir.glob("#{output_dir}/**/*.html").each do |html_file|
+ add_custom_css_to_html(html_file, output_dir)
+ end
- # Fix YARD index file naming: _index.html is the real API index,
- # but index.html is generated from README. Rename them appropriately.
- yard_index = File.join(output_dir, '_index.html')
- readme_index = File.join(output_dir, 'index.html')
+ # Fix YARD index file naming: _index.html is the real API index,
+ # but index.html is generated from README. Rename them appropriately.
+ yard_index = File.join(output_dir, '_index.html')
+ readme_index = File.join(output_dir, 'index.html')
- next unless File.exist?(yard_index)
+ return unless File.exist?(yard_index)
- # Rename README-based index to readme.html
- File.rename(readme_index, File.join(output_dir, 'readme.html')) if File.exist?(readme_index)
- # Rename _index.html to index.html (this is the real API overview)
- File.rename(yard_index, readme_index)
- end
+ # Rename README-based index to readme.html
+ File.rename(readme_index, File.join(output_dir, 'readme.html')) if File.exist?(readme_index)
+ # Rename _index.html to index.html (this is the real API overview)
+ File.rename(yard_index, readme_index)
end
def self.add_custom_css_to_html html_file, base_output_dir
@@ -146,63 +144,22 @@ def self.add_custom_css_to_html html_file, base_output_dir
File.write(html_file, updated_content)
end
- def self.discover_modules
- # Find the main gem name from gemspec
- # NOTE: We will probably do away with this once
- # SchemaGraphy and Sourcerer are spun off
- gemspec_file = Dir.glob('*.gemspec').first
- gemspec_file ? File.basename(gemspec_file, '.gemspec') : nil
-
- # Discover all lib subdirectories that contain Ruby files
- lib_dirs = Dir.glob('lib/*/').map { |dir| File.basename(dir) }
-
- modules = []
- base_nav_order = 2
-
- lib_dirs.each_with_index do |dir_name, index|
- files = Dir.glob(["lib/#{dir_name}.rb", "lib/#{dir_name}/**/*.rb"])
- next if files.empty?
-
- readme_path = "lib/#{dir_name}/README.adoc"
- next unless File.exist?(readme_path)
-
- # Convert directory name to proper module name (e.g., 'releasehx' -> 'ReleaseHx')
- # Unreliable
- module_name = dir_name.split('_').map(&:capitalize).join
-
- # Assign nav_order: only ReleaseHx gets explicit nav_order 4, others get auto-incremented values
- nav_order = if dir_name == 'releasehx'
- 4
- else
- base_nav_order + index + 1
- end
-
- modules << {
- name: module_name,
- files: files,
- readme: readme_path,
- nav_order: nav_order,
- title: "#{module_name} API"
- }
- end
-
- # Sort by nav_order to ensure main gem comes first
- modules.sort_by { |mod| mod[:nav_order] }
+ def self.build_releasehx_module
+ # Build ReleaseHx module (schemagraphy and sourcerer are now externalized)
+ files = Dir.glob(['lib/releasehx.rb', 'lib/releasehx/**/*.rb'])
+ readme_path = 'lib/releasehx/README.adoc'
+
+ {
+ name: 'ReleaseHx',
+ files: files,
+ readme: readme_path,
+ title: 'ReleaseHx API'
+ }
end
def self.add_jekyll_front_matter
puts 'Adding front matter to YARD API docs...'
- # Get the modules to build lookup maps
- modules = discover_modules
- nav_order_map = {}
- title_map = {}
-
- modules.each do |mod|
- nav_order_map[mod[:name].downcase] = mod[:nav_order]
- title_map[mod[:name].downcase] = mod[:title]
- end
-
# Add front matter to YARD API documentation files
api_files = Dir.glob('build/docs/docs/api/**/*.html')
return if api_files.empty?
@@ -213,18 +170,17 @@ def self.add_jekyll_front_matter
content = File.read(file)
next if content.start_with?('---')
- module_name = Pathname.new(file).each_filename.to_a[-2]
-
- if File.basename(file) == 'index.html' && title_map.key?(module_name)
- page_title = title_map[module_name]
+ if File.basename(file) == 'index.html'
+ # Main index gets explicit title and nav_order
front_matter = <<~HEREDOC
---
layout: null
- title: "#{page_title}"
- nav_order: #{nav_order_map[module_name]}
+ title: "ReleaseHx API"
+ nav_order: 4
---
HEREDOC
else
+ # Other files are auto-excluded from nav
doc = Nokogiri::HTML(content)
page_title = doc.title
page_title = if page_title.nil? || page_title.strip.empty?
diff --git a/specs/data/config-def.yml b/specs/data/config-def.yml
index 2e85683..2f30c38 100644
--- a/specs/data/config-def.yml
+++ b/specs/data/config-def.yml
@@ -203,12 +203,59 @@ properties:
The origin markup format for notes.
May be `markdown` or `asciidoc`.
dflt: markdown
- engine:
- type: String
+ engines:
desc: |
- The markup converter to use for the issues.
- Defaults to `asciidoctor` for AsciiDoc and `redcarpet` for Markdown.
- Options include `asciidoctor`, `redcarpet`, `commonmarker`, `kramdown`, or `pandoc`.
+ Specifies the conversion engines to use when enriching to various output formats.
+
+ These settings determine which tool processes the conversion from draft formats to rich output.
+ Intelligent defaults are applied based on source format when engines are not explicitly configured.
+ properties:
+ html:
+ type: String
+ desc: |
+ Engine for converting to HTML format.
+
+ Valid engines:
+
+ [horizontal]
+ `asciidoctor-html5`:: Standard Asciidoctor HTML5 backend (nested div structure)
+ `asciidoctor-html5s`:: Semantic HTML5 backend (cleaner section-based markup)
+ `kramdown`:: Kramdown Markdown converter (for Markdown sources)
+ `pandoc`:: Universal document converter (if available)
+
+ When not specified, intelligent defaults apply:
+
+ - AsciiDoc → HTML: `asciidoctor-html5s` (semantic HTML5)
+ - Markdown → HTML: `kramdown`
+ - RHYML → HTML: Liquid templates (no engine needed)
+ docs: |
+ The html5s backend produces cleaner, more semantic HTML with `` elements
+ instead of nested `
` structures.
+
+ This is particularly useful when you want to apply custom CSS or when generating
+ HTML that will be further processed or embedded in other systems.
+
+ For backwards compatibility or when you need the traditional Asciidoctor structure,
+ use `asciidoctor-html5`.
+ pdf:
+ type: String
+ desc: |
+ Engine for converting to PDF format.
+
+ Valid engines:
+
+ [horizontal]
+ `asciidoctor-pdf`:: Asciidoctor PDF converter (default for AsciiDoc)
+ `asciidoctor-web-pdf`:: Web-based PDF converter using headless Chrome
+ `pandoc`:: Universal document converter (if available)
+
+ When not specified, intelligent defaults apply:
+
+ - AsciiDoc → PDF: `asciidoctor-pdf`
+ - Markdown → PDF: `pandoc` (if available)
+ docs: |
+ The default `asciidoctor-pdf` engine provides excellent typography and layout
+ for technical documentation with support for themes, fonts, and advanced formatting.
extensions:
desc: Default file extensions.
@@ -837,7 +884,7 @@ properties:
desc: |
Default settings for `rhx` command executions.
properties:
- wrapped:
+ html_wrap:
type: Boolean
desc: |
Include (or exclude) head, header, and footer elements when enriching to HTML.
@@ -862,6 +909,7 @@ properties:
Include frontmatter in AsciiDoc drafts.
Uses the `templates.asciidoc_frontmatter` template.
+ dflt: false
fetch:
type: String
desc: |
@@ -1043,6 +1091,74 @@ properties:
It may include `{{ title }}`, `{{ version }}`, `{{ date }}`, as well as any `vars`-scoped variables as you pass in.
dflt: *markdown_frontmatter_tplt
+ html_framework:
+ type: String
+ desc: |
+ The HTML framework to use when enriching to HTML.
+
+ Valid entries:
+
+ [horizontal]
+ `bare`:: minimal HTML structure
+ `bootstrap4`:: Bootstrap 4 framework
+ `bootstrap5`:: Bootstrap 5 framework
+ dflt: bare
+ styling:
+ desc: |
+ Configuration options for HTML styling and CSS framework integration.
+ properties:
+ mode:
+ type: String
+ desc: |
+ The HTML styling approach to use when generating wrapped HTML output.
+
+ Valid options:
+
+ * `minimal` -- Basic semantic HTML with minimal inline styles
+ * `embedded` -- Include comprehensive CSS in `