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.
- checksums.yaml +4 -4
- data/lib/coradoc/cli.rb +9 -6
- data/lib/coradoc/configurable.rb +6 -3
- data/lib/coradoc/coradoc.rb +5 -2
- data/lib/coradoc/core_model/attribute_reference_resolver.rb +19 -7
- data/lib/coradoc/core_model/base.rb +45 -2
- data/lib/coradoc/core_model/comment_line.rb +1 -1
- data/lib/coradoc/core_model/definition_item.rb +22 -1
- data/lib/coradoc/core_model/horizontal_rule_block.rb +4 -0
- data/lib/coradoc/core_model/include_level_offset.rb +1 -1
- data/lib/coradoc/core_model/include_options.rb +1 -0
- data/lib/coradoc/core_model/inline_content.rb +1 -0
- data/lib/coradoc/core_model/list_block.rb +1 -1
- data/lib/coradoc/core_model/listing_block.rb +4 -0
- data/lib/coradoc/core_model/literal_block.rb +4 -0
- data/lib/coradoc/core_model/pass_block.rb +4 -0
- data/lib/coradoc/core_model/source_block.rb +4 -0
- data/lib/coradoc/core_model/stem_block.rb +4 -0
- data/lib/coradoc/dispatch.rb +1 -0
- data/lib/coradoc/errors.rb +19 -0
- data/lib/coradoc/format_catalog.rb +19 -0
- data/lib/coradoc/format_module.rb +10 -0
- data/lib/coradoc/hooks.rb +1 -1
- data/lib/coradoc/include_selectors/level_offset.rb +0 -2
- data/lib/coradoc/include_selectors/lines.rb +2 -2
- data/lib/coradoc/include_selectors/tags.rb +2 -4
- data/lib/coradoc/introspection/element_counter.rb +2 -2
- data/lib/coradoc/link_rewriter/visitor.rb +5 -14
- data/lib/coradoc/pipeline.rb +64 -5
- data/lib/coradoc/query.rb +8 -8
- 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/registry.rb +6 -6
- data/lib/coradoc/resolve_includes.rb +2 -2
- data/lib/coradoc/validation.rb +44 -4
- data/lib/coradoc/version.rb +1 -1
- data/lib/coradoc.rb +1 -1
- metadata +45 -1
data/lib/coradoc/query.rb
CHANGED
|
@@ -234,8 +234,8 @@ module Coradoc
|
|
|
234
234
|
@elements = Array(elements).compact
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
-
def each(&
|
|
238
|
-
@elements.each(&
|
|
237
|
+
def each(&)
|
|
238
|
+
@elements.each(&)
|
|
239
239
|
end
|
|
240
240
|
|
|
241
241
|
def [](index)
|
|
@@ -272,16 +272,16 @@ module Coradoc
|
|
|
272
272
|
ResultSet.new(results.uniq)
|
|
273
273
|
end
|
|
274
274
|
|
|
275
|
-
def map(&
|
|
276
|
-
ResultSet.new(@elements.map(&
|
|
275
|
+
def map(&)
|
|
276
|
+
ResultSet.new(@elements.map(&))
|
|
277
277
|
end
|
|
278
278
|
|
|
279
|
-
def select(&
|
|
280
|
-
ResultSet.new(@elements.select(&
|
|
279
|
+
def select(&)
|
|
280
|
+
ResultSet.new(@elements.select(&))
|
|
281
281
|
end
|
|
282
282
|
|
|
283
|
-
def reject(&
|
|
284
|
-
ResultSet.new(@elements.reject(&
|
|
283
|
+
def reject(&)
|
|
284
|
+
ResultSet.new(@elements.reject(&))
|
|
285
285
|
end
|
|
286
286
|
|
|
287
287
|
def to_a
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Address < Lutaml::Model::Serializable
|
|
6
|
+
# In-document anchor — the most common navigation target.
|
|
7
|
+
# Matches raw strings starting with "#" plus bareword names that
|
|
8
|
+
# don't look like document IDs or external URLs.
|
|
9
|
+
#
|
|
10
|
+
# "#intro" => anchor target "intro"
|
|
11
|
+
# "intro" => anchor target "intro"
|
|
12
|
+
# "footnote-1" => anchor target "footnote-1"
|
|
13
|
+
module Anchor
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def scheme_name
|
|
17
|
+
:anchor
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def matches?(raw)
|
|
21
|
+
return false if raw.nil? || raw.empty?
|
|
22
|
+
|
|
23
|
+
stripped = raw.to_s
|
|
24
|
+
return false if stripped == '#'
|
|
25
|
+
return true if stripped.start_with?('#')
|
|
26
|
+
|
|
27
|
+
stripped.match?(/\A[\w.\-]+\z/)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse(raw)
|
|
31
|
+
target = raw.to_s.delete_prefix('#')
|
|
32
|
+
new_address(target: target)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def serialize(address)
|
|
36
|
+
target = address.target
|
|
37
|
+
return '' if target.nil? || target.empty?
|
|
38
|
+
|
|
39
|
+
"##{target}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def new_address(target:)
|
|
43
|
+
Address.new(scheme: 'anchor', target: target)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Address < Lutaml::Model::Serializable
|
|
6
|
+
# DOI — Digital Object Identifier. Regex per the DOI handbook.
|
|
7
|
+
module Doi
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
DOI_PATTERN = %r{\A(10\.\d{4,}/[^\s#]+)(?:#(.*))?\z}
|
|
11
|
+
|
|
12
|
+
def scheme_name
|
|
13
|
+
:doi
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def matches?(raw)
|
|
17
|
+
return false if raw.nil? || raw.empty?
|
|
18
|
+
|
|
19
|
+
DOI_PATTERN.match?(raw.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse(raw)
|
|
23
|
+
match = DOI_PATTERN.match(raw.to_s)
|
|
24
|
+
raise Address::ParseError, "Invalid DOI: #{raw.inspect}" unless match
|
|
25
|
+
|
|
26
|
+
target = match[1]
|
|
27
|
+
fragment = match[2]
|
|
28
|
+
Address.new(scheme: 'doi', target: target, fragment: fragment)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def serialize(address)
|
|
32
|
+
base = address.target.to_s
|
|
33
|
+
return base unless address.fragment && !address.fragment.empty?
|
|
34
|
+
|
|
35
|
+
"#{base}##{address.fragment}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Address < Lutaml::Model::Serializable
|
|
6
|
+
# ISBN — book identifier. Recognizes an optional "ISBN" prefix
|
|
7
|
+
# (case insensitive) and either ISBN-10 or ISBN-13 digits. Bare
|
|
8
|
+
# numbers must be long enough to be ISBN-shaped so short numeric
|
|
9
|
+
# anchors are not claimed.
|
|
10
|
+
module Isbn
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
ISBN_PATTERN = /\A(?:ISBN\s+)?(\d[\d\-Xx]{8,16})\z/i
|
|
14
|
+
|
|
15
|
+
def scheme_name
|
|
16
|
+
:isbn
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def matches?(raw)
|
|
20
|
+
return false if raw.nil? || raw.empty?
|
|
21
|
+
|
|
22
|
+
ISBN_PATTERN.match?(raw.to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def parse(raw)
|
|
26
|
+
match = ISBN_PATTERN.match(raw.to_s)
|
|
27
|
+
raise Address::ParseError, "Invalid ISBN: #{raw.inspect}" unless match
|
|
28
|
+
|
|
29
|
+
target = match[1]
|
|
30
|
+
Address.new(scheme: 'isbn', target: target)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def serialize(address)
|
|
34
|
+
"ISBN #{address.target}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Address < Lutaml::Model::Serializable
|
|
6
|
+
# Inter-document path — a document identifier plus optional fragment.
|
|
7
|
+
# Matches document-ID-shaped barewords (uppercase + dash + digit,
|
|
8
|
+
# e.g. "ELF-5005-1") and relative/absolute file paths
|
|
9
|
+
# ("images/foo.png", "../shared/common.adoc"). Locality is carried
|
|
10
|
+
# in fragment.
|
|
11
|
+
#
|
|
12
|
+
# "ELF-5005-1" => path target "ELF-5005-1"
|
|
13
|
+
# "ELF-5005-1#sec-3" => path target "ELF-5005-1", fragment "sec-3"
|
|
14
|
+
# "images/foo.png" => path target "images/foo.png"
|
|
15
|
+
module Path
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Doc-ID pattern: uppercase-led bareword ("ELF-5005-1"). Used
|
|
19
|
+
# together with a separate digit scan — two linear passes, no
|
|
20
|
+
# ambiguous adjacent quantifiers (polynomial-ReDoS-safe).
|
|
21
|
+
DOC_ID_PATTERN = /\A[A-Z][A-Z0-9_\-]*\z/
|
|
22
|
+
# Loose pattern used by +parse+ when the scheme is already chosen
|
|
23
|
+
# (via hint). Accepts any non-empty target plus optional fragment.
|
|
24
|
+
LOOSE_PATTERN = /\A([^#]+)(?:#(.*))?\z/
|
|
25
|
+
|
|
26
|
+
def scheme_name
|
|
27
|
+
:path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def matches?(raw)
|
|
31
|
+
return false if raw.nil? || raw.empty?
|
|
32
|
+
|
|
33
|
+
value = raw.to_s
|
|
34
|
+
return true if value.include?('/')
|
|
35
|
+
|
|
36
|
+
# Document IDs are uppercase-led and contain at least one digit
|
|
37
|
+
# (what distinguishes them from anchors like "SECTION").
|
|
38
|
+
id_part = value.split('#', 2).first
|
|
39
|
+
DOC_ID_PATTERN.match?(id_part) && id_part.match?(/\d/)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def parse(raw)
|
|
43
|
+
match = LOOSE_PATTERN.match(raw.to_s)
|
|
44
|
+
raise Address::ParseError, "Invalid path: #{raw.inspect}" unless match
|
|
45
|
+
|
|
46
|
+
Address.new(
|
|
47
|
+
scheme: 'path',
|
|
48
|
+
target: match[1],
|
|
49
|
+
fragment: normalize_fragment(match[2])
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def serialize(address)
|
|
54
|
+
base = address.target.to_s
|
|
55
|
+
return base unless address.fragment && !address.fragment.empty?
|
|
56
|
+
|
|
57
|
+
"#{base}##{address.fragment}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def normalize_fragment(fragment)
|
|
61
|
+
fragment&.empty? ? nil : fragment
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Address < Lutaml::Model::Serializable
|
|
6
|
+
# Scoped path — a path within a collection's namespace. The scope
|
|
7
|
+
# identifies the collection (e.g. "ELF"); the target is the path
|
|
8
|
+
# inside it ("5005:1"); the fragment is locality within that doc.
|
|
9
|
+
#
|
|
10
|
+
# "ELF:5005:1#sec-3" => scope "ELF", target "5005:1", fragment "sec-3"
|
|
11
|
+
module ScopedPath
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
SCOPED_PATTERN = /\A([A-Z][A-Z0-9_\-]*):([\d:]+)(?:#(.*))?\z/
|
|
15
|
+
|
|
16
|
+
def scheme_name
|
|
17
|
+
:scoped_path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def matches?(raw)
|
|
21
|
+
return false if raw.nil? || raw.empty?
|
|
22
|
+
|
|
23
|
+
SCOPED_PATTERN.match?(raw.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse(raw)
|
|
27
|
+
match = SCOPED_PATTERN.match(raw.to_s)
|
|
28
|
+
raise Address::ParseError, "Invalid scoped path: #{raw.inspect}" unless match
|
|
29
|
+
|
|
30
|
+
Address.new(
|
|
31
|
+
scheme: 'scoped_path',
|
|
32
|
+
scope: match[1],
|
|
33
|
+
target: match[2],
|
|
34
|
+
fragment: match[3]
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def serialize(address)
|
|
39
|
+
base = "#{address.scope}:#{address.target}"
|
|
40
|
+
return base unless address.fragment && !address.fragment.empty?
|
|
41
|
+
|
|
42
|
+
"#{base}##{address.fragment}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Address < Lutaml::Model::Serializable
|
|
6
|
+
# Absolute URL — anything with a recognized URL scheme prefix.
|
|
7
|
+
# Fragment is the optional "#..." suffix.
|
|
8
|
+
module Url
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
URL_PATTERN = %r{\A([a-z][a-z0-9+\-.]*)://([^#]+)(?:#(.*))?\z}i
|
|
12
|
+
|
|
13
|
+
def scheme_name
|
|
14
|
+
:url
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def matches?(raw)
|
|
18
|
+
return false if raw.nil? || raw.empty?
|
|
19
|
+
|
|
20
|
+
URL_PATTERN.match?(raw.to_s)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parse(raw)
|
|
24
|
+
match = URL_PATTERN.match(raw.to_s)
|
|
25
|
+
raise Address::ParseError, "Invalid URL: #{raw.inspect}" unless match
|
|
26
|
+
|
|
27
|
+
target = "#{match[1]}://#{match[2]}"
|
|
28
|
+
fragment = match[3]
|
|
29
|
+
Address.new(scheme: 'url', target: target, fragment: fragment)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def serialize(address)
|
|
33
|
+
base = address.target.to_s
|
|
34
|
+
return base unless address.fragment && !address.fragment.empty?
|
|
35
|
+
|
|
36
|
+
"#{base}##{address.fragment}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Reference
|
|
7
|
+
# Scheme-aware locator targeting a Content node.
|
|
8
|
+
#
|
|
9
|
+
# An Address is one of six built-in schemes — +:anchor+, +:path+,
|
|
10
|
+
# +:scoped_path+, +:url+, +:doi+, +:isbn+ — selected by heuristic on
|
|
11
|
+
# parse or by an explicit +hint:+. External gems register additional
|
|
12
|
+
# schemes via +Address.register_scheme+ (OCP).
|
|
13
|
+
#
|
|
14
|
+
# Address.parse("foo") # => anchor "foo"
|
|
15
|
+
# Address.parse("ELF-5005-1#sec-3") # => path "ELF-5005-1", fragment "sec-3"
|
|
16
|
+
# Address.parse("ELF:5005:1#sec-3") # => scoped_path scope "ELF"
|
|
17
|
+
# Address.parse("https://x.y/z") # => url
|
|
18
|
+
# Address.parse("10.1234/abc") # => doi
|
|
19
|
+
# Address.parse("ISBN 978-1-2-3") # => isbn
|
|
20
|
+
#
|
|
21
|
+
# Addresses are value types: two addresses are equal iff every
|
|
22
|
+
# attribute matches. Round-trip via +to_s+.
|
|
23
|
+
class Address < Lutaml::Model::Serializable
|
|
24
|
+
class ParseError < Coradoc::Reference::Error; end
|
|
25
|
+
class UnknownSchemeError < Coradoc::Reference::Error; end
|
|
26
|
+
|
|
27
|
+
attribute :scheme, :string
|
|
28
|
+
attribute :target, :string
|
|
29
|
+
attribute :fragment, :string
|
|
30
|
+
attribute :scope, :string
|
|
31
|
+
|
|
32
|
+
autoload :Url, "#{__dir__}/address/url"
|
|
33
|
+
autoload :Doi, "#{__dir__}/address/doi"
|
|
34
|
+
autoload :Isbn, "#{__dir__}/address/isbn"
|
|
35
|
+
autoload :ScopedPath, "#{__dir__}/address/scoped_path"
|
|
36
|
+
autoload :Path, "#{__dir__}/address/path"
|
|
37
|
+
autoload :Anchor, "#{__dir__}/address/anchor"
|
|
38
|
+
|
|
39
|
+
BUILTIN_SCHEME_ORDER = %i[url doi isbn scoped_path path anchor].freeze
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def parse(raw, hint: nil)
|
|
43
|
+
Scheme.ensure_builtins_registered!
|
|
44
|
+
mod = hint ? scheme_for_hint!(hint) : Scheme.match(raw)
|
|
45
|
+
unless mod
|
|
46
|
+
raise ParseError,
|
|
47
|
+
"Cannot determine address scheme for #{raw.inspect}"
|
|
48
|
+
end
|
|
49
|
+
mod.parse(raw)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def register_scheme(mod)
|
|
53
|
+
Scheme.register(mod)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def scheme_names
|
|
57
|
+
Scheme.ensure_builtins_registered!
|
|
58
|
+
Scheme.names
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def scheme_for_hint!(hint)
|
|
64
|
+
Scheme.for(hint) ||
|
|
65
|
+
raise(UnknownSchemeError,
|
|
66
|
+
"Unknown address scheme #{hint.inspect}. " \
|
|
67
|
+
"Registered: #{Scheme.names.inspect}")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Value equality (==/eql?/hash) comes from Lutaml::Model — all
|
|
72
|
+
# attributes compared, class-aware, safe as Hash keys.
|
|
73
|
+
|
|
74
|
+
def to_s
|
|
75
|
+
Scheme.ensure_builtins_registered!
|
|
76
|
+
mod = Scheme.for(scheme)
|
|
77
|
+
unless mod
|
|
78
|
+
raise UnknownSchemeError,
|
|
79
|
+
"No registered scheme for #{scheme.inspect}. " \
|
|
80
|
+
"Registered: #{Scheme.names.inspect}"
|
|
81
|
+
end
|
|
82
|
+
mod.serialize(self)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Registry of scheme modules. Each module provides:
|
|
86
|
+
# scheme_name -> Symbol
|
|
87
|
+
# matches?(raw) -> Boolean
|
|
88
|
+
# parse(raw) -> Address
|
|
89
|
+
# serialize(addr) -> String
|
|
90
|
+
#
|
|
91
|
+
# +register+ always forces builtin registration first, so an
|
|
92
|
+
# external scheme registered before first use is never clobbered
|
|
93
|
+
# by the lazy builtin pass. The catch-all anchor scheme is always
|
|
94
|
+
# tried LAST on match, so external schemes are never shadowed by
|
|
95
|
+
# it no matter when they register.
|
|
96
|
+
module Scheme
|
|
97
|
+
@registered = []
|
|
98
|
+
@builtins_registered = false
|
|
99
|
+
@registering_builtins = false
|
|
100
|
+
|
|
101
|
+
CATCH_ALL_SCHEME = :anchor
|
|
102
|
+
private_constant :CATCH_ALL_SCHEME
|
|
103
|
+
|
|
104
|
+
MUTEX = Mutex.new
|
|
105
|
+
private_constant :MUTEX
|
|
106
|
+
|
|
107
|
+
class << self
|
|
108
|
+
def register(mod)
|
|
109
|
+
ensure_builtins_registered! unless @registering_builtins
|
|
110
|
+
@registered.delete_if { |m| m.scheme_name == mod.scheme_name }
|
|
111
|
+
@registered << mod
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def names
|
|
115
|
+
@registered.map(&:scheme_name)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def for(name)
|
|
119
|
+
@registered.find { |m| m.scheme_name == name.to_sym }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def match(raw)
|
|
123
|
+
catch_all, specific =
|
|
124
|
+
@registered.partition { |m| m.scheme_name == CATCH_ALL_SCHEME }
|
|
125
|
+
(specific + catch_all).find { |m| m.matches?(raw) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def ensure_builtins_registered!
|
|
129
|
+
MUTEX.synchronize do
|
|
130
|
+
return if @builtins_registered
|
|
131
|
+
|
|
132
|
+
@registering_builtins = true
|
|
133
|
+
begin
|
|
134
|
+
BUILTIN_SCHEME_ORDER.each do |name|
|
|
135
|
+
const_name = name.to_s.split('_').map(&:capitalize).join
|
|
136
|
+
register(Address.const_get(const_name))
|
|
137
|
+
end
|
|
138
|
+
@builtins_registered = true
|
|
139
|
+
ensure
|
|
140
|
+
@registering_builtins = false
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def reset!
|
|
146
|
+
@registered.clear
|
|
147
|
+
@builtins_registered = false
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Catalog
|
|
6
|
+
# Composes multiple catalogs. Lookup asks EVERY child and merges
|
|
7
|
+
# the hits: exactly one hit returns the Content; several hits
|
|
8
|
+
# return an Array (which the Resolver surfaces per the
|
|
9
|
+
# +ambiguous:+ policy). Precedence between catalogs is a
|
|
10
|
+
# resolver/policy concern, not a lookup concern:
|
|
11
|
+
#
|
|
12
|
+
# Composite.new(
|
|
13
|
+
# Local.from_doc(doc),
|
|
14
|
+
# Collection.from_manifest(...),
|
|
15
|
+
# Remote.new(client: http_client)
|
|
16
|
+
# )
|
|
17
|
+
class Composite
|
|
18
|
+
include Catalog::Protocol
|
|
19
|
+
|
|
20
|
+
attr_reader :children
|
|
21
|
+
|
|
22
|
+
def initialize(*children)
|
|
23
|
+
@children = children.flatten
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def lookup(address)
|
|
27
|
+
results = []
|
|
28
|
+
children.each do |catalog|
|
|
29
|
+
result = catalog.lookup(address)
|
|
30
|
+
next if result.nil?
|
|
31
|
+
|
|
32
|
+
results.concat(Array(result))
|
|
33
|
+
end
|
|
34
|
+
return nil if results.empty?
|
|
35
|
+
return results.first if results.size == 1
|
|
36
|
+
|
|
37
|
+
results
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ambiguous?(address)
|
|
41
|
+
result = lookup(address)
|
|
42
|
+
result.is_a?(Array) && result.size > 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def each_pair(&block)
|
|
46
|
+
return to_enum(:each_pair) unless block_given?
|
|
47
|
+
|
|
48
|
+
children.each do |catalog|
|
|
49
|
+
catalog.each_pair(&block)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def recognizes_scheme?(scheme)
|
|
54
|
+
children.any? { |catalog| catalog.recognizes_scheme?(scheme) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Catalog
|
|
6
|
+
# Indexes one CoreModel document. Walks the tree once at
|
|
7
|
+
# construction; indexes every node carrying an +id+ by its anchor
|
|
8
|
+
# Address, plus the document itself by an optional path Address.
|
|
9
|
+
#
|
|
10
|
+
# catalog = Catalog::Local.from_doc(doc, path: "ELF-5005-1")
|
|
11
|
+
# catalog.lookup(Address.parse("ELF-5005-1")) # => doc
|
|
12
|
+
# catalog.lookup(Address.parse("sec-3")) # => section
|
|
13
|
+
class Local
|
|
14
|
+
include Catalog::Protocol
|
|
15
|
+
|
|
16
|
+
attr_reader :document, :document_path
|
|
17
|
+
|
|
18
|
+
def initialize(document:, document_path: nil, index: nil)
|
|
19
|
+
@document = document
|
|
20
|
+
@document_path = document_path
|
|
21
|
+
@index = index || MemoryIndex.new
|
|
22
|
+
populate! unless index
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def from_doc(document, path: nil)
|
|
27
|
+
new(document: document, document_path: path)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def lookup(address)
|
|
32
|
+
@index.lookup(address)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ambiguous?(address)
|
|
36
|
+
@index.ambiguous?(address)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def each_pair(&)
|
|
40
|
+
@index.each_pair(&)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def recognizes_scheme?(scheme)
|
|
44
|
+
return true if scheme.to_sym == :anchor
|
|
45
|
+
return true if scheme.to_sym == :path && document_path
|
|
46
|
+
return true if scheme.to_sym == :scoped_path && document_path&.include?(':')
|
|
47
|
+
|
|
48
|
+
@index.recognizes_scheme?(scheme)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def populate!
|
|
54
|
+
index_document_root!
|
|
55
|
+
walk(document)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def index_document_root!
|
|
59
|
+
return unless document_path
|
|
60
|
+
|
|
61
|
+
address = Coradoc::Reference::Address.parse(document_path, hint: path_hint)
|
|
62
|
+
@index.add(address, document)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def path_hint
|
|
66
|
+
document_path.include?(':') ? :scoped_path : :path
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def walk(node)
|
|
70
|
+
return unless node.is_a?(Coradoc::CoreModel::Base)
|
|
71
|
+
|
|
72
|
+
index_node!(node)
|
|
73
|
+
walk_children(node)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def index_node!(node)
|
|
77
|
+
return unless node.id
|
|
78
|
+
|
|
79
|
+
address = Coradoc::Reference::Address.new(
|
|
80
|
+
scheme: 'anchor',
|
|
81
|
+
target: node.id
|
|
82
|
+
)
|
|
83
|
+
@index.add(address, node)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def walk_children(node)
|
|
87
|
+
return unless node.is_a?(Coradoc::CoreModel::HasChildren)
|
|
88
|
+
|
|
89
|
+
children = node.children
|
|
90
|
+
return if children.nil? || children.empty?
|
|
91
|
+
|
|
92
|
+
children.each { |child| walk(child) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Catalog
|
|
6
|
+
# Shared in-memory index. Built-in catalogs compose this rather
|
|
7
|
+
# than reimplementing the storage layer (DRY). The index is
|
|
8
|
+
# immutable from the outside — entries are added by the catalog
|
|
9
|
+
# during construction, then read-only.
|
|
10
|
+
class MemoryIndex
|
|
11
|
+
include Enumerable
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@by_address = {}
|
|
15
|
+
@schemes = Set.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add(address, content)
|
|
19
|
+
raise ArgumentError, 'address required' unless address.is_a?(Coradoc::Reference::Address)
|
|
20
|
+
raise ArgumentError, 'content required' unless content
|
|
21
|
+
|
|
22
|
+
@by_address[address] = Array(@by_address[address]) << content
|
|
23
|
+
@schemes << address.scheme.to_sym
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def lookup(address)
|
|
28
|
+
entries = @by_address[address]
|
|
29
|
+
return nil unless entries
|
|
30
|
+
return entries.first if entries.size == 1
|
|
31
|
+
|
|
32
|
+
# Copy: callers must not be able to corrupt the index.
|
|
33
|
+
entries.dup
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ambiguous?(address)
|
|
37
|
+
entries = @by_address[address]
|
|
38
|
+
entries && entries.size > 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def each_pair
|
|
42
|
+
return to_enum(:each_pair) unless block_given?
|
|
43
|
+
|
|
44
|
+
@by_address.each do |address, contents|
|
|
45
|
+
contents.each { |c| yield address, c }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def recognizes_scheme?(scheme)
|
|
50
|
+
@schemes.include?(scheme.to_sym)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def size
|
|
54
|
+
@by_address.size
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|