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,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarkdownComposer
4
+ module Where
5
+ GROUP_KEYS = %w[all any none not xor].freeze
6
+ FIELD_TAKE_FIELDS = %w[title text source_text].freeze
7
+
8
+ module_function
9
+
10
+ def normalize(value)
11
+ return parse(value) if value.is_a?(String)
12
+ return nil if value.nil?
13
+
14
+ case value
15
+ when Hash
16
+ hash = value.transform_keys(&:to_s)
17
+ group_key = GROUP_KEYS.find { |key| hash.key?(key) }
18
+ if group_key
19
+ children = hash[group_key]
20
+ return { group_key => Array(children).map { |child| normalize(child) } } unless group_key == "not"
21
+
22
+ return { "not" => normalize(children) }
23
+ end
24
+ hash
25
+ else
26
+ value
27
+ end
28
+ end
29
+
30
+ def parse(text)
31
+ text = text.to_s.strip
32
+ group = GROUP_KEYS.find { |key| text.start_with?("#{key}(") && text.end_with?(")") }
33
+ if group
34
+ inner = text[(group.length + 1)..-2]
35
+ children = split_top_level(inner, ";").map { |part| parse(part) }
36
+ return group == "not" ? { "not" => children.first } : { group => children }
37
+ end
38
+
39
+ field_expression, rest = split_field_and_rest(text)
40
+ if field_expression && (predicate_match = rest&.match(/\A([a-z_]+)\((.*)\)\z/))
41
+ field, field_take = parse_field_expression(field_expression)
42
+ condition = {
43
+ "field" => field,
44
+ "operator" => predicate_match[1].strip,
45
+ "value" => unquote(predicate_match[2].strip)
46
+ }
47
+ condition["field_take"] = field_take if field_take
48
+ return condition
49
+ end
50
+
51
+ if field_expression && rest
52
+ field, field_take = parse_field_expression(field_expression)
53
+ if rest.match?(/\A[a-z_]+\(/)
54
+ condition = {
55
+ "field" => field,
56
+ "operator" => "__malformed_predicate__",
57
+ "value" => rest.strip
58
+ }
59
+ condition["field_take"] = field_take if field_take
60
+ return condition
61
+ end
62
+
63
+ value = unquote(rest.strip)
64
+ if field == "position" && value.include?("..")
65
+ from, to = value.split("..", 2)
66
+ condition = { "field" => field, "operator" => "range", "from" => Take.integer(from), "to" => Take.integer(to) }
67
+ condition["field_take"] = field_take if field_take
68
+ return condition
69
+ end
70
+
71
+ operator = %w[exists true false].include?(value) ? value : "equals"
72
+ condition = { "field" => field, "operator" => operator == "exists" ? "exists" : "equals", "value" => cast(value) }
73
+ condition["field_take"] = field_take if field_take
74
+ return condition
75
+ end
76
+
77
+ if text =~ /\A([^:]+):([a-z_]+)\((.*)\)\z/
78
+ return {
79
+ "field" => Regexp.last_match(1).strip,
80
+ "operator" => Regexp.last_match(2).strip,
81
+ "value" => unquote(Regexp.last_match(3).strip)
82
+ }
83
+ end
84
+
85
+ if text =~ /\A([^:]+):(.+)\z/
86
+ field = Regexp.last_match(1).strip
87
+ value = unquote(Regexp.last_match(2).strip)
88
+ if field == "position" && value.include?("..")
89
+ from, to = value.split("..", 2)
90
+ return { "field" => field, "operator" => "range", "from" => Take.integer(from), "to" => Take.integer(to) }
91
+ end
92
+
93
+ operator = %w[exists true false].include?(value) ? value : "equals"
94
+ return { "field" => field, "operator" => operator == "exists" ? "exists" : "equals", "value" => cast(value) }
95
+ end
96
+
97
+ { "field" => "text", "operator" => "contains", "value" => text }
98
+ end
99
+
100
+ def match?(unit, condition, position:, total:, options: {})
101
+ return true if condition.nil? || condition == {}
102
+
103
+ condition = normalize(condition)
104
+ if condition.is_a?(Hash)
105
+ group_key = GROUP_KEYS.find { |key| condition.key?(key) }
106
+ return match_group?(group_key, condition[group_key], unit, position: position, total: total, options: options) if group_key
107
+ end
108
+
109
+ field = condition["field"].to_s
110
+ operator = condition["operator"].to_s
111
+ value = condition["value"]
112
+ actual = field_value(unit, field, position: position, total: total)
113
+ actual = apply_field_take(actual, condition["field_take"]) if condition["field_take"]
114
+ compare(actual, operator, value, condition, options: options)
115
+ end
116
+
117
+ def validate(condition, diagnostics:, path:, options: {})
118
+ condition = normalize(condition)
119
+ return if condition.nil?
120
+
121
+ if condition.is_a?(Hash)
122
+ group_key = GROUP_KEYS.find { |key| condition.key?(key) }
123
+ if group_key
124
+ if group_key == "not"
125
+ validate(condition[group_key], diagnostics: diagnostics, path: "#{path}.not", options: options)
126
+ return
127
+ end
128
+
129
+ Array(condition[group_key]).each_with_index { |child, index| validate(child, diagnostics: diagnostics, path: "#{path}.#{group_key}[#{index}]", options: options) }
130
+ return
131
+ end
132
+ end
133
+
134
+ field = condition["field"].to_s
135
+ operator = condition["operator"].to_s
136
+ if operator == "__malformed_predicate__"
137
+ diagnostics.error("where.predicate_malformed", "Malformed condition for #{field}: #{condition["value"]}", path: path)
138
+ return
139
+ end
140
+
141
+ registry = Registries.default.where
142
+ field_entry = registry.fields[field]
143
+ diagnostics.error("where.field_unknown", "Unknown condition field #{field.inspect}", path: path) unless field_entry
144
+ diagnostics.error("where.operator_unknown", "Unknown predicate #{operator.inspect}", path: path) unless registry.predicate?(operator) && registry.predicates_for(field).include?(operator)
145
+ if condition["field_take"]
146
+ diagnostics.error("where.field_take_unsupported", "#{field} does not support bracketed where take", path: path) unless field_entry&.field_take
147
+ Take.validate(condition["field_take"]).each { |message| diagnostics.error("where.field_take_invalid", message, path: path) }
148
+ end
149
+ validate_regex(condition["value"], diagnostics: diagnostics, path: path) if operator == "matches"
150
+ diagnostics.error("where.position_zero", "position:0 is invalid", path: path) if field == "position" && condition["value"].to_i.zero? && condition["operator"] == "equals"
151
+ end
152
+
153
+ def field_value(unit, field, position:, total:)
154
+ case field
155
+ when "title"
156
+ unit.respond_to?(:title_text) ? unit.title_text : unit.text
157
+ when "text"
158
+ unit.respond_to?(:text) ? unit.text : unit.title_text
159
+ when "source_text"
160
+ unit.respond_to?(:raw) ? unit.raw : field_value(unit, "text", position: position, total: total)
161
+ when "position"
162
+ position
163
+ when "language"
164
+ unit.respond_to?(:attributes) ? unit.attributes["language"] : nil
165
+ when "diagram_type"
166
+ unit.respond_to?(:attributes) ? unit.attributes["diagram_type"] : nil
167
+ when "format"
168
+ unit.respond_to?(:attributes) ? unit.attributes["format"] : nil
169
+ when "location"
170
+ unit.respond_to?(:attributes) ? unit.attributes["location"] : nil
171
+ when "links"
172
+ nested_nodes(unit).any? { |node| node.type == "link" }
173
+ when "images"
174
+ nested_nodes(unit).any? { |node| node.type == "image" }
175
+ when "code"
176
+ nested_nodes(unit).any? { |node| node.type == "code_block" }
177
+ when "numbers"
178
+ (unit.respond_to?(:text) ? unit.text : unit.title_text).to_s.match?(/\d/)
179
+ when "empty"
180
+ (unit.respond_to?(:text) ? unit.text : unit.title_text).to_s.strip.empty?
181
+ when "length"
182
+ (unit.respond_to?(:text) ? unit.text : unit.title_text).to_s.length
183
+ when "word_count"
184
+ (unit.respond_to?(:text) ? unit.text : unit.title_text).to_s.scan(/\w+/).length
185
+ when "child"
186
+ unit.respond_to?(:child_sections) ? unit.child_sections : []
187
+ end
188
+ end
189
+
190
+ def validate_regex(value, diagnostics:, path:)
191
+ Regexp.new(value.to_s)
192
+ rescue RegexpError => e
193
+ diagnostics.error("where.regex_invalid", "Invalid regular expression: #{e.message}", path: path)
194
+ end
195
+
196
+ def compare(actual, operator, value, condition, options:)
197
+ case operator
198
+ when "contains"
199
+ actual.to_s.include?(value.to_s)
200
+ when "starts_with"
201
+ actual.to_s.start_with?(value.to_s)
202
+ when "ends_with"
203
+ actual.to_s.end_with?(value.to_s)
204
+ when "equals"
205
+ actual.to_s.downcase == value.to_s.downcase
206
+ when "range"
207
+ from = condition["from"].to_i
208
+ to = condition["to"].to_i
209
+ actual.to_i >= from && actual.to_i <= (to.negative? ? Float::INFINITY : to)
210
+ when "exists"
211
+ actual.respond_to?(:any?) ? actual.any? : !!actual
212
+ when "min"
213
+ actual.to_i >= value.to_i
214
+ when "max"
215
+ actual.to_i <= value.to_i
216
+ when "matches"
217
+ Regexp.new(value.to_s).match?(actual.to_s)
218
+ else
219
+ false
220
+ end
221
+ rescue RegexpError
222
+ false
223
+ end
224
+
225
+ def match_group?(group_key, children, unit, position:, total:, options:)
226
+ children = group_key == "not" ? [ children ] : Array(children)
227
+ results = children.map { |child| match?(unit, child, position: position, total: total, options: options) }
228
+ case group_key
229
+ when "all" then results.all?
230
+ when "any" then results.any?
231
+ when "none" then results.none?
232
+ when "not" then !results.first
233
+ when "xor" then results.count(true) == 1
234
+ else false
235
+ end
236
+ end
237
+
238
+ def nested_nodes(unit)
239
+ return [ unit, *unit.children ].compact if unit.respond_to?(:children)
240
+ return [ unit ] unless unit.respond_to?(:all_nodes)
241
+
242
+ [ unit.heading_node, *unit.body_nodes, *unit.all_nodes ].compact
243
+ end
244
+
245
+ def apply_field_take(actual, take)
246
+ Take.apply(field_tokens(actual), take).join(" ")
247
+ end
248
+
249
+ def field_tokens(actual)
250
+ actual.to_s.scan(/\S+/)
251
+ end
252
+
253
+ def parse_field_expression(expression)
254
+ if expression =~ /\A([a-z_]+)\[(.+)\]\z/
255
+ return [ Regexp.last_match(1), Take.parse(Regexp.last_match(2)) ]
256
+ end
257
+
258
+ [ expression.strip, nil ]
259
+ end
260
+
261
+ def split_field_and_rest(text)
262
+ depth = 0
263
+ quote = nil
264
+ text.each_char.with_index do |char, index|
265
+ quote = quote == char ? nil : char if %w[" '].include?(char)
266
+ if quote.nil?
267
+ depth += 1 if char == "["
268
+ depth -= 1 if char == "]"
269
+ end
270
+ return [ text[0...index].strip, text[(index + 1)..].strip ] if char == ":" && depth.zero? && quote.nil?
271
+ end
272
+
273
+ [ nil, nil ]
274
+ end
275
+
276
+ def split_top_level(text, delimiter)
277
+ depth = 0
278
+ bracket_depth = 0
279
+ quote = nil
280
+ current = +""
281
+ parts = []
282
+ text.each_char do |char|
283
+ quote = quote == char ? nil : char if %w[" '].include?(char)
284
+ if quote.nil?
285
+ depth += 1 if char == "("
286
+ depth -= 1 if char == ")"
287
+ bracket_depth += 1 if char == "["
288
+ bracket_depth -= 1 if char == "]"
289
+ end
290
+ if char == delimiter && depth.zero? && bracket_depth.zero? && quote.nil?
291
+ parts << current.strip
292
+ current = +""
293
+ else
294
+ current << char
295
+ end
296
+ end
297
+ parts << current.strip unless current.strip.empty?
298
+ parts
299
+ end
300
+
301
+ def unquote(value)
302
+ value.sub(/\A["']/, "").sub(/["']\z/, "")
303
+ end
304
+
305
+ def cast(value)
306
+ return true if value == "true"
307
+ return false if value == "false"
308
+ return Take.integer(value) if value.to_s.match?(/\A-?\d+\z|last/)
309
+
310
+ value
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+
6
+ require_relative "markdown_composer/version"
7
+ require_relative "markdown_composer/value_object"
8
+ require_relative "markdown_composer/diagnostics"
9
+ require_relative "markdown_composer/registries"
10
+ require_relative "markdown_composer/source"
11
+ require_relative "markdown_composer/source_list_builder"
12
+ require_relative "markdown_composer/plan"
13
+ require_relative "markdown_composer/plan_builder"
14
+ require_relative "markdown_composer/result"
15
+ require_relative "markdown_composer/document_index"
16
+ require_relative "markdown_composer/take"
17
+ require_relative "markdown_composer/where"
18
+ require_relative "markdown_composer/data_path"
19
+ require_relative "markdown_composer/selection_resolver"
20
+ require_relative "markdown_composer/composition_buffer"
21
+ require_relative "markdown_composer/markdown_renderer"
22
+ require_relative "markdown_composer/transform_options"
23
+ require_relative "markdown_composer/validator"
24
+ require_relative "markdown_composer/executor"
25
+ require_relative "markdown_composer/transform_runner"
26
+ require_relative "markdown_composer/transforms/registry"
27
+ require_relative "markdown_composer/capabilities"
28
+
29
+ module MarkdownComposer
30
+ class << self
31
+ def compose(sources:, config: nil, plan: nil, options: {})
32
+ plan ||= config
33
+ raise ArgumentError, "provide config: or plan:" unless plan
34
+
35
+ plan = plan.is_a?(Plan) ? plan : Plan.new(plan)
36
+ Executor.new(sources: sources, plan: plan, options: options).call
37
+ end
38
+
39
+ def normalise(config_or_rows)
40
+ plan = if config_or_rows.is_a?(Plan)
41
+ config_or_rows
42
+ elsif config_or_rows.is_a?(Array)
43
+ Plan.from_rows(config_or_rows)
44
+ else
45
+ Plan.new(config_or_rows)
46
+ end
47
+ plan.to_h
48
+ end
49
+ alias normalize normalise
50
+
51
+ def validate(config:, sources: [], options: {})
52
+ plan = config.is_a?(Plan) ? config : Plan.new(config)
53
+ normalized_sources = Array(sources).map { |source| Source.build(source) }
54
+ diagnostics = Validator.new(plan, sources: normalized_sources, options: options).call
55
+ {
56
+ valid: !diagnostics.any_errors?,
57
+ plan: plan.to_h,
58
+ diagnostics: diagnostics.as_json,
59
+ errors: diagnostics.errors.map(&:to_h)
60
+ }
61
+ end
62
+
63
+ def capabilities(options: {})
64
+ Capabilities.build(options)
65
+ end
66
+
67
+ def plan(&block)
68
+ builder = PlanBuilder.new
69
+ builder.instance_eval(&block) if block
70
+ builder.to_plan
71
+ end
72
+
73
+ def source_list(&block)
74
+ builder = SourceListBuilder.new
75
+ builder.instance_eval(&block) if block
76
+ builder.to_a
77
+ end
78
+
79
+ def parse_yaml(yaml_string)
80
+ Plan.new(YAML.safe_load(yaml_string, permitted_classes: [ Symbol ], aliases: false) || {})
81
+ rescue Psych::SyntaxError => e
82
+ Plan.invalid("yaml.syntax", e.message)
83
+ end
84
+
85
+ def parse_json(json_string)
86
+ Plan.new(JSON.parse(json_string))
87
+ rescue JSON::ParserError => e
88
+ Plan.invalid("json.syntax", e.message)
89
+ end
90
+
91
+ def to_yaml(plan)
92
+ plan = Plan.new(plan) unless plan.is_a?(Plan)
93
+ YAML.dump(plan.to_h)
94
+ end
95
+
96
+ def to_json(plan, pretty: true)
97
+ plan = Plan.new(plan) unless plan.is_a?(Plan)
98
+ pretty ? JSON.pretty_generate(plan.to_h) : JSON.generate(plan.to_h)
99
+ end
100
+
101
+ def parse_rows(row_hashes)
102
+ Plan.from_rows(row_hashes)
103
+ end
104
+
105
+ def to_rows(plan)
106
+ plan = Plan.new(plan) unless plan.is_a?(Plan)
107
+ plan.steps
108
+ end
109
+
110
+ def registries
111
+ Registries.default
112
+ end
113
+ end
114
+ end
metadata ADDED
@@ -0,0 +1,260 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: markdown_composer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.7.0
5
+ platform: ruby
6
+ authors:
7
+ - SocIt2Em
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: commonmarker
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.23.10
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 0.23.10
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '3'
32
+ - !ruby/object:Gem::Dependency
33
+ name: nokogiri
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 1.13.10
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '2'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 1.13.10
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '2'
52
+ - !ruby/object:Gem::Dependency
53
+ name: benchmark
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0.4'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0.4'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '1'
72
+ - !ruby/object:Gem::Dependency
73
+ name: minitest
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '5.15'
79
+ - - "<"
80
+ - !ruby/object:Gem::Version
81
+ version: '6'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '5.15'
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '6'
92
+ - !ruby/object:Gem::Dependency
93
+ name: rake
94
+ requirement: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '13.0'
99
+ - - "<"
100
+ - !ruby/object:Gem::Version
101
+ version: '14'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '13.0'
109
+ - - "<"
110
+ - !ruby/object:Gem::Version
111
+ version: '14'
112
+ description: A pure Ruby, headless composition engine for Markdown-first workflows
113
+ with best-effort HTML input and HTML output support.
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - CHANGELOG.md
119
+ - LICENSE.txt
120
+ - README.md
121
+ - ROADMAP.md
122
+ - docs/_md_composer_architecture.md
123
+ - docs/_md_composer_cheatsheet.md
124
+ - docs/_md_composer_concepts.md
125
+ - docs/_md_composer_dev_guide.md
126
+ - docs/_md_composer_getting_started.md
127
+ - docs/_md_composer_readme.md
128
+ - docs/_md_composer_user_guide.md
129
+ - docs/ai/md_composer_ai_audit.md
130
+ - docs/ai/md_composer_ai_canonical_docs.md
131
+ - docs/ai/md_composer_ai_source_map.md
132
+ - docs/compose/md_composer_compose_actions.md
133
+ - docs/compose/md_composer_compose_anatomy.md
134
+ - docs/compose/md_composer_compose_buffer.md
135
+ - docs/compose/md_composer_compose_examples.md
136
+ - docs/compose/md_composer_compose_include.md
137
+ - docs/compose/md_composer_compose_select.md
138
+ - docs/compose/md_composer_compose_sources.md
139
+ - docs/compose/md_composer_compose_targets.md
140
+ - docs/examples/md_composer_example_basic_compose.md
141
+ - docs/examples/md_composer_example_buffer_target_actions.md
142
+ - docs/examples/md_composer_example_fixtures.md
143
+ - docs/examples/md_composer_example_html_output.md
144
+ - docs/examples/md_composer_example_modify.md
145
+ - docs/examples/md_composer_example_multi_row_compose.md
146
+ - docs/examples/md_composer_example_ruby_plans.md
147
+ - docs/examples/md_composer_example_structured_data.md
148
+ - docs/examples/md_composer_example_transforms.md
149
+ - docs/examples/md_composer_example_yaml_json_rows.md
150
+ - docs/examples/md_composer_examples_readme.md
151
+ - docs/examples/md_composer_runnable_examples.md
152
+ - docs/examples/md_composer_source_ruby_dsl.md
153
+ - docs/reference/md_composer_nested.md
154
+ - docs/reference/md_composer_reference_api.md
155
+ - docs/reference/md_composer_reference_capabilities.md
156
+ - docs/reference/md_composer_reference_diagnostics.md
157
+ - docs/reference/md_composer_reference_plan_schema.md
158
+ - docs/reference/md_composer_reference_registries.md
159
+ - docs/reference/md_composer_take.md
160
+ - docs/reference/md_composer_unit_tokens.md
161
+ - docs/reference/md_composer_where.md
162
+ - docs/transform/md_composer_transform_anatomy.md
163
+ - docs/transform/md_composer_transform_examples.md
164
+ - docs/transform/md_composer_transform_modes.md
165
+ - docs/transform/md_composer_transform_options.md
166
+ - docs/transform/md_composer_transform_scope.md
167
+ - docs/transform/md_composer_transform_transforms.md
168
+ - examples/README.md
169
+ - examples/advanced_composer.rb
170
+ - examples/basic_compose.rb
171
+ - examples/complex_composer.rb
172
+ - examples/example_support.rb
173
+ - examples/fixtures/current.md
174
+ - examples/fixtures/faq.md
175
+ - examples/fixtures/guide.md
176
+ - examples/fixtures/site_intro.md
177
+ - examples/fixtures/source.html
178
+ - examples/html_input.rb
179
+ - examples/output/advanced_composer.md
180
+ - examples/output/basic_compose.md
181
+ - examples/output/complex_composer.md
182
+ - examples/output/html_input.md
183
+ - examples/output/source_list_dsl.md
184
+ - examples/output/standard_composer.md
185
+ - examples/output/standard_sources_buffer.md
186
+ - examples/output/yaml_plan.md
187
+ - examples/plans/basic.yml
188
+ - examples/source_list_dsl.rb
189
+ - examples/standard_composer.rb
190
+ - examples/standard_sources_buffer.rb
191
+ - examples/yaml_plan.rb
192
+ - lib/markdown_composer.rb
193
+ - lib/markdown_composer/capabilities.rb
194
+ - lib/markdown_composer/composition_buffer.rb
195
+ - lib/markdown_composer/data_path.rb
196
+ - lib/markdown_composer/diagnostics.rb
197
+ - lib/markdown_composer/document_index.rb
198
+ - lib/markdown_composer/document_index/html_parser.rb
199
+ - lib/markdown_composer/document_index/markdown_parser.rb
200
+ - lib/markdown_composer/executor.rb
201
+ - lib/markdown_composer/markdown_renderer.rb
202
+ - lib/markdown_composer/plan.rb
203
+ - lib/markdown_composer/plan_builder.rb
204
+ - lib/markdown_composer/registries.rb
205
+ - lib/markdown_composer/registries/action_entries.rb
206
+ - lib/markdown_composer/registries/condition_entries.rb
207
+ - lib/markdown_composer/registries/registry.rb
208
+ - lib/markdown_composer/registries/source_entries.rb
209
+ - lib/markdown_composer/registries/support_values.rb
210
+ - lib/markdown_composer/registries/take_entries.rb
211
+ - lib/markdown_composer/registries/take_registry.rb
212
+ - lib/markdown_composer/registries/target_entries.rb
213
+ - lib/markdown_composer/registries/unit_token_entries.rb
214
+ - lib/markdown_composer/registries/where_registry.rb
215
+ - lib/markdown_composer/result.rb
216
+ - lib/markdown_composer/selection_resolver.rb
217
+ - lib/markdown_composer/source.rb
218
+ - lib/markdown_composer/source_list_builder.rb
219
+ - lib/markdown_composer/take.rb
220
+ - lib/markdown_composer/transform_options.rb
221
+ - lib/markdown_composer/transform_runner.rb
222
+ - lib/markdown_composer/transform_runner/content_placement.rb
223
+ - lib/markdown_composer/transform_runner/field_interpolator.rb
224
+ - lib/markdown_composer/transform_runner/heading_numbering.rb
225
+ - lib/markdown_composer/transform_runner/scope_resolver.rb
226
+ - lib/markdown_composer/transforms/default_entries.rb
227
+ - lib/markdown_composer/transforms/registry.rb
228
+ - lib/markdown_composer/validator.rb
229
+ - lib/markdown_composer/value_object.rb
230
+ - lib/markdown_composer/version.rb
231
+ - lib/markdown_composer/where.rb
232
+ homepage: https://github.com/SocIt2Em/markdown_composer
233
+ licenses:
234
+ - MIT
235
+ metadata:
236
+ source_code_uri: https://github.com/SocIt2Em/markdown_composer
237
+ documentation_uri: https://github.com/SocIt2Em/markdown_composer#readme
238
+ changelog_uri: https://github.com/SocIt2Em/markdown_composer/blob/main/CHANGELOG.md
239
+ bug_tracker_uri: https://github.com/SocIt2Em/markdown_composer/issues
240
+ github_repo: ssh://github.com/SocIt2Em/markdown_composer
241
+ allowed_push_host: https://rubygems.org
242
+ rubygems_mfa_required: 'true'
243
+ rdoc_options: []
244
+ require_paths:
245
+ - lib
246
+ required_ruby_version: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '3.1'
251
+ required_rubygems_version: !ruby/object:Gem::Requirement
252
+ requirements:
253
+ - - ">="
254
+ - !ruby/object:Gem::Version
255
+ version: '0'
256
+ requirements: []
257
+ rubygems_version: 4.0.7
258
+ specification_version: 4
259
+ summary: Headless Markdown composer for selecting and transforming document fragments
260
+ test_files: []