releasehx 0.1.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.
- checksums.yaml +7 -0
- data/README.adoc +2915 -0
- data/bin/releasehx +7 -0
- data/bin/rhx +7 -0
- data/bin/rhx-mcp +7 -0
- data/bin/sourcerer +32 -0
- data/build/docs/CNAME +1 -0
- data/build/docs/Gemfile.lock +95 -0
- data/build/docs/_config.yml +36 -0
- data/build/docs/config-reference.adoc +4104 -0
- data/build/docs/config-reference.json +1546 -0
- data/build/docs/index.adoc +2915 -0
- data/build/docs/landing.adoc +21 -0
- data/build/docs/manpage.adoc +68 -0
- data/build/docs/releasehx.1 +281 -0
- data/build/docs/releasehx_readme.html +367 -0
- data/build/docs/sample-config.adoc +9 -0
- data/build/docs/sample-config.yml +251 -0
- data/build/docs/schemagraphy_readme.html +0 -0
- data/build/docs/sourcerer_readme.html +46 -0
- data/build/snippets/helpscreen.txt +29 -0
- data/lib/docopslab/mcp/asset_packager.rb +30 -0
- data/lib/docopslab/mcp/manifest.rb +67 -0
- data/lib/docopslab/mcp/resource_pack.rb +46 -0
- data/lib/docopslab/mcp/server.rb +92 -0
- data/lib/docopslab/mcp.rb +6 -0
- data/lib/releasehx/cli.rb +937 -0
- data/lib/releasehx/configuration.rb +215 -0
- data/lib/releasehx/generated.rb +17 -0
- data/lib/releasehx/helpers.rb +58 -0
- data/lib/releasehx/mcp/asset_packager.rb +21 -0
- data/lib/releasehx/mcp/assets/agent-config-guide.md +178 -0
- data/lib/releasehx/mcp/assets/config-def.yml +1426 -0
- data/lib/releasehx/mcp/assets/config-reference.adoc +4104 -0
- data/lib/releasehx/mcp/assets/config-reference.json +1546 -0
- data/lib/releasehx/mcp/assets/sample-config.yml +251 -0
- data/lib/releasehx/mcp/manifest.rb +18 -0
- data/lib/releasehx/mcp/resource_pack.rb +26 -0
- data/lib/releasehx/mcp/server.rb +57 -0
- data/lib/releasehx/mcp.rb +7 -0
- data/lib/releasehx/ops/check_ops.rb +136 -0
- data/lib/releasehx/ops/draft_ops.rb +173 -0
- data/lib/releasehx/ops/enrich_ops.rb +221 -0
- data/lib/releasehx/ops/template_ops.rb +61 -0
- data/lib/releasehx/ops/write_ops.rb +124 -0
- data/lib/releasehx/rest/clients/github.yml +46 -0
- data/lib/releasehx/rest/clients/gitlab.yml +31 -0
- data/lib/releasehx/rest/clients/jira.yml +31 -0
- data/lib/releasehx/rest/yaml_client.rb +418 -0
- data/lib/releasehx/rhyml/adapter.rb +740 -0
- data/lib/releasehx/rhyml/change.rb +167 -0
- data/lib/releasehx/rhyml/liquid.rb +13 -0
- data/lib/releasehx/rhyml/loaders.rb +37 -0
- data/lib/releasehx/rhyml/mappings/github.yaml +60 -0
- data/lib/releasehx/rhyml/mappings/gitlab.yaml +73 -0
- data/lib/releasehx/rhyml/mappings/jira.yaml +29 -0
- data/lib/releasehx/rhyml/mappings/verb_past_tenses.yml +98 -0
- data/lib/releasehx/rhyml/release.rb +144 -0
- data/lib/releasehx/rhyml.rb +15 -0
- data/lib/releasehx/sgyml/helpers.rb +45 -0
- data/lib/releasehx/transforms/adf_to_markdown.rb +307 -0
- data/lib/releasehx/version.rb +7 -0
- data/lib/releasehx.rb +69 -0
- data/lib/schemagraphy/attribute_resolver.rb +48 -0
- data/lib/schemagraphy/cfgyml/definition.rb +90 -0
- data/lib/schemagraphy/cfgyml/doc_builder.rb +52 -0
- data/lib/schemagraphy/cfgyml/path_reference.rb +24 -0
- data/lib/schemagraphy/data_query/json_pointer.rb +42 -0
- data/lib/schemagraphy/loader.rb +59 -0
- data/lib/schemagraphy/regexp_utils.rb +215 -0
- data/lib/schemagraphy/safe_expression.rb +189 -0
- data/lib/schemagraphy/schema_utils.rb +124 -0
- data/lib/schemagraphy/tag_utils.rb +32 -0
- data/lib/schemagraphy/templating.rb +104 -0
- data/lib/schemagraphy.rb +17 -0
- data/lib/sourcerer/builder.rb +120 -0
- data/lib/sourcerer/jekyll/bootstrapper.rb +78 -0
- data/lib/sourcerer/jekyll/liquid/file_system.rb +74 -0
- data/lib/sourcerer/jekyll/liquid/filters.rb +215 -0
- data/lib/sourcerer/jekyll/liquid/tags.rb +44 -0
- data/lib/sourcerer/jekyll/monkeypatches.rb +73 -0
- data/lib/sourcerer/jekyll.rb +26 -0
- data/lib/sourcerer/plaintext_converter.rb +75 -0
- data/lib/sourcerer/templating.rb +190 -0
- data/lib/sourcerer.rb +322 -0
- data/specs/data/api-client-schema.yaml +160 -0
- data/specs/data/config-def.yml +1426 -0
- data/specs/data/mcp-manifest.yml +50 -0
- data/specs/data/rhyml-mapping-schema.yaml +410 -0
- data/specs/data/rhyml-schema.yaml +152 -0
- metadata +376 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tilt'
|
|
4
|
+
require_relative '../sourcerer/templating'
|
|
5
|
+
|
|
6
|
+
# frozen_string_literal: true
|
|
7
|
+
|
|
8
|
+
module SchemaGraphy
|
|
9
|
+
# A module for handling templated fields within a data structure based on a schema or definition.
|
|
10
|
+
# It provides methods for pre-compiling and rendering fields using various template engines.
|
|
11
|
+
module Templating
|
|
12
|
+
extend Sourcerer::Templating
|
|
13
|
+
|
|
14
|
+
# Renders a field if it is a template.
|
|
15
|
+
#
|
|
16
|
+
# @param field [Object] The field to render.
|
|
17
|
+
# @param context [Hash] The context to use for rendering.
|
|
18
|
+
# @return [Object] The rendered field, or the original field if it's not a template.
|
|
19
|
+
def self.resolve_field field, context = {}
|
|
20
|
+
render_field_if_template(field, context)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Recursively pre-compiles templated fields in a data structure based on a schema.
|
|
24
|
+
#
|
|
25
|
+
# @param data [Hash] The data to process.
|
|
26
|
+
# @param schema [Hash] The schema defining which fields are templated.
|
|
27
|
+
# @param base_path [String] The base path for the current data level.
|
|
28
|
+
# @param scope [Hash] The scope to use for compilation.
|
|
29
|
+
def self.precompile_from_schema! data, schema, base_path = '', scope: {}
|
|
30
|
+
return unless data.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
data.each do |key, value|
|
|
33
|
+
path = [base_path, key].reject(&:empty?).join('.')
|
|
34
|
+
|
|
35
|
+
precompile_from_schema!(value, schema, path, scope: scope) if value.is_a?(Hash)
|
|
36
|
+
|
|
37
|
+
next unless SchemaGraphy::SchemaUtils.templated_field?(schema, path)
|
|
38
|
+
|
|
39
|
+
compile_templated_fields!(
|
|
40
|
+
data: data,
|
|
41
|
+
schema: schema,
|
|
42
|
+
fields: [{ key: key, path: path }],
|
|
43
|
+
scope: scope)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# An alias for the `Sourcerer::Templating::TemplatedField` class.
|
|
48
|
+
TemplatedField = Sourcerer::Templating::TemplatedField
|
|
49
|
+
|
|
50
|
+
# Compiles templated fields in the data.
|
|
51
|
+
#
|
|
52
|
+
# @param data [Hash] The data containing the fields to compile.
|
|
53
|
+
# @param schema [Hash] The schema definition.
|
|
54
|
+
# @param fields [Array<Hash>] An array of fields to compile, each with a `:key` and `:path`.
|
|
55
|
+
# @param scope [Hash] The scope to use for compilation.
|
|
56
|
+
def self.compile_templated_fields! data:, schema:, fields:, scope: {}
|
|
57
|
+
fields.each do |entry|
|
|
58
|
+
key = entry[:key]
|
|
59
|
+
path = entry[:path]
|
|
60
|
+
val = data[key]
|
|
61
|
+
|
|
62
|
+
next unless val.is_a?(String) || (val.is_a?(Hash) && val['__tag__'] && val['value'])
|
|
63
|
+
|
|
64
|
+
raw = val.is_a?(Hash) ? val['value'] : val
|
|
65
|
+
tagged = val.is_a?(Hash)
|
|
66
|
+
config = SchemaGraphy::SchemaUtils.templating_config_for(schema, path)
|
|
67
|
+
engine = tagged ? val['__tag__'] : (config['default'] || 'liquid')
|
|
68
|
+
|
|
69
|
+
compiled = Sourcerer::Templating::Engines.compile(raw, engine)
|
|
70
|
+
|
|
71
|
+
data[key] = if config['delay']
|
|
72
|
+
Sourcerer::Templating::TemplatedField.new(raw, compiled, engine, tagged, inferred: !tagged)
|
|
73
|
+
else
|
|
74
|
+
Sourcerer::Templating::Engines.render(compiled, engine, scope)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Recursively renders all pre-compiled templated fields in a data structure.
|
|
80
|
+
#
|
|
81
|
+
# @param data [Hash, Array] The data structure to process.
|
|
82
|
+
# @param context [Hash] The context to use for rendering.
|
|
83
|
+
def self.render_all_templated_fields! data, context = {}
|
|
84
|
+
return unless data.is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
data.each do |key, value|
|
|
87
|
+
case value
|
|
88
|
+
when TemplatedField
|
|
89
|
+
data[key] = value.render(context)
|
|
90
|
+
when Hash
|
|
91
|
+
render_all_templated_fields!(value, context)
|
|
92
|
+
when Array
|
|
93
|
+
value.each_with_index do |item, idx|
|
|
94
|
+
if item.is_a?(TemplatedField)
|
|
95
|
+
value[idx] = item.render(context)
|
|
96
|
+
elsif item.is_a?(Hash)
|
|
97
|
+
render_all_templated_fields!(item, context)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/schemagraphy.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'schemagraphy/loader'
|
|
4
|
+
require_relative 'schemagraphy/tag_utils'
|
|
5
|
+
require_relative 'schemagraphy/schema_utils'
|
|
6
|
+
require_relative 'schemagraphy/templating'
|
|
7
|
+
require_relative 'schemagraphy/regexp_utils'
|
|
8
|
+
require_relative 'schemagraphy/cfgyml/doc_builder'
|
|
9
|
+
require_relative 'schemagraphy/data_query/json_pointer'
|
|
10
|
+
require_relative 'schemagraphy/cfgyml/path_reference'
|
|
11
|
+
|
|
12
|
+
# SchemaGraphy is a component for working with schema-driven data structures and extending YAML with robust typing and dynamic directives.
|
|
13
|
+
# It provides utilities for loading, validating, and transforming data based on
|
|
14
|
+
# a schema definition, with a focus on templating and safe expression evaluation.
|
|
15
|
+
# This module is under early development and will be spun off as its own gem after ReleaseHx is generally available.
|
|
16
|
+
module SchemaGraphy
|
|
17
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# lib/sourcerer/builder.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'asciidoctor'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require_relative '../sourcerer'
|
|
7
|
+
|
|
8
|
+
module Sourcerer
|
|
9
|
+
# A build-time code generator that creates assets such as new data, documentation, and even Ruby files from
|
|
10
|
+
# data extracted from AsciiDoc files, such as attributes and tagged regions.
|
|
11
|
+
module Builder
|
|
12
|
+
# Generates a Ruby file at build-time to be used during the build process.
|
|
13
|
+
#
|
|
14
|
+
# @param generated [Hash] A hash with `:path` and `:module` for the generated file.
|
|
15
|
+
# @param attributes [Array<Hash>] A list of attribute sources to build.
|
|
16
|
+
# @param snippets [Array<Hash>] A list of snippets to build.
|
|
17
|
+
# @param regions [Array<Hash>] A list of tagged regions to build.
|
|
18
|
+
# @param templates [Array<Hash>] A list of templates to build (currently unused).
|
|
19
|
+
# @param render [Array<Hash>] A list of render entries (currently unused).
|
|
20
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
21
|
+
def self.generate_prebuild generated: {}, attributes: [], snippets: [], regions: [], templates: [], render: []
|
|
22
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
23
|
+
# NOTE: templates/render parameters are accepted from config but handled separately by Sourcerer.render_outputs
|
|
24
|
+
attr_result = build_attributes(attributes)
|
|
25
|
+
snippet_lookup = build_outputs(snippets, type: :snippet)
|
|
26
|
+
region_lookup = build_outputs(regions, type: :region)
|
|
27
|
+
|
|
28
|
+
File.write(generated[:path].to_s, <<~RUBY)
|
|
29
|
+
# frozen_string_literal: true
|
|
30
|
+
# Auto-generated by Sourcerer::Builder
|
|
31
|
+
|
|
32
|
+
module #{generated[:module]}
|
|
33
|
+
ATTRIBUTES = #{attr_result.inspect}
|
|
34
|
+
|
|
35
|
+
SNIPPET_LOOKUP = #{snippet_lookup.inspect}
|
|
36
|
+
|
|
37
|
+
REGION_LOOKUP = #{region_lookup.inspect}
|
|
38
|
+
|
|
39
|
+
def self.read_built_snippet name
|
|
40
|
+
fname = SNIPPET_LOOKUP[name.to_s] || name.to_s
|
|
41
|
+
path = File.expand_path("../../../build/snippets/\#{fname}", __FILE__)
|
|
42
|
+
raise "Snippet not found: \#{name}" unless File.exist?(path)
|
|
43
|
+
File.read(path)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
RUBY
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @api private
|
|
50
|
+
# Builds a hash of attributes from the given sources.
|
|
51
|
+
# @param attributes [Array<Hash>] The attribute sources.
|
|
52
|
+
# @return [Hash] The built attributes.
|
|
53
|
+
def self.build_attributes attributes
|
|
54
|
+
attributes.each_with_object({}) do |entry, acc|
|
|
55
|
+
source = entry[:source]
|
|
56
|
+
name = entry[:name] || File.basename(source, '.adoc').to_sym
|
|
57
|
+
acc[name.to_sym] = Sourcerer.load_attributes(source)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
# Builds output files from snippets or regions and returns a lookup hash.
|
|
63
|
+
# @param entries [Array<Hash>] The entries to build.
|
|
64
|
+
# @param type [Symbol] The type of output (`:snippet` or `:region`).
|
|
65
|
+
# @return [Hash] A lookup hash mapping names to output filenames.
|
|
66
|
+
def self.build_outputs entries, type:
|
|
67
|
+
lookup = {}
|
|
68
|
+
names = []
|
|
69
|
+
outnames = []
|
|
70
|
+
|
|
71
|
+
entries.each do |entry|
|
|
72
|
+
source = entry[:source] or raise ArgumentError, "#{type} entry is missing :source"
|
|
73
|
+
tag = entry[:tag]
|
|
74
|
+
tags = entry[:tags]
|
|
75
|
+
|
|
76
|
+
raise ArgumentError, 'use only one of :tag or :tags' if tag && tags
|
|
77
|
+
raise ArgumentError, "#{type} must include a :tag or :tags" unless tag || tags
|
|
78
|
+
|
|
79
|
+
name = entry[:name] || tag || File.basename(source, '.adoc')
|
|
80
|
+
outname = entry[:out] || default_output_name(name, type)
|
|
81
|
+
|
|
82
|
+
raise ArgumentError, "name value must be unique; #{name} already used" if names.include? name
|
|
83
|
+
raise ArgumentError, "out value must be unique; #{outname} already used" if outnames.include? outname
|
|
84
|
+
|
|
85
|
+
names << name
|
|
86
|
+
outnames << outname
|
|
87
|
+
|
|
88
|
+
tags = [tag] if tag
|
|
89
|
+
|
|
90
|
+
text =
|
|
91
|
+
case type
|
|
92
|
+
when :snippet then Sourcerer.load_include(source, tags: tags)
|
|
93
|
+
when :region then Sourcerer.extract_tagged_content(source, tags: tags)
|
|
94
|
+
else raise ArgumentError, "Unsupported type: #{type}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
lookup[name.to_s] = outname
|
|
98
|
+
|
|
99
|
+
outpath = File.join("build/#{type}s", outname)
|
|
100
|
+
FileUtils.mkdir_p File.dirname(outpath)
|
|
101
|
+
File.write(outpath, text)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
lookup
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @api private
|
|
108
|
+
# Determines the default output filename for a given name and type.
|
|
109
|
+
# @param name [String] The name of the output.
|
|
110
|
+
# @param type [Symbol] The type of output.
|
|
111
|
+
# @return [String] The default filename.
|
|
112
|
+
def self.default_output_name name, type
|
|
113
|
+
case type
|
|
114
|
+
when :snippet then "#{name}.txt"
|
|
115
|
+
when :region then "#{name}.adoc"
|
|
116
|
+
else raise ArgumentError, "Unknown type: #{type}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jekyll'
|
|
4
|
+
require 'jekyll-asciidoc'
|
|
5
|
+
|
|
6
|
+
module Sourcerer
|
|
7
|
+
module Jekyll
|
|
8
|
+
# This module provides methods for programmatically setting up a Jekyll site
|
|
9
|
+
# environment, which is useful for loading plugins or creating a mock site for rendering.
|
|
10
|
+
module Bootstrapper
|
|
11
|
+
# Loads Jekyll plugins from specified directories.
|
|
12
|
+
#
|
|
13
|
+
# @param plugin_dirs [Array<String>] A list of directories to search for plugins.
|
|
14
|
+
# @return [Jekyll::Site] The initialized Jekyll site object.
|
|
15
|
+
def self.load_plugins plugin_dirs: []
|
|
16
|
+
config = ::Jekyll.configuration(
|
|
17
|
+
{
|
|
18
|
+
'source' => Dir.pwd,
|
|
19
|
+
'destination' => File.join(Dir.pwd, '_site'),
|
|
20
|
+
'quiet' => true,
|
|
21
|
+
'skip_config_files' => true,
|
|
22
|
+
'plugins_dir' => plugin_dirs.map { |d| File.expand_path(d) },
|
|
23
|
+
'disable_disk_cache' => true
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
site = ::Jekyll::Site.new(config)
|
|
27
|
+
site.plugin_manager.conscientious_require
|
|
28
|
+
|
|
29
|
+
::Jekyll::Hooks.trigger :site, :after_init, site
|
|
30
|
+
|
|
31
|
+
site
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Creates an ephemeral Jekyll site instance for rendering purposes.
|
|
35
|
+
# This is useful for leveraging Jekyll's templating outside of a full site build.
|
|
36
|
+
#
|
|
37
|
+
# @param includes_load_paths [Array<String>] Paths to load includes from.
|
|
38
|
+
# @param plugin_dirs [Array<String>] Paths to load plugins from.
|
|
39
|
+
# @return [Jekyll::Site] The initialized fake Jekyll site object.
|
|
40
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
41
|
+
def self.fake_site includes_load_paths: [], plugin_dirs: []
|
|
42
|
+
# NOTE: plugin_dirs parameter is accepted but not yet implemented; reserved for future plugin loading
|
|
43
|
+
::Jekyll.logger.log_level = :error if ::Jekyll.logger.respond_to?(:log_level=)
|
|
44
|
+
|
|
45
|
+
config = ::Jekyll.configuration(
|
|
46
|
+
'source' => Dir.pwd,
|
|
47
|
+
'includes_dir' => includes_load_paths.first,
|
|
48
|
+
'includes_load_paths' => includes_load_paths,
|
|
49
|
+
'destination' => File.join(Dir.pwd, '_site'),
|
|
50
|
+
'quiet' => true,
|
|
51
|
+
'skip_config_files' => true,
|
|
52
|
+
'disable_disk_cache' => true)
|
|
53
|
+
|
|
54
|
+
site = ::Jekyll::Site.new(config)
|
|
55
|
+
|
|
56
|
+
include_paths = site.includes_load_paths || []
|
|
57
|
+
site.inclusions ||= {}
|
|
58
|
+
|
|
59
|
+
include_paths.each do |dir|
|
|
60
|
+
Dir[File.join(dir, '**/*')].each do |file|
|
|
61
|
+
next unless File.file?(file)
|
|
62
|
+
|
|
63
|
+
relative_path = file.sub("#{dir}/", '')
|
|
64
|
+
site.inclusions[relative_path] = File.read(file)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
site.instance_variable_set(:@liquid_renderer, ::Jekyll::LiquidRenderer.new(site))
|
|
69
|
+
|
|
70
|
+
plugin_manager = ::Jekyll::PluginManager.new(site)
|
|
71
|
+
plugin_manager.conscientious_require
|
|
72
|
+
|
|
73
|
+
site
|
|
74
|
+
end
|
|
75
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
|
@@ -0,0 +1,215 @@
|
|
|
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)
|
|
@@ -0,0 +1,44 @@
|
|
|
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)
|