releasehx 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +363 -330
  3. data/build/docs/_config.yml +1 -0
  4. data/build/docs/_release_index.adoc +3 -2
  5. data/build/docs/config-reference.adoc +197 -10
  6. data/build/docs/config-reference.json +56 -7
  7. data/build/docs/index.adoc +315 -59
  8. data/build/docs/landing.adoc +1 -1
  9. data/build/docs/manpage.adoc +2 -2
  10. data/build/docs/release-procedure.adoc +365 -0
  11. data/build/docs/release-procedure.html +87 -0
  12. data/build/docs/releasehx.1 +17 -5
  13. data/build/docs/sample-config.yml +14 -7
  14. data/lib/releasehx/cli.rb +5 -2
  15. data/lib/releasehx/configuration.rb +0 -1
  16. data/lib/releasehx/generated.rb +1 -1
  17. data/lib/releasehx/mcp/assets/agent-config-guide.md +1 -1
  18. data/lib/releasehx/mcp/assets/config-def.yml +122 -6
  19. data/lib/releasehx/mcp/assets/config-reference.adoc +197 -10
  20. data/lib/releasehx/mcp/assets/config-reference.json +56 -7
  21. data/lib/releasehx/mcp/assets/sample-config.yml +14 -7
  22. data/lib/releasehx/mcp/server.rb +0 -1
  23. data/lib/releasehx/ops/enrich_ops.rb +161 -55
  24. data/lib/releasehx/ops/template_ops.rb +1 -1
  25. data/lib/releasehx/rhyml/adapter.rb +0 -3
  26. data/lib/releasehx/rhyml/templates/bootstrap-overrides.css +15 -0
  27. data/lib/releasehx/rhyml/templates/changelog.adoc.liquid +2 -0
  28. data/lib/releasehx/rhyml/templates/changelog.html.liquid +6 -4
  29. data/lib/releasehx/rhyml/templates/changelog.md.liquid +1 -0
  30. data/lib/releasehx/rhyml/templates/embedded.css.liquid +263 -0
  31. data/lib/releasehx/rhyml/templates/entry.adoc.liquid +1 -0
  32. data/lib/releasehx/rhyml/templates/entry.html.liquid +21 -20
  33. data/lib/releasehx/rhyml/templates/entry.md.liquid +15 -21
  34. data/lib/releasehx/rhyml/templates/head-parser.liquid +6 -2
  35. data/lib/releasehx/rhyml/templates/header.liquid +13 -4
  36. data/lib/releasehx/rhyml/templates/history.html.liquid +152 -33
  37. data/lib/releasehx/rhyml/templates/metadata-entry.adoc.liquid +83 -38
  38. data/lib/releasehx/rhyml/templates/metadata-entry.html.liquid +60 -1
  39. data/lib/releasehx/rhyml/templates/metadata-entry.md.liquid +65 -113
  40. data/lib/releasehx/rhyml/templates/metadata-note.adoc.liquid +83 -38
  41. data/lib/releasehx/rhyml/templates/metadata-note.html.liquid +59 -22
  42. data/lib/releasehx/rhyml/templates/metadata-note.md.liquid +68 -23
  43. data/lib/releasehx/rhyml/templates/note.html.liquid +25 -19
  44. data/lib/releasehx/rhyml/templates/note.md.liquid +44 -26
  45. data/lib/releasehx/rhyml/templates/release-notes.adoc.liquid +2 -0
  46. data/lib/releasehx/rhyml/templates/release-notes.html.liquid +6 -4
  47. data/lib/releasehx/rhyml/templates/release-notes.md.liquid +1 -0
  48. data/lib/releasehx/rhyml/templates/release.adoc.liquid +2 -0
  49. data/lib/releasehx/rhyml/templates/release.md.liquid +8 -7
  50. data/lib/releasehx/rhyml/templates/rhyml-change.yaml.liquid +36 -36
  51. data/lib/releasehx/rhyml/templates/wrapper.html.liquid +103 -0
  52. data/lib/releasehx/sgyml/helpers.rb +0 -2
  53. data/lib/releasehx/transforms/adf_to_markdown.rb +1 -1
  54. data/lib/releasehx/version.rb +0 -2
  55. data/lib/releasehx.rb +2 -2
  56. data/specs/data/config-def.yml +122 -6
  57. metadata +48 -25
  58. data/build/docs/schemagraphy_readme.html +0 -0
  59. data/build/docs/sourcerer_readme.html +0 -46
  60. data/lib/schemagraphy/attribute_resolver.rb +0 -48
  61. data/lib/schemagraphy/cfgyml/definition.rb +0 -90
  62. data/lib/schemagraphy/cfgyml/doc_builder.rb +0 -52
  63. data/lib/schemagraphy/cfgyml/path_reference.rb +0 -24
  64. data/lib/schemagraphy/data_query/json_pointer.rb +0 -42
  65. data/lib/schemagraphy/loader.rb +0 -59
  66. data/lib/schemagraphy/regexp_utils.rb +0 -235
  67. data/lib/schemagraphy/safe_expression.rb +0 -189
  68. data/lib/schemagraphy/schema_utils.rb +0 -124
  69. data/lib/schemagraphy/tag_utils.rb +0 -32
  70. data/lib/schemagraphy/templating.rb +0 -104
  71. data/lib/schemagraphy.rb +0 -17
  72. data/lib/sourcerer/builder.rb +0 -120
  73. data/lib/sourcerer/jekyll/bootstrapper.rb +0 -78
  74. data/lib/sourcerer/jekyll/liquid/file_system.rb +0 -74
  75. data/lib/sourcerer/jekyll/liquid/filters.rb +0 -215
  76. data/lib/sourcerer/jekyll/liquid/tags.rb +0 -44
  77. data/lib/sourcerer/jekyll/monkeypatches.rb +0 -73
  78. data/lib/sourcerer/jekyll.rb +0 -26
  79. data/lib/sourcerer/plaintext_converter.rb +0 -75
  80. data/lib/sourcerer/templating.rb +0 -190
  81. data/lib/sourcerer.rb +0 -322
@@ -1,74 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'liquid'
4
-
5
- module Sourcerer
6
- module Jekyll
7
- module Liquid
8
- # A custom Liquid file system that extends `Liquid::LocalFileSystem` to support
9
- # multiple root paths for template lookups. This allows templates to be
10
- # resolved from a prioritized list of directories.
11
- class FileSystem < ::Liquid::LocalFileSystem
12
- # Initializes the file system with one or more root paths.
13
- #
14
- # @param roots_or_root [String, Array<String>] A single root path or an array of root paths.
15
- # rubocop:disable Lint/MissingSuper
16
- # Intentional: Custom implementation that doesn't need parent's initialization
17
- def initialize roots_or_root
18
- if roots_or_root.is_a?(Array)
19
- @roots = roots_or_root.map { |root| File.expand_path(root) }
20
- @multi_root = true
21
- else
22
- @root = File.expand_path(roots_or_root)
23
- @multi_root = false
24
- end
25
- end
26
- # rubocop:enable Lint/MissingSuper
27
-
28
- # Finds the full path of a template, searching through multiple roots if configured.
29
- #
30
- # @param template_path [String] The path to the template.
31
- # @return [String] The full, validated path to the template.
32
- # @raise [Liquid::FileSystemError] if the template is not found.
33
- def full_path template_path
34
- if @multi_root
35
- @roots.each do |root|
36
- full = File.expand_path(File.join(root, template_path))
37
- return full if File.exist?(full) && full.start_with?(root)
38
- end
39
- raise ::Liquid::FileSystemError, "Template not found: '#{template_path}' in paths: #{@roots}"
40
- else
41
- full = File.expand_path(File.join(@root, template_path))
42
- validate_path(full)
43
- end
44
- end
45
-
46
- # Reads the content of a template file.
47
- #
48
- # @param template_path [String] The path to the template.
49
- # @return [String] The content of the template file.
50
- def read_template_file template_path
51
- path = full_path(template_path)
52
- File.read(path)
53
- end
54
-
55
- private
56
-
57
- # Validates that the resolved path is within the allowed root(s).
58
- def validate_path path
59
- if @multi_root
60
- # Check if path starts with any of the allowed roots
61
- unless @roots.any? { |root| path.start_with?(root) }
62
- raise ::Liquid::FileSystemError, "Illegal template path '#{path}'"
63
- end
64
-
65
- else
66
- raise ::Liquid::FileSystemError, "Illegal template path '#{path}'" unless path.start_with?(@root)
67
-
68
- end
69
- path
70
- end
71
- end
72
- end
73
- end
74
- end
@@ -1,215 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'kramdown-asciidoc'
4
- require 'base64'
5
- require 'cgi'
6
-
7
- module Sourcerer
8
- module Jekyll
9
- module Liquid
10
- # This module provides a set of custom filters for use in Liquid templates.
11
- module Filters
12
- # Renders a Liquid template string with a given scope.
13
- # @param input [String, Object] The Liquid template string or a pre-parsed template object.
14
- # @param vars [Hash] A hash of variables to use as the scope.
15
- # @return [String] The rendered output.
16
- def render input, vars = nil
17
- scope = if vars.is_a?(Hash)
18
- vars.transform_keys(&:to_s)
19
- else
20
- {}
21
- end
22
-
23
- template =
24
- if input.respond_to?(:render) && input.respond_to?(:templated?) && input.templated?
25
- input
26
- else
27
- ::Liquid::Template.parse(input.to_s)
28
- end
29
-
30
- template.render(scope)
31
- end
32
-
33
- # Converts a string into a slug.
34
- # @param input [String] The string to convert.
35
- # @param format [String] The desired format (`kebab`, `snake`, `camel`, `pascal`).
36
- # @return [String] The sluggerized string.
37
- def sluggerize input, format = 'kebab'
38
- return input unless input.is_a? String
39
-
40
- case format
41
- when 'kebab' then input.downcase.gsub(/[\s\-_]/, '-')
42
- when 'snake' then input.downcase.gsub(/[\s\-_]/, '_')
43
- when 'camel' then input.downcase.gsub(/[\s\-_]/, '_').camelize(:lower)
44
- when 'pascal' then input.downcase.gsub(/[\s\-_]/, '_').camelize(:upper)
45
- else input
46
- end
47
- end
48
-
49
- # Replaces double newlines with a newline and a plus sign.
50
- # @param input [String] The input string.
51
- # @return [String] The processed string.
52
- def plusify input
53
- input.gsub(/\n\n+/, "\n+\n")
54
- end
55
-
56
- # Converts a Markdown string to AsciiDoc.
57
- # @param input [String] The Markdown string.
58
- # @param wrap [String] The wrapping option for the converter.
59
- # @return [String] The converted AsciiDoc string.
60
- def md_to_adoc input, wrap = 'ventilate'
61
- options = {}
62
- options[:wrap] = wrap.to_sym if wrap
63
- Kramdoc.convert(input, options)
64
- end
65
-
66
- # Indents a string by a given number of spaces.
67
- # @param input [String] The string to indent.
68
- # @param spaces [Integer] The number of spaces for indentation.
69
- # @param line1 [Boolean] Whether to indent the first line.
70
- # @return [String] The indented string.
71
- def indent input, spaces = 2, line1: false
72
- indent = ' ' * spaces
73
- lines = input.split("\n")
74
- indented = if line1
75
- lines.map { |line| indent + line }
76
- else
77
- lines.map.with_index { |line, i| i.zero? ? line : indent + line }
78
- end
79
- indented.join("\n")
80
- end
81
-
82
- # Checks the type of a value in the context of SG-YML.
83
- # @param input [Object] The value to check.
84
- # @return [String] A string representing the type.
85
- def sgyml_type_check input
86
- if input.nil?
87
- 'Null:nil'
88
- elsif input.is_a? Array
89
- # if all items in Array are (integer, float, string, boolean)
90
- if input.all? do |item|
91
- item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(String) ||
92
- item.is_a?(TrueClass) || item.is_a?(FalseClass)
93
- end
94
- 'Compound:ArrayList'
95
- elsif input.all? { |item| item.is_a?(Hash) && (item.keys.length >= 2) }
96
- 'Compound:ArrayTable'
97
- else
98
- 'Compound:Array'
99
- end
100
- elsif input.is_a? Hash
101
- if input.values.all? { |value| value.is_a?(Hash) && (value.keys.length >= 2) }
102
- 'Compound:MapTable'
103
- else
104
- 'Compound:Map'
105
- end
106
- elsif input.is_a? String
107
- 'Scalar:String'
108
- elsif input.is_a? Integer
109
- 'Scalar:Number'
110
- elsif input.is_a? Time
111
- 'Scalar:DateTime'
112
- elsif input.is_a? Float
113
- 'Scalar:Float'
114
- elsif input.is_a?(TrueClass) || input.is_a?(FalseClass)
115
- 'Scalar:Boolean'
116
- else
117
- 'unknown:unknown'
118
- end
119
- end
120
-
121
- # Returns the Ruby class name of a value.
122
- # @param input [Object] The value.
123
- # @return [String] The class name.
124
- def ruby_class input
125
- input.class.name
126
- end
127
-
128
- # Removes markup from a string.
129
- # @param input [String] The string to demarkupify.
130
- # @return [String] The demarkupified string.
131
- def demarkupify input
132
- return input unless input.is_a? String
133
-
134
- input = input.gsub(/`"|"`/, '"')
135
- input = input.gsub(/'`|`'/, "'")
136
- input = input.gsub(/[*_`]/, '')
137
- # change curly quotes to striaght quotes
138
- input = input.gsub(/[“”]/, '"')
139
- input.gsub(/[‘’]/, "'")
140
- end
141
-
142
- # Dumps a value to YAML format.
143
- # @param input [Object] The value to dump.
144
- # @return [String] The YAML representation.
145
- def inspect_yaml input
146
- require 'yaml'
147
- YAML.dump(input)
148
- end
149
-
150
- # Base64 encodes a string.
151
- # @param input [String] The string to encode.
152
- # @return [String] The Base64-encoded string.
153
- def base64 input
154
- return input unless input.is_a? String
155
-
156
- Base64.strict_encode64(input)
157
- end
158
-
159
- # Decodes a Base64-encoded string.
160
- # @param input [String] The string to decode.
161
- # @return [String] The decoded string.
162
- def base64_decode input
163
- return input unless input.is_a? String
164
-
165
- Base64.strict_decode64(input)
166
- rescue ArgumentError
167
- # Return original input if decoding fails
168
- input
169
- end
170
-
171
- # URL-encodes a string.
172
- # @param input [String] The string to encode.
173
- # @return [String] The URL-encoded string.
174
- def url_encode input
175
- return input unless input.is_a? String
176
-
177
- CGI.escape(input)
178
- end
179
-
180
- # Decodes a URL-encoded string.
181
- # @param input [String] The string to decode.
182
- # @return [String] The decoded string.
183
- def url_decode input
184
- return input unless input.is_a? String
185
-
186
- CGI.unescape(input)
187
- rescue ArgumentError
188
- # Return original input if decoding fails
189
- input
190
- end
191
-
192
- # HTML-escapes a string.
193
- # @param input [String] The string to escape.
194
- # @return [String] The HTML-escaped string.
195
- def html_escape input
196
- return input unless input.is_a? String
197
-
198
- CGI.escapeHTML(input)
199
- end
200
-
201
- # Unescapes an HTML-escaped string.
202
- # @param input [String] The string to unescape.
203
- # @return [String] The unescaped string.
204
- def html_unescape input
205
- return input unless input.is_a? String
206
-
207
- CGI.unescapeHTML(input)
208
- end
209
- end
210
- end
211
- end
212
- end
213
-
214
- # Register the filters automatically
215
- Liquid::Template.register_filter(Sourcerer::Jekyll::Liquid::Filters)
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sourcerer
4
- module Jekyll
5
- # This module contains custom Liquid tags for the Sourcerer templating environment.
6
- module Tags
7
- # A Liquid tag for embedding and rendering a file within a template.
8
- # It searches for the file in the configured include paths.
9
- class EmbedTag < ::Liquid::Tag
10
- # @param tag_name [String] The name of the tag ('embed').
11
- # @param markup [String] The name of the partial to embed.
12
- # @param tokens [Array<String>] The list of tokens.
13
- def initialize tag_name, markup, tokens
14
- super
15
- @partial_name = markup.strip
16
- end
17
-
18
- # Renders the embedded file.
19
- #
20
- # @param context [Liquid::Context] The Liquid context.
21
- # @return [String] The rendered content of the embedded file.
22
- # @raise [IOError] if the embed file is not found.
23
- def render context
24
- includes_paths = context.registers[:includes_load_paths] || []
25
-
26
- found_path = includes_paths.find do |base|
27
- candidate = File.expand_path(@partial_name, base)
28
- File.exist?(candidate)
29
- end
30
-
31
- raise "Embed file not found: #{@partial_name}" unless found_path
32
-
33
- full_path = File.expand_path(@partial_name, found_path)
34
- source = File.read(full_path)
35
-
36
- partial = ::Liquid::Template.parse(source)
37
- partial.render!(context)
38
- end
39
- end
40
- end
41
- end
42
- end
43
-
44
- Liquid::Template.register_tag('embed', Sourcerer::Jekyll::Tags::EmbedTag)
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sourcerer
4
- module Jekyll
5
- # This module contains monkeypatches for Jekyll to modify or extend its behavior.
6
- module Monkeypatches
7
- # Patches Jekyll's `OptimizedIncludeTag` to modify its behavior.
8
- # The patch enhances include path resolution and context handling to better
9
- # suit the needs of Sourcerer's templating environment.
10
- def self.patch_jekyll
11
- return unless defined?(::Jekyll::Tags::OptimizedIncludeTag)
12
-
13
- ::Jekyll::Tags::OptimizedIncludeTag.class_eval do
14
- define_method :render do |context|
15
- site = context.registers[:site]
16
- file = render_variable(context) || @file
17
-
18
- context.stack do
19
- context['include'] = parse_params(context) if @params
20
-
21
- source = site.inclusions[file]
22
-
23
- unless source
24
-
25
- # Debug lines before attempting path resolution
26
-
27
- # Safe resolution
28
- paths = context.registers[:includes_load_paths] || []
29
- path = paths
30
- .map { |dir| File.join(dir, file) }
31
- .find { |p| File.file?(p) }
32
-
33
- raise IOError, "Include file not found: #{file}" unless path
34
-
35
- source = File.read(path)
36
- end
37
-
38
- partial = ::Liquid::Template.parse(source)
39
- partial.registers[:site] = context.registers[:site]
40
- partial.assigns['include'] = context['include']
41
-
42
- ::Liquid::Template.register_filter(::Jekyll::Filters)
43
- ::Liquid::Template.register_filter(::Sourcerer::Jekyll::Liquid::Filters)
44
-
45
- # Use an isolated context so we can inspect and copy assigns
46
- subcontext = ::Liquid::Context.new(
47
- [{ 'include' => context['include'] }],
48
- {}, # Environments
49
- context.registers,
50
- rethrow_errors: true)
51
-
52
- rendered = partial.render!(subcontext)
53
-
54
- # Copy assigns from subcontext to parent context
55
- subcontext.environments.each do |env|
56
- env.each do |k, v|
57
- # Avoid clobbering outer include if reentrant
58
- next if k == 'include'
59
-
60
- context.environments.first[k] = v
61
- end
62
- end
63
-
64
- rendered
65
- end
66
- end
67
- end
68
-
69
- ::Liquid::Template.tags['include'] = ::Jekyll::Tags::OptimizedIncludeTag
70
- end
71
- end
72
- end
73
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'jekyll/bootstrapper'
4
- require_relative 'jekyll/monkeypatches'
5
- require_relative 'jekyll/liquid/file_system'
6
- require_relative 'jekyll/liquid/filters'
7
- require_relative 'jekyll/liquid/tags'
8
- require 'jekyll-asciidoc'
9
-
10
- module Sourcerer
11
- # This module encapsulates the logic for initializing a Jekyll-like Liquid
12
- # templating environment. It loads necessary plugins, applies monkeypatches,
13
- # and registers custom Liquid filters and tags.
14
- module Jekyll
15
- # Initializes the Liquid templating runtime by loading plugins,
16
- # applying patches, and registering custom filters.
17
- def self.initialize_liquid_runtime
18
- Bootstrapper.load_plugins
19
- Monkeypatches.patch_jekyll
20
- # Ensure Sourcerer filters are registered
21
- ::Liquid::Template.register_filter(::Sourcerer::Jekyll::Liquid::Filters)
22
- # Ensure jekyll-asciidoc filters are registered
23
- # ::Liquid::Template.register_filter(Jekyll::AsciiDoc::Filters)
24
- end
25
- end
26
- end
@@ -1,75 +0,0 @@
1
- # This module will likely spin off into a gem
2
-
3
- # frozen_string_literal: true
4
-
5
- require 'asciidoctor'
6
-
7
- module Sourcerer
8
- # A custom Asciidoctor converter that outputs plain text.
9
- # It is registered for the "plaintext" backend and can be used to extract
10
- # the raw text content or attributes from an AsciiDoc document.
11
- class PlainTextConverter < Asciidoctor::Converter::Base
12
- # Identify ourselves as a converter for the "plaintext" backend
13
- register_for 'plaintext'
14
-
15
- # The main entry point for the converter.
16
- # It is called by Asciidoctor to convert a node.
17
- #
18
- # @param node [Asciidoctor::AbstractNode] The node to convert.
19
- # @param _transform [String] The transform to apply (unused).
20
- # @param _opts [Hash] Options for the conversion (unused).
21
- # @return [String] The converted plain text output.
22
- def convert node, _transform = nil, _opts = {}
23
- if respond_to?("convert_#{node.node_name}", true)
24
- send("convert_#{node.node_name}", node)
25
- elsif node.respond_to?(:content)
26
- node.content.to_s
27
- elsif node.respond_to?(:text)
28
- node.text.to_s
29
- else
30
- ''
31
- end
32
- end
33
-
34
- private
35
-
36
- # Converts the document node.
37
- def convert_document node
38
- emit_attrs = node.attr('sourcerer_mode') == 'emit_attrs'
39
-
40
- if emit_attrs
41
- # only emit attribute lines directly, nothing else
42
- attrs = node.attributes.select do |k, v|
43
- k.is_a?(String) && !v.nil? && !k.start_with?('backend-', 'safe-mode', 'doctype', 'sourcerer_mode')
44
- end
45
-
46
- formatted_attrs = attrs.map { |k, v| ":#{k}: #{v}" }
47
- formatted_attrs.join("\n") # NO EXTRA SPACES OR LINES
48
- else
49
- node.blocks.map { |block| convert block }.join("\n")
50
- end
51
- end
52
-
53
- # Converts a section node.
54
- def convert_section node
55
- title = node.title? ? node.title : ''
56
- body = node.blocks.map { |block| convert block }.join("\n")
57
- [title, body].reject(&:empty?).join("\n")
58
- end
59
-
60
- # Converts a paragraph node.
61
- def convert_paragraph node
62
- node.lines.join("\n")
63
- end
64
-
65
- # Converts a listing node.
66
- def convert_listing node
67
- node.content
68
- end
69
-
70
- # Converts a literal node.
71
- def convert_literal node
72
- node.content
73
- end
74
- end
75
- end
@@ -1,190 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'liquid'
4
-
5
- module Sourcerer
6
- # This module provides the core templating functionality for Sourcerer.
7
- # It includes modules for template engines, and classes for representing
8
- # templated fields and their context.
9
- module Templating
10
- # This module handles the compilation and rendering of templates.
11
- module Engines
12
- module_function
13
-
14
- # A hash of supported template engines.
15
- SUPPORTED_ENGINES = {
16
- 'liquid' => 'liquid',
17
- 'erb' => 'erb'
18
- }.freeze
19
-
20
- # Compiles a template string using the specified engine.
21
- #
22
- # @param str [String] The template string to compile.
23
- # @param engine [String] The name of the template engine to use.
24
- # @return [Object] The compiled template object.
25
- # @raise [ArgumentError] if the engine is not supported.
26
- def compile str, engine
27
- case engine.to_s
28
- when 'liquid'
29
- Liquid::Template.parse(str)
30
- when 'erb'
31
- require 'erb'
32
- ERB.new(str)
33
- else
34
- raise ArgumentError, "Unsupported engine: #{engine}"
35
- end
36
- end
37
-
38
- # Renders a compiled template with the given variables.
39
- #
40
- # @param compiled [Object] The compiled template object.
41
- # @param engine [String] The name of the template engine.
42
- # @param vars [Hash] A hash of variables to use for rendering.
43
- # @return [String] The rendered output.
44
- def render compiled, engine, vars = {}
45
- case engine.to_s
46
- when 'liquid'
47
- compiled.render(vars)
48
- when 'erb'
49
- compiled.result_with_hash(vars)
50
- else
51
- compiled.to_s
52
- end
53
- end
54
- end
55
-
56
- # Represents a field that will be rendered by a template engine.
57
- class TemplatedField
58
- # @return [String] The raw, un-rendered template string.
59
- attr_reader :raw
60
- # @return [Object] The compiled template object.
61
- attr_reader :compiled
62
- # @return [String] The name of the template engine.
63
- attr_reader :engine
64
- # @return [Boolean] Whether the template was explicitly tagged.
65
- attr_reader :tagged
66
- # @return [Boolean] Whether the template engine was inferred.
67
- attr_reader :inferred
68
-
69
- # @param raw [String] The raw template string.
70
- # @param compiled [Object] The compiled template object.
71
- # @param engine [String] The name of the template engine.
72
- # @param tagged [Boolean] Whether the template was explicitly tagged.
73
- # @param inferred [Boolean] Whether the template engine was inferred.
74
- def initialize raw, compiled, engine, tagged, inferred
75
- @raw = raw
76
- @compiled = compiled
77
- @engine = engine
78
- @tagged = tagged
79
- @inferred = inferred
80
- end
81
-
82
- # @return [true] Always returns true to indicate this is a templated field.
83
- def templated?
84
- true
85
- end
86
-
87
- # @return [Boolean] True if the field is deferred (not yet compiled).
88
- def deferred?
89
- compiled.nil?
90
- end
91
-
92
- # @return [self] Returns self for Liquid compatibility.
93
- def to_liquid
94
- self
95
- end
96
-
97
- # Renders the template with the given context.
98
- # @param context [Hash, Liquid::Context] The context for rendering.
99
- # @return [String] The rendered output.
100
- def render context = {}
101
- scope = context.respond_to?(:environments) ? context.environments.first : context
102
- Engines.render(compiled, engine, scope)
103
- end
104
-
105
- # Renders the template with an empty context.
106
- # @return [String] The rendered output.
107
- def to_s
108
- render({})
109
- end
110
- end
111
-
112
- # Holds contextual information for templating.
113
- class Context
114
- # @return [Symbol] The rendering stage (e.g., `:load`).
115
- attr_reader :stage
116
- # @return [Boolean] Whether to use strict rendering.
117
- attr_reader :strict
118
- # @return [Hash] A hash of scopes for rendering.
119
- attr_reader :scopes
120
-
121
- # @param stage [Symbol] The rendering stage.
122
- # @param strict [Boolean] Whether to use strict rendering.
123
- # @param scopes [Hash] A hash of scopes.
124
- def initialize stage: :load, strict: false, scopes: {}
125
- @stage = stage.to_sym
126
- @strict = strict
127
- @scopes = scopes.transform_keys(&:to_sym)
128
- end
129
-
130
- # Creates a new Context object from a schema fragment.
131
- # @param schema_fragment [Hash] The schema fragment containing templating info.
132
- # @return [Context] The new Context object.
133
- def self.from_schema schema_fragment
134
- render_conf = schema_fragment['templating'] || {}
135
-
136
- stage = (render_conf['stage'] || :load).to_sym
137
- strict = render_conf['strict'] == true
138
- scopes = (render_conf['scopes'] || {}).transform_keys(&:to_sym)
139
-
140
- new(stage: stage, strict: strict, scopes: scopes)
141
- end
142
-
143
- # Merges all scopes into a single hash.
144
- # @return [Hash] The merged scope.
145
- def merged_scope
146
- scopes.values.reduce({}) { |acc, s| acc.merge(s) }
147
- end
148
- end
149
-
150
- # Compiles templated fields in a data structure.
151
- # @param data [Hash] The data to process.
152
- # @param schema [Hash] The schema defining the fields.
153
- # @param fields [Array<Hash>] The fields to compile.
154
- # @param scope [Hash] The scope for rendering.
155
- def self.compile_templated_fields! data:, schema:, fields:, scope: {}
156
- fields.each do |field_entry|
157
- key = field_entry[:key]
158
- path = field_entry[:path]
159
- val = data[key]
160
-
161
- next unless val.is_a?(String) || (val.is_a?(Hash) && val['__tag__'] && val['value'])
162
-
163
- raw = val.is_a?(Hash) ? val['value'] : val
164
- tagged = val.is_a?(Hash)
165
- config = SchemaGraphy::SchemaUtils.templating_config_for(schema, path)
166
- engine = tagged ? val['__tag__'] : (config['default'] || 'liquid')
167
-
168
- compiled = Engines.compile(raw, engine)
169
-
170
- data[key] = if config['delay']
171
- TemplatedField.new(raw, compiled, engine, tagged, inferred: !tagged)
172
- else
173
- Engines.render(compiled, engine, scope)
174
- end
175
- end
176
- end
177
-
178
- # Renders a field if it is a template.
179
- # @param val [Object] The value to render.
180
- # @param context [Hash] The context for rendering.
181
- # @return [Object] The rendered value, or the original value if not a template.
182
- def self.render_field_if_template val, context = {}
183
- if val.respond_to?(:templated?) && val.templated?
184
- val.render(context)
185
- else
186
- val
187
- end
188
- end
189
- end
190
- end