coradoc 2.0.28 → 2.0.30
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 +4 -4
- data/lib/coradoc/cli.rb +4 -1
- data/lib/coradoc/coradoc.rb +3 -0
- data/lib/coradoc/core_model/list_block.rb +45 -60
- data/lib/coradoc/errors.rb +19 -0
- data/lib/coradoc/format_module.rb +10 -0
- data/lib/coradoc/pipeline.rb +60 -1
- data/lib/coradoc/reference/address/anchor.rb +48 -0
- data/lib/coradoc/reference/address/doi.rb +40 -0
- data/lib/coradoc/reference/address/isbn.rb +39 -0
- data/lib/coradoc/reference/address/path.rb +66 -0
- data/lib/coradoc/reference/address/scoped_path.rb +47 -0
- data/lib/coradoc/reference/address/url.rb +41 -0
- data/lib/coradoc/reference/address.rb +153 -0
- data/lib/coradoc/reference/catalog/composite.rb +59 -0
- data/lib/coradoc/reference/catalog/local.rb +97 -0
- data/lib/coradoc/reference/catalog/memory_index.rb +59 -0
- data/lib/coradoc/reference/catalog.rb +33 -0
- data/lib/coradoc/reference/edge/citation_options.rb +16 -0
- data/lib/coradoc/reference/edge/footnote_ref_options.rb +13 -0
- data/lib/coradoc/reference/edge/image_ref_options.rb +15 -0
- data/lib/coradoc/reference/edge/include_options.rb +15 -0
- data/lib/coradoc/reference/edge/kind.rb +72 -0
- data/lib/coradoc/reference/edge/link_options.rb +12 -0
- data/lib/coradoc/reference/edge/navigation_options.rb +12 -0
- data/lib/coradoc/reference/edge/options.rb +13 -0
- data/lib/coradoc/reference/edge.rb +80 -0
- data/lib/coradoc/reference/edge_search.rb +128 -0
- data/lib/coradoc/reference/materializer/base.rb +44 -0
- data/lib/coradoc/reference/materializer/passthrough.rb +31 -0
- data/lib/coradoc/reference/materializer/registry.rb +116 -0
- data/lib/coradoc/reference/materializer.rb +14 -0
- data/lib/coradoc/reference/presentation/base.rb +47 -0
- data/lib/coradoc/reference/presentation/custom_hierarchy.rb +104 -0
- data/lib/coradoc/reference/presentation/page.rb +20 -0
- data/lib/coradoc/reference/presentation/single_document.rb +35 -0
- data/lib/coradoc/reference/presentation/split_pages.rb +118 -0
- data/lib/coradoc/reference/presentation.rb +16 -0
- data/lib/coradoc/reference/resolution.rb +184 -0
- data/lib/coradoc/reference/resolver/base.rb +15 -0
- data/lib/coradoc/reference/resolver/caching.rb +37 -0
- data/lib/coradoc/reference/resolver/catalog.rb +69 -0
- data/lib/coradoc/reference/resolver/chain.rb +33 -0
- data/lib/coradoc/reference/resolver.rb +20 -0
- data/lib/coradoc/reference/result/ambiguous.rb +23 -0
- data/lib/coradoc/reference/result/base.rb +46 -0
- data/lib/coradoc/reference/result/missing.rb +14 -0
- data/lib/coradoc/reference/result/resolved.rb +22 -0
- data/lib/coradoc/reference/result.rb +20 -0
- data/lib/coradoc/reference.rb +65 -0
- data/lib/coradoc/validation.rb +40 -0
- data/lib/coradoc/version.rb +1 -1
- metadata +44 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
# Orchestrator: resolves every Edge in the tree via the Resolver,
|
|
6
|
+
# enforces the missing/ambiguous policies, and — only when asked
|
|
7
|
+
# to materialize — rebuilds the tree with rendered inline nodes
|
|
8
|
+
# via the Presentation layout and the Materializer registry.
|
|
9
|
+
#
|
|
10
|
+
# Two-step contract:
|
|
11
|
+
# - +materialize: false+ validates references and returns the
|
|
12
|
+
# INPUT document (never mutated, never copied).
|
|
13
|
+
# - +materialize: true+ returns a NEW document. Untouched subtrees
|
|
14
|
+
# are shared with the input (structural sharing) — treat both as
|
|
15
|
+
# immutable.
|
|
16
|
+
#
|
|
17
|
+
# Wired from the public API +Coradoc.resolve_references+.
|
|
18
|
+
class Resolution
|
|
19
|
+
attr_reader :catalog, :presentation, :resolver, :missing_policy,
|
|
20
|
+
:ambiguous_policy, :materialize_policy, :format,
|
|
21
|
+
:materializer_registry
|
|
22
|
+
|
|
23
|
+
def initialize(catalog:, presentation:, missing:, ambiguous:,
|
|
24
|
+
materialize:, resolver: nil, format: nil,
|
|
25
|
+
materializer_registry: default_registry)
|
|
26
|
+
@catalog = catalog
|
|
27
|
+
@presentation = presentation
|
|
28
|
+
@missing_policy = missing
|
|
29
|
+
@ambiguous_policy = ambiguous
|
|
30
|
+
@materialize_policy = materialize
|
|
31
|
+
@format = format
|
|
32
|
+
@materializer_registry = materializer_registry
|
|
33
|
+
@resolver = resolver || Resolver::Catalog.new(
|
|
34
|
+
catalog: catalog, ambiguous: ambiguous, missing: missing
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def call(document)
|
|
39
|
+
results = resolve_all_edges(document)
|
|
40
|
+
report_missing(results)
|
|
41
|
+
return document unless materialize_policy
|
|
42
|
+
|
|
43
|
+
materialize_tree(document, results)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Keyed by the Edge itself (value equality), so every distinct
|
|
49
|
+
# edge gets its own Result while identical edges resolve once.
|
|
50
|
+
def resolve_all_edges(document)
|
|
51
|
+
results = {}
|
|
52
|
+
EdgeSearch.each_edge(document) do |_parent, edge|
|
|
53
|
+
results[edge] = resolver.resolve(edge)
|
|
54
|
+
end
|
|
55
|
+
results
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def report_missing(results)
|
|
59
|
+
results.each_value do |result|
|
|
60
|
+
next unless result.missing?
|
|
61
|
+
|
|
62
|
+
case missing_policy
|
|
63
|
+
when :error
|
|
64
|
+
raise Coradoc::Reference::MissingReferenceError.new(
|
|
65
|
+
address: result.address
|
|
66
|
+
)
|
|
67
|
+
when :warn
|
|
68
|
+
Coradoc::Logger.warn("Reference not found: #{result.address}")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def materialize_tree(document, results)
|
|
74
|
+
pages = presentation.layout(document)
|
|
75
|
+
ReplaceWalker.new(
|
|
76
|
+
pages: pages,
|
|
77
|
+
results: results,
|
|
78
|
+
presentation: presentation,
|
|
79
|
+
registry: materializer_registry,
|
|
80
|
+
missing_policy: missing_policy,
|
|
81
|
+
format: format
|
|
82
|
+
).visit(document)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def default_registry
|
|
86
|
+
Materializer::Registry.new
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Rebuilds the tree, replacing every Edge-bearing node with the
|
|
90
|
+
# materializer's output. The walker is single-purpose and only
|
|
91
|
+
# lives inside Resolution — no need for a separate file.
|
|
92
|
+
class ReplaceWalker
|
|
93
|
+
attr_reader :pages, :results, :presentation, :registry,
|
|
94
|
+
:missing_policy, :format
|
|
95
|
+
|
|
96
|
+
def initialize(pages:, results:, presentation:, registry:,
|
|
97
|
+
missing_policy:, format:)
|
|
98
|
+
@pages = pages
|
|
99
|
+
@results = results
|
|
100
|
+
@presentation = presentation
|
|
101
|
+
@registry = registry
|
|
102
|
+
@missing_policy = missing_policy
|
|
103
|
+
@format = format
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def visit(node)
|
|
107
|
+
return node unless node.is_a?(Coradoc::CoreModel::Base)
|
|
108
|
+
|
|
109
|
+
replaced = replace_node(node)
|
|
110
|
+
return replaced unless replaced.is_a?(Coradoc::CoreModel::HasChildren)
|
|
111
|
+
|
|
112
|
+
rebuild_children_of(replaced)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def replace_node(node)
|
|
118
|
+
edges = EdgeSearch.edges_for(node)
|
|
119
|
+
return node if edges.empty?
|
|
120
|
+
|
|
121
|
+
materialize_edge(node, edges.first)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def materialize_edge(node, edge)
|
|
125
|
+
result = results[edge]
|
|
126
|
+
return drop_or_keep(node) if result.nil? || result.missing?
|
|
127
|
+
|
|
128
|
+
invoke_materializer(node, edge, result)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# :silent drops the unresolved node; :warn (already logged) and
|
|
132
|
+
# :passthrough keep the original node so round-tripping is safe.
|
|
133
|
+
def drop_or_keep(node)
|
|
134
|
+
return nil if missing_policy == :silent
|
|
135
|
+
|
|
136
|
+
node
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def invoke_materializer(node, edge, result)
|
|
140
|
+
klass = lookup_materializer(edge)
|
|
141
|
+
return node unless klass
|
|
142
|
+
|
|
143
|
+
klass.new.materialize(
|
|
144
|
+
edge: edge,
|
|
145
|
+
result: result,
|
|
146
|
+
node: node,
|
|
147
|
+
presentation: presentation,
|
|
148
|
+
pages: pages
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def lookup_materializer(edge)
|
|
153
|
+
registry.lookup(
|
|
154
|
+
kind: edge.kind.to_sym,
|
|
155
|
+
presentation: presentation.key,
|
|
156
|
+
format: format || :any
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def rebuild_children_of(node)
|
|
161
|
+
children = node.children
|
|
162
|
+
return node unless children
|
|
163
|
+
return node if children.empty?
|
|
164
|
+
|
|
165
|
+
new_children = children.filter_map { |c| visit(c) }
|
|
166
|
+
rebuild_with(node, new_children)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def rebuild_with(node, new_children)
|
|
170
|
+
return node if identical_children?(new_children, node.children)
|
|
171
|
+
|
|
172
|
+
duplicate = node.dup
|
|
173
|
+
duplicate.children = new_children
|
|
174
|
+
duplicate
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def identical_children?(new_children, old_children)
|
|
178
|
+
new_children.length == old_children.length &&
|
|
179
|
+
new_children.each_with_index.all? { |c, i| c.equal?(old_children[i]) }
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Resolver
|
|
6
|
+
# Protocol base. Concrete resolvers implement +#resolve(edge)+
|
|
7
|
+
# and return a Result sum type instance — never +nil+.
|
|
8
|
+
class Base
|
|
9
|
+
def resolve(edge)
|
|
10
|
+
raise NotImplementedError
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Resolver
|
|
6
|
+
# Wraps another resolver with an address-keyed memoization cache.
|
|
7
|
+
# Addresses are value types, so cache keys are stable across calls.
|
|
8
|
+
# Use to avoid re-asking the catalog for the same Edge.address.
|
|
9
|
+
class Caching < Resolver::Base
|
|
10
|
+
attr_reader :inner
|
|
11
|
+
|
|
12
|
+
def initialize(inner:)
|
|
13
|
+
super()
|
|
14
|
+
@inner = inner
|
|
15
|
+
@cache = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def resolve(edge)
|
|
19
|
+
cached = if @cache.key?(edge.address)
|
|
20
|
+
@cache[edge.address]
|
|
21
|
+
else
|
|
22
|
+
@cache[edge.address] = @inner.resolve(edge)
|
|
23
|
+
end
|
|
24
|
+
cached.for_edge(edge)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def clear!
|
|
28
|
+
@cache.clear
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def size
|
|
32
|
+
@cache.size
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Resolver
|
|
6
|
+
# Asks one catalog. Applies the +ambiguous:+ policy in-line.
|
|
7
|
+
# Does NOT apply the +missing:+ policy — that's the caller's
|
|
8
|
+
# call (Resolution orchestrator) — but it does surface the
|
|
9
|
+
# Missing Result so the caller can decide.
|
|
10
|
+
class Catalog < Resolver::Base
|
|
11
|
+
attr_reader :catalog, :ambiguous_policy, :missing_policy
|
|
12
|
+
|
|
13
|
+
def initialize(catalog:, ambiguous: :disambiguate, missing: :warn)
|
|
14
|
+
super()
|
|
15
|
+
@catalog = catalog
|
|
16
|
+
@ambiguous_policy = ambiguous
|
|
17
|
+
@missing_policy = missing
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def resolve(edge)
|
|
21
|
+
address = edge.address
|
|
22
|
+
return missing_for(edge, address) unless catalog.recognizes_scheme?(address.scheme)
|
|
23
|
+
|
|
24
|
+
result = catalog.lookup(address)
|
|
25
|
+
return missing_for(edge, address) if result.nil?
|
|
26
|
+
|
|
27
|
+
return resolved_for(edge, address, result) unless result.is_a?(Array)
|
|
28
|
+
|
|
29
|
+
resolve_ambiguous(edge, address, result)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def resolved_for(edge, address, target)
|
|
35
|
+
Coradoc::Reference::Result::Resolved.build(
|
|
36
|
+
edge: edge, address: address, target: target
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def resolve_ambiguous(edge, address, candidates)
|
|
41
|
+
return ambiguous_error(address, candidates) if ambiguous_policy == :error
|
|
42
|
+
return resolved_for(edge, address, candidates.first) if ambiguous_policy == :first
|
|
43
|
+
|
|
44
|
+
# :disambiguate — warn, then deterministically take the first
|
|
45
|
+
Coradoc::Logger.warn(
|
|
46
|
+
"Reference #{address} is ambiguous " \
|
|
47
|
+
"(#{candidates.size} candidates); using first"
|
|
48
|
+
)
|
|
49
|
+
resolved_for(edge, address, candidates.first)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ambiguous_error(address, candidates)
|
|
53
|
+
raise Coradoc::Reference::AmbiguousReferenceError,
|
|
54
|
+
"Address #{address} matched #{candidates.size} candidates"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def missing_for(edge, address)
|
|
58
|
+
case missing_policy
|
|
59
|
+
when :error
|
|
60
|
+
raise Coradoc::Reference::MissingReferenceError,
|
|
61
|
+
"Address #{address} not found"
|
|
62
|
+
else
|
|
63
|
+
Coradoc::Reference::Result::Missing.build(edge: edge, address: address)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Resolver
|
|
6
|
+
# Tries each child resolver in order. First non-Missing result
|
|
7
|
+
# wins (Resolved or Ambiguous short-circuit). Use to combine a
|
|
8
|
+
# local catalog with a remote one without composite catalog
|
|
9
|
+
# indirection.
|
|
10
|
+
class Chain < Resolver::Base
|
|
11
|
+
attr_reader :resolvers
|
|
12
|
+
|
|
13
|
+
def initialize(*resolvers)
|
|
14
|
+
super()
|
|
15
|
+
@resolvers = resolvers.flatten
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def resolve(edge)
|
|
19
|
+
last = nil
|
|
20
|
+
@resolvers.each do |resolver|
|
|
21
|
+
result = resolver.resolve(edge)
|
|
22
|
+
return result unless result.is_a?(Coradoc::Reference::Result::Missing)
|
|
23
|
+
|
|
24
|
+
last = result
|
|
25
|
+
end
|
|
26
|
+
last || Coradoc::Reference::Result::Missing.build(
|
|
27
|
+
edge: edge, address: edge.address
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
# Kind-agnostic resolver: given an Edge, ask the catalog, return a
|
|
6
|
+
# Result. Never materializes, never slices, never orders.
|
|
7
|
+
#
|
|
8
|
+
# resolver = Resolver::Catalog.new(catalog: catalog, ambiguous: :first, missing: :warn)
|
|
9
|
+
# case resolver.resolve(edge)
|
|
10
|
+
# in Result::Resolved => r ; use_target(r.target)
|
|
11
|
+
# in Result::Missing ; warn("could not resolve #{edge.address}")
|
|
12
|
+
# end
|
|
13
|
+
module Resolver
|
|
14
|
+
autoload :Base, "#{__dir__}/resolver/base"
|
|
15
|
+
autoload :Catalog, "#{__dir__}/resolver/catalog"
|
|
16
|
+
autoload :Chain, "#{__dir__}/resolver/chain"
|
|
17
|
+
autoload :Caching, "#{__dir__}/resolver/caching"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Result
|
|
6
|
+
# Multiple candidates matched. The catalog returned an Array.
|
|
7
|
+
# Callers' +ambiguous:+ policy decides what to do.
|
|
8
|
+
class Ambiguous < Base
|
|
9
|
+
attribute :candidates, Coradoc::CoreModel::Base, collection: true
|
|
10
|
+
|
|
11
|
+
def self.build(edge:, address:, candidates:)
|
|
12
|
+
new(edge: edge, address: address, candidates: candidates)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def result_data
|
|
18
|
+
{ candidates: candidates }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Reference
|
|
7
|
+
module Result
|
|
8
|
+
# Base class for resolution outcomes. Subclasses are value types.
|
|
9
|
+
# Every Result carries the Edge that asked — so callers can log,
|
|
10
|
+
# trace, and re-route without re-resolving.
|
|
11
|
+
class Base < Lutaml::Model::Serializable
|
|
12
|
+
attribute :edge, Coradoc::Reference::Edge
|
|
13
|
+
attribute :address, Coradoc::Reference::Address
|
|
14
|
+
|
|
15
|
+
def resolved?
|
|
16
|
+
is_a?(Result::Resolved)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ambiguous?
|
|
20
|
+
is_a?(Result::Ambiguous)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def missing?
|
|
24
|
+
is_a?(Result::Missing)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# This Result as seen by +edge+. Returns self when the edge is
|
|
28
|
+
# value-equal to the one embedded; otherwise rebuilds the same
|
|
29
|
+
# outcome (target/candidates preserved) for the asking edge.
|
|
30
|
+
# Used by caching resolvers so two edges sharing an Address
|
|
31
|
+
# never see each other's identity.
|
|
32
|
+
def for_edge(edge)
|
|
33
|
+
return self if self.edge == edge
|
|
34
|
+
|
|
35
|
+
self.class.build(edge: edge, address: address, **result_data)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def result_data
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Result
|
|
6
|
+
# No catalog knows this address. Caller's +missing:+ policy decides.
|
|
7
|
+
class Missing < Base
|
|
8
|
+
def self.build(edge:, address:)
|
|
9
|
+
new(edge: edge, address: address)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Result
|
|
6
|
+
# Catalog knew the address; exactly one Content was returned.
|
|
7
|
+
class Resolved < Base
|
|
8
|
+
attribute :target, Coradoc::CoreModel::Base
|
|
9
|
+
|
|
10
|
+
def self.build(edge:, address:, target:)
|
|
11
|
+
new(edge: edge, address: address, target: target)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def result_data
|
|
17
|
+
{ target: target }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
# Sum type for resolution outcomes. Callers pattern-match on the
|
|
6
|
+
# concrete subclass — never check +nil+.
|
|
7
|
+
#
|
|
8
|
+
# case resolver.resolve(edge)
|
|
9
|
+
# in Result::Resolved => r # have the target
|
|
10
|
+
# in Result::Ambiguous => a # multiple candidates
|
|
11
|
+
# in Result::Missing => m # catalog does not know
|
|
12
|
+
# end
|
|
13
|
+
module Result
|
|
14
|
+
autoload :Base, "#{__dir__}/result/base"
|
|
15
|
+
autoload :Resolved, "#{__dir__}/result/resolved"
|
|
16
|
+
autoload :Ambiguous, "#{__dir__}/result/ambiguous"
|
|
17
|
+
autoload :Missing, "#{__dir__}/result/missing"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'coradoc/errors'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
# Unified content-graph reference resolution.
|
|
7
|
+
#
|
|
8
|
+
# Every referenceable thing in a document is a Content node; every
|
|
9
|
+
# reference is a directed Edge with a kind. The four primitives:
|
|
10
|
+
#
|
|
11
|
+
# Content — CoreModel::Base with an id (already on Base).
|
|
12
|
+
# Document — Content that contains Content (composite).
|
|
13
|
+
# Reference::Edge — directed {source Content, target Address, kind}.
|
|
14
|
+
# Reference::Address — scheme-aware locator (anchor, path, url, ...).
|
|
15
|
+
#
|
|
16
|
+
# Same Content graph → N Presentations → M Materializers. The public
|
|
17
|
+
# entry point is +Coradoc.resolve_references+.
|
|
18
|
+
module Reference
|
|
19
|
+
# Base class for reference-resolution errors. All subclasses inherit
|
|
20
|
+
# from here so callers can rescue the family with one +rescue+ clause.
|
|
21
|
+
class Error < Coradoc::Error; end
|
|
22
|
+
|
|
23
|
+
# Raised when no catalog knew the requested Address and the
|
|
24
|
+
# +missing:+ policy is :error.
|
|
25
|
+
class MissingReferenceError < Error
|
|
26
|
+
attr_reader :address
|
|
27
|
+
|
|
28
|
+
def initialize(message = nil, address: nil)
|
|
29
|
+
@address = address
|
|
30
|
+
super(message || "Reference not found: #{address}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Raised when multiple catalogs matched an Address and the
|
|
35
|
+
# +ambiguous:+ policy is :error.
|
|
36
|
+
class AmbiguousReferenceError < Error
|
|
37
|
+
attr_reader :address, :candidates
|
|
38
|
+
|
|
39
|
+
def initialize(message = nil, address: nil, candidates: nil)
|
|
40
|
+
@address = address
|
|
41
|
+
@candidates = candidates
|
|
42
|
+
super(message || "Reference is ambiguous: #{address}")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Raised when the catalog index is malformed — a programmer
|
|
47
|
+
# error, not a runtime condition. Surfaces as a clear message
|
|
48
|
+
# rather than a vague NoMethodError.
|
|
49
|
+
class InvalidCatalogError < Error; end
|
|
50
|
+
|
|
51
|
+
# Raised when building an Edge whose kind was never registered.
|
|
52
|
+
# External kinds register via +Edge.register_kind+ (OCP).
|
|
53
|
+
class UnknownKindError < Error; end
|
|
54
|
+
|
|
55
|
+
autoload :Address, "#{__dir__}/reference/address"
|
|
56
|
+
autoload :Edge, "#{__dir__}/reference/edge"
|
|
57
|
+
autoload :Catalog, "#{__dir__}/reference/catalog"
|
|
58
|
+
autoload :Result, "#{__dir__}/reference/result"
|
|
59
|
+
autoload :Resolver, "#{__dir__}/reference/resolver"
|
|
60
|
+
autoload :Presentation, "#{__dir__}/reference/presentation"
|
|
61
|
+
autoload :Materializer, "#{__dir__}/reference/materializer"
|
|
62
|
+
autoload :EdgeSearch, "#{__dir__}/reference/edge_search"
|
|
63
|
+
autoload :Resolution, "#{__dir__}/reference/resolution"
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/coradoc/validation.rb
CHANGED
|
@@ -28,6 +28,46 @@ module Coradoc
|
|
|
28
28
|
# end
|
|
29
29
|
#
|
|
30
30
|
module Validation
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# Targets of every unresolved +include::+ edge in the tree
|
|
34
|
+
# (deduplicated, in document order). A document is "unresolved"
|
|
35
|
+
# when it was parsed in graph mode and never went through
|
|
36
|
+
# +Coradoc.resolve_includes+.
|
|
37
|
+
def unresolved_include_targets(model)
|
|
38
|
+
targets = []
|
|
39
|
+
walk_nodes(model) do |node|
|
|
40
|
+
targets << node.target if node.is_a?(Coradoc::CoreModel::Include)
|
|
41
|
+
end
|
|
42
|
+
targets.uniq
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Serialization boundary check: formats that cannot represent an
|
|
46
|
+
# unresolved include edge (FormatModule::Interface
|
|
47
|
+
# #preserves_unresolved_includes? == false) must not silently drop
|
|
48
|
+
# the content — raise UnresolvedIncludesError instead, pointing at
|
|
49
|
+
# the hydration step (resolve_includes).
|
|
50
|
+
def guard_unresolved_includes!(model, format_module)
|
|
51
|
+
return if format_module.preserves_unresolved_includes?
|
|
52
|
+
|
|
53
|
+
targets = unresolved_include_targets(model)
|
|
54
|
+
return if targets.empty?
|
|
55
|
+
|
|
56
|
+
raise Coradoc::UnresolvedIncludesError, targets
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def walk_nodes(node, &block)
|
|
60
|
+
return unless node.is_a?(Coradoc::CoreModel::Base)
|
|
61
|
+
|
|
62
|
+
yield node
|
|
63
|
+
return unless node.is_a?(Coradoc::CoreModel::HasChildren)
|
|
64
|
+
|
|
65
|
+
children = node.children
|
|
66
|
+
return unless children
|
|
67
|
+
|
|
68
|
+
children.each { |child| walk_nodes(child, &block) }
|
|
69
|
+
end
|
|
70
|
+
|
|
31
71
|
# A single validation error
|
|
32
72
|
class Error
|
|
33
73
|
attr_reader :path, :message, :code, :element
|
data/lib/coradoc/version.rb
CHANGED
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.
|
|
4
|
+
version: 2.0.30
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -131,6 +131,49 @@ files:
|
|
|
131
131
|
- lib/coradoc/performance_regression.rb
|
|
132
132
|
- lib/coradoc/pipeline.rb
|
|
133
133
|
- lib/coradoc/query.rb
|
|
134
|
+
- lib/coradoc/reference.rb
|
|
135
|
+
- lib/coradoc/reference/address.rb
|
|
136
|
+
- lib/coradoc/reference/address/anchor.rb
|
|
137
|
+
- lib/coradoc/reference/address/doi.rb
|
|
138
|
+
- lib/coradoc/reference/address/isbn.rb
|
|
139
|
+
- lib/coradoc/reference/address/path.rb
|
|
140
|
+
- lib/coradoc/reference/address/scoped_path.rb
|
|
141
|
+
- lib/coradoc/reference/address/url.rb
|
|
142
|
+
- lib/coradoc/reference/catalog.rb
|
|
143
|
+
- lib/coradoc/reference/catalog/composite.rb
|
|
144
|
+
- lib/coradoc/reference/catalog/local.rb
|
|
145
|
+
- lib/coradoc/reference/catalog/memory_index.rb
|
|
146
|
+
- lib/coradoc/reference/edge.rb
|
|
147
|
+
- lib/coradoc/reference/edge/citation_options.rb
|
|
148
|
+
- lib/coradoc/reference/edge/footnote_ref_options.rb
|
|
149
|
+
- lib/coradoc/reference/edge/image_ref_options.rb
|
|
150
|
+
- lib/coradoc/reference/edge/include_options.rb
|
|
151
|
+
- lib/coradoc/reference/edge/kind.rb
|
|
152
|
+
- lib/coradoc/reference/edge/link_options.rb
|
|
153
|
+
- lib/coradoc/reference/edge/navigation_options.rb
|
|
154
|
+
- lib/coradoc/reference/edge/options.rb
|
|
155
|
+
- lib/coradoc/reference/edge_search.rb
|
|
156
|
+
- lib/coradoc/reference/materializer.rb
|
|
157
|
+
- lib/coradoc/reference/materializer/base.rb
|
|
158
|
+
- lib/coradoc/reference/materializer/passthrough.rb
|
|
159
|
+
- lib/coradoc/reference/materializer/registry.rb
|
|
160
|
+
- lib/coradoc/reference/presentation.rb
|
|
161
|
+
- lib/coradoc/reference/presentation/base.rb
|
|
162
|
+
- lib/coradoc/reference/presentation/custom_hierarchy.rb
|
|
163
|
+
- lib/coradoc/reference/presentation/page.rb
|
|
164
|
+
- lib/coradoc/reference/presentation/single_document.rb
|
|
165
|
+
- lib/coradoc/reference/presentation/split_pages.rb
|
|
166
|
+
- lib/coradoc/reference/resolution.rb
|
|
167
|
+
- lib/coradoc/reference/resolver.rb
|
|
168
|
+
- lib/coradoc/reference/resolver/base.rb
|
|
169
|
+
- lib/coradoc/reference/resolver/caching.rb
|
|
170
|
+
- lib/coradoc/reference/resolver/catalog.rb
|
|
171
|
+
- lib/coradoc/reference/resolver/chain.rb
|
|
172
|
+
- lib/coradoc/reference/result.rb
|
|
173
|
+
- lib/coradoc/reference/result/ambiguous.rb
|
|
174
|
+
- lib/coradoc/reference/result/base.rb
|
|
175
|
+
- lib/coradoc/reference/result/missing.rb
|
|
176
|
+
- lib/coradoc/reference/result/resolved.rb
|
|
134
177
|
- lib/coradoc/registry.rb
|
|
135
178
|
- lib/coradoc/relative_path.rb
|
|
136
179
|
- lib/coradoc/resolve_includes.rb
|