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,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
RegistryEntry = MarkdownComposer.value_object(:token, :aliases, :label, :tooltip, :meaning, :row_sentence, :support, :source_formats, :condition_fields) do
|
|
5
|
+
def supports?(consumer, options = {})
|
|
6
|
+
value = support.fetch(consumer.to_sym, :disabled).to_sym
|
|
7
|
+
return true if %i[normal advanced output_only include_only].include?(value)
|
|
8
|
+
return options.fetch(:optional_tokens, false) if value == :optional
|
|
9
|
+
return options.fetch(:adapter_policy_tokens, false) if value == :adapter_policy
|
|
10
|
+
|
|
11
|
+
false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
{
|
|
16
|
+
token: token,
|
|
17
|
+
aliases: aliases,
|
|
18
|
+
label: label,
|
|
19
|
+
tooltip: tooltip,
|
|
20
|
+
meaning: meaning,
|
|
21
|
+
row_sentence: row_sentence,
|
|
22
|
+
support: support,
|
|
23
|
+
source_formats: source_formats,
|
|
24
|
+
condition_fields: condition_fields
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Registry
|
|
30
|
+
attr_reader :entries
|
|
31
|
+
|
|
32
|
+
def initialize(entries)
|
|
33
|
+
@entries = entries
|
|
34
|
+
@by_token = entries.to_h { |entry| [ entry.token, entry ] }
|
|
35
|
+
@aliases = entries.each_with_object({}) do |entry, map|
|
|
36
|
+
entry.aliases.each { |alias_token| map[alias_token] = entry.token }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def [](token)
|
|
41
|
+
@by_token[normalise(token)]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def key?(token)
|
|
45
|
+
!self[token].nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fetch(token, default = nil)
|
|
49
|
+
self[token] || default
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalise(token)
|
|
53
|
+
value = token.to_s
|
|
54
|
+
@by_token.key?(value) ? value : @aliases[value]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def keys
|
|
58
|
+
tokens
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def tokens
|
|
62
|
+
@by_token.keys
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_a
|
|
66
|
+
@entries.map(&:to_h)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
module SourceRegistryEntries
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def source_entries
|
|
8
|
+
[
|
|
9
|
+
[ "current", [], "Current Source", "Use the main source attached to this composer context." ],
|
|
10
|
+
[ "explicit", [ "md_doc" ], "Explicit Source", "Use a selected source resolved by the host." ],
|
|
11
|
+
[ "inherited", [ "effective_md_doc", "block_md_doc", "region_md_doc", "page_md_doc" ], "Inherited Source", "Use a host-resolved inherited source." ],
|
|
12
|
+
[ "previous", [ "previous_source", "same_source" ], "Previous Source", "Use the effective source reference from the previous Composer row." ],
|
|
13
|
+
[ "buffer", [ "output", "current_output", "composed_output" ], "Previous Output", "Use the current composition buffer." ],
|
|
14
|
+
[ "inline", [], "Inline Source", "Use inline content stored in this config." ]
|
|
15
|
+
].map { |token, aliases, label, tooltip| RegistryEntry.new(token: token, aliases: aliases, label: label, tooltip: tooltip, meaning: label, row_sentence: nil, support: { source: :normal }, source_formats: %i[markdown html], condition_fields: []) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
module RegistrySupportValues
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def all_normal
|
|
8
|
+
{ select: :normal, include: :normal, target: :normal, scope: :normal }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def all_advanced
|
|
12
|
+
{ select: :advanced, include: :advanced, target: :advanced, scope: :advanced }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all_optional
|
|
16
|
+
{ select: :optional, include: :optional, target: :optional, scope: :optional }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def all_adapter_policy
|
|
20
|
+
{ select: :adapter_policy, include: :adapter_policy, target: :adapter_policy, scope: :adapter_policy }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
module TakeRegistryEntries
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def take_entries
|
|
8
|
+
@take_entries ||= [
|
|
9
|
+
[ "all", [], "All", "Keep every matching unit.", "Keep all matches.", :normal, :none, true ],
|
|
10
|
+
[ "first", [], "First", "Keep the first N matches.", "Keep first matches.", :normal, :positive_integer, true ],
|
|
11
|
+
[ "last", [], "Last", "Keep the last N matches.", "Keep last matches.", :normal, :positive_integer, true ],
|
|
12
|
+
[ "position", [], "Position", "Keep one or more 1-based positions.", "Keep selected positions.", :normal, :integer_list, true ],
|
|
13
|
+
[ "ranges", [ "range" ], "Range", "Keep one or more inclusive ranges.", "Keep selected ranges.", :normal, :range_list, true ],
|
|
14
|
+
[ "skip", [], "Skip First", "Ignore the first N matches.", "Skip first matches.", :normal, :positive_integer, true ],
|
|
15
|
+
[ "skip_last", [], "Skip Last", "Ignore the last N matches.", "Skip last matches.", :normal, :positive_integer, true ],
|
|
16
|
+
[ "every", [], "Every Nth", "Keep every Nth match.", "Keep periodic matches.", :normal, :positive_integer, true ],
|
|
17
|
+
[ "odd", [], "Odd", "Keep odd 1-based positions.", "Keep odd positions.", :normal, :none, true ],
|
|
18
|
+
[ "even", [], "Even", "Keep even 1-based positions.", "Keep even positions.", :normal, :none, true ],
|
|
19
|
+
[ "except", [], "Except", "Keep all except selected positions.", "Exclude selected positions.", :normal, :integer_list, true ],
|
|
20
|
+
[ "top_percent", [], "Top Percent", "Keep a percentage from the start.", "Keep top percentage.", :advanced, :percent, true ],
|
|
21
|
+
[ "bottom_percent", [], "Bottom Percent", "Keep a percentage from the end.", "Keep bottom percentage.", :advanced, :percent, true ],
|
|
22
|
+
[ "middle", [], "Middle", "Keep N matches from the middle.", "Keep middle matches.", :advanced, :positive_integer, true ],
|
|
23
|
+
[ "middle_percent", [], "Middle Percent", "Keep a percentage from the middle.", "Keep middle percentage.", :advanced, :percent, true ],
|
|
24
|
+
[ "alternate", [], "Alternate", "Keep alternating groups of matches.", "Keep alternating groups.", :advanced, :positive_integer, true ],
|
|
25
|
+
[ "random", [], "Random", "Keep N seeded random matches.", "Keep seeded random matches.", :advanced, :positive_integer, true ]
|
|
26
|
+
].map do |token, aliases, label, tooltip, meaning, support, value_type, deterministic|
|
|
27
|
+
TakeRegistryEntry.new(token: token, aliases: aliases, label: label, tooltip: tooltip, meaning: meaning, support: { take: support }, value_type: value_type, deterministic: deterministic)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
TakeRegistryEntry = MarkdownComposer.value_object(:token, :aliases, :label, :tooltip, :meaning, :support, :value_type, :deterministic) do
|
|
5
|
+
def to_h
|
|
6
|
+
{
|
|
7
|
+
token: token,
|
|
8
|
+
aliases: aliases,
|
|
9
|
+
label: label,
|
|
10
|
+
tooltip: tooltip,
|
|
11
|
+
meaning: meaning,
|
|
12
|
+
support: support,
|
|
13
|
+
value_type: value_type,
|
|
14
|
+
deterministic: deterministic
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
module TargetRegistryEntries
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def target_entries
|
|
8
|
+
%w[output start end selector before after between in_place].map do |token|
|
|
9
|
+
RegistryEntry.new(
|
|
10
|
+
token: token,
|
|
11
|
+
aliases: target_aliases(token),
|
|
12
|
+
label: token.split("_").map(&:capitalize).join(" "),
|
|
13
|
+
tooltip: target_tooltip(token),
|
|
14
|
+
meaning: token,
|
|
15
|
+
row_sentence: nil,
|
|
16
|
+
support: { target: target_support(token) },
|
|
17
|
+
source_formats: [],
|
|
18
|
+
condition_fields: []
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def target_aliases(token)
|
|
24
|
+
return [ "whole output", "whole_output" ] if token == "output"
|
|
25
|
+
return [ "in place" ] if token == "in_place"
|
|
26
|
+
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def target_support(token)
|
|
31
|
+
%w[between in_place].include?(token) ? :advanced : :normal
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def target_tooltip(token)
|
|
35
|
+
return "Replace the selected included buffer content inline. Only valid with action modify and source buffer." if token == "in_place"
|
|
36
|
+
|
|
37
|
+
"Target #{token}."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
module UnitTokenRegistryEntries
|
|
5
|
+
include RegistrySupportValues
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def unit_token_entries
|
|
10
|
+
base = canonical_unit_token_definitions
|
|
11
|
+
|
|
12
|
+
(1..6).each do |level|
|
|
13
|
+
base << unit_token("heading_#{level}_section", [ "h#{level}_section" ], "H#{level} Section", "Match H#{level} sections.", "Heading #{level} section.", all_normal)
|
|
14
|
+
base << unit_token("heading_#{level}", [ "h#{level}" ], "H#{level} Heading", "Match H#{level} heading nodes.", "Heading #{level} node.", all_normal)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
base.map do |definition|
|
|
18
|
+
RegistryEntry.new(**definition.merge(row_sentence: nil, source_formats: %i[markdown html], condition_fields: condition_field_entries.map(&:token)))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def canonical_unit_token_definitions
|
|
23
|
+
[
|
|
24
|
+
unit_token("all", [], "Whole Source / All", "Use the whole available content.", "Whole source or selected unit.", { select: :normal, include: :normal, target: :disabled, scope: :disabled }),
|
|
25
|
+
unit_token("output", [], "Output", "Use the whole composed output.", "Composition buffer output.", { select: :disabled, include: :disabled, target: :normal, scope: :output_only }),
|
|
26
|
+
unit_token("content", [], "Content", "Keep content inside the selected unit.", "Nested content excluding selected heading.", { select: :disabled, include: :include_only, target: :disabled, scope: :advanced }),
|
|
27
|
+
unit_token("heading_title", [ "title" ], "Heading Title", "Keep only the heading title.", "Section heading node.", { select: :disabled, include: :include_only, target: :advanced, scope: :advanced }),
|
|
28
|
+
unit_token("section", [ "sections" ], "Section", "Match heading-defined sections at any level.", "Heading-defined section.", all_normal),
|
|
29
|
+
unit_token("heading", [ "headings" ], "Heading", "Match heading nodes at any level.", "Heading node.", all_normal),
|
|
30
|
+
unit_token("paragraph", [ "p" ], "Paragraph", "Match paragraph nodes.", "Paragraph node.", all_normal),
|
|
31
|
+
unit_token("link", [ "a" ], "Link", "Match links.", "Link node.", all_normal),
|
|
32
|
+
unit_token("image", [ "img" ], "Image", "Match images.", "Image node.", all_normal),
|
|
33
|
+
unit_token("list", [], "List", "Match list nodes.", "Ordered or unordered list.", all_normal),
|
|
34
|
+
unit_token("unordered_list", [ "ul" ], "Unordered List", "Match bullet lists.", "Unordered list node.", all_normal),
|
|
35
|
+
unit_token("ordered_list", [ "ol" ], "Ordered List", "Match numbered lists.", "Ordered list node.", all_normal),
|
|
36
|
+
unit_token("list_item", [ "li" ], "List Item", "Match list items.", "List item node.", all_normal),
|
|
37
|
+
unit_token("blockquote", [], "Blockquote", "Match blockquotes.", "Block quote node.", all_normal),
|
|
38
|
+
unit_token("table", [], "Table", "Match whole tables.", "Table node.", all_normal),
|
|
39
|
+
unit_token("table_head", [ "thead" ], "Table Head", "Match table header groups.", "Table header group.", all_advanced),
|
|
40
|
+
unit_token("table_body", [ "tbody" ], "Table Body", "Match table body groups.", "Table body group.", all_advanced),
|
|
41
|
+
unit_token("table_row", [ "tr" ], "Table Row", "Match table rows.", "Table row.", all_advanced),
|
|
42
|
+
unit_token("table_header", [ "th" ], "Header Cell", "Match table header cells.", "Table header cell.", all_advanced),
|
|
43
|
+
unit_token("table_cell", [ "td" ], "Table Cell", "Match table cells.", "Table cell.", all_advanced),
|
|
44
|
+
unit_token("code_block", [], "Code Block", "Match fenced or block code.", "Code block.", all_normal),
|
|
45
|
+
unit_token("inline_code", [], "Inline Code", "Match inline code spans.", "Inline code.", all_normal),
|
|
46
|
+
unit_token("data_block", [], "Data Block", "Match structured YAML/JSON blocks.", "Structured data block.", { select: :normal, include: :normal, target: :advanced, scope: :advanced }),
|
|
47
|
+
unit_token("data_path", [], "Data Path", "Keep a structured data path.", "Structured data path.", { select: :disabled, include: :include_only, target: :disabled, scope: :advanced }),
|
|
48
|
+
unit_token("data_record", [], "Data Record", "Match composed data records.", "Structured output record.", { select: :disabled, include: :disabled, target: :advanced, scope: :output_only }),
|
|
49
|
+
unit_token("data_value", [], "Data Value", "Match composed data values.", "Structured output value.", { select: :disabled, include: :disabled, target: :advanced, scope: :output_only }),
|
|
50
|
+
unit_token("mermaid", [], "Mermaid", "Match Mermaid diagrams.", "Mermaid diagram.", all_normal),
|
|
51
|
+
unit_token("math_block", [], "Math Block", "Match display math blocks.", "Display math.", all_normal),
|
|
52
|
+
unit_token("inline_math", [], "Inline Math", "Match inline math spans.", "Inline math.", all_normal),
|
|
53
|
+
unit_token("comment", [], "Comment", "Match comments.", "HTML comment.", all_normal),
|
|
54
|
+
unit_token("raw_html", [], "Raw HTML", "Match raw HTML nodes.", "Raw HTML node.", all_adapter_policy)
|
|
55
|
+
]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def unit_token(token, aliases, label, tooltip, meaning, support)
|
|
59
|
+
{ token: token, aliases: aliases, label: label, tooltip: tooltip, meaning: meaning, support: support }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
ConditionFieldRegistryEntry = MarkdownComposer.value_object(:token, :aliases, :label, :tooltip, :meaning, :predicates, :applies_to, :source_formats, :field_take) do
|
|
5
|
+
def to_h
|
|
6
|
+
{
|
|
7
|
+
token: token,
|
|
8
|
+
aliases: aliases,
|
|
9
|
+
label: label,
|
|
10
|
+
tooltip: tooltip,
|
|
11
|
+
meaning: meaning,
|
|
12
|
+
predicates: predicates,
|
|
13
|
+
applies_to: applies_to,
|
|
14
|
+
source_formats: source_formats,
|
|
15
|
+
field_take: field_take
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
PredicateRegistryEntry = MarkdownComposer.value_object(:token, :aliases, :label, :tooltip, :meaning, :value_type, :support) do
|
|
21
|
+
def to_h
|
|
22
|
+
{
|
|
23
|
+
token: token,
|
|
24
|
+
aliases: aliases,
|
|
25
|
+
label: label,
|
|
26
|
+
tooltip: tooltip,
|
|
27
|
+
meaning: meaning,
|
|
28
|
+
value_type: value_type,
|
|
29
|
+
support: support
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
WhereGroupRegistryEntry = MarkdownComposer.value_object(:token, :aliases, :label, :tooltip, :meaning, :arity) do
|
|
35
|
+
def to_h
|
|
36
|
+
{
|
|
37
|
+
token: token,
|
|
38
|
+
aliases: aliases,
|
|
39
|
+
label: label,
|
|
40
|
+
tooltip: tooltip,
|
|
41
|
+
meaning: meaning,
|
|
42
|
+
arity: arity
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class WhereRegistry
|
|
48
|
+
attr_reader :fields, :predicates, :groups
|
|
49
|
+
|
|
50
|
+
def initialize(fields:, predicates:, groups:)
|
|
51
|
+
@fields = Registry.new(fields)
|
|
52
|
+
@predicates = Registry.new(predicates)
|
|
53
|
+
@groups = Registry.new(groups)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def field?(token)
|
|
57
|
+
fields.key?(token)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def predicate?(token)
|
|
61
|
+
predicates.key?(token)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def group?(token)
|
|
65
|
+
groups.key?(token)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def predicates_for(field)
|
|
69
|
+
fields[field]&.predicates || []
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def condition_map
|
|
73
|
+
fields.entries.to_h { |entry| [ entry.token, entry.predicates ] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_h
|
|
77
|
+
{
|
|
78
|
+
fields: fields.to_a,
|
|
79
|
+
predicates: predicates.to_a,
|
|
80
|
+
groups: groups.to_a
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "registries/registry"
|
|
4
|
+
require_relative "registries/take_registry"
|
|
5
|
+
require_relative "registries/where_registry"
|
|
6
|
+
require_relative "registries/support_values"
|
|
7
|
+
require_relative "registries/unit_token_entries"
|
|
8
|
+
require_relative "registries/source_entries"
|
|
9
|
+
require_relative "registries/action_entries"
|
|
10
|
+
require_relative "registries/target_entries"
|
|
11
|
+
require_relative "registries/take_entries"
|
|
12
|
+
require_relative "registries/condition_entries"
|
|
13
|
+
require_relative "transforms/default_entries"
|
|
14
|
+
|
|
15
|
+
module MarkdownComposer
|
|
16
|
+
class Registries
|
|
17
|
+
include UnitTokenRegistryEntries
|
|
18
|
+
include SourceRegistryEntries
|
|
19
|
+
include ActionRegistryEntries
|
|
20
|
+
include TargetRegistryEntries
|
|
21
|
+
include TakeRegistryEntries
|
|
22
|
+
include WhereRegistryEntries
|
|
23
|
+
include DefaultTransformEntries
|
|
24
|
+
|
|
25
|
+
attr_reader :unit_tokens, :sources, :actions, :targets, :transforms, :take,
|
|
26
|
+
:where, :condition_fields, :predicates, :where_groups, :conditions
|
|
27
|
+
|
|
28
|
+
def self.default
|
|
29
|
+
@default ||= new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@unit_tokens = Registry.new(unit_token_entries)
|
|
34
|
+
@sources = Registry.new(source_entries)
|
|
35
|
+
@actions = Registry.new(action_entries)
|
|
36
|
+
@targets = Registry.new(target_entries)
|
|
37
|
+
@transforms = Registry.new(transform_entries)
|
|
38
|
+
@take = Registry.new(take_entries)
|
|
39
|
+
@condition_fields = Registry.new(condition_field_entries)
|
|
40
|
+
@predicates = Registry.new(predicate_entries)
|
|
41
|
+
@where_groups = Registry.new(where_group_entries)
|
|
42
|
+
@where = WhereRegistry.new(fields: condition_field_entries, predicates: predicate_entries, groups: where_group_entries)
|
|
43
|
+
@conditions = @where.condition_map
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :output, :buffer, :markdown, :html, :diagnostics, :errors, :stages
|
|
6
|
+
|
|
7
|
+
def initialize(output:, buffer:, markdown:, html:, diagnostics:, errors:, stages: {})
|
|
8
|
+
@output = output
|
|
9
|
+
@buffer = buffer
|
|
10
|
+
@markdown = markdown
|
|
11
|
+
@html = html
|
|
12
|
+
@diagnostics = diagnostics
|
|
13
|
+
@errors = errors
|
|
14
|
+
@stages = stages
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def success?
|
|
18
|
+
errors.empty?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
{
|
|
23
|
+
success: success?,
|
|
24
|
+
output: output,
|
|
25
|
+
buffer: buffer&.to_h,
|
|
26
|
+
markdown: markdown,
|
|
27
|
+
html: html,
|
|
28
|
+
diagnostics: diagnostics.map(&:to_h),
|
|
29
|
+
errors: errors.map(&:to_h),
|
|
30
|
+
stages: stages
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
class SelectionResolver
|
|
5
|
+
attr_reader :index, :options, :diagnostics, :path
|
|
6
|
+
|
|
7
|
+
def initialize(index:, options: {}, diagnostics: Diagnostics.new, path: nil)
|
|
8
|
+
@index = index
|
|
9
|
+
@options = options
|
|
10
|
+
@diagnostics = diagnostics
|
|
11
|
+
@path = path
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def resolve(selector, within: nil)
|
|
15
|
+
selector ||= { "type" => "all" }
|
|
16
|
+
matches = if selector["types"]
|
|
17
|
+
selector["types"].flat_map { |type| matches_for(type, within: within) }.uniq(&:id)
|
|
18
|
+
else
|
|
19
|
+
matches_for(selector["type"] || "all", within: within)
|
|
20
|
+
end
|
|
21
|
+
matches = apply_where(matches, selector["where"]) if selector["where"]
|
|
22
|
+
matches = Take.apply(matches, selector["take"], diagnostics: diagnostics, path: path, seed: options.fetch(:seed, 0)) if selector["take"]
|
|
23
|
+
diagnostics.warn("selection.empty", "Selection matched no content", path: path) if matches.empty?
|
|
24
|
+
matches
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resolve_with_includes(selector, include_config)
|
|
28
|
+
resolve(selector).flat_map do |unit|
|
|
29
|
+
include_units(unit, include_config || [ { "type" => "all" } ])
|
|
30
|
+
end.uniq(&:id).sort_by(&:source_position)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def include_units(unit, include_config)
|
|
34
|
+
included = []
|
|
35
|
+
excluded = []
|
|
36
|
+
|
|
37
|
+
Array(include_config).each do |item|
|
|
38
|
+
if item["exclude"]
|
|
39
|
+
excluded.concat(resolve_nested(unit, item["exclude"]))
|
|
40
|
+
else
|
|
41
|
+
included.concat(resolve_nested(unit, item))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
(included - excluded).uniq(&:id).sort_by(&:source_position)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def resolve_nested(unit, item)
|
|
51
|
+
type = item["type"] || "all"
|
|
52
|
+
matches = case type
|
|
53
|
+
when "all"
|
|
54
|
+
emit_all(unit)
|
|
55
|
+
when "content"
|
|
56
|
+
nested_nodes(unit, include_heading: false)
|
|
57
|
+
when "heading_title"
|
|
58
|
+
unit.respond_to?(:heading_node) ? [ unit.heading_node ].compact : []
|
|
59
|
+
when "data_path"
|
|
60
|
+
DataPath.resolve(unit, item["path"], diagnostics: diagnostics, diagnostic_path: path)
|
|
61
|
+
else
|
|
62
|
+
matches_for(type, within: unit)
|
|
63
|
+
end
|
|
64
|
+
matches = apply_where(matches, item["where"]) if item["where"]
|
|
65
|
+
matches = Take.apply(matches, item["take"], diagnostics: diagnostics, path: path, seed: options.fetch(:seed, 0)) if item["take"]
|
|
66
|
+
|
|
67
|
+
if item["include"]
|
|
68
|
+
matches.flat_map { |match| include_units(match, item["include"]) }
|
|
69
|
+
else
|
|
70
|
+
matches.flat_map { |match| expand_unit(match) }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def matches_for(type, within:)
|
|
75
|
+
type = Registries.default.unit_tokens.normalise(type) || type.to_s
|
|
76
|
+
if type == "all"
|
|
77
|
+
return within ? emit_all(within) : [ *index.root.body_nodes, *index.root.child_sections ]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
collection = within ? nested_candidates(within) : all_candidates
|
|
81
|
+
if type == "section"
|
|
82
|
+
section_candidates(within)
|
|
83
|
+
elsif type == "heading"
|
|
84
|
+
heading_candidates(within)
|
|
85
|
+
elsif section_token?(type)
|
|
86
|
+
level = type[/heading_(\d)_section/, 1].to_i
|
|
87
|
+
collection.select { |unit| unit.respond_to?(:heading_node) && unit.level == level }
|
|
88
|
+
elsif heading_token?(type)
|
|
89
|
+
level = type[/heading_(\d)/, 1].to_i
|
|
90
|
+
collection.select { |unit| unit.is_a?(ComposerNode) && unit.type == "heading_#{level}" }
|
|
91
|
+
elsif type == "list"
|
|
92
|
+
collection.select { |unit| unit.is_a?(ComposerNode) && %w[ordered_list unordered_list].include?(unit.type) }
|
|
93
|
+
else
|
|
94
|
+
collection.select { |unit| unit.is_a?(ComposerNode) && unit.type == type }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def all_candidates
|
|
99
|
+
[ *index.sections, *index.nodes, *index.nodes.flat_map(&:children) ].flatten
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def section_candidates(within)
|
|
103
|
+
sections = if within.nil?
|
|
104
|
+
index.sections
|
|
105
|
+
elsif within.respond_to?(:child_sections)
|
|
106
|
+
descendant_sections(within)
|
|
107
|
+
else
|
|
108
|
+
[]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
sections.compact.uniq(&:id).sort_by(&:source_position)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def descendant_sections(section)
|
|
115
|
+
section.child_sections.flat_map { |child| [ child, *descendant_sections(child) ] }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def heading_candidates(within)
|
|
119
|
+
nodes = if within.nil?
|
|
120
|
+
index.nodes
|
|
121
|
+
elsif within.respond_to?(:heading_node)
|
|
122
|
+
[ *within.all_nodes, *within.all_nodes.flat_map(&:children) ].flatten
|
|
123
|
+
elsif within.is_a?(ComposerNode)
|
|
124
|
+
[ within, *within.children ]
|
|
125
|
+
else
|
|
126
|
+
[]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
nodes.compact.uniq(&:id).select { |unit| unit.is_a?(ComposerNode) && unit.heading? }.sort_by(&:source_position)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def nested_candidates(unit)
|
|
133
|
+
return all_candidates unless unit
|
|
134
|
+
return unit.children if unit.is_a?(ComposerNode) && unit.children.any?
|
|
135
|
+
return [] unless unit.respond_to?(:heading_node)
|
|
136
|
+
|
|
137
|
+
[ *unit.child_sections, *unit.body_nodes, *unit.all_nodes, *unit.all_nodes.flat_map(&:children) ].flatten.compact.uniq(&:id)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def nested_nodes(unit, include_heading:)
|
|
141
|
+
if unit.respond_to?(:heading_node)
|
|
142
|
+
nodes = []
|
|
143
|
+
nodes << unit.heading_node if include_heading && unit.heading_node
|
|
144
|
+
nodes.concat(unit.body_nodes.reject { |node| derived_node?(node) })
|
|
145
|
+
unit.child_sections.each { |child| nodes.concat(nested_nodes(child, include_heading: true)) }
|
|
146
|
+
nodes.compact.uniq(&:id).sort_by(&:source_position)
|
|
147
|
+
elsif unit.is_a?(ComposerNode)
|
|
148
|
+
[ unit ]
|
|
149
|
+
else
|
|
150
|
+
[]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def emit_all(unit)
|
|
155
|
+
expand_unit(unit)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def expand_unit(unit)
|
|
159
|
+
nested_nodes(unit, include_heading: true)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def derived_node?(node)
|
|
163
|
+
node.respond_to?(:attributes) && node.attributes["derived"]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def apply_where(matches, where)
|
|
167
|
+
total = matches.length
|
|
168
|
+
matches.each_with_index.select do |(unit, index)|
|
|
169
|
+
Where.match?(unit, where, position: index + 1, total: total, options: options)
|
|
170
|
+
end.map(&:first)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def section_token?(type)
|
|
174
|
+
type.match?(/\Aheading_[1-6]_section\z/)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def heading_token?(type)
|
|
178
|
+
type.match?(/\Aheading_[1-6]\z/)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MarkdownComposer
|
|
4
|
+
class Source
|
|
5
|
+
attr_reader :key, :type, :title, :markdown, :html, :preferred_format, :metadata
|
|
6
|
+
|
|
7
|
+
def self.build(value)
|
|
8
|
+
value.is_a?(Source) ? value : new(**symbolize(value))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.symbolize(value)
|
|
12
|
+
value.to_h.transform_keys(&:to_sym)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(key:, type: "explicit", title: nil, markdown: nil, html: nil, preferred_format: :markdown, metadata: {})
|
|
16
|
+
@key = key.to_s
|
|
17
|
+
@type = type.to_s
|
|
18
|
+
@title = title
|
|
19
|
+
@markdown = markdown
|
|
20
|
+
@html = html
|
|
21
|
+
@preferred_format = preferred_format.to_sym
|
|
22
|
+
@metadata = metadata || {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def format
|
|
26
|
+
return :html if preferred_format == :html && html_present?
|
|
27
|
+
return :markdown if markdown_present?
|
|
28
|
+
return :html if html_present?
|
|
29
|
+
|
|
30
|
+
preferred_format
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def content
|
|
34
|
+
format == :html ? html.to_s : markdown.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def markdown_present?
|
|
38
|
+
!markdown.to_s.empty?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def html_present?
|
|
42
|
+
!html.to_s.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_h
|
|
46
|
+
{
|
|
47
|
+
key: key,
|
|
48
|
+
type: type,
|
|
49
|
+
title: title,
|
|
50
|
+
markdown: markdown,
|
|
51
|
+
html: html,
|
|
52
|
+
preferred_format: preferred_format,
|
|
53
|
+
metadata: metadata
|
|
54
|
+
}.compact
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|