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.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/README.adoc +2915 -0
  3. data/bin/releasehx +7 -0
  4. data/bin/rhx +7 -0
  5. data/bin/rhx-mcp +7 -0
  6. data/bin/sourcerer +32 -0
  7. data/build/docs/CNAME +1 -0
  8. data/build/docs/Gemfile.lock +95 -0
  9. data/build/docs/_config.yml +36 -0
  10. data/build/docs/config-reference.adoc +4104 -0
  11. data/build/docs/config-reference.json +1546 -0
  12. data/build/docs/index.adoc +2915 -0
  13. data/build/docs/landing.adoc +21 -0
  14. data/build/docs/manpage.adoc +68 -0
  15. data/build/docs/releasehx.1 +281 -0
  16. data/build/docs/releasehx_readme.html +367 -0
  17. data/build/docs/sample-config.adoc +9 -0
  18. data/build/docs/sample-config.yml +251 -0
  19. data/build/docs/schemagraphy_readme.html +0 -0
  20. data/build/docs/sourcerer_readme.html +46 -0
  21. data/build/snippets/helpscreen.txt +29 -0
  22. data/lib/docopslab/mcp/asset_packager.rb +30 -0
  23. data/lib/docopslab/mcp/manifest.rb +67 -0
  24. data/lib/docopslab/mcp/resource_pack.rb +46 -0
  25. data/lib/docopslab/mcp/server.rb +92 -0
  26. data/lib/docopslab/mcp.rb +6 -0
  27. data/lib/releasehx/cli.rb +937 -0
  28. data/lib/releasehx/configuration.rb +215 -0
  29. data/lib/releasehx/generated.rb +17 -0
  30. data/lib/releasehx/helpers.rb +58 -0
  31. data/lib/releasehx/mcp/asset_packager.rb +21 -0
  32. data/lib/releasehx/mcp/assets/agent-config-guide.md +178 -0
  33. data/lib/releasehx/mcp/assets/config-def.yml +1426 -0
  34. data/lib/releasehx/mcp/assets/config-reference.adoc +4104 -0
  35. data/lib/releasehx/mcp/assets/config-reference.json +1546 -0
  36. data/lib/releasehx/mcp/assets/sample-config.yml +251 -0
  37. data/lib/releasehx/mcp/manifest.rb +18 -0
  38. data/lib/releasehx/mcp/resource_pack.rb +26 -0
  39. data/lib/releasehx/mcp/server.rb +57 -0
  40. data/lib/releasehx/mcp.rb +7 -0
  41. data/lib/releasehx/ops/check_ops.rb +136 -0
  42. data/lib/releasehx/ops/draft_ops.rb +173 -0
  43. data/lib/releasehx/ops/enrich_ops.rb +221 -0
  44. data/lib/releasehx/ops/template_ops.rb +61 -0
  45. data/lib/releasehx/ops/write_ops.rb +124 -0
  46. data/lib/releasehx/rest/clients/github.yml +46 -0
  47. data/lib/releasehx/rest/clients/gitlab.yml +31 -0
  48. data/lib/releasehx/rest/clients/jira.yml +31 -0
  49. data/lib/releasehx/rest/yaml_client.rb +418 -0
  50. data/lib/releasehx/rhyml/adapter.rb +740 -0
  51. data/lib/releasehx/rhyml/change.rb +167 -0
  52. data/lib/releasehx/rhyml/liquid.rb +13 -0
  53. data/lib/releasehx/rhyml/loaders.rb +37 -0
  54. data/lib/releasehx/rhyml/mappings/github.yaml +60 -0
  55. data/lib/releasehx/rhyml/mappings/gitlab.yaml +73 -0
  56. data/lib/releasehx/rhyml/mappings/jira.yaml +29 -0
  57. data/lib/releasehx/rhyml/mappings/verb_past_tenses.yml +98 -0
  58. data/lib/releasehx/rhyml/release.rb +144 -0
  59. data/lib/releasehx/rhyml.rb +15 -0
  60. data/lib/releasehx/sgyml/helpers.rb +45 -0
  61. data/lib/releasehx/transforms/adf_to_markdown.rb +307 -0
  62. data/lib/releasehx/version.rb +7 -0
  63. data/lib/releasehx.rb +69 -0
  64. data/lib/schemagraphy/attribute_resolver.rb +48 -0
  65. data/lib/schemagraphy/cfgyml/definition.rb +90 -0
  66. data/lib/schemagraphy/cfgyml/doc_builder.rb +52 -0
  67. data/lib/schemagraphy/cfgyml/path_reference.rb +24 -0
  68. data/lib/schemagraphy/data_query/json_pointer.rb +42 -0
  69. data/lib/schemagraphy/loader.rb +59 -0
  70. data/lib/schemagraphy/regexp_utils.rb +215 -0
  71. data/lib/schemagraphy/safe_expression.rb +189 -0
  72. data/lib/schemagraphy/schema_utils.rb +124 -0
  73. data/lib/schemagraphy/tag_utils.rb +32 -0
  74. data/lib/schemagraphy/templating.rb +104 -0
  75. data/lib/schemagraphy.rb +17 -0
  76. data/lib/sourcerer/builder.rb +120 -0
  77. data/lib/sourcerer/jekyll/bootstrapper.rb +78 -0
  78. data/lib/sourcerer/jekyll/liquid/file_system.rb +74 -0
  79. data/lib/sourcerer/jekyll/liquid/filters.rb +215 -0
  80. data/lib/sourcerer/jekyll/liquid/tags.rb +44 -0
  81. data/lib/sourcerer/jekyll/monkeypatches.rb +73 -0
  82. data/lib/sourcerer/jekyll.rb +26 -0
  83. data/lib/sourcerer/plaintext_converter.rb +75 -0
  84. data/lib/sourcerer/templating.rb +190 -0
  85. data/lib/sourcerer.rb +322 -0
  86. data/specs/data/api-client-schema.yaml +160 -0
  87. data/specs/data/config-def.yml +1426 -0
  88. data/specs/data/mcp-manifest.yml +50 -0
  89. data/specs/data/rhyml-mapping-schema.yaml +410 -0
  90. data/specs/data/rhyml-schema.yaml +152 -0
  91. 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
@@ -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)