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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ebca4a6342273f099bbbd2b47471ea28416669106fe33ee08dc929387019334
|
|
4
|
+
data.tar.gz: ee9464a2876d263910ff6f03694a3370dc259a6d040fc7ea27e9ef7a13d66430
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0df65e13a8ef23c1a09b31fe9872cf1b5fec5b71528765c4c09651eb63edc9d4ec18d88b64b4276e881a6de04ce1cf8fd75c3b88ab45d2516c5da1fd5160eee
|
|
7
|
+
data.tar.gz: d1ee6224713d08bbd2270917c59f7106235216d2253a0a5639c6079497649561dee38cb6962fc8515ee6688619e71ce745fe365132c9334fd5484796fd1188b4
|
data/lib/coradoc/cli.rb
CHANGED
|
@@ -32,6 +32,9 @@ module Coradoc
|
|
|
32
32
|
default: 64
|
|
33
33
|
option :allow_unsafe_includes, desc: 'Disable path-traversal protection (asciidoctor :unsafe mode)',
|
|
34
34
|
type: :boolean, default: false
|
|
35
|
+
option :allow_unresolved_includes, desc: 'Allow serialization with unresolved includes ' \
|
|
36
|
+
'(formats that cannot represent them drop them)',
|
|
37
|
+
type: :boolean, default: false
|
|
35
38
|
def convert(file)
|
|
36
39
|
source_format = resolve_format(file, :from)
|
|
37
40
|
target_format = options[:to] ? Coradoc.normalize_format(options[:to]) : Coradoc.resolve_output_format(options[:output])
|
|
@@ -213,7 +216,7 @@ module Coradoc
|
|
|
213
216
|
|
|
214
217
|
CONVERT_OPTIONS = %i[
|
|
215
218
|
toc toc_levels section_numbers section_number_levels
|
|
216
|
-
lang theme asset_delivery
|
|
219
|
+
lang theme asset_delivery allow_unresolved_includes
|
|
217
220
|
].freeze
|
|
218
221
|
private_constant :CONVERT_OPTIONS
|
|
219
222
|
|
data/lib/coradoc/coradoc.rb
CHANGED
|
@@ -70,6 +70,8 @@ module Coradoc
|
|
|
70
70
|
|
|
71
71
|
def resolve_includes(document, **) = Pipeline.resolve_includes(document, **)
|
|
72
72
|
|
|
73
|
+
def resolve_references(document, **) = Pipeline.resolve_references(document, **)
|
|
74
|
+
|
|
73
75
|
def rewrite_links(...) = Pipeline.rewrite_links(...)
|
|
74
76
|
|
|
75
77
|
def convert(text, **) = Pipeline.convert(text, **)
|
|
@@ -150,6 +152,7 @@ module Coradoc
|
|
|
150
152
|
autoload :FormatCatalog, "#{__dir__}/format_catalog"
|
|
151
153
|
autoload :Introspection, "#{__dir__}/introspection"
|
|
152
154
|
autoload :Dispatch, "#{__dir__}/dispatch"
|
|
155
|
+
autoload :Reference, "#{__dir__}/reference"
|
|
153
156
|
end
|
|
154
157
|
|
|
155
158
|
# Format gems self-register via Coradoc.register_format when they are required.
|
|
@@ -2,57 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Coradoc
|
|
4
4
|
module CoreModel
|
|
5
|
-
# Represents a single item within a list
|
|
6
|
-
#
|
|
7
|
-
# A list item can contain:
|
|
8
|
-
# - Simple text content
|
|
9
|
-
# - A nested list
|
|
10
|
-
# - Child elements (paragraphs, blocks, etc.)
|
|
11
|
-
#
|
|
12
|
-
# Note: The nested_list attribute type is set after ListBlock is defined
|
|
13
|
-
# to avoid circular dependency issues.
|
|
14
|
-
#
|
|
15
|
-
# @example Simple list item
|
|
16
|
-
# item = ListItem.new(
|
|
17
|
-
# marker: "*",
|
|
18
|
-
# content: "Item text"
|
|
19
|
-
# )
|
|
20
|
-
#
|
|
21
|
-
# @example List item with nested list
|
|
22
|
-
# item = ListItem.new(
|
|
23
|
-
# marker: "*",
|
|
24
|
-
# content: "Parent item",
|
|
25
|
-
# nested_list: nested_list_block
|
|
26
|
-
# )
|
|
27
|
-
class ListItem < Base
|
|
28
|
-
# @!attribute marker
|
|
29
|
-
# @return [String, nil] the marker character(s) for this item
|
|
30
|
-
# (e.g., '*', '**', '.', '..', '-')
|
|
31
|
-
attribute :marker, :string
|
|
32
|
-
|
|
33
|
-
# @!attribute content
|
|
34
|
-
# @return [String, nil] text content of the list item
|
|
35
|
-
attribute :content, :string
|
|
36
|
-
|
|
37
|
-
# @!attribute nested_list
|
|
38
|
-
# @return [ListBlock, nil] nested list within this item
|
|
39
|
-
# Note: Typed as string initially, retyped after ListBlock defined
|
|
40
|
-
attribute :nested_list, :string
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
# Attributes to compare for semantic equivalence
|
|
45
|
-
#
|
|
46
|
-
# List items are semantically equivalent if they have the same
|
|
47
|
-
# content, nested list, and children, regardless of the specific
|
|
48
|
-
# marker used.
|
|
49
|
-
#
|
|
50
|
-
# @return [Array<Symbol>] list of comparable attributes
|
|
51
|
-
def comparable_attributes
|
|
52
|
-
%i[content nested_list children]
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
5
|
# Represents a list block with proper nesting support
|
|
57
6
|
#
|
|
58
7
|
# Handles all list types:
|
|
@@ -106,7 +55,7 @@ module Coradoc
|
|
|
106
55
|
|
|
107
56
|
# @!attribute items
|
|
108
57
|
# @return [Array<ListItem>] collection of list items
|
|
109
|
-
attribute :items, ListItem, collection: true
|
|
58
|
+
attribute :items, 'Coradoc::CoreModel::ListItem', collection: true
|
|
110
59
|
|
|
111
60
|
# -- Fluent construction helpers (paired with Base.build) --
|
|
112
61
|
|
|
@@ -138,15 +87,51 @@ module Coradoc
|
|
|
138
87
|
end
|
|
139
88
|
end
|
|
140
89
|
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
90
|
+
# Represents a single item within a list
|
|
91
|
+
#
|
|
92
|
+
# A list item can contain:
|
|
93
|
+
# - Simple text content
|
|
94
|
+
# - A nested list
|
|
95
|
+
# - Child elements (paragraphs, blocks, etc.)
|
|
96
|
+
#
|
|
97
|
+
# @example Simple list item
|
|
98
|
+
# item = ListItem.new(
|
|
99
|
+
# marker: "*",
|
|
100
|
+
# content: "Item text"
|
|
101
|
+
# )
|
|
102
|
+
#
|
|
103
|
+
# @example List item with nested list
|
|
104
|
+
# item = ListItem.new(
|
|
105
|
+
# marker: "*",
|
|
106
|
+
# content: "Parent item",
|
|
107
|
+
# nested_list: nested_list_block
|
|
108
|
+
# )
|
|
109
|
+
class ListItem < Base
|
|
110
|
+
# @!attribute marker
|
|
111
|
+
# @return [String, nil] the marker character(s) for this item
|
|
112
|
+
# (e.g., '*', '**', '.', '..', '-')
|
|
113
|
+
attribute :marker, :string
|
|
114
|
+
|
|
115
|
+
# @!attribute content
|
|
116
|
+
# @return [String, nil] text content of the list item
|
|
117
|
+
attribute :content, :string
|
|
118
|
+
|
|
119
|
+
# @!attribute nested_list
|
|
120
|
+
# @return [ListBlock, nil] nested list within this item
|
|
121
|
+
attribute :nested_list, Coradoc::CoreModel::ListBlock
|
|
122
|
+
|
|
123
|
+
private
|
|
147
124
|
|
|
148
|
-
#
|
|
149
|
-
|
|
125
|
+
# Attributes to compare for semantic equivalence
|
|
126
|
+
#
|
|
127
|
+
# List items are semantically equivalent if they have the same
|
|
128
|
+
# content, nested list, and children, regardless of the specific
|
|
129
|
+
# marker used.
|
|
130
|
+
#
|
|
131
|
+
# @return [Array<Symbol>] list of comparable attributes
|
|
132
|
+
def comparable_attributes
|
|
133
|
+
%i[content nested_list children]
|
|
134
|
+
end
|
|
150
135
|
end
|
|
151
136
|
end
|
|
152
137
|
end
|
data/lib/coradoc/errors.rb
CHANGED
|
@@ -295,6 +295,25 @@ module Coradoc
|
|
|
295
295
|
end
|
|
296
296
|
end
|
|
297
297
|
|
|
298
|
+
# Raised when a document still contains unresolved +include::+ edges
|
|
299
|
+
# at serialization time and the target format cannot represent them
|
|
300
|
+
# (it would silently drop the content). Resolve the graph first with
|
|
301
|
+
# +Coradoc.resolve_includes+, or opt out with
|
|
302
|
+
# +allow_unresolved_includes: true+.
|
|
303
|
+
class UnresolvedIncludesError < Error
|
|
304
|
+
attr_reader :targets
|
|
305
|
+
|
|
306
|
+
def initialize(targets)
|
|
307
|
+
@targets = targets
|
|
308
|
+
super(
|
|
309
|
+
"Document has #{targets.size} unresolved include directive(s): " \
|
|
310
|
+
"#{targets.join(', ')}. Resolve them before serializing with " \
|
|
311
|
+
'Coradoc.resolve_includes(doc, base_dir: ...), or pass ' \
|
|
312
|
+
'allow_unresolved_includes: true.'
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
298
317
|
# Error raised when an include chain exceeds the configured depth limit.
|
|
299
318
|
class IncludeDepthExceededError < Error
|
|
300
319
|
attr_reader :depth, :target
|
|
@@ -30,6 +30,16 @@ module Coradoc
|
|
|
30
30
|
true
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Whether the serializer can represent unresolved include edges
|
|
34
|
+
# (graph-mode CoreModel::Include nodes) without losing them.
|
|
35
|
+
# Formats that round-trip the directive natively (asciidoc,
|
|
36
|
+
# mirror) or preserve it explicitly (markdown comments) override
|
|
37
|
+
# this to true; formats that would silently drop the content keep
|
|
38
|
+
# the default false, and the pipeline raises instead of losing it.
|
|
39
|
+
def preserves_unresolved_includes?
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
# Parse a file to CoreModel, handling file-specific concerns like include resolution.
|
|
34
44
|
# Format modules that support include directives or file-relative references
|
|
35
45
|
# should override this method.
|
data/lib/coradoc/pipeline.rb
CHANGED
|
@@ -46,6 +46,63 @@ module Coradoc
|
|
|
46
46
|
Coradoc::LinkRewriter.rewrite(document, rewriter: rewriter, &)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# Resolve every reference (xref, citation, link, include, image,
|
|
50
|
+
# footnote) in a parsed document using a unified content-graph
|
|
51
|
+
# model. Mirrors +resolve_includes+ in shape: two-step, immutable.
|
|
52
|
+
#
|
|
53
|
+
# Step one always runs: every Edge is resolved through the
|
|
54
|
+
# Resolver and the +missing+/+ambiguous+ policies are enforced
|
|
55
|
+
# (raise or warn). Step two is opt-in: with +materialize: true+
|
|
56
|
+
# the tree is rebuilt with each Edge replaced by the output of
|
|
57
|
+
# the Materializer registered for its
|
|
58
|
+
# [kind, presentation, format] tuple.
|
|
59
|
+
#
|
|
60
|
+
# The input document is never mutated. With +materialize: false+
|
|
61
|
+
# the input document itself is returned; with +materialize: true+
|
|
62
|
+
# a new document is returned that structurally shares untouched
|
|
63
|
+
# subtrees with the input (treat both as immutable). Nodes whose
|
|
64
|
+
# kind has no registered materializer are preserved unchanged.
|
|
65
|
+
#
|
|
66
|
+
# @param document [CoreModel::Base] parsed document
|
|
67
|
+
# @param catalog [Reference::Catalog::*] index of addressable Content
|
|
68
|
+
# @param presentation [Reference::Presentation::Base] slicing and ordering
|
|
69
|
+
# @param resolver [Reference::Resolver::Base, nil] defaults to CatalogResolver
|
|
70
|
+
# @param missing [Symbol] :warn (default), :silent, :error, :passthrough
|
|
71
|
+
# @param ambiguous [Symbol] :disambiguate (default), :first, :error
|
|
72
|
+
# @param materialize [Boolean] when true, replace edges with rendered inlines
|
|
73
|
+
# @param format [Symbol, nil] target format for materializer lookup
|
|
74
|
+
# (:html, :asciidoc, ...); nil matches format-agnostic materializers
|
|
75
|
+
# @return [CoreModel::Base] the input document (validation only) or
|
|
76
|
+
# a new materialized document
|
|
77
|
+
#
|
|
78
|
+
# @example Resolve cross-references into HTML links
|
|
79
|
+
# doc = Coradoc.parse(text, format: :asciidoc)
|
|
80
|
+
# catalog = Coradoc::Reference::Catalog::Local.from_doc(doc)
|
|
81
|
+
# presentation = Coradoc::Reference::Presentation::SingleDocument.new
|
|
82
|
+
# resolved = Coradoc.resolve_references(
|
|
83
|
+
# doc,
|
|
84
|
+
# catalog: catalog,
|
|
85
|
+
# presentation: presentation,
|
|
86
|
+
# materialize: true,
|
|
87
|
+
# format: :html
|
|
88
|
+
# )
|
|
89
|
+
def resolve_references(document, catalog:, presentation:,
|
|
90
|
+
resolver: nil,
|
|
91
|
+
missing: :warn,
|
|
92
|
+
ambiguous: :disambiguate,
|
|
93
|
+
materialize: false,
|
|
94
|
+
format: nil)
|
|
95
|
+
Coradoc::Reference::Resolution.new(
|
|
96
|
+
catalog: catalog,
|
|
97
|
+
presentation: presentation,
|
|
98
|
+
resolver: resolver,
|
|
99
|
+
missing: missing,
|
|
100
|
+
ambiguous: ambiguous,
|
|
101
|
+
materialize: materialize,
|
|
102
|
+
format: format
|
|
103
|
+
).call(document)
|
|
104
|
+
end
|
|
105
|
+
|
|
49
106
|
def convert(text, from:, to:, **)
|
|
50
107
|
core = parse(text, format: from)
|
|
51
108
|
serialize(core, to: to, **)
|
|
@@ -63,10 +120,12 @@ module Coradoc
|
|
|
63
120
|
raise TransformationError, "No transformer found for #{model.class}"
|
|
64
121
|
end
|
|
65
122
|
|
|
66
|
-
def serialize(model, to:, **)
|
|
123
|
+
def serialize(model, to:, allow_unresolved_includes: false, **)
|
|
67
124
|
format_module = FormatCatalog.get_format(to)
|
|
68
125
|
raise UnsupportedFormatError.new(to, available: FormatCatalog.registered_formats) unless format_module
|
|
69
126
|
|
|
127
|
+
Coradoc::Validation.guard_unresolved_includes!(model, format_module) unless allow_unresolved_includes
|
|
128
|
+
|
|
70
129
|
model = Hooks.invoke(:before_serialize, model, format: to)
|
|
71
130
|
result = format_module.serialize(model, **)
|
|
72
131
|
Hooks.invoke(:after_serialize, result, format: to)
|
|
@@ -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
|