coradoc 2.0.20 → 2.0.22

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.
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ # Flat-mode include processor — the explicit "flatten" step.
5
+ #
6
+ # Walks a parsed CoreModel and expands every {CoreModel::Include} link
7
+ # node into the parsed content of its target, recursing into the result.
8
+ # The original CoreModel is NOT modified — a new subtree is constructed
9
+ # and spliced into place.
10
+ #
11
+ # Invoked via the public API +Coradoc.resolve_includes(doc, base_dir:)+.
12
+ # Callers control resolution strategy (filesystem, HTTP, custom),
13
+ # missing-include policy, recursion depth, and path-traversal safety.
14
+ #
15
+ # Honors:
16
+ # - +missing_include+ policy: :error (default) | :warn | :silent | :passthrough
17
+ # - +max_depth+ limit (raises Coradoc::IncludeDepthExceededError)
18
+ # - circular detection (raises Coradoc::CircularIncludeError)
19
+ # - tags/lines/indent selectors (applied to raw text before parse)
20
+ # - leveloffset selector (applied to parsed CoreModel)
21
+ # - +base_dir+ re-rooting (recursive includes resolve relative to
22
+ # the including file — SPEC 7.2)
23
+ class ResolveIncludes
24
+ DEFAULT_MAX_DEPTH = 64
25
+
26
+ class << self
27
+ def call(core, resolver:, base_dir:, **opts)
28
+ new(resolver: resolver, base_dir: base_dir, **opts).call(core)
29
+ end
30
+ end
31
+
32
+ # @param resolver [#call] anything responding to +#call(target:, base_dir:, options:, context:)+
33
+ # @param base_dir [String] absolute path to the document root directory
34
+ # @param missing_include [Symbol] :error | :warn | :silent | :passthrough
35
+ # @param max_depth [Integer] recursion cap
36
+ # @param parse_format [Symbol] format to use when re-parsing included content
37
+ def initialize(resolver:, base_dir:,
38
+ missing_include: :error,
39
+ max_depth: DEFAULT_MAX_DEPTH,
40
+ parse_format: :asciidoc)
41
+ @resolver = Coradoc::IncludeResolver.coerce(resolver, base_dir: base_dir)
42
+ @base_dir = base_dir
43
+ @missing_policy = missing_include
44
+ @max_depth = max_depth
45
+ @parse_format = parse_format
46
+ end
47
+
48
+ # Walk + transform. Returns a NEW CoreModel with includes expanded.
49
+ def call(core)
50
+ expand_node(core, base_dir: File.expand_path(@base_dir), chain: [], depth: 0)
51
+ end
52
+
53
+ private
54
+
55
+ def expand_node(node, base_dir:, chain:, depth:)
56
+ return node unless node.is_a?(Coradoc::CoreModel::Base)
57
+
58
+ case node
59
+ when Coradoc::CoreModel::Include
60
+ expand_include(node, base_dir: base_dir, chain: chain, depth: depth)
61
+ when Coradoc::CoreModel::StructuralElement, Coradoc::CoreModel::Block
62
+ expand_container(node, base_dir: base_dir, chain: chain, depth: depth)
63
+ else
64
+ node
65
+ end
66
+ end
67
+
68
+ def expand_container(node, base_dir:, chain:, depth:)
69
+ return node if node.children.nil? || node.children.empty?
70
+
71
+ expanded_children = node.children.flat_map do |child|
72
+ expanded = expand_node(child, base_dir: base_dir, chain: chain, depth: depth)
73
+ Array(expanded)
74
+ end
75
+
76
+ return node if expanded_children.equal?(node.children) || same_children?(expanded_children, node.children)
77
+
78
+ duplicate_with_children(node, expanded_children)
79
+ end
80
+
81
+ # The processor must not mutate its input. Each container that has
82
+ # expanded includes is replaced by a shallow copy with a new
83
+ # +children+ array — the original document tree stays intact so the
84
+ # caller can re-resolve with different options.
85
+ def duplicate_with_children(node, new_children)
86
+ duplicate = node.dup
87
+ duplicate.children = new_children
88
+ duplicate
89
+ end
90
+
91
+ def same_children?(expanded, original)
92
+ return false unless expanded.length == original.length
93
+
94
+ expanded.each_with_index.all? { |node, i| node.equal?(original[i]) }
95
+ end
96
+
97
+ def expand_include(include_node, base_dir:, chain:, depth:)
98
+ enforce_depth!(include_node, depth)
99
+ enforce_cycle!(include_node, base_dir: base_dir, chain: chain)
100
+
101
+ target = include_node.target
102
+ new_chain = chain + [resolve_target_path(target, base_dir)]
103
+
104
+ content = fetch_content(include_node, base_dir: base_dir)
105
+ return replacement_for_missing(include_node) if missing_content?(content)
106
+
107
+ applied = apply_text_selectors(content, include_node.options)
108
+ parsed = parse_included(applied)
109
+
110
+ shifted = Coradoc::IncludeSelectors::LevelOffset.call(parsed, options: include_node.options)
111
+
112
+ new_base_dir = File.dirname(resolve_target_path(target, base_dir))
113
+ expand_subtree(shifted, base_dir: new_base_dir, chain: new_chain, depth: depth + 1)
114
+ end
115
+
116
+ def missing_content?(content)
117
+ content.nil? || content == :passthrough
118
+ end
119
+
120
+ def fetch_content(include_node, base_dir:)
121
+ @resolver.call(
122
+ target: include_node.target,
123
+ base_dir: base_dir,
124
+ options: include_node.options,
125
+ context: {}
126
+ )
127
+ rescue Coradoc::IncludeNotFoundError => e
128
+ handle_missing(include_node, e)
129
+ end
130
+
131
+ def handle_missing(include_node, error)
132
+ case @missing_policy
133
+ when :error then raise error
134
+ when :warn
135
+ Coradoc::Logger.warn("Include target not found: #{include_node.target}")
136
+ nil
137
+ when :silent then nil
138
+ when :passthrough then :passthrough
139
+ else raise error
140
+ end
141
+ end
142
+
143
+ def replacement_for_missing(include_node)
144
+ return [include_node] if @missing_policy == :passthrough
145
+
146
+ []
147
+ end
148
+
149
+ def apply_text_selectors(text, options)
150
+ text = apply_lines_or_tags(text, options)
151
+ Coradoc::IncludeSelectors::Indent.call(text, options: options)
152
+ end
153
+
154
+ def apply_lines_or_tags(text, options)
155
+ # lines wins when both specified (SPEC 3.5)
156
+ return Coradoc::IncludeSelectors::Lines.call(text, options: options) if options.lines?
157
+
158
+ Coradoc::IncludeSelectors::Tags.call(text, options: options)
159
+ end
160
+
161
+ def parse_included(text)
162
+ return empty_core if text.nil? || text.empty?
163
+
164
+ Coradoc.parse(text, format: @parse_format)
165
+ end
166
+
167
+ def empty_core
168
+ Coradoc::CoreModel::DocumentElement.new
169
+ end
170
+
171
+ def expand_subtree(core, base_dir:, chain:, depth:)
172
+ expanded = expand_node(core, base_dir: base_dir, chain: chain, depth: depth)
173
+ return [expanded] unless expanded.is_a?(Coradoc::CoreModel::StructuralElement)
174
+
175
+ expanded.children || []
176
+ end
177
+
178
+ def enforce_depth!(include_node, depth)
179
+ return if depth < @max_depth
180
+
181
+ raise Coradoc::IncludeDepthExceededError.new(
182
+ target: include_node.target,
183
+ depth: depth,
184
+ max: @max_depth
185
+ )
186
+ end
187
+
188
+ def enforce_cycle!(include_node, base_dir:, chain:)
189
+ full = resolve_target_path(include_node.target, base_dir)
190
+ return unless chain.include?(full)
191
+
192
+ raise Coradoc::CircularIncludeError.new(
193
+ target: include_node.target,
194
+ chain: chain
195
+ )
196
+ end
197
+
198
+ def resolve_target_path(target, base_dir)
199
+ File.expand_path(target, base_dir)
200
+ end
201
+ end
202
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coradoc
4
- VERSION = '2.0.20'
4
+ VERSION = '2.0.22'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coradoc
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.20
4
+ version: 2.0.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -73,11 +73,16 @@ files:
73
73
  - lib/coradoc/core_model/frontmatter.rb
74
74
  - lib/coradoc/core_model/frontmatter/codec.rb
75
75
  - lib/coradoc/core_model/frontmatter/field_transform.rb
76
+ - lib/coradoc/core_model/frontmatter/frontmatter_value.rb
76
77
  - lib/coradoc/core_model/frontmatter/schema_resolver.rb
77
78
  - lib/coradoc/core_model/frontmatter/text_splitter.rb
79
+ - lib/coradoc/core_model/has_children.rb
78
80
  - lib/coradoc/core_model/horizontal_rule_block.rb
79
81
  - lib/coradoc/core_model/id_generator.rb
80
82
  - lib/coradoc/core_model/image.rb
83
+ - lib/coradoc/core_model/include.rb
84
+ - lib/coradoc/core_model/include_level_offset.rb
85
+ - lib/coradoc/core_model/include_options.rb
81
86
  - lib/coradoc/core_model/inline_element.rb
82
87
  - lib/coradoc/core_model/list_block.rb
83
88
  - lib/coradoc/core_model/list_item.rb
@@ -88,6 +93,7 @@ files:
88
93
  - lib/coradoc/core_model/paragraph_block.rb
89
94
  - lib/coradoc/core_model/pass_block.rb
90
95
  - lib/coradoc/core_model/quote_block.rb
96
+ - lib/coradoc/core_model/raw_inline_element.rb
91
97
  - lib/coradoc/core_model/reviewer_block.rb
92
98
  - lib/coradoc/core_model/sidebar_block.rb
93
99
  - lib/coradoc/core_model/source_block.rb
@@ -103,6 +109,13 @@ files:
103
109
  - lib/coradoc/errors.rb
104
110
  - lib/coradoc/format_module.rb
105
111
  - lib/coradoc/hooks.rb
112
+ - lib/coradoc/include_resolver.rb
113
+ - lib/coradoc/include_resolver/filesystem.rb
114
+ - lib/coradoc/include_selectors.rb
115
+ - lib/coradoc/include_selectors/indent.rb
116
+ - lib/coradoc/include_selectors/level_offset.rb
117
+ - lib/coradoc/include_selectors/lines.rb
118
+ - lib/coradoc/include_selectors/tags.rb
106
119
  - lib/coradoc/input.rb
107
120
  - lib/coradoc/logger.rb
108
121
  - lib/coradoc/output.rb
@@ -110,6 +123,7 @@ files:
110
123
  - lib/coradoc/processor_registry.rb
111
124
  - lib/coradoc/query.rb
112
125
  - lib/coradoc/registry.rb
126
+ - lib/coradoc/resolve_includes.rb
113
127
  - lib/coradoc/serializer/registry.rb
114
128
  - lib/coradoc/transform.rb
115
129
  - lib/coradoc/transform/base.rb