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