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