ast-template 7.1.10 → 7.1.11
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
- checksums.yaml.gz.sig +0 -0
- data/lib/ast/template/file_types.rb +50 -0
- data/lib/ast/template/merge_dispatch.rb +74 -0
- data/lib/ast/template/strategies.rb +92 -0
- data/lib/ast/template/token_application.rb +71 -0
- data/lib/ast/template/version.rb +1 -1
- data/lib/ast/template.rb +4 -0
- data.tar.gz.sig +0 -0
- metadata +30 -6
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d333d88ffe0b32f1cf3bce7401deaaad28eaca1f40defae1d787e3fe782c6ebd
|
|
4
|
+
data.tar.gz: 903c10dc9ad0b5825ade7d2cd41fc0d9a2b0722b1ee0be425d41a5d70efe5c52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 618776085a1f8680cba8e8640149129a71c8d9589a08c6a0536bef146a15d38db0fb5832b450d596d2368dc6a30c198be22f2e2b2c57a8e8ffa5a23343e8e991
|
|
7
|
+
data.tar.gz: f86daa1f153f431e3b27c8fd0fb9d63a4cf5f9af1657a6ea88116361e89db7de473e3e46d743d4bcd0e324be8a04b382977dcbe2b5816a9511402307b7ecdd84
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Template
|
|
5
|
+
# Destination-path file-type detection. Contract: structuredmerge/spec
|
|
6
|
+
# slice-1043, fixtures at
|
|
7
|
+
# structuredmerge/fixtures/templating/slice-1043-merge-dispatch.
|
|
8
|
+
#
|
|
9
|
+
# First match wins; the rule order is load-bearing (e.g. `.gemfile`
|
|
10
|
+
# shadows the appraisals rule for `Appraisal.root.gemfile` — preserved
|
|
11
|
+
# from kettle-jem's effective behavior and documented in the slice).
|
|
12
|
+
module FileTypes
|
|
13
|
+
RUBY_BASENAMES = %w[Gemfile Rakefile Appraisals Appraisal.root.gemfile .simplecov].freeze
|
|
14
|
+
RUBY_SUFFIXES = %w[.gemspec .gemfile].freeze
|
|
15
|
+
RUBY_EXTENSIONS = %w[.rb .rake].freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# @param path [String] logical destination path
|
|
20
|
+
# @param configured [String, Symbol, nil] destination-config override
|
|
21
|
+
# @return [Symbol] the file type
|
|
22
|
+
def detect(path, configured: nil)
|
|
23
|
+
return configured.to_sym unless configured.to_s.empty?
|
|
24
|
+
|
|
25
|
+
relative_path = path.to_s
|
|
26
|
+
basename = File.basename(relative_path)
|
|
27
|
+
extension = File.extname(relative_path).downcase
|
|
28
|
+
return :gitignore if basename == ".gitignore"
|
|
29
|
+
return :gemfile if basename == "Gemfile" || basename.end_with?(".gemfile")
|
|
30
|
+
return :appraisals if basename.start_with?("Appraisals")
|
|
31
|
+
return :gemspec if basename.end_with?(".gemspec")
|
|
32
|
+
return :rakefile if basename == "Rakefile" || extension == ".rake"
|
|
33
|
+
return :ruby if RUBY_BASENAMES.include?(basename) ||
|
|
34
|
+
RUBY_SUFFIXES.any? { |suffix| basename.end_with?(suffix) } ||
|
|
35
|
+
RUBY_EXTENSIONS.include?(extension)
|
|
36
|
+
return :yaml if extension.match?(/\A\.ya?ml\z/) || basename.casecmp("citation.cff").zero?
|
|
37
|
+
return :toml if extension == ".toml"
|
|
38
|
+
return :jsonc if relative_path == ".devcontainer/devcontainer.json"
|
|
39
|
+
return :jsonc if extension == ".jsonc"
|
|
40
|
+
return :json5 if extension == ".json5"
|
|
41
|
+
return :json if extension == ".json"
|
|
42
|
+
return :markdown if extension.match?(/\A\.md(?:own)?\z/)
|
|
43
|
+
return :dotenv if basename.start_with?(".env") || basename.end_with?(".env") || extension == ".env"
|
|
44
|
+
return :rbs if extension == ".rbs"
|
|
45
|
+
|
|
46
|
+
:text
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Template
|
|
5
|
+
# Merge dispatch: route a file type to its merger adapter. Contract:
|
|
6
|
+
# structuredmerge/spec slice-1043, fixtures at
|
|
7
|
+
# structuredmerge/fixtures/templating/slice-1043-merge-dispatch.
|
|
8
|
+
#
|
|
9
|
+
# Default adapters wrap the ast-merge family gems and are required LAZILY —
|
|
10
|
+
# ast-template does not hard-depend on merger gems; the consumer's bundle
|
|
11
|
+
# provides them. Types needing consumer policy (gemspec, the Ruby family,
|
|
12
|
+
# json variants, dotenv, rbs, bash, gitignore) have no default adapter —
|
|
13
|
+
# register one.
|
|
14
|
+
module MergeDispatch
|
|
15
|
+
Adapter = Struct.new(:gem_name, :require_path, :invoke)
|
|
16
|
+
|
|
17
|
+
DEFAULT_ADAPTERS = {
|
|
18
|
+
yaml: Adapter.new("psych-merge", "psych/merge", ->(t, d, **opts) { Psych::Merge.merge_yaml(t, d, "yaml", **opts) }),
|
|
19
|
+
toml: Adapter.new("citrus-toml-merge", "citrus/toml/merge", ->(t, d, **opts) { Citrus::Toml::Merge.merge_toml(t, d, "toml", **opts) }),
|
|
20
|
+
markdown: Adapter.new("markdown-merge", "markdown/merge", ->(t, d, **opts) { Markdown::Merge.merge_markdown(t, d, "markdown", **opts) })
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
@adapters = {}
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
# Register an adapter for a file type. The callable receives
|
|
27
|
+
# (template_content, destination_content, **options) and returns the
|
|
28
|
+
# merger result hash ({ok:, output:, diagnostics:}).
|
|
29
|
+
def register(file_type, callable = nil, &block)
|
|
30
|
+
adapter = callable || block
|
|
31
|
+
raise ArgumentError, "adapter callable required" unless adapter.respond_to?(:call)
|
|
32
|
+
|
|
33
|
+
adapters[file_type.to_sym] = adapter
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Whether any adapter (registered or default) exists for the type.
|
|
37
|
+
def registered?(file_type)
|
|
38
|
+
adapters.key?(file_type.to_sym) || DEFAULT_ADAPTERS.key?(file_type.to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Dispatch to the type's adapter. Returns nil when no adapter exists —
|
|
42
|
+
# the caller's fallback is the template content.
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash, nil] the merger result, or nil when unregistered
|
|
45
|
+
def dispatch(file_type, template_content, destination_content, **options)
|
|
46
|
+
type = file_type.to_sym
|
|
47
|
+
registered = adapters[type]
|
|
48
|
+
return registered.call(template_content, destination_content, **options) if registered
|
|
49
|
+
|
|
50
|
+
default = DEFAULT_ADAPTERS[type]
|
|
51
|
+
return nil unless default
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
require default.require_path
|
|
55
|
+
rescue LoadError
|
|
56
|
+
raise LoadError, "merge dispatch for #{type} requires #{default.gem_name}; add it to the consumer bundle"
|
|
57
|
+
end
|
|
58
|
+
default.invoke.call(template_content, destination_content, **options)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Test support: clear consumer-registered adapters.
|
|
62
|
+
def reset!
|
|
63
|
+
@adapters = {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def adapters
|
|
69
|
+
@adapters ||= {}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ast
|
|
4
|
+
module Template
|
|
5
|
+
# Strategy resolution and application decisions for template entries.
|
|
6
|
+
# Contract: structuredmerge/spec slice-1042, conformance fixtures at
|
|
7
|
+
# structuredmerge/fixtures/templating/slice-1042-strategy-application.
|
|
8
|
+
#
|
|
9
|
+
# The vocabulary (execution_action / write_action / status) matches the
|
|
10
|
+
# slice-353 session transport fixtures. The merger itself is the next
|
|
11
|
+
# contract surface (slice-1043); `merge` on differing content stops at
|
|
12
|
+
# the decision here.
|
|
13
|
+
module Strategies
|
|
14
|
+
# The four strategy names a destination config may declare.
|
|
15
|
+
STRATEGIES = %w[merge keep_destination raw_copy accept_template].freeze
|
|
16
|
+
DEFAULT_STRATEGY = "merge"
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Effective strategy for a destination path: the matching override wins,
|
|
21
|
+
# else the default. Overrides are TemplateStrategyOverride-shaped
|
|
22
|
+
# entries ({path:, strategy:} with string or symbol keys).
|
|
23
|
+
#
|
|
24
|
+
# @param default [String] strategy when no override matches
|
|
25
|
+
# @param overrides [Array<Hash>] per-path strategy overrides
|
|
26
|
+
# @param path [String] logical destination path
|
|
27
|
+
# @return [String] the effective strategy
|
|
28
|
+
def resolve(default: DEFAULT_STRATEGY, overrides: [], path:)
|
|
29
|
+
override = Array(overrides).find { |entry| (entry[:path] || entry["path"]) == path }
|
|
30
|
+
strategy = override && (override[:strategy] || override["strategy"])
|
|
31
|
+
strategy.to_s.empty? ? default : strategy.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The decision for one entry. Content is present only when this contract
|
|
35
|
+
# owns it (raw_copy, accept_template, merge-with-empty-destination);
|
|
36
|
+
# keep decisions and merge-of-differences return no content (the latter
|
|
37
|
+
# defers to the merger).
|
|
38
|
+
#
|
|
39
|
+
# @param strategy [String] resolved strategy for the entry
|
|
40
|
+
# @param template_content [String] prepared template content
|
|
41
|
+
# @param destination_content [String, nil] current destination content
|
|
42
|
+
# (nil when the destination file is absent)
|
|
43
|
+
# @return [Hash] {execution_action:, write_action:, status:, content:}
|
|
44
|
+
def plan_entry(strategy:, template_content:, destination_content:)
|
|
45
|
+
strategy = strategy.to_s
|
|
46
|
+
return keep_decision if strategy == "keep_destination"
|
|
47
|
+
|
|
48
|
+
absent = destination_content.nil?
|
|
49
|
+
if strategy == "merge"
|
|
50
|
+
trivial = trivial_merge_outcome(template_content, destination_content)
|
|
51
|
+
return keep_decision if trivial == :identical
|
|
52
|
+
return write_decision("merge_prepared_content", absent, template_content) if trivial == :empty_destination
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# accept_template and merge-of-differences both write prepared content;
|
|
56
|
+
# for merge-of-differences the content is the merger's, not ours.
|
|
57
|
+
content = (strategy == "merge") ? nil : template_content
|
|
58
|
+
write_decision(execution_action_for(strategy), absent, content)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Whether the strategy leaves the destination untouched. Absent
|
|
62
|
+
# destinations are not created under keep_destination.
|
|
63
|
+
def keep_destination?(strategy)
|
|
64
|
+
strategy.to_s == "keep_destination"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The merge short-circuits that never invoke a merger:
|
|
68
|
+
# :empty_destination when there is nothing to merge into (template
|
|
69
|
+
# becomes the content), :identical when destination already equals
|
|
70
|
+
# template (destination is kept), nil when a real merge is required.
|
|
71
|
+
def trivial_merge_outcome(template_content, destination_content)
|
|
72
|
+
return :empty_destination if destination_content.to_s.strip.empty?
|
|
73
|
+
return :identical if destination_content == template_content
|
|
74
|
+
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def keep_decision
|
|
79
|
+
{execution_action: "keep", write_action: "keep", status: "keep", content: nil}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write_decision(execution_action, create, content)
|
|
83
|
+
action = create ? "create" : "update"
|
|
84
|
+
{execution_action: execution_action, write_action: action, status: action, content: content}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def execution_action_for(strategy)
|
|
88
|
+
(strategy == "raw_copy") ? "raw_copy" : "merge_prepared_content"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'token/resolver'
|
|
4
|
+
|
|
5
|
+
module Ast
|
|
6
|
+
module Template
|
|
7
|
+
# Token application policy: normalize replacements, resolve with
|
|
8
|
+
# on-missing keep, then police tokens that survive in the consumer's
|
|
9
|
+
# namespace. Token mechanics (parsing, grammar) live in token-resolver;
|
|
10
|
+
# this module is the contract surface defined by
|
|
11
|
+
# structuredmerge/spec slice-1041 and its conformance fixtures.
|
|
12
|
+
module TokenApplication
|
|
13
|
+
# Raised when resolved output still contains tokens in the consumer's
|
|
14
|
+
# namespace. Carries the sorted, deduplicated canonical full keys so
|
|
15
|
+
# consumers can re-raise with their own branded message.
|
|
16
|
+
class UnresolvedTokensError < ArgumentError
|
|
17
|
+
# @return [Array<String>] canonical full token keys, sorted
|
|
18
|
+
attr_reader :tokens
|
|
19
|
+
|
|
20
|
+
def initialize(tokens)
|
|
21
|
+
@tokens = tokens
|
|
22
|
+
super("unresolved template tokens: #{tokens.map { |token| "{#{token}}" }.join(', ')}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# Resolve tokens in content, then (when scanning) raise if any token in
|
|
29
|
+
# the consumer's namespace survived. Missing replacements are kept
|
|
30
|
+
# verbatim — in canonical form, so the scan sees canonical keys.
|
|
31
|
+
#
|
|
32
|
+
# @param content [String] template content
|
|
33
|
+
# @param replacements [Hash] token canonical key => value (keys and
|
|
34
|
+
# values are stringified before resolution)
|
|
35
|
+
# @param config [Token::Resolver::Config] token structure config
|
|
36
|
+
# @param namespace [String] first-segment namespace the consumer owns
|
|
37
|
+
# @param scan_unresolved [Boolean] when false, unresolved tokens pass
|
|
38
|
+
# @return [String] resolved content
|
|
39
|
+
# @raise [UnresolvedTokensError] unresolved namespaced tokens remain
|
|
40
|
+
def apply_tokens(content, replacements, config:, namespace:, scan_unresolved: true)
|
|
41
|
+
document = Token::Resolver::Document.new(content.to_s, config: config)
|
|
42
|
+
resolved = Token::Resolver::Resolve.new(on_missing: :keep).resolve(document, stringify_replacements(replacements))
|
|
43
|
+
return resolved unless scan_unresolved
|
|
44
|
+
|
|
45
|
+
unresolved = unresolved_namespaced_tokens(resolved, config: config, namespace: namespace)
|
|
46
|
+
return resolved if unresolved.empty?
|
|
47
|
+
|
|
48
|
+
raise UnresolvedTokensError, unresolved
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sorted, deduplicated canonical keys of tokens in content whose first
|
|
52
|
+
# segment equals namespace.
|
|
53
|
+
#
|
|
54
|
+
# @return [Array<String>]
|
|
55
|
+
def unresolved_namespaced_tokens(content, config:, namespace:)
|
|
56
|
+
Token::Resolver::Document.new(content.to_s, config: config)
|
|
57
|
+
.token_keys
|
|
58
|
+
.select { |key| key.split(config.separators.first, 2).first == namespace }
|
|
59
|
+
.uniq
|
|
60
|
+
.sort
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Normalize replacements for the resolver: string keys and values.
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash{String => String}]
|
|
66
|
+
def stringify_replacements(replacements)
|
|
67
|
+
replacements.to_h.transform_keys(&:to_s).transform_values(&:to_s)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/ast/template/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Ast
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = '7.1.
|
|
8
|
+
VERSION = '7.1.11'
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/ast/template.rb
CHANGED
|
@@ -25,7 +25,11 @@ module Ast
|
|
|
25
25
|
'rust' => 'Rust',
|
|
26
26
|
'typescript' => 'TypeScript'
|
|
27
27
|
}.freeze
|
|
28
|
+
autoload :FileTypes, 'ast/template/file_types'
|
|
29
|
+
autoload :MergeDispatch, 'ast/template/merge_dispatch'
|
|
28
30
|
autoload :RustHostProvider, 'ast/template/rust_host_provider'
|
|
31
|
+
autoload :Strategies, 'ast/template/strategies'
|
|
32
|
+
autoload :TokenApplication, 'ast/template/token_application'
|
|
29
33
|
|
|
30
34
|
class << self
|
|
31
35
|
def rust_host_provider
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ast-template
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -43,14 +43,34 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - '='
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 7.1.
|
|
46
|
+
version: 7.1.11
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - '='
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 7.1.
|
|
53
|
+
version: 7.1.11
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: token-resolver
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 2.0.10
|
|
64
|
+
type: :runtime
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - "~>"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '2.0'
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 2.0.10
|
|
54
74
|
- !ruby/object:Gem::Dependency
|
|
55
75
|
name: version_gem
|
|
56
76
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -285,7 +305,11 @@ files:
|
|
|
285
305
|
- README.md
|
|
286
306
|
- lib/ast-template.rb
|
|
287
307
|
- lib/ast/template.rb
|
|
308
|
+
- lib/ast/template/file_types.rb
|
|
309
|
+
- lib/ast/template/merge_dispatch.rb
|
|
288
310
|
- lib/ast/template/rust_host_provider.rb
|
|
311
|
+
- lib/ast/template/strategies.rb
|
|
312
|
+
- lib/ast/template/token_application.rb
|
|
289
313
|
- lib/ast/template/typed_core_bridge.rb
|
|
290
314
|
- lib/ast/template/version.rb
|
|
291
315
|
- sig/ast/template.rbs
|
|
@@ -295,10 +319,10 @@ licenses:
|
|
|
295
319
|
- PolyForm-Small-Business-1.0.0
|
|
296
320
|
metadata:
|
|
297
321
|
homepage_uri: https://structuredmerge.org
|
|
298
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
299
|
-
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.
|
|
322
|
+
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.11
|
|
323
|
+
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.11/CHANGELOG.md
|
|
300
324
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
301
|
-
documentation_uri: https://www.rubydoc.info/gems/ast-template/7.1.
|
|
325
|
+
documentation_uri: https://www.rubydoc.info/gems/ast-template/7.1.11
|
|
302
326
|
funding_uri: https://github.com/sponsors/pboling
|
|
303
327
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
304
328
|
news_uri: https://www.railsbling.com/tags/ast-template
|
metadata.gz.sig
CHANGED
|
Binary file
|