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.
- checksums.yaml +4 -4
- data/README.adoc +363 -330
- data/build/docs/_config.yml +1 -0
- data/build/docs/_release_index.adoc +3 -2
- data/build/docs/config-reference.adoc +197 -10
- data/build/docs/config-reference.json +56 -7
- data/build/docs/index.adoc +315 -59
- data/build/docs/landing.adoc +1 -1
- data/build/docs/manpage.adoc +2 -2
- data/build/docs/release-procedure.adoc +365 -0
- data/build/docs/release-procedure.html +87 -0
- data/build/docs/releasehx.1 +17 -5
- data/build/docs/sample-config.yml +14 -7
- data/lib/releasehx/cli.rb +5 -2
- data/lib/releasehx/configuration.rb +0 -1
- data/lib/releasehx/generated.rb +1 -1
- data/lib/releasehx/mcp/assets/agent-config-guide.md +1 -1
- data/lib/releasehx/mcp/assets/config-def.yml +122 -6
- data/lib/releasehx/mcp/assets/config-reference.adoc +197 -10
- data/lib/releasehx/mcp/assets/config-reference.json +56 -7
- data/lib/releasehx/mcp/assets/sample-config.yml +14 -7
- data/lib/releasehx/mcp/server.rb +0 -1
- data/lib/releasehx/ops/enrich_ops.rb +161 -55
- data/lib/releasehx/ops/template_ops.rb +1 -1
- data/lib/releasehx/rhyml/adapter.rb +0 -3
- data/lib/releasehx/rhyml/templates/bootstrap-overrides.css +15 -0
- data/lib/releasehx/rhyml/templates/changelog.adoc.liquid +2 -0
- data/lib/releasehx/rhyml/templates/changelog.html.liquid +6 -4
- data/lib/releasehx/rhyml/templates/changelog.md.liquid +1 -0
- data/lib/releasehx/rhyml/templates/embedded.css.liquid +263 -0
- data/lib/releasehx/rhyml/templates/entry.adoc.liquid +1 -0
- data/lib/releasehx/rhyml/templates/entry.html.liquid +21 -20
- data/lib/releasehx/rhyml/templates/entry.md.liquid +15 -21
- data/lib/releasehx/rhyml/templates/head-parser.liquid +6 -2
- data/lib/releasehx/rhyml/templates/header.liquid +13 -4
- data/lib/releasehx/rhyml/templates/history.html.liquid +152 -33
- data/lib/releasehx/rhyml/templates/metadata-entry.adoc.liquid +83 -38
- data/lib/releasehx/rhyml/templates/metadata-entry.html.liquid +60 -1
- data/lib/releasehx/rhyml/templates/metadata-entry.md.liquid +65 -113
- data/lib/releasehx/rhyml/templates/metadata-note.adoc.liquid +83 -38
- data/lib/releasehx/rhyml/templates/metadata-note.html.liquid +59 -22
- data/lib/releasehx/rhyml/templates/metadata-note.md.liquid +68 -23
- data/lib/releasehx/rhyml/templates/note.html.liquid +25 -19
- data/lib/releasehx/rhyml/templates/note.md.liquid +44 -26
- data/lib/releasehx/rhyml/templates/release-notes.adoc.liquid +2 -0
- data/lib/releasehx/rhyml/templates/release-notes.html.liquid +6 -4
- data/lib/releasehx/rhyml/templates/release-notes.md.liquid +1 -0
- data/lib/releasehx/rhyml/templates/release.adoc.liquid +2 -0
- data/lib/releasehx/rhyml/templates/release.md.liquid +8 -7
- data/lib/releasehx/rhyml/templates/rhyml-change.yaml.liquid +36 -36
- data/lib/releasehx/rhyml/templates/wrapper.html.liquid +103 -0
- data/lib/releasehx/sgyml/helpers.rb +0 -2
- data/lib/releasehx/transforms/adf_to_markdown.rb +1 -1
- data/lib/releasehx/version.rb +0 -2
- data/lib/releasehx.rb +2 -2
- data/specs/data/config-def.yml +122 -6
- metadata +48 -25
- data/build/docs/schemagraphy_readme.html +0 -0
- data/build/docs/sourcerer_readme.html +0 -46
- data/lib/schemagraphy/attribute_resolver.rb +0 -48
- data/lib/schemagraphy/cfgyml/definition.rb +0 -90
- data/lib/schemagraphy/cfgyml/doc_builder.rb +0 -52
- data/lib/schemagraphy/cfgyml/path_reference.rb +0 -24
- data/lib/schemagraphy/data_query/json_pointer.rb +0 -42
- data/lib/schemagraphy/loader.rb +0 -59
- data/lib/schemagraphy/regexp_utils.rb +0 -235
- data/lib/schemagraphy/safe_expression.rb +0 -189
- data/lib/schemagraphy/schema_utils.rb +0 -124
- data/lib/schemagraphy/tag_utils.rb +0 -32
- data/lib/schemagraphy/templating.rb +0 -104
- data/lib/schemagraphy.rb +0 -17
- data/lib/sourcerer/builder.rb +0 -120
- data/lib/sourcerer/jekyll/bootstrapper.rb +0 -78
- data/lib/sourcerer/jekyll/liquid/file_system.rb +0 -74
- data/lib/sourcerer/jekyll/liquid/filters.rb +0 -215
- data/lib/sourcerer/jekyll/liquid/tags.rb +0 -44
- data/lib/sourcerer/jekyll/monkeypatches.rb +0 -73
- data/lib/sourcerer/jekyll.rb +0 -26
- data/lib/sourcerer/plaintext_converter.rb +0 -75
- data/lib/sourcerer/templating.rb +0 -190
- data/lib/sourcerer.rb +0 -322
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
<div class="paragraph">
|
|
2
|
-
<p>This gem introduces a module called Sourcerer, by which AsciiDoc files can be parsed and their contents harvested for use in the application build.
|
|
3
|
-
The module also handles Liquid template processing with enhanced attribute resolution capabilities.</p>
|
|
4
|
-
</div>
|
|
5
|
-
<div class="admonitionblock note">
|
|
6
|
-
<table>
|
|
7
|
-
<tr>
|
|
8
|
-
<td class="icon">
|
|
9
|
-
<div class="title">Note</div>
|
|
10
|
-
</td>
|
|
11
|
-
<td class="content">
|
|
12
|
-
Sourcerer is intended to be spun off as its own gem once it successfully proves the concept in this gem.
|
|
13
|
-
It will probably be called <em>AsciiSourcerer</em> and may replace an older and unmaintained utility of mine called LiquiDoc.
|
|
14
|
-
</td>
|
|
15
|
-
</tr>
|
|
16
|
-
</table>
|
|
17
|
-
</div>
|
|
18
|
-
<div class="paragraph">
|
|
19
|
-
<p>It is invoked in the Rakefile, establishing global namespaces:</p>
|
|
20
|
-
</div>
|
|
21
|
-
<div class="ulist">
|
|
22
|
-
<ul>
|
|
23
|
-
<li>
|
|
24
|
-
<p><code>ReleaseHx::ATTRIBUTES[:globals]</code> (derived from this <code>README.adoc</code> file)</p>
|
|
25
|
-
</li>
|
|
26
|
-
<li>
|
|
27
|
-
<p><code>ReleaseHx.read_built_snippet(:<name>)</code> (such as <code>:helpscreen</code>)</p>
|
|
28
|
-
</li>
|
|
29
|
-
</ul>
|
|
30
|
-
</div>
|
|
31
|
-
<div class="paragraph">
|
|
32
|
-
<p>The Sourcerer module also generates files like <code>build/docs/manpage.adoc</code>, which generates the formatted terminal manual, using content from <code>build/docs/config-reference.adoc</code> and this README (<code>tags="cli_options"</code>, for instance).</p>
|
|
33
|
-
</div>
|
|
34
|
-
<div class="paragraph">
|
|
35
|
-
<p>It also generates an AsciiDoc-formatted configuration reference, a machine-readable JSON reference, and a sample config using the <code>specs/data/config-def.yml</code> file.
|
|
36
|
-
The Sourcerer system now supports <strong>attribute resolution</strong> from AsciiDoc source files, enabling templates to access README attributes during rendering, ensuring configuration documentation reflects actual default values.</p>
|
|
37
|
-
</div>
|
|
38
|
-
<div class="paragraph">
|
|
39
|
-
<p>Sourcerer also performs basic testing of select commands in the ASciiDoc file that have been assigned a <code>testable</code> role.</p>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="paragraph">
|
|
42
|
-
<p>This is mostly just showing off what Sourcerer can do, and hopefully setting into habit some best practices for my more complicated apps.</p>
|
|
43
|
-
</div>
|
|
44
|
-
<div class="paragraph">
|
|
45
|
-
<p>Sourcerer is also where integration with Jekyll’s extensions of Liquid occurs, bringing ReleaseHx’s templating powers closely in line with how Jekyll’s work, as described in <a href="#custom-liquid">[custom-liquid]</a>.</p>
|
|
46
|
-
</div>
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SchemaGraphy
|
|
4
|
-
# The AttributeResolver module provides methods for resolving AsciiDoc attribute references
|
|
5
|
-
# within a schema hash. It is used to substitute placeholders like `\{attribute_name}`
|
|
6
|
-
# with actual values.
|
|
7
|
-
module AttributeResolver
|
|
8
|
-
# Recursively walk a schema Hash and resolve `\{attribute_name}` references
|
|
9
|
-
# in 'dflt' values.
|
|
10
|
-
#
|
|
11
|
-
# @param schema [Hash] The schema or definition hash to process.
|
|
12
|
-
# @param attrs [Hash] The key-value pairs from AsciiDoc attributes to use for resolution.
|
|
13
|
-
# @return [Hash] The schema with resolved attributes.
|
|
14
|
-
def self.resolve_attributes! schema, attrs
|
|
15
|
-
case schema
|
|
16
|
-
when Hash
|
|
17
|
-
schema.transform_values! do |value|
|
|
18
|
-
if value.is_a?(Hash)
|
|
19
|
-
if value.key?('dflt') && value['dflt'].is_a?(String)
|
|
20
|
-
value['dflt'] = resolve_attribute_reference(value['dflt'], attrs)
|
|
21
|
-
end
|
|
22
|
-
resolve_attributes!(value, attrs)
|
|
23
|
-
else
|
|
24
|
-
value
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
schema
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Replace `\{attribute_name}` patterns with corresponding values from the attrs hash.
|
|
32
|
-
#
|
|
33
|
-
# @param value [String] The string to process.
|
|
34
|
-
# @param attrs [Hash] The attributes to use for resolution.
|
|
35
|
-
# @return [String] The processed string with attribute references replaced.
|
|
36
|
-
def self.resolve_attribute_reference value, attrs
|
|
37
|
-
# Handle \{attribute_name} references
|
|
38
|
-
if value.match?(/\{[^}]+\}/)
|
|
39
|
-
value.gsub(/\{([^}]+)\}/) do |match|
|
|
40
|
-
attr_name = ::Regexp.last_match(1)
|
|
41
|
-
attrs[attr_name] || match # Keep original if no matching attribute
|
|
42
|
-
end
|
|
43
|
-
else
|
|
44
|
-
value
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../loader'
|
|
4
|
-
require_relative '../schema_utils'
|
|
5
|
-
|
|
6
|
-
module SchemaGraphy
|
|
7
|
-
# A module for handling CFGYML, a schema-driven configuration system.
|
|
8
|
-
module CFGYML
|
|
9
|
-
# Represents a configuration definition loaded from a schema file.
|
|
10
|
-
# It provides methods for accessing defaults and rendering documentation.
|
|
11
|
-
class Definition
|
|
12
|
-
# @return [Hash] The loaded schema hash.
|
|
13
|
-
attr_reader :schema
|
|
14
|
-
|
|
15
|
-
# @return [Hash] The attributes used for resolving placeholders in the schema.
|
|
16
|
-
attr_reader :attributes
|
|
17
|
-
|
|
18
|
-
# @param schema_path [String] The path to the schema YAML file.
|
|
19
|
-
# @param attrs [Hash] A hash of attributes for placeholder resolution.
|
|
20
|
-
def initialize schema_path, attrs = {}
|
|
21
|
-
@schema = Loader.load_yaml_with_attributes(schema_path, attrs)
|
|
22
|
-
@attributes = attrs
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Extract default values from the loaded schema.
|
|
26
|
-
# @return [Hash] A hash of default values.
|
|
27
|
-
def defaults
|
|
28
|
-
SchemaUtils.crawl_defaults(@schema)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Get the search paths for templates.
|
|
32
|
-
# @return [Array<String>] An array of template paths.
|
|
33
|
-
def template_paths
|
|
34
|
-
@template_paths ||= [
|
|
35
|
-
File.join(File.dirname(__FILE__), '..', 'templates', 'cfgyml'),
|
|
36
|
-
*additional_template_paths
|
|
37
|
-
]
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Render a configuration reference or sample in the specified format.
|
|
41
|
-
#
|
|
42
|
-
# @param format [Symbol] The output format (`:adoc` or `:yaml`).
|
|
43
|
-
# @return [String] The rendered output.
|
|
44
|
-
# @raise [ArgumentError] if the format is unsupported.
|
|
45
|
-
def render_reference format = :adoc
|
|
46
|
-
template = case format
|
|
47
|
-
when :adoc
|
|
48
|
-
'config-reference.adoc.liquid'
|
|
49
|
-
when :yaml
|
|
50
|
-
'sample-config.yaml.liquid'
|
|
51
|
-
else
|
|
52
|
-
raise ArgumentError, "Unsupported format: #{format}"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
render_template(template)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
# Render a template using the Liquid engine.
|
|
61
|
-
def render_template template_name
|
|
62
|
-
template_path = find_template(template_name)
|
|
63
|
-
raise "Template not found: #{template_name}" unless template_path
|
|
64
|
-
|
|
65
|
-
require 'liquid'
|
|
66
|
-
template_content = File.read(template_path)
|
|
67
|
-
template = Liquid::Template.parse(template_content)
|
|
68
|
-
|
|
69
|
-
template.render(
|
|
70
|
-
'config_def' => @schema,
|
|
71
|
-
'attrs' => @attributes)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Find a template file in the configured template paths.
|
|
75
|
-
def find_template name
|
|
76
|
-
template_paths.each do |path|
|
|
77
|
-
file = File.join(path, name)
|
|
78
|
-
return file if File.exist?(file)
|
|
79
|
-
end
|
|
80
|
-
nil
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Provides an extension point for subclasses to add more template paths.
|
|
84
|
-
def additional_template_paths
|
|
85
|
-
# Can be overridden by subclasses
|
|
86
|
-
[]
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
|
-
module SchemaGraphy
|
|
6
|
-
module CFGYML
|
|
7
|
-
# Builds documentation-friendly CFGYML references for machine consumption.
|
|
8
|
-
module DocBuilder
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
def call schema, options = {}
|
|
12
|
-
pretty = options.fetch(:pretty, true)
|
|
13
|
-
data = reference_hash(schema)
|
|
14
|
-
pretty ? JSON.pretty_generate(data) : JSON.generate(data)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def reference_hash schema
|
|
18
|
-
{
|
|
19
|
-
'format' => 'releasehx-config-reference',
|
|
20
|
-
'version' => 1,
|
|
21
|
-
'properties' => build_properties(schema['properties'], [])
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def build_properties properties, path
|
|
26
|
-
return {} unless properties.is_a?(Hash)
|
|
27
|
-
|
|
28
|
-
properties.each_with_object({}) do |(key, definition), acc|
|
|
29
|
-
next unless definition.is_a?(Hash)
|
|
30
|
-
|
|
31
|
-
current_path = path + [key]
|
|
32
|
-
entry = build_entry(current_path, definition)
|
|
33
|
-
children = build_properties(definition['properties'], current_path)
|
|
34
|
-
entry['properties'] = children unless children.empty?
|
|
35
|
-
acc[key] = entry
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def build_entry path, definition
|
|
40
|
-
entry = {
|
|
41
|
-
'path' => path.join('.'),
|
|
42
|
-
'desc' => definition['desc'],
|
|
43
|
-
'docs' => definition['docs'],
|
|
44
|
-
'type' => definition['type'],
|
|
45
|
-
'templating' => definition['templating'],
|
|
46
|
-
'default' => definition.key?('dflt') ? definition['dflt'] : nil
|
|
47
|
-
}
|
|
48
|
-
entry.compact
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
|
-
module SchemaGraphy
|
|
6
|
-
module CFGYML
|
|
7
|
-
# Loads and queries a JSON config reference using JSON Pointer.
|
|
8
|
-
class PathReference
|
|
9
|
-
def initialize data
|
|
10
|
-
@data = data
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def self.load path
|
|
14
|
-
new(JSON.parse(File.read(path)))
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def get pointer
|
|
18
|
-
SchemaGraphy::DataQuery::JSONPointer.resolve(@data, pointer)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
Reference = PathReference
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SchemaGraphy
|
|
4
|
-
module DataQuery
|
|
5
|
-
# Resolves JSON Pointer queries against a Hash or Array.
|
|
6
|
-
module JSONPointer
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
|
-
def resolve data, pointer
|
|
10
|
-
return data if pointer.nil? || pointer == ''
|
|
11
|
-
raise ArgumentError, "Invalid JSON Pointer: #{pointer}" unless pointer.start_with?('/')
|
|
12
|
-
|
|
13
|
-
tokens = pointer.split('/')[1..]
|
|
14
|
-
tokens.reduce(data) do |current, token|
|
|
15
|
-
key = unescape(token)
|
|
16
|
-
resolve_token(current, key, pointer)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def resolve_token current, key, pointer
|
|
21
|
-
case current
|
|
22
|
-
when Array
|
|
23
|
-
index = Integer(key, 10)
|
|
24
|
-
current.fetch(index)
|
|
25
|
-
when Hash
|
|
26
|
-
return current.fetch(key) if current.key?(key)
|
|
27
|
-
return current.fetch(key.to_sym) if current.key?(key.to_sym)
|
|
28
|
-
|
|
29
|
-
raise KeyError, "JSON Pointer not found: #{pointer}"
|
|
30
|
-
else
|
|
31
|
-
raise KeyError, "JSON Pointer not found: #{pointer}"
|
|
32
|
-
end
|
|
33
|
-
rescue ArgumentError, IndexError, KeyError
|
|
34
|
-
raise KeyError, "JSON Pointer not found: #{pointer}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def unescape token
|
|
38
|
-
token.gsub('~1', '/').gsub('~0', '~')
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
data/lib/schemagraphy/loader.rb
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'yaml'
|
|
4
|
-
require 'psych'
|
|
5
|
-
require_relative 'attribute_resolver'
|
|
6
|
-
|
|
7
|
-
module SchemaGraphy
|
|
8
|
-
# The Loader class provides methods for loading YAML files while preserving
|
|
9
|
-
# custom tags and resolving attribute references.
|
|
10
|
-
class Loader
|
|
11
|
-
# Load a YAML file and resolve AsciiDoc attribute references like `\{attribute_name}`.
|
|
12
|
-
#
|
|
13
|
-
# @param path [String] The path to the YAML file.
|
|
14
|
-
# @param attrs [Hash] The AsciiDoc attributes to use for resolution.
|
|
15
|
-
# @return [Hash] The loaded YAML data with attributes resolved.
|
|
16
|
-
def self.load_yaml_with_attributes path, attrs = {}
|
|
17
|
-
raw_data = load_yaml_with_tags(path)
|
|
18
|
-
AttributeResolver.resolve_attributes!(raw_data, attrs)
|
|
19
|
-
raw_data
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Load a YAML file, preserving any custom tags (e.g., `!foo`).
|
|
23
|
-
# Custom tags are attached to the data structure.
|
|
24
|
-
#
|
|
25
|
-
# @param path [String] The path to the YAML file.
|
|
26
|
-
# @return [Hash] The loaded YAML data with custom tags attached.
|
|
27
|
-
def self.load_yaml_with_tags path
|
|
28
|
-
return {} if File.empty?(path)
|
|
29
|
-
|
|
30
|
-
data = Psych.load_file(path, aliases: true, permitted_classes: [Date, Time])
|
|
31
|
-
ast = Psych.parse_file(path)
|
|
32
|
-
attach_tags(ast.root, data)
|
|
33
|
-
data
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Recursively attach YAML tags to the loaded data structure for template processing.
|
|
37
|
-
#
|
|
38
|
-
# @param node [Psych::Nodes::Node] The current AST node.
|
|
39
|
-
# @param data [Object] The data corresponding to the current node.
|
|
40
|
-
# @api private
|
|
41
|
-
def self.attach_tags node, data
|
|
42
|
-
return unless node.is_a?(Psych::Nodes::Mapping)
|
|
43
|
-
|
|
44
|
-
node.children.each_slice(2) do |key_node, val_node|
|
|
45
|
-
key = key_node.value
|
|
46
|
-
|
|
47
|
-
if val_node.respond_to?(:tag) && val_node.tag && data[key].is_a?(String)
|
|
48
|
-
normalized_tag = val_node.tag.sub(/^!+/, '').sub(/^.*:/, '')
|
|
49
|
-
data[key] = {
|
|
50
|
-
'value' => data[key],
|
|
51
|
-
'__tag__' => normalized_tag
|
|
52
|
-
}
|
|
53
|
-
elsif data[key].is_a?(Hash)
|
|
54
|
-
attach_tags(val_node, data[key])
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'to_regexp'
|
|
4
|
-
|
|
5
|
-
module SchemaGraphy
|
|
6
|
-
# A utility module for robustly parsing and using regular expressions.
|
|
7
|
-
# It handles various formats, including literals and plain strings,
|
|
8
|
-
# and provides helpers for extracting captured content.
|
|
9
|
-
module RegexpUtils
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
# Parse a regex pattern string using the `to_regexp` gem for robust parsing.
|
|
13
|
-
# Handles `/pattern/flags`, `%r{pattern}flags`, and plain text formats.
|
|
14
|
-
#
|
|
15
|
-
# @example
|
|
16
|
-
# parse_pattern("/^hello.*$/im")
|
|
17
|
-
# # => { pattern: "^hello.*$", flags: "im", regexp: /^hello.*$/im, options: 6 }
|
|
18
|
-
#
|
|
19
|
-
# @example
|
|
20
|
-
# parse_pattern("hello world")
|
|
21
|
-
# # => { pattern: "hello world", flags: "", regexp: /hello world/, options: 0 }
|
|
22
|
-
#
|
|
23
|
-
# @example
|
|
24
|
-
# parse_pattern("hello world", "i")
|
|
25
|
-
# # => { pattern: "hello world", flags: "i", regexp: /hello world/i, options: 1 }
|
|
26
|
-
#
|
|
27
|
-
# @param input [String] The input string, e.g., "/pattern/flags" or "plain pattern".
|
|
28
|
-
# @param default_flags [String] Default flags to apply if none are specified (default: "").
|
|
29
|
-
# @return [Hash, nil] A hash with `:pattern`, `:flags`, `:regexp`, and `:options`, or `nil`.
|
|
30
|
-
def parse_pattern input, default_flags = ''
|
|
31
|
-
return nil if input.nil? || input.to_s.strip.empty?
|
|
32
|
-
|
|
33
|
-
input_str = input.to_s.strip
|
|
34
|
-
|
|
35
|
-
# Remove surrounding quotes that might come from YAML parsing
|
|
36
|
-
clean_input = input_str.gsub(/^["']|["']$/, '')
|
|
37
|
-
|
|
38
|
-
# Manual parsing for /pattern/flags format (common in YAML configs)
|
|
39
|
-
if clean_input =~ %r{^/(.+)/([a-z]*)$}
|
|
40
|
-
pattern_str = Regexp.last_match(1)
|
|
41
|
-
flags_str = Regexp.last_match(2)
|
|
42
|
-
options = flags_to_options(flags_str)
|
|
43
|
-
|
|
44
|
-
begin
|
|
45
|
-
regexp_obj = Regexp.new(pattern_str, options)
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
pattern: pattern_str,
|
|
49
|
-
flags: flags_str,
|
|
50
|
-
regexp: regexp_obj,
|
|
51
|
-
options: options
|
|
52
|
-
}
|
|
53
|
-
rescue RegexpError => e
|
|
54
|
-
raise RegexpError, "Invalid regex pattern '#{input}': #{e.message}"
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Heuristic to detect if it's a Regexp literal
|
|
59
|
-
is_literal = clean_input.start_with?('%r{')
|
|
60
|
-
|
|
61
|
-
if is_literal
|
|
62
|
-
# Try to parse as regex literal using to_regexp
|
|
63
|
-
begin
|
|
64
|
-
regexp_obj = clean_input.to_regexp(detect: true)
|
|
65
|
-
|
|
66
|
-
# Extract pattern and flags from the compiled regexp
|
|
67
|
-
pattern_str = regexp_obj.source
|
|
68
|
-
flags_str = extract_flags_from_regexp(regexp_obj)
|
|
69
|
-
|
|
70
|
-
{
|
|
71
|
-
pattern: pattern_str,
|
|
72
|
-
flags: flags_str,
|
|
73
|
-
regexp: regexp_obj,
|
|
74
|
-
options: regexp_obj.options
|
|
75
|
-
}
|
|
76
|
-
rescue RegexpError => e
|
|
77
|
-
# Malformed literal is an error
|
|
78
|
-
raise RegexpError, "Invalid regex literal '#{input}': #{e.message}"
|
|
79
|
-
end
|
|
80
|
-
else
|
|
81
|
-
# Treat as plain pattern string with default flags
|
|
82
|
-
flags_str = default_flags.to_s
|
|
83
|
-
options = flags_to_options(flags_str)
|
|
84
|
-
|
|
85
|
-
begin
|
|
86
|
-
regexp_obj = Regexp.new(clean_input, options)
|
|
87
|
-
|
|
88
|
-
{
|
|
89
|
-
pattern: clean_input,
|
|
90
|
-
flags: flags_str,
|
|
91
|
-
regexp: regexp_obj,
|
|
92
|
-
options: options
|
|
93
|
-
}
|
|
94
|
-
rescue RegexpError => e
|
|
95
|
-
raise RegexpError, "Invalid regex pattern '#{input}': #{e.message}"
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# @note Not yet implemented.
|
|
101
|
-
# Future enhancement to parse structured pattern definitions from a Hash.
|
|
102
|
-
# @param pattern_hash [Hash] A hash with 'pattern' and 'flags' keys.
|
|
103
|
-
# @raise [NotImplementedError] Always raises this error.
|
|
104
|
-
def parse_structured_pattern pattern_hash
|
|
105
|
-
# TODO: Implement structured pattern parsing
|
|
106
|
-
# pattern_hash should have 'pattern' and 'flags' keys
|
|
107
|
-
# flags can be string or array
|
|
108
|
-
raise NotImplementedError, 'Structured pattern parsing not yet implemented'
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
# @note Not yet implemented.
|
|
112
|
-
# Future enhancement to parse custom YAML tags for regular expressions.
|
|
113
|
-
# @param tagged_input [String] The input string with a YAML tag.
|
|
114
|
-
# @param tag_type [Symbol] The type of tag, e.g., `:literal` or `:pattern`.
|
|
115
|
-
# @raise [NotImplementedError] Always raises this error.
|
|
116
|
-
def parse_tagged_pattern tagged_input, tag_type
|
|
117
|
-
# TODO: Implement custom YAML tag parsing
|
|
118
|
-
# tag_type would be :literal or :pattern
|
|
119
|
-
raise NotImplementedError, 'Tagged pattern parsing not yet implemented'
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Convert a flags string (ex: "im") to a Regexp options integer.
|
|
123
|
-
#
|
|
124
|
-
# @param flags [String] String containing regex flags.
|
|
125
|
-
# @return [Integer] Regexp options integer.
|
|
126
|
-
def flags_to_options flags
|
|
127
|
-
options = 0
|
|
128
|
-
flags = flags.to_s
|
|
129
|
-
|
|
130
|
-
options |= Regexp::IGNORECASE if flags.include?('i')
|
|
131
|
-
options |= Regexp::MULTILINE if flags.include?('m')
|
|
132
|
-
options |= Regexp::EXTENDED if flags.include?('x')
|
|
133
|
-
|
|
134
|
-
# NOTE: 'g' (global) and 'o' (once) are not standard Ruby flags
|
|
135
|
-
# encoding flags ('n', 'e', 's', 'u') are handled by to_regexp
|
|
136
|
-
|
|
137
|
-
options
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
# Extract a flags string from a compiled Regexp object.
|
|
141
|
-
#
|
|
142
|
-
# @param regexp [Regexp] A compiled regexp object.
|
|
143
|
-
# @return [String] String representation of the flags (e.g., "im").
|
|
144
|
-
def extract_flags_from_regexp regexp
|
|
145
|
-
flags = ''
|
|
146
|
-
flags += 'i' if regexp.options.anybits?(Regexp::IGNORECASE)
|
|
147
|
-
flags += 'm' if regexp.options.anybits?(Regexp::MULTILINE)
|
|
148
|
-
flags += 'x' if regexp.options.anybits?(Regexp::EXTENDED)
|
|
149
|
-
flags
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Create a Regexp object from a pattern string and explicit flags.
|
|
153
|
-
#
|
|
154
|
-
# @param pattern [String] The regex pattern (without delimiters).
|
|
155
|
-
# @param flags [String] The flags string (ex: "im").
|
|
156
|
-
# @return [Regexp] The compiled Regexp object.
|
|
157
|
-
def create_regexp pattern, flags = ''
|
|
158
|
-
options = flags_to_options(flags)
|
|
159
|
-
Regexp.new(pattern, options)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Extract content using named or positional capture groups.
|
|
163
|
-
#
|
|
164
|
-
# @param text [String] The text to match against.
|
|
165
|
-
# @param pattern_info [Hash] The hash result from `parse_pattern`.
|
|
166
|
-
# @param capture_name [String] The name of the capture group to extract (optional).
|
|
167
|
-
# @return [String, nil] The extracted text, or `nil` if no match is found.
|
|
168
|
-
def extract_capture text, pattern_info, capture_name = nil
|
|
169
|
-
return nil unless text && pattern_info
|
|
170
|
-
|
|
171
|
-
regexp = pattern_info[:regexp]
|
|
172
|
-
match = text.match(regexp)
|
|
173
|
-
|
|
174
|
-
return nil unless match
|
|
175
|
-
|
|
176
|
-
if capture_name && match.names.include?(capture_name.to_s)
|
|
177
|
-
# Extract named capture group
|
|
178
|
-
match[capture_name.to_s]
|
|
179
|
-
elsif match.captures.any?
|
|
180
|
-
# Extract first capture group
|
|
181
|
-
match[1]
|
|
182
|
-
else
|
|
183
|
-
# Return the entire match
|
|
184
|
-
match[0]
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# Extract all named capture groups as a hash or positional captures as an array.
|
|
189
|
-
#
|
|
190
|
-
# @param text [String] The text to match against.
|
|
191
|
-
# @param pattern_info [Hash] The hash result from `parse_pattern`.
|
|
192
|
-
# @return [Hash, Array, nil] A hash of named captures, an array of positional captures, or `nil`.
|
|
193
|
-
def extract_all_captures text, pattern_info
|
|
194
|
-
return nil unless text && pattern_info
|
|
195
|
-
|
|
196
|
-
regexp = pattern_info[:regexp]
|
|
197
|
-
match = text.match(regexp)
|
|
198
|
-
|
|
199
|
-
return nil unless match
|
|
200
|
-
|
|
201
|
-
if match.names.any?
|
|
202
|
-
# Return hash of named captures
|
|
203
|
-
match.names.each_with_object({}) do |name, captures|
|
|
204
|
-
captures[name] = match[name]
|
|
205
|
-
end
|
|
206
|
-
else
|
|
207
|
-
# Return array of positional captures
|
|
208
|
-
match.captures
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# A convenience method that combines parsing and a single extraction.
|
|
213
|
-
#
|
|
214
|
-
# @param text [String] The text to match against.
|
|
215
|
-
# @param pattern_input [String] The pattern string (with or without /flags/).
|
|
216
|
-
# @param capture_name [String] Name of the capture group to extract (optional).
|
|
217
|
-
# @param default_flags [String] Default flags if the pattern has no flags.
|
|
218
|
-
# @return [String, nil] The extracted text, or `nil` if no match is found.
|
|
219
|
-
def parse_and_extract text, pattern_input, capture_name = nil, default_flags = ''
|
|
220
|
-
pattern_info = parse_pattern(pattern_input, default_flags)
|
|
221
|
-
extract_capture(text, pattern_info, capture_name)
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
# A convenience method that combines parsing and extraction of all captures.
|
|
225
|
-
#
|
|
226
|
-
# @param text [String] The text to match against.
|
|
227
|
-
# @param pattern_input [String] The pattern string (with or without /flags/).
|
|
228
|
-
# @param default_flags [String] Default flags if the pattern has no flags.
|
|
229
|
-
# @return [Hash, Array, nil] All captured content, or `nil` if no match is found.
|
|
230
|
-
def parse_and_extract_all text, pattern_input, default_flags = ''
|
|
231
|
-
pattern_info = parse_pattern(pattern_input, default_flags)
|
|
232
|
-
extract_all_captures(text, pattern_info)
|
|
233
|
-
end
|
|
234
|
-
end
|
|
235
|
-
end
|