coradoc 2.0.28 → 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 +4 -1
- data/lib/coradoc/coradoc.rb +3 -0
- 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,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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
# Externally-built index from Address → Content. coradoc queries
|
|
6
|
+
# catalogs; it never owns the collection truth.
|
|
7
|
+
#
|
|
8
|
+
# A Catalog is any object responding to +lookup+, +each_pair+, and
|
|
9
|
+
# +recognizes_scheme?+. Built-in catalogs share an in-memory index
|
|
10
|
+
# (MemoryIndex) for implementation. Catalogs compose via CompositeCatalog.
|
|
11
|
+
module Catalog
|
|
12
|
+
autoload :MemoryIndex, "#{__dir__}/catalog/memory_index"
|
|
13
|
+
autoload :Local, "#{__dir__}/catalog/local"
|
|
14
|
+
autoload :Composite, "#{__dir__}/catalog/composite"
|
|
15
|
+
|
|
16
|
+
# Protocol methods every Catalog must answer. Implementations may
|
|
17
|
+
# include this module for documentation only; the protocol is
|
|
18
|
+
# duck-typed but explicit at the type-check sites via these method
|
|
19
|
+
# names.
|
|
20
|
+
module Protocol
|
|
21
|
+
# Resolve +address+ to a Content node (CoreModel::Base) or nil.
|
|
22
|
+
def lookup(address) = raise(NotImplementedError)
|
|
23
|
+
|
|
24
|
+
# Enumerate every (Address, Content) pair this catalog knows.
|
|
25
|
+
def each_pair(&) = raise(NotImplementedError)
|
|
26
|
+
|
|
27
|
+
# Does this catalog index addresses of the given scheme? Used by
|
|
28
|
+
# composite catalogs to skip irrelevant children.
|
|
29
|
+
def recognizes_scheme?(scheme) = raise(NotImplementedError)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Edge < Lutaml::Model::Serializable
|
|
6
|
+
# Options for citation edges (bib references). Style is the
|
|
7
|
+
# citation style name (e.g. "ieee", "apa", "chicago"); locality
|
|
8
|
+
# is the optional page/section reference carried separately on
|
|
9
|
+
# Address.fragment, not here.
|
|
10
|
+
class CitationOptions < Edge::Options
|
|
11
|
+
attribute :style, :string
|
|
12
|
+
attribute :suppress_author, :boolean, default: -> { false }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Edge < Lutaml::Model::Serializable
|
|
6
|
+
# Options for footnote references. The footnote body lives in
|
|
7
|
+
# the catalog as a Content node; the Edge points at it.
|
|
8
|
+
class FootnoteRefOptions < Edge::Options
|
|
9
|
+
attribute :footnote_id, :string
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Edge < Lutaml::Model::Serializable
|
|
6
|
+
# Options for image references.
|
|
7
|
+
class ImageRefOptions < Edge::Options
|
|
8
|
+
attribute :alt_text, :string
|
|
9
|
+
attribute :width, :string
|
|
10
|
+
attribute :height, :string
|
|
11
|
+
attribute :role, :string
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Edge < Lutaml::Model::Serializable
|
|
6
|
+
# Options for include edges: embeds the canonical
|
|
7
|
+
# CoreModel::IncludeOptions — the include directive's typed
|
|
8
|
+
# selectors (tags/wildcards, lines, typed leveloffset, indent,
|
|
9
|
+
# encoding). One typed form, never a re-flattened mirror.
|
|
10
|
+
class IncludeOptions < Edge::Options
|
|
11
|
+
attribute :include_options, Coradoc::CoreModel::IncludeOptions
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Edge < Lutaml::Model::Serializable
|
|
6
|
+
# Registry mapping kind symbol → Entry(name, options_class).
|
|
7
|
+
# Built-in kinds are registered lazily on first access. External
|
|
8
|
+
# gems add kinds via +Edge.register_kind+ (OCP).
|
|
9
|
+
#
|
|
10
|
+
# +register+ always forces builtin registration first, so an
|
|
11
|
+
# external registration made before first use is never clobbered
|
|
12
|
+
# by the lazy builtin pass.
|
|
13
|
+
module Kind
|
|
14
|
+
@entries = {}
|
|
15
|
+
@builtins_registered = false
|
|
16
|
+
@registering_builtins = false
|
|
17
|
+
|
|
18
|
+
MUTEX = Mutex.new
|
|
19
|
+
private_constant :MUTEX
|
|
20
|
+
|
|
21
|
+
Entry = Struct.new(:name, :options_class)
|
|
22
|
+
private_constant :Entry
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def register(name, options_class: nil)
|
|
26
|
+
ensure_builtins_registered! unless @registering_builtins
|
|
27
|
+
@entries[name.to_sym] = Entry.new(name.to_sym, options_class)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def names
|
|
31
|
+
ensure_builtins_registered!
|
|
32
|
+
@entries.keys
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def options_class_for(name)
|
|
36
|
+
ensure_builtins_registered!
|
|
37
|
+
@entries[name.to_sym]&.options_class
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def entry_for(name)
|
|
41
|
+
ensure_builtins_registered!
|
|
42
|
+
@entries[name.to_sym]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def reset!
|
|
46
|
+
@entries.clear
|
|
47
|
+
@builtins_registered = false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ensure_builtins_registered!
|
|
51
|
+
MUTEX.synchronize do
|
|
52
|
+
return if @builtins_registered
|
|
53
|
+
|
|
54
|
+
@registering_builtins = true
|
|
55
|
+
begin
|
|
56
|
+
register(:navigation, options_class: Edge::NavigationOptions)
|
|
57
|
+
register(:citation, options_class: Edge::CitationOptions)
|
|
58
|
+
register(:link, options_class: Edge::LinkOptions)
|
|
59
|
+
register(:include, options_class: Edge::IncludeOptions)
|
|
60
|
+
register(:image_ref, options_class: Edge::ImageRefOptions)
|
|
61
|
+
register(:footnote_ref, options_class: Edge::FootnoteRefOptions)
|
|
62
|
+
@builtins_registered = true
|
|
63
|
+
ensure
|
|
64
|
+
@registering_builtins = false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Edge < Lutaml::Model::Serializable
|
|
6
|
+
# Options specific to navigation edges (xref, anchor).
|
|
7
|
+
class NavigationOptions < Edge::Options
|
|
8
|
+
attribute :tooltip, :string
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
class Edge < Lutaml::Model::Serializable
|
|
6
|
+
# Options base class. Subclasses are typed value objects per kind —
|
|
7
|
+
# never +:hash+. Value equality (==/eql?/hash) comes from
|
|
8
|
+
# Lutaml::Model: class-aware, all attributes compared.
|
|
9
|
+
class Options < Lutaml::Model::Serializable
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Reference
|
|
7
|
+
# Directed edge: a Content node references another Content via an Address.
|
|
8
|
+
#
|
|
9
|
+
# Every reference in coradoc — navigation, citation, hyperlink,
|
|
10
|
+
# include, image, footnote — is one Edge with a different +kind+.
|
|
11
|
+
# The kind is a label for materialization; resolution is kind-agnostic.
|
|
12
|
+
#
|
|
13
|
+
# Edge.new(
|
|
14
|
+
# kind: :navigation,
|
|
15
|
+
# address: Address.parse("ELF-5005-1#sec-3"),
|
|
16
|
+
# source_id: "para-42",
|
|
17
|
+
# label: "Section 3"
|
|
18
|
+
# )
|
|
19
|
+
class Edge < Lutaml::Model::Serializable
|
|
20
|
+
autoload :Kind, "#{__dir__}/edge/kind"
|
|
21
|
+
autoload :Options, "#{__dir__}/edge/options"
|
|
22
|
+
autoload :NavigationOptions,
|
|
23
|
+
"#{__dir__}/edge/navigation_options"
|
|
24
|
+
autoload :CitationOptions,
|
|
25
|
+
"#{__dir__}/edge/citation_options"
|
|
26
|
+
autoload :LinkOptions, "#{__dir__}/edge/link_options"
|
|
27
|
+
autoload :IncludeOptions, "#{__dir__}/edge/include_options"
|
|
28
|
+
autoload :ImageRefOptions, "#{__dir__}/edge/image_ref_options"
|
|
29
|
+
autoload :FootnoteRefOptions,
|
|
30
|
+
"#{__dir__}/edge/footnote_ref_options"
|
|
31
|
+
|
|
32
|
+
attribute :kind, :string
|
|
33
|
+
attribute :address, Coradoc::Reference::Address
|
|
34
|
+
attribute :source_id, :string
|
|
35
|
+
attribute :label, :string
|
|
36
|
+
attribute :options, Coradoc::Reference::Edge::Options
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Build an Edge with the given kind. Options are coerced to the
|
|
40
|
+
# kind's options class (if any) via the Kind registry — never
|
|
41
|
+
# hand-rolled. Raises UnknownKindError for kinds that were never
|
|
42
|
+
# registered — external kinds register via +register_kind+ (OCP).
|
|
43
|
+
def build(kind:, address:, source_id: nil, label: nil, options: nil)
|
|
44
|
+
entry = Kind.entry_for(kind)
|
|
45
|
+
unless entry
|
|
46
|
+
raise Coradoc::Reference::UnknownKindError,
|
|
47
|
+
"Unknown reference kind #{kind.inspect} — " \
|
|
48
|
+
'register it via Edge.register_kind'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
new(
|
|
52
|
+
kind: kind.to_s,
|
|
53
|
+
address: address,
|
|
54
|
+
source_id: source_id,
|
|
55
|
+
label: label,
|
|
56
|
+
options: coerce_options(options, entry.options_class)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def register_kind(name, options_class: nil)
|
|
61
|
+
Kind.register(name, options_class: options_class)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def kinds
|
|
65
|
+
Kind.names
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def coerce_options(value, options_class)
|
|
71
|
+
klass = options_class || Options
|
|
72
|
+
return klass.new if value.nil?
|
|
73
|
+
return value if value.is_a?(klass)
|
|
74
|
+
|
|
75
|
+
klass.new(value)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
# Walks a CoreModel tree and yields every Edge found in it. For
|
|
6
|
+
# Phase 1, Edges are wrapped inside existing node types —
|
|
7
|
+
# +CrossReferenceElement+, +LinkElement+, +Image+, +Include+,
|
|
8
|
+
# +FootnoteElement+. The Phase 3 migration will move Edges to
|
|
9
|
+
# first-class attributes; this module abstracts the extraction so
|
|
10
|
+
# the rest of the resolver doesn't change.
|
|
11
|
+
#
|
|
12
|
+
# The walker is read-only: it never mutates the input tree.
|
|
13
|
+
module EdgeSearch
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Yields (parent_node, edge) for every Edge found in the tree.
|
|
17
|
+
# The parent_node is the node that owns the edge — the
|
|
18
|
+
# materializer replaces the edge in-place inside parent_node's
|
|
19
|
+
# children list.
|
|
20
|
+
def each_edge(root)
|
|
21
|
+
return to_enum(:each_edge, root) unless block_given?
|
|
22
|
+
|
|
23
|
+
walk(root) { |node| edges_for(node).each { |edge| yield node, edge } }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Walk tree, yield each Base node. Stops at inline leaves.
|
|
27
|
+
def walk(node, &block)
|
|
28
|
+
return unless node.is_a?(Coradoc::CoreModel::Base)
|
|
29
|
+
return unless block
|
|
30
|
+
|
|
31
|
+
yield(node)
|
|
32
|
+
return unless node.is_a?(Coradoc::CoreModel::HasChildren)
|
|
33
|
+
|
|
34
|
+
children = node.children
|
|
35
|
+
return unless children
|
|
36
|
+
|
|
37
|
+
children.each { |child| walk(child, &block) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Extract every Edge the given node owns. Pure function of node.
|
|
41
|
+
def edges_for(node)
|
|
42
|
+
edge_extractor = EDGE_EXTRACTORS[node.class]
|
|
43
|
+
return [] unless edge_extractor
|
|
44
|
+
|
|
45
|
+
edge = edge_extractor.call(node)
|
|
46
|
+
edge ? [edge] : []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def edge_from_cross_reference(node)
|
|
50
|
+
Edge.build(
|
|
51
|
+
kind: :navigation,
|
|
52
|
+
address: parse_address(node.target, hint: xref_hint(node.target)),
|
|
53
|
+
source_id: node.id,
|
|
54
|
+
label: node.content
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def edge_from_link(node)
|
|
59
|
+
Edge.build(
|
|
60
|
+
kind: :link,
|
|
61
|
+
address: parse_address(node.target),
|
|
62
|
+
source_id: node.id,
|
|
63
|
+
label: node.content
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def edge_from_include(node)
|
|
68
|
+
Edge.build(
|
|
69
|
+
kind: :include,
|
|
70
|
+
address: parse_address(node.target, hint: path_or_url_hint(node.target)),
|
|
71
|
+
source_id: node.id,
|
|
72
|
+
options: { include_options: node.options }
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def edge_from_image(node)
|
|
77
|
+
Edge.build(
|
|
78
|
+
kind: :image_ref,
|
|
79
|
+
address: parse_address(node.src, hint: path_or_url_hint(node.src)),
|
|
80
|
+
source_id: node.id,
|
|
81
|
+
label: node.alt,
|
|
82
|
+
options: { alt_text: node.alt }
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def edge_from_footnote(node)
|
|
87
|
+
Edge.build(
|
|
88
|
+
kind: :footnote_ref,
|
|
89
|
+
address: parse_address(node.target || node.id, hint: :anchor),
|
|
90
|
+
source_id: node.id,
|
|
91
|
+
options: { footnote_id: node.id }
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Xrefs are document-internal by definition (AsciiDoc): bare
|
|
96
|
+
# targets are anchors, even uppercase document-ID-shaped ones
|
|
97
|
+
# ("SEC-2"). Only an explicit "document#fragment" shape or a URL
|
|
98
|
+
# points outside the current document.
|
|
99
|
+
def xref_hint(target)
|
|
100
|
+
raw = target.to_s
|
|
101
|
+
return :url if Coradoc::Reference::Address::Url.matches?(raw)
|
|
102
|
+
return :path if raw.include?('#') && !raw.start_with?('#')
|
|
103
|
+
|
|
104
|
+
:anchor
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# File-like targets (include, image): URLs stay urls, everything
|
|
108
|
+
# else is a path — including bare filenames ("foo.png") that the
|
|
109
|
+
# anchor bareword heuristic would otherwise claim.
|
|
110
|
+
def path_or_url_hint(target)
|
|
111
|
+
Coradoc::Reference::Address::Url.matches?(target.to_s) ? :url : :path
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_address(target, hint: nil)
|
|
115
|
+
Coradoc::Reference::Address.parse(target.to_s, hint: hint)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
EDGE_EXTRACTORS = {
|
|
119
|
+
Coradoc::CoreModel::CrossReferenceElement => method(:edge_from_cross_reference),
|
|
120
|
+
Coradoc::CoreModel::LinkElement => method(:edge_from_link),
|
|
121
|
+
Coradoc::CoreModel::Include => method(:edge_from_include),
|
|
122
|
+
Coradoc::CoreModel::Image => method(:edge_from_image),
|
|
123
|
+
Coradoc::CoreModel::FootnoteElement => method(:edge_from_footnote)
|
|
124
|
+
}.freeze
|
|
125
|
+
private_constant :EDGE_EXTRACTORS
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Materializer
|
|
6
|
+
# Protocol base. Subclasses declare their (kind, presentation,
|
|
7
|
+
# format) tuple via class-level +.kind+, +.presentation+, +.format+
|
|
8
|
+
# methods, then implement +#materialize+ to produce a
|
|
9
|
+
# CoreModel::InlineElement subtree.
|
|
10
|
+
#
|
|
11
|
+
# Materializers consume Results — they never touch the catalog
|
|
12
|
+
# directly. The Presentation tells them where the target lives.
|
|
13
|
+
class Base
|
|
14
|
+
class << self
|
|
15
|
+
def kind
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def presentation
|
|
20
|
+
:any
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def format
|
|
24
|
+
:any
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Render one resolved edge.
|
|
29
|
+
#
|
|
30
|
+
# @param edge [Edge]
|
|
31
|
+
# @param result [Result::Base] the resolved outcome
|
|
32
|
+
# @param node [CoreModel::Base] the original edge-bearing node;
|
|
33
|
+
# return it unchanged to keep the node as authored
|
|
34
|
+
# @param presentation [Presentation::Base]
|
|
35
|
+
# @param pages [Array<Presentation::Page>]
|
|
36
|
+
# @return [CoreModel::Base, nil] replacement node, the original
|
|
37
|
+
# +node+ to keep it, or nil to drop it from the tree
|
|
38
|
+
def materialize(edge:, result:, node:, presentation:, pages:)
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Materializer
|
|
6
|
+
# Default fallback. Returns the original edge-bearing node
|
|
7
|
+
# unchanged — an unresolvable or unrenderable reference survives
|
|
8
|
+
# materialization exactly as authored, so round-tripping is
|
|
9
|
+
# always safe and no content is ever lost.
|
|
10
|
+
class Passthrough < Base
|
|
11
|
+
class << self
|
|
12
|
+
def kind
|
|
13
|
+
:any
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def presentation
|
|
17
|
+
:any
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def format
|
|
21
|
+
:any
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def materialize(node:, **)
|
|
26
|
+
node
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|