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,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
|