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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +278 -0
  5. data/ROADMAP.md +80 -0
  6. data/docs/_md_composer_architecture.md +50 -0
  7. data/docs/_md_composer_cheatsheet.md +72 -0
  8. data/docs/_md_composer_concepts.md +64 -0
  9. data/docs/_md_composer_dev_guide.md +55 -0
  10. data/docs/_md_composer_getting_started.md +114 -0
  11. data/docs/_md_composer_readme.md +93 -0
  12. data/docs/_md_composer_user_guide.md +65 -0
  13. data/docs/ai/md_composer_ai_audit.md +35 -0
  14. data/docs/ai/md_composer_ai_canonical_docs.md +44 -0
  15. data/docs/ai/md_composer_ai_source_map.md +39 -0
  16. data/docs/compose/md_composer_compose_actions.md +338 -0
  17. data/docs/compose/md_composer_compose_anatomy.md +156 -0
  18. data/docs/compose/md_composer_compose_buffer.md +81 -0
  19. data/docs/compose/md_composer_compose_examples.md +31 -0
  20. data/docs/compose/md_composer_compose_include.md +136 -0
  21. data/docs/compose/md_composer_compose_select.md +198 -0
  22. data/docs/compose/md_composer_compose_sources.md +161 -0
  23. data/docs/compose/md_composer_compose_targets.md +194 -0
  24. data/docs/examples/md_composer_example_basic_compose.md +57 -0
  25. data/docs/examples/md_composer_example_buffer_target_actions.md +83 -0
  26. data/docs/examples/md_composer_example_fixtures.md +62 -0
  27. data/docs/examples/md_composer_example_html_output.md +50 -0
  28. data/docs/examples/md_composer_example_modify.md +77 -0
  29. data/docs/examples/md_composer_example_multi_row_compose.md +67 -0
  30. data/docs/examples/md_composer_example_ruby_plans.md +62 -0
  31. data/docs/examples/md_composer_example_structured_data.md +68 -0
  32. data/docs/examples/md_composer_example_transforms.md +68 -0
  33. data/docs/examples/md_composer_example_yaml_json_rows.md +56 -0
  34. data/docs/examples/md_composer_examples_readme.md +45 -0
  35. data/docs/examples/md_composer_runnable_examples.md +374 -0
  36. data/docs/examples/md_composer_source_ruby_dsl.md +88 -0
  37. data/docs/reference/md_composer_nested.md +170 -0
  38. data/docs/reference/md_composer_reference_api.md +71 -0
  39. data/docs/reference/md_composer_reference_capabilities.md +63 -0
  40. data/docs/reference/md_composer_reference_diagnostics.md +54 -0
  41. data/docs/reference/md_composer_reference_plan_schema.md +75 -0
  42. data/docs/reference/md_composer_reference_registries.md +63 -0
  43. data/docs/reference/md_composer_take.md +221 -0
  44. data/docs/reference/md_composer_unit_tokens.md +228 -0
  45. data/docs/reference/md_composer_where.md +227 -0
  46. data/docs/transform/md_composer_transform_anatomy.md +112 -0
  47. data/docs/transform/md_composer_transform_examples.md +30 -0
  48. data/docs/transform/md_composer_transform_modes.md +83 -0
  49. data/docs/transform/md_composer_transform_options.md +142 -0
  50. data/docs/transform/md_composer_transform_scope.md +97 -0
  51. data/docs/transform/md_composer_transform_transforms.md +99 -0
  52. data/examples/README.md +20 -0
  53. data/examples/advanced_composer.rb +207 -0
  54. data/examples/basic_compose.rb +24 -0
  55. data/examples/complex_composer.rb +235 -0
  56. data/examples/example_support.rb +18 -0
  57. data/examples/fixtures/current.md +179 -0
  58. data/examples/fixtures/faq.md +58 -0
  59. data/examples/fixtures/guide.md +62 -0
  60. data/examples/fixtures/site_intro.md +29 -0
  61. data/examples/fixtures/source.html +22 -0
  62. data/examples/html_input.rb +26 -0
  63. data/examples/output/advanced_composer.md +76 -0
  64. data/examples/output/basic_compose.md +25 -0
  65. data/examples/output/complex_composer.md +85 -0
  66. data/examples/output/html_input.md +4 -0
  67. data/examples/output/source_list_dsl.md +126 -0
  68. data/examples/output/standard_composer.md +46 -0
  69. data/examples/output/standard_sources_buffer.md +31 -0
  70. data/examples/output/yaml_plan.md +43 -0
  71. data/examples/plans/basic.yml +20 -0
  72. data/examples/source_list_dsl.rb +41 -0
  73. data/examples/standard_composer.rb +42 -0
  74. data/examples/standard_sources_buffer.rb +62 -0
  75. data/examples/yaml_plan.rb +17 -0
  76. data/lib/markdown_composer/capabilities.rb +223 -0
  77. data/lib/markdown_composer/composition_buffer.rb +378 -0
  78. data/lib/markdown_composer/data_path.rb +313 -0
  79. data/lib/markdown_composer/diagnostics.rb +63 -0
  80. data/lib/markdown_composer/document_index/html_parser.rb +84 -0
  81. data/lib/markdown_composer/document_index/markdown_parser.rb +338 -0
  82. data/lib/markdown_composer/document_index.rb +94 -0
  83. data/lib/markdown_composer/executor.rb +284 -0
  84. data/lib/markdown_composer/markdown_renderer.rb +105 -0
  85. data/lib/markdown_composer/plan.rb +436 -0
  86. data/lib/markdown_composer/plan_builder.rb +111 -0
  87. data/lib/markdown_composer/registries/action_entries.rb +26 -0
  88. data/lib/markdown_composer/registries/condition_entries.rb +58 -0
  89. data/lib/markdown_composer/registries/registry.rb +69 -0
  90. data/lib/markdown_composer/registries/source_entries.rb +18 -0
  91. data/lib/markdown_composer/registries/support_values.rb +23 -0
  92. data/lib/markdown_composer/registries/take_entries.rb +31 -0
  93. data/lib/markdown_composer/registries/take_registry.rb +18 -0
  94. data/lib/markdown_composer/registries/target_entries.rb +40 -0
  95. data/lib/markdown_composer/registries/unit_token_entries.rb +62 -0
  96. data/lib/markdown_composer/registries/where_registry.rb +84 -0
  97. data/lib/markdown_composer/registries.rb +46 -0
  98. data/lib/markdown_composer/result.rb +34 -0
  99. data/lib/markdown_composer/selection_resolver.rb +181 -0
  100. data/lib/markdown_composer/source.rb +57 -0
  101. data/lib/markdown_composer/source_list_builder.rb +47 -0
  102. data/lib/markdown_composer/take.rb +129 -0
  103. data/lib/markdown_composer/transform_options.rb +66 -0
  104. data/lib/markdown_composer/transform_runner/content_placement.rb +63 -0
  105. data/lib/markdown_composer/transform_runner/field_interpolator.rb +213 -0
  106. data/lib/markdown_composer/transform_runner/heading_numbering.rb +106 -0
  107. data/lib/markdown_composer/transform_runner/scope_resolver.rb +87 -0
  108. data/lib/markdown_composer/transform_runner.rb +264 -0
  109. data/lib/markdown_composer/transforms/default_entries.rb +31 -0
  110. data/lib/markdown_composer/transforms/registry.rb +11 -0
  111. data/lib/markdown_composer/validator.rb +378 -0
  112. data/lib/markdown_composer/value_object.rb +15 -0
  113. data/lib/markdown_composer/version.rb +5 -0
  114. data/lib/markdown_composer/where.rb +313 -0
  115. data/lib/markdown_composer.rb +114 -0
  116. 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