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,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarkdownComposer
4
+ class TransformRunner
5
+ module ScopeResolver
6
+ private
7
+
8
+ def replace_in_scope(scope, path:)
9
+ ranges = scope_ranges(scope, path: path)
10
+ lines = buffer.markdown.lines
11
+ ranges.sort_by(&:begin).reverse_each do |range|
12
+ text = lines[(range.begin - 1)..(range.end - 1)].join
13
+ lines[(range.begin - 1)..(range.end - 1)] = [ yield(text) ]
14
+ end
15
+ set_markdown(lines.join)
16
+ end
17
+
18
+ def rewrite_matching_lines(scope)
19
+ idx = buffer.index
20
+ matches = if output_scope?(scope)
21
+ idx.nodes
22
+ else
23
+ resolver = SelectionResolver.new(index: idx, options: options, diagnostics: diagnostics, path: "scope")
24
+ scope["include"] ? resolver.resolve_with_includes(scope, scope["include"]) : resolver.resolve(scope)
25
+ end
26
+ counters = Hash.new(0)
27
+ lines = buffer.markdown.lines
28
+ matches.each do |unit|
29
+ next unless unit.is_a?(ComposerNode)
30
+
31
+ line_index = unit.start_line - 1
32
+ lines[line_index] = yield(lines[line_index], unit, counters)
33
+ end
34
+ set_markdown(lines.join)
35
+ end
36
+
37
+ def scope_ranges(scope, path:)
38
+ scope_matches(scope, path: path).map(&:first)
39
+ end
40
+
41
+ def scope_matches(scope, path:)
42
+ return [ [ 1..buffer.markdown.lines.length, buffer.index.root, 1 ] ] if output_scope?(scope)
43
+
44
+ idx = buffer.index
45
+ scope_path = "#{path}.scope"
46
+ resolver = SelectionResolver.new(index: idx, options: options, diagnostics: diagnostics, path: scope_path)
47
+ matches = scope["include"] ? resolver.resolve_with_includes(scope, scope["include"]) : resolver.resolve(scope)
48
+ diagnostics.warn("transform.scope_empty", "Transform scope matched no content", path: scope_path) if matches.empty?
49
+ matches.each_with_index.map { |unit, index| [ unit.start_line..unit.end_line, unit, index + 1 ] }
50
+ end
51
+
52
+ def output_scope?(scope)
53
+ scope && scope["type"] == "output"
54
+ end
55
+
56
+ def replace_ranges(ranges, content)
57
+ lines = buffer.markdown.lines
58
+ ranges.sort_by(&:begin).reverse_each do |range|
59
+ lines[(range.begin - 1)..(range.end - 1)] = content.empty? ? [] : [ ensure_block(content) ]
60
+ end
61
+ set_markdown(lines.join)
62
+ end
63
+
64
+ def replace_matches(matches, transform)
65
+ lines = buffer.markdown.lines
66
+ matches.sort_by { |range, _unit, _index| range.begin }.reverse_each do |range, unit, index|
67
+ content = content_for(transform, unit: unit, index: index)
68
+ lines[(range.begin - 1)..(range.end - 1)] = content.empty? ? [] : [ ensure_block(content) ]
69
+ end
70
+ set_markdown(lines.join)
71
+ end
72
+
73
+ def nested_target_ranges(target, unit, path:)
74
+ return [] unless unit
75
+ return [ unit.start_line..unit.start_line ] if target["position"] == "start"
76
+ return [ unit.end_line..unit.end_line ] if target["position"] == "end"
77
+
78
+ selector = target.reject { |key, _| %w[placement start end position].include?(key) }
79
+ idx = buffer.index
80
+ resolver = SelectionResolver.new(index: idx, options: options, diagnostics: diagnostics, path: path)
81
+ ranges = resolver.resolve(selector, within: unit).map { |match| match.start_line..match.end_line }
82
+ diagnostics.warn("transform.target_empty", "Transform target matched no scoped content", path: path) if ranges.empty?
83
+ ranges
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "transform_runner/field_interpolator"
4
+ require_relative "transform_runner/heading_numbering"
5
+ require_relative "transform_runner/scope_resolver"
6
+ require_relative "transform_runner/content_placement"
7
+
8
+ module MarkdownComposer
9
+ class TransformRunner
10
+ include FieldInterpolator
11
+ include HeadingNumbering
12
+ include ScopeResolver
13
+ include ContentPlacement
14
+
15
+ attr_reader :buffer, :transforms, :output, :options, :diagnostics, :stages, :path_prefix
16
+
17
+ def initialize(buffer:, transforms:, output:, options:, diagnostics:, stages:, path_prefix: "transform")
18
+ @buffer = buffer
19
+ @transforms = transforms
20
+ @output = output
21
+ @options = options
22
+ @diagnostics = diagnostics
23
+ @stages = stages
24
+ @path_prefix = path_prefix
25
+ end
26
+
27
+ def call
28
+ transforms.each_with_index do |transform, index|
29
+ path = "#{path_prefix}[#{index}]"
30
+ before = buffer.markdown
31
+ apply_transform(transform, path: path)
32
+ diagnostics.warn("transform.noop", "#{transform["transform"]} made no changes", path: "#{path}.transform", details: { stage: "transformed_markdown" }) if before == buffer.markdown
33
+ stages["transform_#{index + 1}"] = buffer.markdown if options.fetch(:stages, false)
34
+ end
35
+ stages["transformed_markdown"] = buffer.markdown if options.fetch(:stages, false)
36
+ stages["final"] = output == "markdown" ? buffer.markdown : nil if options.fetch(:stages, false)
37
+ buffer
38
+ end
39
+
40
+ private
41
+
42
+ def apply_transform(transform, path:)
43
+ case transform["transform"]
44
+ when "heading_numbers" then transform_heading_numbers(transform, path: path)
45
+ when "replace_text" then transform_replace_text(transform, path: path)
46
+ when "links" then transform_links(transform, path: path)
47
+ when "heading_levels" then transform_heading_levels(transform, path: path)
48
+ when "remove_empty" then transform_remove_empty(transform, path: path)
49
+ when "insert_before" then transform_insert(transform, :before, path: path)
50
+ when "insert_after" then transform_insert(transform, :after, path: path)
51
+ when "prepend_content" then transform_insert(transform, :prepend, path: path)
52
+ when "append_content" then transform_insert(transform, :append, path: path)
53
+ when "replace_content" then transform_replace_content(transform, path: path)
54
+ when "remove_content" then transform_remove_content(transform, path: path)
55
+ when "dedupe" then transform_dedupe(transform, path: path)
56
+ when "order" then transform_order(transform, path: path)
57
+ when "sanitise", "adapter"
58
+ diagnostics.warn("transform.adapter_skipped", "#{transform["transform"]} is adapter-policy gated and was not run inside the standalone gem", path: path)
59
+ end
60
+ end
61
+
62
+ def transform_heading_numbers(transform, path:)
63
+ mode = transform["mode"].to_s
64
+ levels = heading_levels(transform.dig("options", "levels"))
65
+ start_at = [ transform.dig("options", "start").to_i, 1 ].max
66
+ skip_if_present = transform.dig("options", "skip_if_present") == true
67
+ formats = heading_number_formats(transform.dig("options", "format"))
68
+ candidates = heading_number_candidates(transform["scope"], path: path)
69
+ counters = Hash.new(0)
70
+ lines = buffer.markdown.lines
71
+
72
+ buffer.index.nodes.select(&:heading?).sort_by(&:source_position).each do |node|
73
+ counters[node.level] = start_at - 1 if counters[node.level].zero?
74
+ counters[node.level] += 1
75
+ ((node.level + 1)..6).each { |level| counters[level] = 0 }
76
+
77
+ next unless candidates[node.id]
78
+ next unless levels.empty? || levels.include?(node.level)
79
+
80
+ line_index = node.start_line - 1
81
+ title = heading_title_from_line(lines[line_index])
82
+ lines[line_index] = case mode
83
+ when "keep"
84
+ lines[line_index]
85
+ when "strip"
86
+ "#{"#" * node.level} #{title}\n"
87
+ when "add", "rebuild"
88
+ next lines[line_index] if mode == "add" && skip_if_present && visible_heading_number(node) != ""
89
+
90
+ number = heading_number(counters, node.level, formats)
91
+ "#{"#" * node.level} #{number}#{heading_number_title_separator(number)}#{title}\n"
92
+ else
93
+ diagnostics.error("transform.mode_invalid", "Invalid heading_numbers mode #{mode}", path: path)
94
+ lines[line_index]
95
+ end
96
+ end
97
+ set_markdown(lines.join)
98
+ end
99
+
100
+ def transform_replace_text(transform, path:)
101
+ from = transform.dig("options", "from").to_s
102
+ to = transform.dig("options", "to").to_s
103
+ mode = transform["mode"].to_s
104
+ case_sensitive = transform.dig("options", "case_sensitive") == true
105
+ limit = transform.dig("options", "limit")&.to_i
106
+
107
+ replace_in_scope(transform["scope"], path: path) do |text|
108
+ flags = case_sensitive ? nil : Regexp::IGNORECASE
109
+ pattern = case mode
110
+ when "literal" then Regexp.new(Regexp.escape(from), flags)
111
+ when "word" then Regexp.new("\\b#{Regexp.escape(from)}\\b", flags)
112
+ when "regex"
113
+ Regexp.new(from, flags)
114
+ else
115
+ diagnostics.error("transform.mode_invalid", "Invalid replace_text mode #{mode}", path: path)
116
+ next text
117
+ end
118
+ count = 0
119
+ text.gsub(pattern) do
120
+ count += 1
121
+ limit && count > limit ? Regexp.last_match(0) : to
122
+ end
123
+ end
124
+ end
125
+
126
+ def transform_links(transform, path:)
127
+ mode = transform["mode"].to_s
128
+ replace_in_scope(transform["scope"], path: path) do |text|
129
+ case mode
130
+ when "keep"
131
+ text
132
+ when "unwrap"
133
+ text.gsub(/(?<!!)\[([^\]]+)\]\([^)]+\)/, "\\1")
134
+ when "remove"
135
+ text.gsub(/(?<!!)\[[^\]]+\]\([^)]+\)/, "")
136
+ when "rewrite_url"
137
+ from = transform.dig("options", "from").to_s
138
+ to = transform.dig("options", "to").to_s
139
+ text.gsub(/\[([^\]]+)\]\(([^)]+)\)/) { "[#{Regexp.last_match(1)}](#{Regexp.last_match(2).sub(from, to)})" }
140
+ when "nofollow", "target_blank"
141
+ diagnostics.warn("transform.html_only", "#{mode} is only meaningful for HTML output", path: path) if output == "markdown"
142
+ text
143
+ else
144
+ diagnostics.error("transform.mode_invalid", "Invalid links mode #{mode}", path: path)
145
+ text
146
+ end
147
+ end
148
+ end
149
+
150
+ def transform_heading_levels(transform, path:)
151
+ mode = transform["mode"].to_s
152
+ by = transform.dig("options", "by").to_i
153
+ to = heading_level(transform.dig("options", "to"))
154
+ rewrite_matching_lines(transform["scope"]) do |line, node, _counters|
155
+ next line unless node.heading?
156
+
157
+ new_level = case mode
158
+ when "promote" then [ node.level - by, 1 ].max
159
+ when "demote" then [ node.level + by, 6 ].min
160
+ when "normalise" then to || node.level
161
+ else
162
+ diagnostics.error("transform.mode_invalid", "Invalid heading_levels mode #{mode}", path: path)
163
+ node.level
164
+ end
165
+ line.sub(/\A\#{1,6}/, "#" * new_level)
166
+ end
167
+ end
168
+
169
+ def transform_remove_empty(transform, path:)
170
+ scope = transform["scope"]
171
+ ranges = scope_ranges(scope, path: path)
172
+ replace_ranges(ranges.select { |range| buffer.markdown.lines[(range.begin - 1)..(range.end - 1)].join.gsub(/[#>*`\-\s|]/, "").empty? }, "")
173
+ end
174
+
175
+ def transform_insert(transform, placement, path:)
176
+ matches = scope_matches(transform["scope"], path: path)
177
+ lines = buffer.markdown.lines
178
+ inserted_content = {}
179
+ matches.sort_by { |range, _unit, _index| range.begin }.reverse_each do |range, unit, index|
180
+ content = ensure_block(content_for(transform, unit: unit, index: index))
181
+ next if duplicate_insert?(content, transform, inserted_content)
182
+
183
+ inserted_content[content.strip] = true
184
+ insertion_index = insertion_index_for(transform, placement, range, unit, path: path)
185
+ lines.insert(insertion_index, spaced_insert_content(content, transform, lines, insertion_index))
186
+ end
187
+ set_markdown(lines.join)
188
+ end
189
+
190
+ def transform_replace_content(transform, path:)
191
+ replace_matches(scope_matches(transform["scope"], path: path), transform)
192
+ end
193
+
194
+ def transform_remove_content(transform, path:)
195
+ replace_ranges(scope_ranges(transform["scope"], path: path), "")
196
+ end
197
+
198
+ def transform_dedupe(transform, path:)
199
+ mode = transform["mode"].to_s
200
+ if mode == "source_node_id"
201
+ dedupe_current_buffer_by_origin(path: path)
202
+ return
203
+ end
204
+ seen = {}
205
+ lines = []
206
+ case_sensitive = transform.dig("options", "case_sensitive") == true
207
+ current_blocks.each do |block|
208
+ key = mode == "normalised_text" ? normalised_dedupe_key(block, case_sensitive: case_sensitive) : block
209
+ if seen[key]
210
+ diagnostics.info("transform.deduped", "Removed duplicate content", path: path)
211
+ next
212
+ end
213
+ seen[key] = true
214
+ lines << block
215
+ end
216
+ set_markdown(lines.join("\n"))
217
+ end
218
+
219
+ def transform_order(transform, path:)
220
+ mode = transform["mode"].to_s
221
+ return if mode == "action_order"
222
+
223
+ idx = buffer.index
224
+ ordered = case mode
225
+ when "source_order"
226
+ idx.nodes.sort_by(&:source_position).map(&:raw).join
227
+ when "target_order"
228
+ diagnostics.warn("transform.order_target_skipped", "target_order requires host-specific ordering policy", path: path)
229
+ buffer.markdown
230
+ else
231
+ buffer.markdown
232
+ end
233
+ set_markdown(ordered)
234
+ end
235
+
236
+ def dedupe_current_buffer_by_origin(path:)
237
+ current = buffer.index.nodes
238
+ return if current.empty?
239
+
240
+ origins_by_raw = buffer.origin_nodes.group_by { |node| normalised_dedupe_key(node.raw, case_sensitive: true) }
241
+ seen = {}
242
+ ranges_to_remove = []
243
+ current.each do |node|
244
+ origin = origins_by_raw[normalised_dedupe_key(node.raw, case_sensitive: true)]&.shift
245
+ key = origin&.attributes&.fetch("origin_id", nil) || "buffer:#{node.id}"
246
+ next if key.empty?
247
+
248
+ if seen[key]
249
+ diagnostics.info("transform.deduped", "Removed duplicate content", path: path)
250
+ ranges_to_remove << (node.start_line..node.end_line)
251
+ else
252
+ seen[key] = true
253
+ end
254
+ end
255
+ replace_ranges(ranges_to_remove, "")
256
+ buffer.sync_origins_to_current!
257
+ end
258
+
259
+ def set_markdown(markdown)
260
+ buffer.replace_markdown(markdown)
261
+ end
262
+
263
+ end
264
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarkdownComposer
4
+ module DefaultTransformEntries
5
+ private
6
+
7
+ def transform_entries
8
+ {
9
+ heading_numbers: %w[keep strip add rebuild],
10
+ replace_text: %w[literal word regex],
11
+ links: %w[keep unwrap remove rewrite_url nofollow target_blank],
12
+ heading_levels: %w[promote demote normalise],
13
+ remove_empty: %w[remove],
14
+ insert_before: %w[insert],
15
+ insert_after: %w[insert],
16
+ prepend_content: %w[insert],
17
+ append_content: %w[insert],
18
+ replace_content: %w[replace],
19
+ remove_content: %w[remove],
20
+ dedupe: %w[source_node_id normalised_text],
21
+ order: %w[action_order source_order target_order],
22
+ sanitise: %w[block_safe text_only links_unwrapped strict],
23
+ adapter: []
24
+ }.map do |token, modes|
25
+ support = %i[order sanitise adapter].include?(token) ? :advanced : :normal
26
+ support = :adapter_policy if %i[sanitise adapter].include?(token)
27
+ RegistryEntry.new(token: token.to_s, aliases: [], label: token.to_s.split("_").map(&:capitalize).join(" "), tooltip: "Run #{token} transform.", meaning: token.to_s, row_sentence: nil, support: { transform: support, modes: modes }, source_formats: [], condition_fields: [])
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MarkdownComposer
4
+ module Transforms
5
+ class Registry
6
+ def self.default
7
+ MarkdownComposer::Registries.default.transforms
8
+ end
9
+ end
10
+ end
11
+ end