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,436 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarkdownComposer
4
+ class Plan
5
+ attr_reader :config, :diagnostics
6
+
7
+ def self.invalid(code, message)
8
+ plan = new({})
9
+ plan.diagnostics.error(code, message)
10
+ plan
11
+ end
12
+
13
+ def self.from_rows(row_hashes)
14
+ new("compose" => Array(row_hashes).map { |row| normalize_hash(row) })
15
+ end
16
+
17
+ def self.normalize_hash(value)
18
+ value.to_h.transform_keys { |key| key.to_s.downcase.tr(" ", "_") }
19
+ end
20
+
21
+ def initialize(config)
22
+ @diagnostics = Diagnostics.new
23
+ @config = normalize(config || {})
24
+ end
25
+
26
+ def version
27
+ config.fetch("version", 1)
28
+ end
29
+
30
+ def output
31
+ config.fetch("output", "markdown").to_s
32
+ end
33
+
34
+ def steps
35
+ Array(compose_config).map { |step| normalize_step(step) }
36
+ end
37
+
38
+ def transforms
39
+ Array(transform_config).map.with_index { |transform, index| normalize_transform(transform, path: "transform[#{index}]") }
40
+ end
41
+
42
+ def to_h
43
+ canonical = config.reject { |key, _value| %w[steps transforms compose transform].include?(key) }
44
+ canonical.merge("compose" => steps, "transform" => transforms, "output" => output)
45
+ end
46
+
47
+ private
48
+
49
+ def compose_config
50
+ config.key?("compose") ? config["compose"] : config["steps"]
51
+ end
52
+
53
+ def transform_config
54
+ config.key?("transform") ? config["transform"] : config["transforms"]
55
+ end
56
+
57
+ def normalize(value)
58
+ case value
59
+ when Hash
60
+ value.each_with_object({}) { |(key, child), hash| hash[key.to_s] = normalize(child) }
61
+ when Array
62
+ value.map { |child| normalize(child) }
63
+ else
64
+ value
65
+ end
66
+ end
67
+
68
+ def normalize_step(step)
69
+ step = normalize(step)
70
+ step["source"] = normalize_source_ref(step["source"])
71
+ step["select"] = normalize_selector(step["select"] || step["selector"] || { "type" => "all" }, :select)
72
+ step["include"] = normalize_include(step["include"])
73
+ step["action"] = normalize_action(step["action"] || "set")
74
+ step["target"] = normalize_target(step["target"]) if step.key?("target")
75
+ step["transforms"] = normalize_row_transforms(step["transforms"]) if step.key?("transforms")
76
+ step
77
+ end
78
+
79
+ def normalize_transform(transform, path: nil)
80
+ transform = normalize(transform)
81
+ transform["scope"] = normalize_selector(transform["scope"] || { "type" => "output" }, :scope)
82
+ transform["transform"] = normalize_token(transform["transform"], :transform)
83
+ transform["options"] = normalize_options(transform["options"] || {}, path: path && "#{path}.options")
84
+ transform
85
+ end
86
+
87
+ def normalize_source_ref(source)
88
+ source = { "type" => "current" } if source.nil?
89
+ source = { "type" => source } if source.is_a?(String) || source.is_a?(Symbol)
90
+ source = normalize(source)
91
+ type = Registries.default.sources.normalise(source["type"] || "current") || source["type"].to_s
92
+ source.merge("type" => type)
93
+ end
94
+
95
+ def normalize_action(action)
96
+ Registries.default.actions.normalise(action.to_s.tr(" ", "_").downcase) || action.to_s
97
+ end
98
+
99
+ def normalize_token(token, consumer)
100
+ registry = consumer == :transform ? Registries.default.transforms : Registries.default.unit_tokens
101
+ registry.normalise(token.to_s) || token.to_s
102
+ end
103
+
104
+ def normalize_selector(selector, consumer)
105
+ return parse_selector(selector, consumer) if selector.is_a?(String)
106
+
107
+ selector = normalize(selector || {})
108
+ if selector["types"]
109
+ selector["types"] = Array(selector["types"]).map { |type| normalize_token(type, consumer) }
110
+ else
111
+ selector["type"] = normalize_token(selector["type"] || "all", consumer)
112
+ end
113
+ selector["take"] = normalize_take(selector["take"]) if selector.key?("take")
114
+ selector["where"] = normalize_where(selector["where"]) if selector.key?("where")
115
+ selector["include"] = normalize_include(selector["include"]) if selector.key?("include")
116
+ selector
117
+ end
118
+
119
+ def normalize_include(include_config)
120
+ include_config = [ { "type" => "all" } ] if include_config.nil? || include_config == ""
121
+ include_config = [ include_config ] unless include_config.is_a?(Array)
122
+ include_config.flat_map { |item| normalize_include_item(item) }
123
+ end
124
+
125
+ def normalize_include_item(item)
126
+ return parse_data_path(item) if item.is_a?(String) && data_path_syntax?(item)
127
+ return parse_include(item) if item.is_a?(String)
128
+
129
+ item = normalize(item)
130
+ if item.key?("exclude")
131
+ excluded = normalize_include_item(item["exclude"]).first
132
+ return [ { "exclude" => excluded } ]
133
+ end
134
+
135
+ item["type"] = normalize_token(item["type"] || "all", :include)
136
+ item["take"] = normalize_take(item["take"]) if item.key?("take")
137
+ item["where"] = normalize_where(item["where"]) if item.key?("where")
138
+ item["include"] = normalize_include(item["include"]) if item.key?("include")
139
+ [ item ]
140
+ end
141
+
142
+ def normalize_target(target)
143
+ return parse_target(target) if target.is_a?(String)
144
+
145
+ target = normalize(target || {})
146
+ target["position"] = normalize_target_position(target["position"]) if target["position"]
147
+ if target["type"].to_s.match?(/\Ain[_ ]place\z/i)
148
+ target["position"] = normalize_target_position(target.delete("type"))
149
+ end
150
+ target["type"] = normalize_token(target["type"], :target) if target["type"]
151
+ target["take"] = normalize_take(target["take"]) if target.key?("take")
152
+ target["where"] = normalize_where(target["where"]) if target.key?("where")
153
+ target["start"] = normalize_target(target["start"]) if target["start"]
154
+ target["end"] = normalize_target(target["end"]) if target["end"]
155
+ target
156
+ end
157
+
158
+ def normalize_options(options, path: nil)
159
+ options = parse_options(options, path: path) if options.is_a?(String)
160
+ options = normalize(options)
161
+ unless options.respond_to?(:transform_values)
162
+ diagnostics.error(
163
+ "transform.options_syntax",
164
+ "Options must be a readable options string or object",
165
+ path: path
166
+ )
167
+ return {}
168
+ end
169
+
170
+ options.transform_values do |value|
171
+ case value
172
+ when Array
173
+ value.map { |item| normalize_option_value(item) }
174
+ else
175
+ normalize_option_value(value)
176
+ end
177
+ end
178
+ .tap do |hash|
179
+ hash["target"] = normalize_target(hash["target"]) if hash["target"].is_a?(String) || hash["target"].is_a?(Hash)
180
+ end
181
+ end
182
+
183
+ def parse_options(string, path: nil)
184
+ split_option_parts(string, ";", path: path).each_with_object({}) do |part, hash|
185
+ key, value = split_option_pair(part)
186
+ hash[key] = parse_option_value(key, value) unless key.empty?
187
+ end
188
+ end
189
+
190
+ def split_option_parts(text, separator, path: nil)
191
+ parts = []
192
+ current = +""
193
+ depth = 0
194
+ bracket_depth = 0
195
+ brace_depth = 0
196
+ quote = nil
197
+ chars = text.to_s.each_char.to_a
198
+
199
+ chars.each do |char|
200
+ quote = next_quote_state(quote, char)
201
+ if quote.nil?
202
+ depth += 1 if char == "("
203
+ depth -= 1 if char == ")"
204
+ bracket_depth += 1 if char == "["
205
+ bracket_depth -= 1 if char == "]"
206
+ brace_depth += 1 if char == "{"
207
+ brace_depth -= 1 if char == "}"
208
+ end
209
+
210
+ if char == separator && quote.nil? && depth.zero? && bracket_depth.zero? && brace_depth.zero?
211
+ parts << current.strip unless current.strip.empty?
212
+ current = +""
213
+ else
214
+ current << char
215
+ end
216
+ end
217
+
218
+ diagnostics.error("transform.options_syntax", "Missing closing #{quote} quote in options", path: path) if quote
219
+ parts << current.strip unless current.strip.empty?
220
+ parts
221
+ end
222
+
223
+ def split_option_pair(part)
224
+ text = part.to_s.strip
225
+ index = top_level_separator_index(text, ":")
226
+ return [ text.downcase.tr(" ", "_"), true ] unless index
227
+
228
+ [
229
+ text[0...index].strip.downcase.tr(" ", "_"),
230
+ text[(index + 1)..].to_s.strip
231
+ ]
232
+ end
233
+
234
+ def parse_option_value(key, value)
235
+ text = value.to_s.strip
236
+ return "" if text.empty?
237
+ return unquote(text) if quoted?(text)
238
+ return true if text == "true"
239
+ return false if text == "false"
240
+ return nil if text == "null" || text == "nil"
241
+ return text.to_i if text.match?(/\A-?\d+\z/)
242
+
243
+ if key == "levels" && text.include?(",")
244
+ return split_include_parts(text, ",").map { |item| parse_option_value(key, item) }
245
+ end
246
+
247
+ text
248
+ end
249
+
250
+ def top_level_separator_index(text, separator)
251
+ depth = 0
252
+ bracket_depth = 0
253
+ brace_depth = 0
254
+ quote = nil
255
+ text.to_s.each_char.with_index do |char, index|
256
+ quote = next_quote_state(quote, char)
257
+ if quote.nil?
258
+ depth += 1 if char == "("
259
+ depth -= 1 if char == ")"
260
+ bracket_depth += 1 if char == "["
261
+ bracket_depth -= 1 if char == "]"
262
+ brace_depth += 1 if char == "{"
263
+ brace_depth -= 1 if char == "}"
264
+ end
265
+
266
+ return index if char == separator && depth.zero? && bracket_depth.zero? && brace_depth.zero? && quote.nil?
267
+ end
268
+ nil
269
+ end
270
+
271
+ def quoted?(value)
272
+ text = value.to_s
273
+ (text.start_with?('"') && text.end_with?('"')) || (text.start_with?("'") && text.end_with?("'"))
274
+ end
275
+
276
+ def unquote(value)
277
+ value.to_s[1..-2]
278
+ end
279
+
280
+ def normalize_option_value(value)
281
+ token = Registries.default.unit_tokens.normalise(value.to_s)
282
+ token || value
283
+ end
284
+
285
+ def normalize_take(take)
286
+ return Take.parse(take) if take.is_a?(String) || take.is_a?(Symbol)
287
+
288
+ take = normalize(take || {})
289
+ take["position"] = Array(take["position"]).map(&:to_i) if take.key?("position")
290
+ take
291
+ end
292
+
293
+ def normalize_where(where)
294
+ Where.normalize(where)
295
+ end
296
+
297
+ def parse_selector(string, consumer)
298
+ text = string.strip
299
+ nested = nil
300
+ if text =~ /\{(.+)\}\s*\z/
301
+ nested = Regexp.last_match(1)
302
+ text = text.sub(/\{.+\}\s*\z/, "").strip
303
+ end
304
+
305
+ where = nil
306
+ if text =~ /\s+where\s+(.+)\z/i
307
+ where = Regexp.last_match(1).strip
308
+ text = text.sub(/\s+where\s+.+\z/i, "").strip
309
+ end
310
+
311
+ take = nil
312
+ if text =~ /\[(.+)\]\s*\z/
313
+ take = Regexp.last_match(1)
314
+ text = text.sub(/\[(.+)\]\s*\z/, "").strip
315
+ end
316
+
317
+ if text.include?(",")
318
+ selector = { "types" => text.split(",").map { |part| normalize_token(part.strip, consumer) } }
319
+ else
320
+ selector = { "type" => normalize_token(text, consumer) }
321
+ end
322
+ selector["take"] = normalize_take(take) if take
323
+ selector["where"] = normalize_where(where) if where
324
+ selector["include"] = parse_include(nested, separator: ";") if nested
325
+ selector
326
+ end
327
+
328
+ def parse_include(string, separator: ",")
329
+ parts = split_include_parts(string, separator)
330
+ parts.flat_map do |part|
331
+ if data_path_syntax?(part)
332
+ [ parse_data_path(part) ]
333
+ elsif part =~ /\A(.+)\s+except\s+(.+)\z/i
334
+ [ normalize_selector(Regexp.last_match(1), :include), { "exclude" => normalize_selector(Regexp.last_match(2), :include) } ]
335
+ else
336
+ [ normalize_selector(part, :include) ]
337
+ end
338
+ end
339
+ end
340
+
341
+ def parse_target(string)
342
+ text = string.strip
343
+ return { "position" => "output" } if text.match?(/\A(?:whole[_ ]output|output)\z/i)
344
+ return { "position" => "start" } if text.match?(/\Astart\z/i)
345
+ return { "position" => "end" } if text.match?(/\Aend\z/i)
346
+ return { "position" => "in_place" } if text.match?(/\Ain[_ ]place\z/i)
347
+
348
+ if text =~ /\Abefore\s+(.+)\z/i
349
+ return parse_selector(Regexp.last_match(1), :target).merge("placement" => "before")
350
+ end
351
+ if text =~ /\Aafter\s+(.+)\z/i
352
+ return parse_selector(Regexp.last_match(1), :target).merge("placement" => "after")
353
+ end
354
+ if text =~ /\Abetween\s+(.+)\s+and\s+(.+)\z/i
355
+ return {
356
+ "placement" => "between",
357
+ "start" => parse_selector(Regexp.last_match(1), :target),
358
+ "end" => parse_selector(Regexp.last_match(2), :target)
359
+ }
360
+ end
361
+
362
+ parse_selector(text, :target)
363
+ end
364
+
365
+ def normalize_target_position(position)
366
+ text = position.to_s.strip.downcase.tr(" ", "_")
367
+ return "output" if text == "whole_output"
368
+ return "in_place" if text == "in_place"
369
+
370
+ text
371
+ end
372
+
373
+ def normalize_row_transforms(transforms)
374
+ transforms = normalize(transforms)
375
+ items = transforms.is_a?(Array) ? transforms : [ transforms ]
376
+ items.map { |transform| normalize_row_transform(transform) }
377
+ end
378
+
379
+ def normalize_row_transform(transform)
380
+ raw_transform = normalize(transform)
381
+ scope_missing = !raw_transform.key?("scope")
382
+ normalized = normalize_transform(raw_transform)
383
+ normalized["_scope_missing"] = true if scope_missing
384
+ normalized
385
+ end
386
+
387
+ def data_path_syntax?(text)
388
+ text.to_s.strip.match?(/\Adata_path\((["']).*\1\)\z/)
389
+ end
390
+
391
+ def parse_data_path(text)
392
+ if text.to_s.strip =~ /\Adata_path\((["'])(.*)\1\)\z/
393
+ { "type" => "data_path", "path" => Regexp.last_match(2) }
394
+ else
395
+ { "type" => "data_path", "path" => "" }
396
+ end
397
+ end
398
+
399
+ def split_include_parts(text, separator)
400
+ depth = 0
401
+ bracket_depth = 0
402
+ brace_depth = 0
403
+ quote = nil
404
+ current = +""
405
+ parts = []
406
+ text.to_s.each_char do |char|
407
+ quote = next_quote_state(quote, char)
408
+ if quote.nil?
409
+ depth += 1 if char == "("
410
+ depth -= 1 if char == ")"
411
+ bracket_depth += 1 if char == "["
412
+ bracket_depth -= 1 if char == "]"
413
+ brace_depth += 1 if char == "{"
414
+ brace_depth -= 1 if char == "}"
415
+ end
416
+
417
+ if char == separator && depth.zero? && bracket_depth.zero? && brace_depth.zero? && quote.nil?
418
+ parts << current.strip unless current.strip.empty?
419
+ current = +""
420
+ else
421
+ current << char
422
+ end
423
+ end
424
+ parts << current.strip unless current.strip.empty?
425
+ parts
426
+ end
427
+
428
+ def next_quote_state(quote, char)
429
+ return quote unless %w[" '].include?(char)
430
+ return nil if quote == char
431
+ return char if quote.nil?
432
+
433
+ quote
434
+ end
435
+ end
436
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarkdownComposer
4
+ class PlanBuilder
5
+ def initialize
6
+ @rows = []
7
+ @transforms = []
8
+ @current = {}
9
+ @output = nil
10
+ end
11
+
12
+ def from(source)
13
+ @current["source"] = source
14
+ end
15
+
16
+ def select(selector)
17
+ @current["select"] = selector
18
+ end
19
+
20
+ def include(*items)
21
+ existing = Array(@current["include"])
22
+ @current["include"] = existing.concat(items.flatten).compact
23
+ end
24
+
25
+ def target(target)
26
+ @current["target"] = target
27
+ end
28
+
29
+ def output(format)
30
+ @output = format.to_s
31
+ end
32
+
33
+ def set(target = nil)
34
+ finish("set", target)
35
+ end
36
+
37
+ def append(target = nil)
38
+ finish("append", target)
39
+ end
40
+
41
+ def prepend(target = nil)
42
+ finish("prepend", target)
43
+ end
44
+
45
+ def insert_before(target)
46
+ finish("insert_before", target)
47
+ end
48
+
49
+ def insert_after(target)
50
+ finish("insert_after", target)
51
+ end
52
+
53
+ def insert_between(target)
54
+ finish("insert_between", target)
55
+ end
56
+
57
+ def replace(target)
58
+ finish("replace", target)
59
+ end
60
+
61
+ def copy(target)
62
+ finish("copy", target)
63
+ end
64
+
65
+ def move(target)
66
+ finish("move", target)
67
+ end
68
+
69
+ def modify(target = nil, transforms: nil)
70
+ @current["transforms"] = Array(transforms) if transforms
71
+ finish("modify", target)
72
+ end
73
+
74
+ def remove_buffer_target(target = nil)
75
+ target ||= @current["select"]
76
+ finish("remove_buffer_target", target)
77
+ end
78
+
79
+ def transform_buffer_target(target = nil, transforms: nil)
80
+ @current["transforms"] = Array(transforms) if transforms
81
+ finish("transform_buffer_target", target)
82
+ end
83
+
84
+ def transform(scope, transform, mode, options = {})
85
+ @transforms << {
86
+ "scope" => scope,
87
+ "transform" => transform,
88
+ "mode" => mode,
89
+ "options" => options
90
+ }
91
+ end
92
+
93
+ def to_plan
94
+ config = { "compose" => @rows }
95
+ config["transform"] = @transforms if @transforms.any?
96
+ config["output"] = @output if @output
97
+ Plan.new(config)
98
+ end
99
+
100
+ private
101
+
102
+ def finish(action, target)
103
+ row = @current.dup
104
+ row["action"] = action
105
+ row["target"] = target if target
106
+ row["include"] = "all" if action == "remove_buffer_target" && !row.key?("include")
107
+ @rows << row
108
+ @current = {}
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarkdownComposer
4
+ module ActionRegistryEntries
5
+ private
6
+
7
+ def action_entries
8
+ {
9
+ set: [ "Set", "Set the output buffer to this content.", "Set (action) the output buffer from [Source] (source) using [Select] (select), including [Include] (include).", :default, "output", true, :normal ],
10
+ append: [ "Append", "Append this content to the output buffer.", "Append (action) to the output buffer from [Source] (source) using [Select] (select), including [Include] (include).", :default, "end", true, :normal ],
11
+ prepend: [ "Prepend", "Prepend this content to the output buffer.", "Prepend (action) to the output buffer from [Source] (source) using [Select] (select), including [Include] (include).", :default, "start", true, :normal ],
12
+ insert_before: [ "Insert Before", "Insert this content before a target in the output buffer.", "Insert before (action) [Target] (target) from [Source] (source) using [Select] (select), including [Include] (include).", :required, nil, true, :normal ],
13
+ insert_after: [ "Insert After", "Insert this content after a target in the output buffer.", "Insert after (action) [Target] (target) from [Source] (source) using [Select] (select), including [Include] (include).", :required, nil, true, :normal ],
14
+ insert_between: [ "Insert Between", "Insert this content between two targets in the output buffer.", "Insert between (action) [Target] (target) from [Source] (source) using [Select] (select), including [Include] (include).", :required, nil, true, :advanced ],
15
+ replace: [ "Replace", "Replace target content in the output buffer with this content.", "Replace (action) [Target] (target) in the output buffer with content from [Source] (source) using [Select] (select), including [Include] (include).", :required, nil, true, :normal ],
16
+ copy: [ "Copy", "Copy this content to a target in the output buffer.", "Copy (action) content from [Source] (source) to [Target] (target) using [Select] (select), including [Include] (include).", :required, nil, true, :advanced ],
17
+ move: [ "Move", "Move selected content within the output buffer.", "Move (action) content within the output buffer using [Select] (select), including [Include] (include), to [Target] (target).", :required, nil, true, :advanced ],
18
+ modify: [ "Modify", "Transform selected source content, then place the transformed fragment in the output buffer.", "Modify (action) content from [Source] (source) using [Select] (select), including [Include] (include), transform it with [Transform] (transform), [Mode] (mode), using [Options] (options), then place it at [Target] (target).", :default, "end", true, :normal ],
19
+ remove_buffer_target: [ "Remove Buffer Target", "Remove target content from the output buffer.", "Remove buffer target (action) [Target] (target) from the output buffer.", :required, nil, false, :normal ],
20
+ transform_buffer_target: [ "Transform Buffer Target", "Transform target content in the output buffer.", "Transform buffer target (action) [Target] (target) in the output buffer with [Transform] (transform), [Mode] (mode), using [Options] (options).", :required, nil, false, :advanced ]
21
+ }.map do |token, (label, tooltip, row_sentence, target_rule, default_target, uses_selected_content, support)|
22
+ RegistryEntry.new(token: token.to_s, aliases: [ label.downcase.tr(" ", "_") ], label: label, tooltip: tooltip, meaning: row_sentence, row_sentence: row_sentence, support: { action: support, target_rule: target_rule, default_target: default_target, uses_selected_content: uses_selected_content }, source_formats: [], condition_fields: [])
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarkdownComposer
4
+ module WhereRegistryEntries
5
+ private
6
+
7
+ def condition_field_entries
8
+ @condition_field_entries ||= [
9
+ [ "title", "Title", "Match heading title text.", %w[contains starts_with ends_with equals matches], :text, true ],
10
+ [ "text", "Text", "Match visible text content.", %w[contains starts_with ends_with equals matches], :text, true ],
11
+ [ "source_text", "Source Text", "Match retained source representation.", %w[contains starts_with ends_with equals matches], :text, true ],
12
+ [ "position", "Position", "Match by 1-based position or inclusive range.", %w[equals range], :ordered, false ],
13
+ [ "language", "Code Language", "Match fenced code language.", %w[equals], :code, false ],
14
+ [ "diagram_type", "Diagram Type", "Match normalised Mermaid diagram type.", %w[equals], :mermaid, false ],
15
+ [ "format", "Data Format", "Match structured data format.", %w[equals], :data, false ],
16
+ [ "location", "Data Location", "Match where structured data came from.", %w[equals], :data, false ],
17
+ [ "links", "Has Links", "Match units containing links.", %w[exists], :nested, false ],
18
+ [ "images", "Has Images", "Match units containing images.", %w[exists], :nested, false ],
19
+ [ "code", "Has Code", "Match units containing code.", %w[exists], :nested, false ],
20
+ [ "numbers", "Has Numbers", "Match text containing digits.", %w[exists], :text, false ],
21
+ [ "empty", "Empty", "Match empty or non-empty units.", %w[equals], :node, false ],
22
+ [ "length", "Text Length", "Match by character length.", %w[min max range], :text, false ],
23
+ [ "word_count", "Word Count", "Match by word count.", %w[min max range], :text, false ],
24
+ [ "child", "Has Child", "Match units with nested child sections.", %w[exists], :section, false ]
25
+ ].map do |token, label, tooltip, predicates, applies_to, field_take|
26
+ ConditionFieldRegistryEntry.new(token: token, aliases: [], label: label, tooltip: tooltip, meaning: tooltip, predicates: predicates, applies_to: applies_to, source_formats: %i[markdown html], field_take: field_take)
27
+ end
28
+ end
29
+
30
+ def predicate_entries
31
+ @predicate_entries ||= [
32
+ [ "contains", "Contains", "Match when the field includes this text.", :text, :normal ],
33
+ [ "starts_with", "Starts With", "Match when the field starts with this text.", :text, :normal ],
34
+ [ "ends_with", "Ends With", "Match when the field ends with this text.", :text, :normal ],
35
+ [ "equals", "Equals", "Match exact normalised value.", :scalar, :normal ],
36
+ [ "range", "Range", "Match values inside an inclusive range.", :range, :normal ],
37
+ [ "exists", "Exists", "Match when this content exists.", :none, :normal ],
38
+ [ "min", "Minimum", "Match values at or above this minimum.", :number, :normal ],
39
+ [ "max", "Maximum", "Match values at or below this maximum.", :number, :normal ],
40
+ [ "matches", "Matches Regex", "Match with a host-approved regular expression.", :regex, :advanced ]
41
+ ].map do |token, label, tooltip, value_type, support|
42
+ PredicateRegistryEntry.new(token: token, aliases: [], label: label, tooltip: tooltip, meaning: tooltip, value_type: value_type, support: { predicate: support })
43
+ end
44
+ end
45
+
46
+ def where_group_entries
47
+ @where_group_entries ||= [
48
+ [ "all", "All", "Every child condition must match.", :many ],
49
+ [ "any", "Any", "At least one child condition must match.", :many ],
50
+ [ "none", "None", "No child condition may match.", :many ],
51
+ [ "not", "Not", "Negate one condition or group.", :one ],
52
+ [ "xor", "Exactly One", "Exactly one child condition must match.", :many ]
53
+ ].map do |token, label, tooltip, arity|
54
+ WhereGroupRegistryEntry.new(token: token, aliases: [], label: label, tooltip: tooltip, meaning: tooltip, arity: arity)
55
+ end
56
+ end
57
+ end
58
+ end