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
|
@@ -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
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Materializer
|
|
6
|
+
# Registry of materializers keyed by [kind, presentation, format].
|
|
7
|
+
# Lookup falls back on +:any+ — so a materializer registered for
|
|
8
|
+
# +[:link, :any, :html]+ handles every presentation in HTML.
|
|
9
|
+
# Most specific key wins (concrete > :any).
|
|
10
|
+
#
|
|
11
|
+
# Format gems register their materializers process-wide via
|
|
12
|
+
# +.register_global+ (OCP): every registry instance picks them up
|
|
13
|
+
# on first use, and dispatch code never changes.
|
|
14
|
+
class Registry
|
|
15
|
+
# Core ships only the node-preserving fallback. Format-specific
|
|
16
|
+
# materializers live in the format gems (coradoc-html,
|
|
17
|
+
# coradoc-adoc, ...) and register globally at load time.
|
|
18
|
+
BUILTINS = [Materializer::Passthrough].freeze
|
|
19
|
+
|
|
20
|
+
@global_registrations = []
|
|
21
|
+
|
|
22
|
+
GLOBAL_MUTEX = Mutex.new
|
|
23
|
+
private_constant :GLOBAL_MUTEX
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def register_global(klass)
|
|
27
|
+
GLOBAL_MUTEX.synchronize do
|
|
28
|
+
@global_registrations << klass unless @global_registrations.include?(klass)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def global_registrations
|
|
33
|
+
GLOBAL_MUTEX.synchronize { @global_registrations.dup }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_globals!
|
|
37
|
+
GLOBAL_MUTEX.synchronize { @global_registrations.clear }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize
|
|
42
|
+
@by_key = {}
|
|
43
|
+
@builtins_registered = false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def register(klass)
|
|
47
|
+
ensure_builtins_registered!
|
|
48
|
+
@by_key[EntryKey.new(klass.kind, klass.presentation, klass.format)] = klass
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def lookup(kind:, presentation:, format:)
|
|
52
|
+
ensure_builtins_registered!
|
|
53
|
+
find_most_specific(kind, presentation, format) ||
|
|
54
|
+
find_most_specific(kind, presentation, :any) ||
|
|
55
|
+
find_most_specific(kind, :any, format) ||
|
|
56
|
+
find_most_specific(kind, :any, :any) ||
|
|
57
|
+
find_most_specific(:any, :any, :any)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def registered
|
|
61
|
+
ensure_builtins_registered!
|
|
62
|
+
@by_key.dup
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reset!
|
|
66
|
+
@by_key.clear
|
|
67
|
+
@builtins_registered = false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def find_most_specific(kind, presentation, format)
|
|
73
|
+
@by_key[EntryKey.new(kind.to_sym, presentation.to_sym, format.to_sym)]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ensure_builtins_registered!
|
|
77
|
+
return if @builtins_registered
|
|
78
|
+
|
|
79
|
+
register_builtins!
|
|
80
|
+
@builtins_registered = true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def register_builtins!
|
|
84
|
+
(BUILTINS + self.class.global_registrations).each do |k|
|
|
85
|
+
@by_key[EntryKey.new(k.kind, k.presentation, k.format)] = k
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Internal composite key for the registry. Equality based on
|
|
90
|
+
# the three symbol values so two equivalent tuples compare equal.
|
|
91
|
+
class EntryKey
|
|
92
|
+
attr_reader :kind, :presentation, :format
|
|
93
|
+
|
|
94
|
+
def initialize(kind, presentation, format)
|
|
95
|
+
@kind = kind.to_sym
|
|
96
|
+
@presentation = presentation.to_sym
|
|
97
|
+
@format = format.to_sym
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ==(other)
|
|
101
|
+
other.is_a?(EntryKey) &&
|
|
102
|
+
kind == other.kind &&
|
|
103
|
+
presentation == other.presentation &&
|
|
104
|
+
format == other.format
|
|
105
|
+
end
|
|
106
|
+
alias eql? ==
|
|
107
|
+
|
|
108
|
+
def hash
|
|
109
|
+
[kind, presentation, format].hash
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
private_constant :EntryKey
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
# Render an Edge per (kind, presentation, format). Registry-based:
|
|
6
|
+
# adding a new output format or citation style is registering a
|
|
7
|
+
# new materializer, not editing switch statements.
|
|
8
|
+
module Materializer
|
|
9
|
+
autoload :Base, "#{__dir__}/materializer/base"
|
|
10
|
+
autoload :Registry, "#{__dir__}/materializer/registry"
|
|
11
|
+
autoload :Passthrough, "#{__dir__}/materializer/passthrough"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Reference
|
|
5
|
+
module Presentation
|
|
6
|
+
# Protocol base. Subclasses implement +#layout+ (resolved graph →
|
|
7
|
+
# Pages) and +#locate_page+ (find which page a target lives on).
|
|
8
|
+
#
|
|
9
|
+
# A Presentation NEVER reads the original document — it consumes
|
|
10
|
+
# the resolved graph produced by the Resolver. It never renders.
|
|
11
|
+
class Base
|
|
12
|
+
# Registry key used in materializer lookup tuples
|
|
13
|
+
# [kind, presentation, format]. Subclasses override so
|
|
14
|
+
# materializers can target a specific layout.
|
|
15
|
+
def self.key
|
|
16
|
+
:any
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def key
|
|
20
|
+
self.class.key
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Lay out the resolved graph as a tree of Pages. Subclasses
|
|
24
|
+
# decide slicing, ordering, hierarchy.
|
|
25
|
+
#
|
|
26
|
+
# @param resolved_graph [CoreModel::Base] the document the
|
|
27
|
+
# caller wants to present. Already resolved (edges → targets).
|
|
28
|
+
# @return [Array<Page>]
|
|
29
|
+
def layout(resolved_graph)
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Given an Edge and its resolved target, find the Page where
|
|
34
|
+
# the target lives. This is what makes cross-references
|
|
35
|
+
# survive re-pagination.
|
|
36
|
+
#
|
|
37
|
+
# @param edge [Edge]
|
|
38
|
+
# @param target_content [CoreModel::Base]
|
|
39
|
+
# @param pages [Array<Page>]
|
|
40
|
+
# @return [Page, nil]
|
|
41
|
+
def locate_page(edge, target_content, pages:)
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|