markdown_composer 0.7.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/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +278 -0
- data/ROADMAP.md +80 -0
- data/docs/_md_composer_architecture.md +50 -0
- data/docs/_md_composer_cheatsheet.md +72 -0
- data/docs/_md_composer_concepts.md +64 -0
- data/docs/_md_composer_dev_guide.md +55 -0
- data/docs/_md_composer_getting_started.md +114 -0
- data/docs/_md_composer_readme.md +93 -0
- data/docs/_md_composer_user_guide.md +65 -0
- data/docs/ai/md_composer_ai_audit.md +35 -0
- data/docs/ai/md_composer_ai_canonical_docs.md +44 -0
- data/docs/ai/md_composer_ai_source_map.md +39 -0
- data/docs/compose/md_composer_compose_actions.md +338 -0
- data/docs/compose/md_composer_compose_anatomy.md +156 -0
- data/docs/compose/md_composer_compose_buffer.md +81 -0
- data/docs/compose/md_composer_compose_examples.md +31 -0
- data/docs/compose/md_composer_compose_include.md +136 -0
- data/docs/compose/md_composer_compose_select.md +198 -0
- data/docs/compose/md_composer_compose_sources.md +161 -0
- data/docs/compose/md_composer_compose_targets.md +194 -0
- data/docs/examples/md_composer_example_basic_compose.md +57 -0
- data/docs/examples/md_composer_example_buffer_target_actions.md +83 -0
- data/docs/examples/md_composer_example_fixtures.md +62 -0
- data/docs/examples/md_composer_example_html_output.md +50 -0
- data/docs/examples/md_composer_example_modify.md +77 -0
- data/docs/examples/md_composer_example_multi_row_compose.md +67 -0
- data/docs/examples/md_composer_example_ruby_plans.md +62 -0
- data/docs/examples/md_composer_example_structured_data.md +68 -0
- data/docs/examples/md_composer_example_transforms.md +68 -0
- data/docs/examples/md_composer_example_yaml_json_rows.md +56 -0
- data/docs/examples/md_composer_examples_readme.md +45 -0
- data/docs/examples/md_composer_runnable_examples.md +374 -0
- data/docs/examples/md_composer_source_ruby_dsl.md +88 -0
- data/docs/reference/md_composer_nested.md +170 -0
- data/docs/reference/md_composer_reference_api.md +71 -0
- data/docs/reference/md_composer_reference_capabilities.md +63 -0
- data/docs/reference/md_composer_reference_diagnostics.md +54 -0
- data/docs/reference/md_composer_reference_plan_schema.md +75 -0
- data/docs/reference/md_composer_reference_registries.md +63 -0
- data/docs/reference/md_composer_take.md +221 -0
- data/docs/reference/md_composer_unit_tokens.md +228 -0
- data/docs/reference/md_composer_where.md +227 -0
- data/docs/transform/md_composer_transform_anatomy.md +112 -0
- data/docs/transform/md_composer_transform_examples.md +30 -0
- data/docs/transform/md_composer_transform_modes.md +83 -0
- data/docs/transform/md_composer_transform_options.md +142 -0
- data/docs/transform/md_composer_transform_scope.md +97 -0
- data/docs/transform/md_composer_transform_transforms.md +99 -0
- data/examples/README.md +20 -0
- data/examples/advanced_composer.rb +207 -0
- data/examples/basic_compose.rb +24 -0
- data/examples/complex_composer.rb +235 -0
- data/examples/example_support.rb +18 -0
- data/examples/fixtures/current.md +179 -0
- data/examples/fixtures/faq.md +58 -0
- data/examples/fixtures/guide.md +62 -0
- data/examples/fixtures/site_intro.md +29 -0
- data/examples/fixtures/source.html +22 -0
- data/examples/html_input.rb +26 -0
- data/examples/output/advanced_composer.md +76 -0
- data/examples/output/basic_compose.md +25 -0
- data/examples/output/complex_composer.md +85 -0
- data/examples/output/html_input.md +4 -0
- data/examples/output/source_list_dsl.md +126 -0
- data/examples/output/standard_composer.md +46 -0
- data/examples/output/standard_sources_buffer.md +31 -0
- data/examples/output/yaml_plan.md +43 -0
- data/examples/plans/basic.yml +20 -0
- data/examples/source_list_dsl.rb +41 -0
- data/examples/standard_composer.rb +42 -0
- data/examples/standard_sources_buffer.rb +62 -0
- data/examples/yaml_plan.rb +17 -0
- data/lib/markdown_composer/capabilities.rb +223 -0
- data/lib/markdown_composer/composition_buffer.rb +378 -0
- data/lib/markdown_composer/data_path.rb +313 -0
- data/lib/markdown_composer/diagnostics.rb +63 -0
- data/lib/markdown_composer/document_index/html_parser.rb +84 -0
- data/lib/markdown_composer/document_index/markdown_parser.rb +338 -0
- data/lib/markdown_composer/document_index.rb +94 -0
- data/lib/markdown_composer/executor.rb +284 -0
- data/lib/markdown_composer/markdown_renderer.rb +105 -0
- data/lib/markdown_composer/plan.rb +436 -0
- data/lib/markdown_composer/plan_builder.rb +111 -0
- data/lib/markdown_composer/registries/action_entries.rb +26 -0
- data/lib/markdown_composer/registries/condition_entries.rb +58 -0
- data/lib/markdown_composer/registries/registry.rb +69 -0
- data/lib/markdown_composer/registries/source_entries.rb +18 -0
- data/lib/markdown_composer/registries/support_values.rb +23 -0
- data/lib/markdown_composer/registries/take_entries.rb +31 -0
- data/lib/markdown_composer/registries/take_registry.rb +18 -0
- data/lib/markdown_composer/registries/target_entries.rb +40 -0
- data/lib/markdown_composer/registries/unit_token_entries.rb +62 -0
- data/lib/markdown_composer/registries/where_registry.rb +84 -0
- data/lib/markdown_composer/registries.rb +46 -0
- data/lib/markdown_composer/result.rb +34 -0
- data/lib/markdown_composer/selection_resolver.rb +181 -0
- data/lib/markdown_composer/source.rb +57 -0
- data/lib/markdown_composer/source_list_builder.rb +47 -0
- data/lib/markdown_composer/take.rb +129 -0
- data/lib/markdown_composer/transform_options.rb +66 -0
- data/lib/markdown_composer/transform_runner/content_placement.rb +63 -0
- data/lib/markdown_composer/transform_runner/field_interpolator.rb +213 -0
- data/lib/markdown_composer/transform_runner/heading_numbering.rb +106 -0
- data/lib/markdown_composer/transform_runner/scope_resolver.rb +87 -0
- data/lib/markdown_composer/transform_runner.rb +264 -0
- data/lib/markdown_composer/transforms/default_entries.rb +31 -0
- data/lib/markdown_composer/transforms/registry.rb +11 -0
- data/lib/markdown_composer/validator.rb +378 -0
- data/lib/markdown_composer/value_object.rb +15 -0
- data/lib/markdown_composer/version.rb +5 -0
- data/lib/markdown_composer/where.rb +313 -0
- data/lib/markdown_composer.rb +114 -0
- metadata +260 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "markdown_composer"
|
|
4
|
+
require_relative "example_support"
|
|
5
|
+
|
|
6
|
+
root = File.expand_path(__dir__)
|
|
7
|
+
|
|
8
|
+
sources = MarkdownComposer.source_list do
|
|
9
|
+
current File.read(File.join(root, "fixtures/current.md"))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Select one H2 section, keep focused content, then promote the heading levels.
|
|
13
|
+
plan = MarkdownComposer.plan do
|
|
14
|
+
output :markdown
|
|
15
|
+
|
|
16
|
+
from :current
|
|
17
|
+
select 'h2_section where title:equals("Feature Matrix")'
|
|
18
|
+
include "heading_title"
|
|
19
|
+
include "paragraph[first:1]"
|
|
20
|
+
include "link[first:1]"
|
|
21
|
+
include "table[first:1]"
|
|
22
|
+
include "heading_3_section[position:1,2] { heading_title; paragraph[first:1]; code_block; table }"
|
|
23
|
+
append
|
|
24
|
+
|
|
25
|
+
transform "heading_2", "replace_text", "literal", {
|
|
26
|
+
"from" => "Feature Matrix",
|
|
27
|
+
"to" => "Feature Matrix Summary"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
transform "heading", "heading_levels", "promote", {
|
|
31
|
+
"by" => 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
transform "paragraph[first:1]", "insert_after", "insert", {
|
|
35
|
+
"content" => "\n## Summary\n\nThis generated summary highlights the selected feature table and the two focused child sections.",
|
|
36
|
+
"as" => "paragraph"
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
result = MarkdownComposer.compose(sources: sources, plan: plan)
|
|
41
|
+
|
|
42
|
+
ExampleSupport.write_output(root, "standard_composer", result)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "markdown_composer"
|
|
4
|
+
require_relative "example_support"
|
|
5
|
+
|
|
6
|
+
root = File.expand_path(__dir__)
|
|
7
|
+
|
|
8
|
+
sources = MarkdownComposer.source_list do
|
|
9
|
+
current File.read(File.join(root, "fixtures/current.md"))
|
|
10
|
+
explicit :guide, File.read(File.join(root, "fixtures/guide.md"))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Compose from current, reuse the explicit source with previous, then trim via buffer.
|
|
14
|
+
plan = MarkdownComposer.plan do
|
|
15
|
+
output :markdown
|
|
16
|
+
|
|
17
|
+
from :current
|
|
18
|
+
select "heading_1_section[first:1]"
|
|
19
|
+
include "heading_title"
|
|
20
|
+
include "paragraph[first:1]"
|
|
21
|
+
set
|
|
22
|
+
|
|
23
|
+
from :current
|
|
24
|
+
select 'h2_section where title:equals("Operations")'
|
|
25
|
+
include "heading_title"
|
|
26
|
+
include "paragraph[first:1]"
|
|
27
|
+
include "heading_3_section[first:1] { heading_title; paragraph[first:1]; code_block }"
|
|
28
|
+
append
|
|
29
|
+
|
|
30
|
+
from({ type: "explicit", key: "guide" })
|
|
31
|
+
select 'h2_section where title:equals("Install")'
|
|
32
|
+
include "all"
|
|
33
|
+
append
|
|
34
|
+
|
|
35
|
+
from :previous
|
|
36
|
+
select 'h2_section where title:equals("Validate")'
|
|
37
|
+
include "all"
|
|
38
|
+
append
|
|
39
|
+
|
|
40
|
+
from :buffer
|
|
41
|
+
select 'h2_section where title:equals("Install")'
|
|
42
|
+
remove_buffer_target
|
|
43
|
+
|
|
44
|
+
transform "heading_2", "replace_text", "literal", {
|
|
45
|
+
"from" => "Operations",
|
|
46
|
+
"to" => "Standard Operations"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
transform "heading_3", "replace_text", "literal", {
|
|
50
|
+
"from" => "Deployment",
|
|
51
|
+
"to" => "Release Steps"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
transform "output", "heading_numbers", "rebuild", {
|
|
55
|
+
"levels" => [ "h2", "h3" ],
|
|
56
|
+
"start" => 1
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result = MarkdownComposer.compose(sources: sources, plan: plan)
|
|
61
|
+
|
|
62
|
+
ExampleSupport.write_output(root, "standard_sources_buffer", result)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "markdown_composer"
|
|
4
|
+
require_relative "example_support"
|
|
5
|
+
|
|
6
|
+
root = File.expand_path(__dir__)
|
|
7
|
+
|
|
8
|
+
sources = MarkdownComposer.source_list do
|
|
9
|
+
current File.read(File.join(root, "fixtures/current.md"))
|
|
10
|
+
explicit :guide, File.read(File.join(root, "fixtures/guide.md"))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Load the compose and transform plan from YAML instead of the Ruby DSL.
|
|
14
|
+
plan = MarkdownComposer.parse_yaml(File.read(File.join(root, "plans/basic.yml")))
|
|
15
|
+
result = MarkdownComposer.compose(sources: sources, plan: plan)
|
|
16
|
+
|
|
17
|
+
ExampleSupport.write_output(root, "yaml_plan", result)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
module Capabilities
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
VERSION = 1
|
|
8
|
+
|
|
9
|
+
def build(options = {})
|
|
10
|
+
registries = Registries.default
|
|
11
|
+
json_compatible({
|
|
12
|
+
version: VERSION,
|
|
13
|
+
headless: true,
|
|
14
|
+
fields: field_metadata,
|
|
15
|
+
formats: format_metadata,
|
|
16
|
+
sources: entries(registries.sources, :source, options),
|
|
17
|
+
actions: entries(registries.actions, :action, options),
|
|
18
|
+
targets: entries(registries.targets, :target, options),
|
|
19
|
+
unit_tokens: entries(registries.unit_tokens, nil, options),
|
|
20
|
+
take: entries(registries.take, :take, options),
|
|
21
|
+
where: {
|
|
22
|
+
fields: entries(registries.condition_fields, nil, options),
|
|
23
|
+
predicates: entries(registries.predicates, :predicate, options),
|
|
24
|
+
groups: entries(registries.where_groups, nil, options),
|
|
25
|
+
condition_map: registries.conditions
|
|
26
|
+
},
|
|
27
|
+
transforms: transform_entries(registries.transforms, options),
|
|
28
|
+
outputs: output_metadata,
|
|
29
|
+
diagnostic_code_families: diagnostic_code_families,
|
|
30
|
+
policy_options: policy_options
|
|
31
|
+
})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def entries(registry, support_key, options)
|
|
35
|
+
registry.to_a.map do |entry|
|
|
36
|
+
support = entry[:support] || entry["support"] || {}
|
|
37
|
+
base = entry.dup
|
|
38
|
+
base[:support_status] = support_key ? support_status(support[support_key] || support[support_key.to_s], options) : support_status_from_support_hash(support, options)
|
|
39
|
+
base[:enabled] = enabled_status?(base[:support_status])
|
|
40
|
+
base
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def transform_entries(registry, options)
|
|
45
|
+
registry.entries.map do |entry|
|
|
46
|
+
modes = Array(entry.support[:modes])
|
|
47
|
+
mode_metadata = modes.map { |mode| transform_mode_metadata(entry, mode, options) }
|
|
48
|
+
support_status = support_status(entry.support[:transform], options)
|
|
49
|
+
{
|
|
50
|
+
token: entry.token,
|
|
51
|
+
aliases: entry.aliases,
|
|
52
|
+
label: entry.label,
|
|
53
|
+
tooltip: entry.tooltip,
|
|
54
|
+
meaning: entry.meaning,
|
|
55
|
+
row_sentence: entry.row_sentence,
|
|
56
|
+
support: entry.support,
|
|
57
|
+
support_status: support_status,
|
|
58
|
+
enabled: enabled_status?(support_status),
|
|
59
|
+
modes: mode_metadata,
|
|
60
|
+
source_formats: entry.source_formats,
|
|
61
|
+
condition_fields: entry.condition_fields
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def transform_mode_metadata(entry, mode, options)
|
|
67
|
+
required = TransformOptions.required(entry.token, mode)
|
|
68
|
+
optional = TransformOptions.optional(entry.token, mode, adapter_option_keys: options[:adapter_option_keys])
|
|
69
|
+
status = transform_mode_status(entry.token, mode, options)
|
|
70
|
+
{
|
|
71
|
+
token: mode,
|
|
72
|
+
label: mode.to_s.split("_").map(&:capitalize).join(" "),
|
|
73
|
+
support_status: status,
|
|
74
|
+
enabled: enabled_status?(status),
|
|
75
|
+
required_options: required,
|
|
76
|
+
optional_options: optional,
|
|
77
|
+
policy_gated: %w[policy_gated disabled_policy].include?(status),
|
|
78
|
+
required_output: required_output(entry.token, mode),
|
|
79
|
+
required_policy_options: required_policy_options(entry.token, mode)
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def transform_mode_status(transform, mode, options)
|
|
84
|
+
transform = transform.to_s
|
|
85
|
+
mode = mode.to_s
|
|
86
|
+
return "policy_gated" if transform == "adapter"
|
|
87
|
+
return "policy_gated" if transform == "sanitise"
|
|
88
|
+
return "policy_gated" if transform == "order" && mode == "target_order"
|
|
89
|
+
|
|
90
|
+
"normal"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def required_output(transform, mode)
|
|
94
|
+
return "html" if transform.to_s == "links" && %w[nofollow target_blank].include?(mode.to_s)
|
|
95
|
+
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def required_policy_options(transform, mode)
|
|
100
|
+
transform = transform.to_s
|
|
101
|
+
mode = mode.to_s
|
|
102
|
+
return [ "adapter_transforms" ] if transform == "adapter" || transform == "sanitise"
|
|
103
|
+
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def support_status(value, options)
|
|
108
|
+
case value.to_sym
|
|
109
|
+
when :normal then "normal"
|
|
110
|
+
when :advanced then "advanced"
|
|
111
|
+
when :output_only then "output_only"
|
|
112
|
+
when :include_only then "include_only"
|
|
113
|
+
when :optional then options.fetch(:optional_tokens, false) ? "optional" : "disabled_optional"
|
|
114
|
+
when :adapter_policy then options.fetch(:adapter_policy_tokens, false) ? "policy_gated" : "disabled_policy"
|
|
115
|
+
else "disabled"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def support_status_from_support_hash(support, options)
|
|
120
|
+
statuses = support.values.map { |value| support_status(value, options) }
|
|
121
|
+
return "disabled" if statuses.empty?
|
|
122
|
+
|
|
123
|
+
%w[normal include_only output_only advanced optional policy_gated].find { |status| statuses.include?(status) } || statuses.first
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def enabled_status?(status)
|
|
127
|
+
!%w[disabled disabled_optional disabled_policy policy_gated].include?(status.to_s)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def format_metadata
|
|
131
|
+
{
|
|
132
|
+
input: [
|
|
133
|
+
{ from: "markdown", support_status: "normal", fidelity: "source" },
|
|
134
|
+
{ from: "html", support_status: "best_effort", fidelity: "lossy" }
|
|
135
|
+
],
|
|
136
|
+
conversions: [
|
|
137
|
+
{ from: "markdown", to: "markdown", support_status: "normal", fidelity: "source" },
|
|
138
|
+
{ from: "markdown", to: "html", support_status: "normal", fidelity: "rendered" },
|
|
139
|
+
{ from: "html", to: "markdown", support_status: "best_effort", fidelity: "lossy" },
|
|
140
|
+
{ from: "html", to: "html", support_status: "best_effort", fidelity: "lossy_intermediate_markdown" }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def field_metadata
|
|
146
|
+
{
|
|
147
|
+
source: {
|
|
148
|
+
label: "Source",
|
|
149
|
+
tooltip: "First row normally uses current or explicit sources; previous falls back to current when no prior row exists. Later rows may use inherited, previous source, or previous output after a row has produced output.",
|
|
150
|
+
first_row: %w[current explicit previous],
|
|
151
|
+
later_rows: %w[current explicit inherited previous buffer]
|
|
152
|
+
},
|
|
153
|
+
document: {
|
|
154
|
+
label: "Document",
|
|
155
|
+
tooltip: "Select a host document when Source is Explicit or Inherited. Current Source uses the preview source supplied by the host."
|
|
156
|
+
},
|
|
157
|
+
select: {
|
|
158
|
+
label: "Select",
|
|
159
|
+
tooltip: "Selector token from the registry. Supports take and where syntax such as heading_2_section[first:1]."
|
|
160
|
+
},
|
|
161
|
+
include: {
|
|
162
|
+
label: "Include",
|
|
163
|
+
tooltip: "Include token from the registry. Use all for the full selected content or a unit token such as heading_title."
|
|
164
|
+
},
|
|
165
|
+
action: {
|
|
166
|
+
label: "Action",
|
|
167
|
+
tooltip: "Composition action from the registry. Some actions require a target."
|
|
168
|
+
},
|
|
169
|
+
target: {
|
|
170
|
+
label: "Target",
|
|
171
|
+
tooltip: "Target selector or position used by insert, replace, copy, move, remove_buffer_target, and transform_buffer_target actions. For modify with source buffer, in_place replaces selected buffer content inline."
|
|
172
|
+
},
|
|
173
|
+
transform: {
|
|
174
|
+
label: "Transform",
|
|
175
|
+
tooltip: "Registered transform. GUIs should filter transforms and modes by enabled capability metadata."
|
|
176
|
+
},
|
|
177
|
+
mode: {
|
|
178
|
+
label: "Mode",
|
|
179
|
+
tooltip: "Transform mode from the capability contract. Some modes require options or HTML output."
|
|
180
|
+
},
|
|
181
|
+
scope: {
|
|
182
|
+
label: "Scope",
|
|
183
|
+
tooltip: "Transform scope selector. Usually output or a registry selector such as heading_2_section."
|
|
184
|
+
},
|
|
185
|
+
options: {
|
|
186
|
+
label: "Options JSON",
|
|
187
|
+
tooltip: "JSON object passed to the selected transform mode. Required and optional keys come from capabilities."
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def output_metadata
|
|
193
|
+
[
|
|
194
|
+
{ token: "markdown", aliases: [ "md" ], label: "Markdown", support_status: "normal" },
|
|
195
|
+
{ token: "html", aliases: [], label: "HTML", support_status: "normal" }
|
|
196
|
+
]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def diagnostic_code_families
|
|
200
|
+
%w[action json output source selector take target token transform where yaml]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def policy_options
|
|
204
|
+
[
|
|
205
|
+
{ token: "adapter_policy_tokens", default: false, unlocks: "host-policy token families such as raw_html" },
|
|
206
|
+
{ token: "adapter_transforms", default: false, unlocks: "host-policy transform families" }
|
|
207
|
+
]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def json_compatible(value)
|
|
211
|
+
case value
|
|
212
|
+
when Hash
|
|
213
|
+
value.each_with_object({}) { |(key, child), hash| hash[key.to_s] = json_compatible(child) }
|
|
214
|
+
when Array
|
|
215
|
+
value.map { |child| json_compatible(child) }
|
|
216
|
+
when Symbol
|
|
217
|
+
value.to_s
|
|
218
|
+
else
|
|
219
|
+
value
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|