coradoc 2.0.27 → 2.0.29

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/cli.rb +9 -6
  3. data/lib/coradoc/configurable.rb +6 -3
  4. data/lib/coradoc/coradoc.rb +5 -2
  5. data/lib/coradoc/core_model/attribute_reference_resolver.rb +19 -7
  6. data/lib/coradoc/core_model/base.rb +45 -2
  7. data/lib/coradoc/core_model/comment_line.rb +1 -1
  8. data/lib/coradoc/core_model/definition_item.rb +22 -1
  9. data/lib/coradoc/core_model/horizontal_rule_block.rb +4 -0
  10. data/lib/coradoc/core_model/include_level_offset.rb +1 -1
  11. data/lib/coradoc/core_model/include_options.rb +1 -0
  12. data/lib/coradoc/core_model/inline_content.rb +1 -0
  13. data/lib/coradoc/core_model/list_block.rb +1 -1
  14. data/lib/coradoc/core_model/listing_block.rb +4 -0
  15. data/lib/coradoc/core_model/literal_block.rb +4 -0
  16. data/lib/coradoc/core_model/pass_block.rb +4 -0
  17. data/lib/coradoc/core_model/source_block.rb +4 -0
  18. data/lib/coradoc/core_model/stem_block.rb +4 -0
  19. data/lib/coradoc/dispatch.rb +1 -0
  20. data/lib/coradoc/errors.rb +19 -0
  21. data/lib/coradoc/format_catalog.rb +19 -0
  22. data/lib/coradoc/format_module.rb +10 -0
  23. data/lib/coradoc/hooks.rb +1 -1
  24. data/lib/coradoc/include_selectors/level_offset.rb +0 -2
  25. data/lib/coradoc/include_selectors/lines.rb +2 -2
  26. data/lib/coradoc/include_selectors/tags.rb +2 -4
  27. data/lib/coradoc/introspection/element_counter.rb +2 -2
  28. data/lib/coradoc/link_rewriter/visitor.rb +5 -14
  29. data/lib/coradoc/pipeline.rb +64 -5
  30. data/lib/coradoc/query.rb +8 -8
  31. data/lib/coradoc/reference/address/anchor.rb +48 -0
  32. data/lib/coradoc/reference/address/doi.rb +40 -0
  33. data/lib/coradoc/reference/address/isbn.rb +39 -0
  34. data/lib/coradoc/reference/address/path.rb +66 -0
  35. data/lib/coradoc/reference/address/scoped_path.rb +47 -0
  36. data/lib/coradoc/reference/address/url.rb +41 -0
  37. data/lib/coradoc/reference/address.rb +153 -0
  38. data/lib/coradoc/reference/catalog/composite.rb +59 -0
  39. data/lib/coradoc/reference/catalog/local.rb +97 -0
  40. data/lib/coradoc/reference/catalog/memory_index.rb +59 -0
  41. data/lib/coradoc/reference/catalog.rb +33 -0
  42. data/lib/coradoc/reference/edge/citation_options.rb +16 -0
  43. data/lib/coradoc/reference/edge/footnote_ref_options.rb +13 -0
  44. data/lib/coradoc/reference/edge/image_ref_options.rb +15 -0
  45. data/lib/coradoc/reference/edge/include_options.rb +15 -0
  46. data/lib/coradoc/reference/edge/kind.rb +72 -0
  47. data/lib/coradoc/reference/edge/link_options.rb +12 -0
  48. data/lib/coradoc/reference/edge/navigation_options.rb +12 -0
  49. data/lib/coradoc/reference/edge/options.rb +13 -0
  50. data/lib/coradoc/reference/edge.rb +80 -0
  51. data/lib/coradoc/reference/edge_search.rb +128 -0
  52. data/lib/coradoc/reference/materializer/base.rb +44 -0
  53. data/lib/coradoc/reference/materializer/passthrough.rb +31 -0
  54. data/lib/coradoc/reference/materializer/registry.rb +116 -0
  55. data/lib/coradoc/reference/materializer.rb +14 -0
  56. data/lib/coradoc/reference/presentation/base.rb +47 -0
  57. data/lib/coradoc/reference/presentation/custom_hierarchy.rb +104 -0
  58. data/lib/coradoc/reference/presentation/page.rb +20 -0
  59. data/lib/coradoc/reference/presentation/single_document.rb +35 -0
  60. data/lib/coradoc/reference/presentation/split_pages.rb +118 -0
  61. data/lib/coradoc/reference/presentation.rb +16 -0
  62. data/lib/coradoc/reference/resolution.rb +184 -0
  63. data/lib/coradoc/reference/resolver/base.rb +15 -0
  64. data/lib/coradoc/reference/resolver/caching.rb +37 -0
  65. data/lib/coradoc/reference/resolver/catalog.rb +69 -0
  66. data/lib/coradoc/reference/resolver/chain.rb +33 -0
  67. data/lib/coradoc/reference/resolver.rb +20 -0
  68. data/lib/coradoc/reference/result/ambiguous.rb +23 -0
  69. data/lib/coradoc/reference/result/base.rb +46 -0
  70. data/lib/coradoc/reference/result/missing.rb +14 -0
  71. data/lib/coradoc/reference/result/resolved.rb +22 -0
  72. data/lib/coradoc/reference/result.rb +20 -0
  73. data/lib/coradoc/reference.rb +65 -0
  74. data/lib/coradoc/registry.rb +6 -6
  75. data/lib/coradoc/resolve_includes.rb +2 -2
  76. data/lib/coradoc/validation.rb +44 -4
  77. data/lib/coradoc/version.rb +1 -1
  78. data/lib/coradoc.rb +1 -1
  79. metadata +45 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f0a260bb97d25d5c24d430e81c1f4c8c4a0d816612d4826ab890f8d96b22d14
4
- data.tar.gz: e65497a27738a34a7bfb8800386318419202314e12844b551f49ee6a02894aaf
3
+ metadata.gz: 72d8a4fe9a86a7ed8f5dc806492ea147f321367f7a470b3c934747de9ea2b305
4
+ data.tar.gz: '042824f44538b6b64252c0667eb8d56f2ed059de0dab8866631c84394bee6a64'
5
5
  SHA512:
6
- metadata.gz: d45b3690762230b10766ffabc1228b42533f30d8acc28016f226a6ea692bc4403f9589996e8bf61d059f8fddf29c1f9d6adaa1c98b4593089886089bedc4e1e5
7
- data.tar.gz: 9c70fcfee207fd897113f62beb55c08c1cff19212daa65b8590f65a2af4a4221d00162529df154c50b38b3578f48c96adf431fea03c1dda4ed59228ebc57c680
6
+ metadata.gz: 6a603de0773f6b27449ccb511c3ec53860e6ec37aeb55a62f63f5902b7d0c4c0e74c5681dd1749b5f09411bd04ca34ff7bca45e4da4f9e8d4bd74f4cec3972b9
7
+ data.tar.gz: ebbc0d4ae30878f6793964a437dfd62a0ba79aafdfa57f3307d6e0b5c865650627bb6f025aa94f2c18813b16973819e3358a1458eecd3d6d9db5ac47af99efb8
data/lib/coradoc/cli.rb CHANGED
@@ -23,15 +23,18 @@ module Coradoc
23
23
  option :section_number_levels, desc: 'Section numbering depth (1-6)', type: :numeric, default: 3
24
24
  option :lang, desc: 'Document language code', type: :string, default: 'en'
25
25
  option :resolve_includes, desc: 'Resolve include:: directives inline (default: leave as link nodes)',
26
- type: :boolean, default: false
26
+ type: :boolean, default: false
27
27
  option :base_dir, desc: 'Base directory for include resolution (default: dirname of FILE)',
28
- type: :string
28
+ type: :string
29
29
  option :missing_include, desc: 'Policy for missing includes: error, warn, silent, passthrough',
30
- type: :string, default: 'error'
30
+ type: :string, default: 'error'
31
31
  option :max_include_depth, desc: 'Maximum include nesting depth', type: :numeric,
32
- default: 64
32
+ default: 64
33
33
  option :allow_unsafe_includes, desc: 'Disable path-traversal protection (asciidoctor :unsafe mode)',
34
- type: :boolean, default: false
34
+ type: :boolean, default: false
35
+ option :allow_unresolved_includes, desc: 'Allow serialization with unresolved includes ' \
36
+ '(formats that cannot represent them drop them)',
37
+ type: :boolean, default: false
35
38
  def convert(file)
36
39
  source_format = resolve_format(file, :from)
37
40
  target_format = options[:to] ? Coradoc.normalize_format(options[:to]) : Coradoc.resolve_output_format(options[:output])
@@ -213,7 +216,7 @@ module Coradoc
213
216
 
214
217
  CONVERT_OPTIONS = %i[
215
218
  toc toc_levels section_numbers section_number_levels
216
- lang theme asset_delivery
219
+ lang theme asset_delivery allow_unresolved_includes
217
220
  ].freeze
218
221
  private_constant :CONVERT_OPTIONS
219
222
 
@@ -99,7 +99,10 @@ module Coradoc
99
99
  def apply_options(options)
100
100
  options.each do |key, value|
101
101
  setter = "#{key}="
102
- instance_variable_set("@#{key}", value) if public_methods.include?(setter.to_sym)
102
+ # Use public_send (not instance_variable_set) so we route
103
+ # through the public setter method — encapsulation is
104
+ # preserved and any setter-side validation runs.
105
+ public_send(setter, value) if public_methods.include?(setter.to_sym)
103
106
  end
104
107
  end
105
108
  end
@@ -521,7 +524,7 @@ module Coradoc
521
524
  #
522
525
  # @yield [Configuration]
523
526
  # @return [void]
524
- def self.configure(&block)
525
- Configurable.configure(&block) if block_given?
527
+ def self.configure(&)
528
+ Configurable.configure(&) if block_given?
526
529
  end
527
530
  end
@@ -56,8 +56,8 @@ module Coradoc
56
56
 
57
57
  def registry = FormatCatalog.registry
58
58
 
59
- def register_format(format_name, format_module, **options)
60
- FormatCatalog.register_format(format_name, format_module, **options)
59
+ def register_format(format_name, format_module, **)
60
+ FormatCatalog.register_format(format_name, format_module, **)
61
61
  end
62
62
 
63
63
  def get_format(format_name) = FormatCatalog.get_format(format_name)
@@ -70,6 +70,8 @@ module Coradoc
70
70
 
71
71
  def resolve_includes(document, **) = Pipeline.resolve_includes(document, **)
72
72
 
73
+ def resolve_references(document, **) = Pipeline.resolve_references(document, **)
74
+
73
75
  def rewrite_links(...) = Pipeline.rewrite_links(...)
74
76
 
75
77
  def convert(text, **) = Pipeline.convert(text, **)
@@ -150,6 +152,7 @@ module Coradoc
150
152
  autoload :FormatCatalog, "#{__dir__}/format_catalog"
151
153
  autoload :Introspection, "#{__dir__}/introspection"
152
154
  autoload :Dispatch, "#{__dir__}/dispatch"
155
+ autoload :Reference, "#{__dir__}/reference"
153
156
  end
154
157
 
155
158
  # Format gems self-register via Coradoc.register_format when they are required.
@@ -61,7 +61,7 @@ module Coradoc
61
61
  end
62
62
 
63
63
  def rebuild_children(node)
64
- return node unless node.respond_to?(:children)
64
+ return node unless node.is_a?(CoreModel::HasChildren)
65
65
 
66
66
  original = node.children
67
67
  return node if original.nil?
@@ -79,23 +79,27 @@ module Coradoc
79
79
  original = node.public_send(attr_name)
80
80
  return node if original.nil?
81
81
 
82
- updated = original.is_a?(Array) ?
83
- original.map { |c| visit(c) } :
82
+ updated = if original.is_a?(Array)
83
+ original.map { |c| visit(c) }
84
+ else
84
85
  visit(original)
86
+ end
85
87
  return node if updated == original
86
88
 
87
89
  node.dup.tap { |copy| copy.public_send("#{attr_name}=", updated) }
88
90
  end
89
91
 
90
92
  def rebuild_block(node)
91
- return node unless node.respond_to?(:children)
93
+ return node unless node.is_a?(CoreModel::HasChildren)
92
94
 
93
95
  original_children = node.children
94
96
  return node if original_children.nil?
95
97
 
96
- updated_children = original_children.is_a?(Array) ?
97
- original_children.map { |c| visit(c) } :
98
+ updated_children = if original_children.is_a?(Array)
99
+ original_children.map { |c| visit(c) }
100
+ else
98
101
  visit(original_children)
102
+ end
99
103
 
100
104
  if inline_resolvable?(node)
101
105
  original_content = node.content
@@ -113,7 +117,15 @@ module Coradoc
113
117
  end
114
118
 
115
119
  def inline_resolvable?(node)
116
- node.respond_to?(:content) && node.content.is_a?(String)
120
+ # Capability check: nodes that include ChildrenContent expose a
121
+ # `content` attribute alongside `children`. We resolve attribute
122
+ # references only when content is a String (plain-text inline
123
+ # content). Using is_a?(ChildrenContent) instead of
124
+ # respond_to?(:content) catches type drift early: if a node
125
+ # acquires string content without including ChildrenContent,
126
+ # this method returns false (no resolution) rather than
127
+ # silently resolving on a mis-typed node.
128
+ node.is_a?(CoreModel::ChildrenContent) && node.content.is_a?(String)
117
129
  end
118
130
 
119
131
  # Resolve attribute references inside a flat content string.
@@ -147,7 +147,7 @@ module Coradoc
147
147
  end
148
148
 
149
149
  def flat_text
150
- ""
150
+ ''
151
151
  end
152
152
 
153
153
  # Flatten this element to a plain-text string.
@@ -160,7 +160,50 @@ module Coradoc
160
160
  #
161
161
  # @return [String]
162
162
  def flat_text
163
- ""
163
+ ''
164
+ end
165
+
166
+ # True when this node counts as real body content for empty-body
167
+ # detection and similar structural queries. Default is true;
168
+ # ephemeral and metadata nodes override to false.
169
+ #
170
+ # Override sites: CommentBlock, CommentLine, FrontmatterBlock.
171
+ # Adding a new "skip me" type = overriding this method locally —
172
+ # no central walker to edit (OCP).
173
+ #
174
+ # @return [Boolean]
175
+ def body_content?
176
+ true
177
+ end
178
+
179
+ # True when this node's text content should be processed by text
180
+ # consumers (link rewriters, spell checkers, search indexers, etc.).
181
+ # Default delegates to +body_content?+ — ephemeral/metadata nodes
182
+ # (CommentBlock, FrontmatterBlock, …) are not prose because they
183
+ # are not body content. Verbatim block subclasses (SourceBlock,
184
+ # ListingBlock, LiteralBlock, PassBlock, StemBlock) override this
185
+ # to false because their text is literal, not subject to
186
+ # substitution or spell-checking.
187
+ #
188
+ # Override sites for verbatim: SourceBlock, ListingBlock,
189
+ # LiteralBlock, PassBlock, StemBlock, HorizontalRuleBlock.
190
+ #
191
+ # Distinct from +body_content?+: a SourceBlock IS body content
192
+ # (the doc has real substance) but its text is not prose (a link
193
+ # shaped string inside source is literal text, not a link).
194
+ #
195
+ # @return [Boolean]
196
+ def prose?
197
+ body_content?
198
+ end
199
+
200
+ # True when this node is structurally present but carries no
201
+ # visible characters (e.g., a paragraph whose text strips to
202
+ # empty). Default is false; inline content and paragraphs override.
203
+ #
204
+ # @return [Boolean]
205
+ def whitespace_only?
206
+ false
164
207
  end
165
208
 
166
209
  # Accept a visitor to traverse this element
@@ -20,4 +20,4 @@ module Coradoc
20
20
  attribute :text, :string
21
21
  end
22
22
  end
23
- end
23
+ end
@@ -3,14 +3,35 @@
3
3
  module Coradoc
4
4
  module CoreModel
5
5
  class DefinitionItem < Base
6
+ # Primary term (first term in source order). For multi-term `<dt>`'s
7
+ # (AsciiDoc `term1::\nterm2::`), use {#terms} which carries the
8
+ # full list. `term` is kept as the singular accessor for legacy
9
+ # consumers; new consumers should prefer {#terms}.
6
10
  attribute :term, :string
11
+
12
+ # All terms on this `<dt>`. Single-element for the common case;
13
+ # multi-element for shared-`<dd>` AsciiDoc forms like
14
+ # term1::
15
+ # term2::
16
+ # shared definition
17
+ # Order matches source order. Renderers emit one `<dt>` per entry.
18
+ attribute :terms, :string, collection: true, default: []
19
+
7
20
  attribute :definitions, :string, collection: true
8
21
  attribute :nested, DefinitionList
22
+ # `+`-continuation blocks attached to this dd in AsciiDoc source.
23
+ # Each entry is a CoreModel block (ParagraphBlock, AdmonitionBlock,
24
+ # etc.) that followed the dd after a `+` line — rendered as
25
+ # additional children of the dd.
26
+ attribute :attached_children, Base, collection: true, initialize_empty: true
9
27
 
10
28
  def initialize(args = {})
11
29
  @term_children = args.delete(:term_children) || []
12
30
  @definition_children = args.delete(:definition_children) || []
13
31
  super
32
+ # Backward-compat: if caller passed only `term:` (no `terms:`),
33
+ # seed the collection so #terms is always consistent with #term.
34
+ self.terms = [term].compact if terms.empty? && !term.to_s.empty?
14
35
  end
15
36
 
16
37
  attr_reader :term_children, :definition_children
@@ -40,7 +61,7 @@ module Coradoc
40
61
  private
41
62
 
42
63
  def comparable_attributes
43
- super + %i[term definitions nested term_children definition_children]
64
+ super + %i[term terms definitions nested term_children definition_children attached_children]
44
65
  end
45
66
  end
46
67
  end
@@ -7,6 +7,10 @@ module Coradoc
7
7
  def self.semantic_type
8
8
  :horizontal_rule
9
9
  end
10
+
11
+ def prose?
12
+ false
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -58,7 +58,7 @@ module Coradoc
58
58
  private
59
59
 
60
60
  def matched_offset(trimmed)
61
- %r{\A(?<sign>[+-]?)(?<digits>\d+)\z}.match(trimmed) do |m|
61
+ /\A(?<sign>[+-]?)(?<digits>\d+)\z/.match(trimmed) do |m|
62
62
  digits = m[:digits].to_i
63
63
  signed = m[:sign] == '-' ? -digits : digits
64
64
  mode = m[:sign].empty? ? 'absolute' : 'relative'
@@ -73,6 +73,7 @@ module Coradoc
73
73
 
74
74
  def parse_tags(raw)
75
75
  return [] if raw.nil?
76
+
76
77
  trimmed = raw.strip
77
78
  return [] if trimmed.empty? || trimmed == '*' || trimmed == '**'
78
79
 
@@ -36,6 +36,7 @@ module Coradoc
36
36
 
37
37
  first_idx = content.index { |i| text_carrier?(i) }
38
38
  return content if first_idx.nil?
39
+
39
40
  last_idx = content.rindex { |i| text_carrier?(i) }
40
41
 
41
42
  content.map.with_index do |item, idx|
@@ -119,7 +119,7 @@ module Coradoc
119
119
  # end
120
120
  #
121
121
  # Returns self for chaining at the list level.
122
- def add_item(marker: self.marker_type == 'ordered' ? '.' : '*')
122
+ def add_item(marker: marker_type == 'ordered' ? '.' : '*')
123
123
  item = ListItem.build(marker: marker) { |li| yield li if block_given? }
124
124
  self.items = Array(items) + [item]
125
125
  self
@@ -10,6 +10,10 @@ module Coradoc
10
10
  def self.semantic_type
11
11
  :listing
12
12
  end
13
+
14
+ def prose?
15
+ false
16
+ end
13
17
  end
14
18
  end
15
19
  end
@@ -7,6 +7,10 @@ module Coradoc
7
7
  def self.semantic_type
8
8
  :literal
9
9
  end
10
+
11
+ def prose?
12
+ false
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -7,6 +7,10 @@ module Coradoc
7
7
  def self.semantic_type
8
8
  :pass
9
9
  end
10
+
11
+ def prose?
12
+ false
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -7,6 +7,10 @@ module Coradoc
7
7
  def self.semantic_type
8
8
  :source_code
9
9
  end
10
+
11
+ def prose?
12
+ false
13
+ end
10
14
  end
11
15
  end
12
16
  end
@@ -16,6 +16,10 @@ module Coradoc
16
16
  def self.semantic_type
17
17
  :stem
18
18
  end
19
+
20
+ def prose?
21
+ false
22
+ end
19
23
  end
20
24
  end
21
25
  end
@@ -89,6 +89,7 @@ module Coradoc
89
89
 
90
90
  def apply_default(key)
91
91
  return @default.call(key) if @default
92
+
92
93
  nil
93
94
  end
94
95
  end
@@ -295,6 +295,25 @@ module Coradoc
295
295
  end
296
296
  end
297
297
 
298
+ # Raised when a document still contains unresolved +include::+ edges
299
+ # at serialization time and the target format cannot represent them
300
+ # (it would silently drop the content). Resolve the graph first with
301
+ # +Coradoc.resolve_includes+, or opt out with
302
+ # +allow_unresolved_includes: true+.
303
+ class UnresolvedIncludesError < Error
304
+ attr_reader :targets
305
+
306
+ def initialize(targets)
307
+ @targets = targets
308
+ super(
309
+ "Document has #{targets.size} unresolved include directive(s): " \
310
+ "#{targets.join(', ')}. Resolve them before serializing with " \
311
+ 'Coradoc.resolve_includes(doc, base_dir: ...), or pass ' \
312
+ 'allow_unresolved_includes: true.'
313
+ )
314
+ end
315
+ end
316
+
298
317
  # Error raised when an include chain exceeds the configured depth limit.
299
318
  class IncludeDepthExceededError < Error
300
319
  attr_reader :depth, :target
@@ -18,6 +18,25 @@ module Coradoc
18
18
  end
19
19
 
20
20
  def get_format(format_name)
21
+ registry.get(format_name) || lazy_load_format(format_name)
22
+ end
23
+
24
+ # If a format gem (e.g. coradoc-adoc) is in the bundle but has not
25
+ # been required yet, attempt to require it on first lookup. This
26
+ # removes the friction where `Coradoc.parse(text, format: :asciidoc)`
27
+ # blows up just because the user only wrote `require "coradoc"` and
28
+ # never explicitly required `coradoc/asciidoc`. The require is for
29
+ # an external gem, not internal library code, so it stays within
30
+ # the autoload-over-require_relative rule.
31
+ def lazy_load_format(format_name)
32
+ return nil if format_name.nil? || format_name.to_s.empty?
33
+
34
+ begin
35
+ require "coradoc/#{format_name}"
36
+ rescue LoadError
37
+ return nil
38
+ end
39
+
21
40
  registry.get(format_name)
22
41
  end
23
42
 
@@ -30,6 +30,16 @@ module Coradoc
30
30
  true
31
31
  end
32
32
 
33
+ # Whether the serializer can represent unresolved include edges
34
+ # (graph-mode CoreModel::Include nodes) without losing them.
35
+ # Formats that round-trip the directive natively (asciidoc,
36
+ # mirror) or preserve it explicitly (markdown comments) override
37
+ # this to true; formats that would silently drop the content keep
38
+ # the default false, and the pipeline raises instead of losing it.
39
+ def preserves_unresolved_includes?
40
+ false
41
+ end
42
+
33
43
  # Parse a file to CoreModel, handling file-specific concerns like include resolution.
34
44
  # Format modules that support include directives or file-relative references
35
45
  # should override this method.
data/lib/coradoc/hooks.rb CHANGED
@@ -89,7 +89,7 @@ module Coradoc
89
89
  modified_args = invoke(hook_point, *args, **kwargs)
90
90
  result = yield(*modified_args)
91
91
 
92
- after_point = "after_#{hook_point.to_s.sub('before_', '')}".to_sym
92
+ after_point = :"after_#{hook_point.to_s.sub('before_', '')}"
93
93
  result = invoke(after_point, result, **kwargs) if HOOK_POINTS.key?(after_point)
94
94
 
95
95
  result
@@ -50,8 +50,6 @@ module Coradoc
50
50
  node.level || 1
51
51
  when Coradoc::CoreModel::StructuralElement, Coradoc::CoreModel::Block
52
52
  walk_for_first_level(node.children)
53
- else
54
- nil
55
53
  end
56
54
  end
57
55
 
@@ -11,12 +11,12 @@ module Coradoc
11
11
  # Out-of-bounds clamps gracefully (SPEC 3.4). One-based indexing
12
12
  # (asciidoctor convention).
13
13
  module Lines
14
- SPEC_PART = %r{
14
+ SPEC_PART = /
15
15
  \A
16
16
  (?<start>\d+)
17
17
  (?:\.\.(?<finish>\d+))?
18
18
  \z
19
- }x.freeze
19
+ /x
20
20
 
21
21
  # @param text [String]
22
22
  # @param options [Coradoc::CoreModel::IncludeOptions]
@@ -17,8 +17,8 @@ module Coradoc
17
17
  # Markers may appear on their own line; they must be the first
18
18
  # non-whitespace token on that line (asciidoctor convention).
19
19
  module Tags
20
- MARKER_OPEN = /\A[[:space:]]*(?:\/\/+|#+)[[:space:]]*tag::([^\[\]]+)\[[[:space:]]*\]/
21
- MARKER_CLOSE = /\A[[:space:]]*(?:\/\/+|#+)[[:space:]]*end::([^\[\]]+)\[[[:space:]]*\]/
20
+ MARKER_OPEN = %r{\A[[:space:]]*(?://+|#+)[[:space:]]*tag::([^\[\]]+)\[[[:space:]]*\]}
21
+ MARKER_CLOSE = %r{\A[[:space:]]*(?://+|#+)[[:space:]]*end::([^\[\]]+)\[[[:space:]]*\]}
22
22
 
23
23
  # @param text [String] raw included file content
24
24
  # @param options [Coradoc::CoreModel::IncludeOptions]
@@ -134,5 +134,3 @@ module Coradoc
134
134
  end
135
135
  end
136
136
  end
137
-
138
- require 'set'
@@ -17,10 +17,10 @@ module Coradoc
17
17
  attr_reader :counts
18
18
 
19
19
  def visit(element)
20
- return super(element) unless element.is_a?(CoreModel::Base)
20
+ return super unless element.is_a?(CoreModel::Base)
21
21
 
22
22
  @counts[type_key_for(element)] += 1
23
- super(element)
23
+ super
24
24
  end
25
25
 
26
26
  private
@@ -12,23 +12,14 @@ module Coradoc
12
12
  # new link-bearing subclass means overriding +link_kind+ on it,
13
13
  # not editing a case/when here (OCP).
14
14
  #
15
- # Verbatim block types are also closed: +SourceBlock+, +ListingBlock+,
16
- # +LiteralBlock+, +PassBlock+, +StemBlock+. The visitor returns them
17
- # unchanged so the rewriter never sees link-shaped text that is, in
18
- # fact, raw code/math.
15
+ # Verbatim block recognition is delegated to +CoreModel::Base#prose?+
16
+ # each block class answers "is my text live or literal?". Adding a
17
+ # new verbatim block type means overriding +prose?+ on it, not editing
18
+ # a list here. The visitor is closed for that change.
19
19
  #
20
20
  # Dispatch is class-based (no +respond_to?+ duck-typing). Unrecognized
21
21
  # classes are returned unchanged — the visitor is closed by design.
22
22
  class Visitor
23
- # Verbatim block classes — content is raw, no link semantics.
24
- VERBATIM_TYPES = [
25
- Coradoc::CoreModel::SourceBlock,
26
- Coradoc::CoreModel::ListingBlock,
27
- Coradoc::CoreModel::LiteralBlock,
28
- Coradoc::CoreModel::PassBlock,
29
- Coradoc::CoreModel::StemBlock
30
- ].freeze
31
-
32
23
  # Structural/container classes that own a child collection. Each
33
24
  # entry maps the class to the reader method that exposes its
34
25
  # children. MECE — every "recurse into the children" case lands
@@ -68,7 +59,7 @@ module Coradoc
68
59
  private
69
60
 
70
61
  def visit_subtree(node)
71
- return node if VERBATIM_TYPES.any? { |type| node.is_a?(type) }
62
+ return node unless node.prose?
72
63
  return rewrite_inline(node) if node.is_a?(Coradoc::CoreModel::InlineElement)
73
64
 
74
65
  reader = reader_for(node)
@@ -42,8 +42,65 @@ module Coradoc
42
42
  )
43
43
  end
44
44
 
45
- def rewrite_links(document, rewriter: nil, &block)
46
- Coradoc::LinkRewriter.rewrite(document, rewriter: rewriter, &block)
45
+ def rewrite_links(document, rewriter: nil, &)
46
+ Coradoc::LinkRewriter.rewrite(document, rewriter: rewriter, &)
47
+ end
48
+
49
+ # Resolve every reference (xref, citation, link, include, image,
50
+ # footnote) in a parsed document using a unified content-graph
51
+ # model. Mirrors +resolve_includes+ in shape: two-step, immutable.
52
+ #
53
+ # Step one always runs: every Edge is resolved through the
54
+ # Resolver and the +missing+/+ambiguous+ policies are enforced
55
+ # (raise or warn). Step two is opt-in: with +materialize: true+
56
+ # the tree is rebuilt with each Edge replaced by the output of
57
+ # the Materializer registered for its
58
+ # [kind, presentation, format] tuple.
59
+ #
60
+ # The input document is never mutated. With +materialize: false+
61
+ # the input document itself is returned; with +materialize: true+
62
+ # a new document is returned that structurally shares untouched
63
+ # subtrees with the input (treat both as immutable). Nodes whose
64
+ # kind has no registered materializer are preserved unchanged.
65
+ #
66
+ # @param document [CoreModel::Base] parsed document
67
+ # @param catalog [Reference::Catalog::*] index of addressable Content
68
+ # @param presentation [Reference::Presentation::Base] slicing and ordering
69
+ # @param resolver [Reference::Resolver::Base, nil] defaults to CatalogResolver
70
+ # @param missing [Symbol] :warn (default), :silent, :error, :passthrough
71
+ # @param ambiguous [Symbol] :disambiguate (default), :first, :error
72
+ # @param materialize [Boolean] when true, replace edges with rendered inlines
73
+ # @param format [Symbol, nil] target format for materializer lookup
74
+ # (:html, :asciidoc, ...); nil matches format-agnostic materializers
75
+ # @return [CoreModel::Base] the input document (validation only) or
76
+ # a new materialized document
77
+ #
78
+ # @example Resolve cross-references into HTML links
79
+ # doc = Coradoc.parse(text, format: :asciidoc)
80
+ # catalog = Coradoc::Reference::Catalog::Local.from_doc(doc)
81
+ # presentation = Coradoc::Reference::Presentation::SingleDocument.new
82
+ # resolved = Coradoc.resolve_references(
83
+ # doc,
84
+ # catalog: catalog,
85
+ # presentation: presentation,
86
+ # materialize: true,
87
+ # format: :html
88
+ # )
89
+ def resolve_references(document, catalog:, presentation:,
90
+ resolver: nil,
91
+ missing: :warn,
92
+ ambiguous: :disambiguate,
93
+ materialize: false,
94
+ format: nil)
95
+ Coradoc::Reference::Resolution.new(
96
+ catalog: catalog,
97
+ presentation: presentation,
98
+ resolver: resolver,
99
+ missing: missing,
100
+ ambiguous: ambiguous,
101
+ materialize: materialize,
102
+ format: format
103
+ ).call(document)
47
104
  end
48
105
 
49
106
  def convert(text, from:, to:, **)
@@ -63,17 +120,19 @@ module Coradoc
63
120
  raise TransformationError, "No transformer found for #{model.class}"
64
121
  end
65
122
 
66
- def serialize(model, to:, **)
123
+ def serialize(model, to:, allow_unresolved_includes: false, **)
67
124
  format_module = FormatCatalog.get_format(to)
68
125
  raise UnsupportedFormatError.new(to, available: FormatCatalog.registered_formats) unless format_module
69
126
 
127
+ Coradoc::Validation.guard_unresolved_includes!(model, format_module) unless allow_unresolved_includes
128
+
70
129
  model = Hooks.invoke(:before_serialize, model, format: to)
71
130
  result = format_module.serialize(model, **)
72
131
  Hooks.invoke(:after_serialize, result, format: to)
73
132
  end
74
133
 
75
- def build(&block)
76
- CoreModel::DocumentElement.build(children: [], &block)
134
+ def build(&)
135
+ CoreModel::DocumentElement.build(children: [], &)
77
136
  end
78
137
 
79
138
  def parse_file(path, format: nil)