coradoc 2.0.22 → 2.0.24
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/coradoc.rb +35 -402
- data/lib/coradoc/core_model/base.rb +39 -0
- data/lib/coradoc/core_model/comment_block.rb +4 -0
- data/lib/coradoc/core_model/comment_line.rb +4 -0
- data/lib/coradoc/core_model/frontmatter/codec.rb +66 -19
- data/lib/coradoc/core_model/frontmatter.rb +4 -0
- data/lib/coradoc/core_model/inline_content.rb +77 -0
- data/lib/coradoc/core_model/inline_element.rb +52 -26
- data/lib/coradoc/core_model/list_block.rb +17 -0
- data/lib/coradoc/core_model/list_item.rb +21 -0
- data/lib/coradoc/core_model/output_artifact.rb +48 -0
- data/lib/coradoc/core_model/paragraph_block.rb +4 -0
- data/lib/coradoc/core_model/stem_block.rb +21 -0
- data/lib/coradoc/core_model/structural_element.rb +41 -0
- data/lib/coradoc/core_model/text_content.rb +4 -0
- data/lib/coradoc/core_model.rb +3 -0
- data/lib/coradoc/dispatch.rb +95 -0
- data/lib/coradoc/format_catalog.rb +83 -0
- data/lib/coradoc/introspection/element_counter.rb +48 -0
- data/lib/coradoc/introspection.rb +72 -0
- data/lib/coradoc/link_rewriter/identity.rb +13 -0
- data/lib/coradoc/link_rewriter/visitor.rb +157 -0
- data/lib/coradoc/link_rewriter.rb +37 -0
- data/lib/coradoc/pipeline.rb +108 -0
- data/lib/coradoc/relative_path.rb +32 -0
- data/lib/coradoc/version.rb +1 -1
- data/lib/coradoc.rb +7 -13
- metadata +13 -9
- data/lib/coradoc/document_builder.rb +0 -184
- data/lib/coradoc/document_manipulator.rb +0 -203
- data/lib/coradoc/input.rb +0 -22
- data/lib/coradoc/output.rb +0 -22
- data/lib/coradoc/processor_registry.rb +0 -50
- data/lib/coradoc/serializer/registry.rb +0 -150
- data/lib/coradoc/transform/base.rb +0 -21
- data/lib/coradoc/transform.rb +0 -7
|
@@ -10,42 +10,89 @@ module Coradoc
|
|
|
10
10
|
# No other code in any gem may call YAML directly for frontmatter.
|
|
11
11
|
# This isolates permitted-classes configuration and error handling
|
|
12
12
|
# in one MECE location (DRY).
|
|
13
|
+
#
|
|
14
|
+
# The Codec emits flat YAML — values rendered with their natural
|
|
15
|
+
# YAML type. This is what Jekyll, Hugo, VitePress, VuePress, 11ty
|
|
16
|
+
# and every SSG expects: +title: Foo+ / +date: 2024-01-01+.
|
|
17
|
+
# Round-trip fidelity for typed values (Date, Time, Symbol) is
|
|
18
|
+
# preserved by Psych's permitted-classes mechanism, not by a
|
|
19
|
+
# custom discriminator scheme.
|
|
20
|
+
#
|
|
21
|
+
# For the typed-tree representation used by the coradoc-mirror JSON
|
|
22
|
+
# pipeline, see +Coradoc::Mirror::Node::FrontmatterValue+ and
|
|
23
|
+
# +Coradoc::Mirror::Handlers::Frontmatter+. The typed-tree concern
|
|
24
|
+
# lives in the mirror gem; this Codec stays focused on YAML.
|
|
13
25
|
module Codec
|
|
14
26
|
PERMITTED_CLASSES = [Date, Time, DateTime, Symbol].freeze
|
|
15
27
|
|
|
16
28
|
class << self
|
|
17
|
-
# Parse
|
|
18
|
-
#
|
|
19
|
-
#
|
|
29
|
+
# Parse YAML text into a FrontmatterBlock. Returns an empty
|
|
30
|
+
# FrontmatterBlock on malformed YAML or non-Hash payload.
|
|
31
|
+
# Logs a warning so the conversion pipeline can surface the
|
|
32
|
+
# skip rather than silently dropping user-authored content.
|
|
20
33
|
def from_yaml(yaml_text)
|
|
21
34
|
return FrontmatterBlock.new if yaml_text.nil? || yaml_text.strip.empty?
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
aliases: true
|
|
27
|
-
)
|
|
28
|
-
return FrontmatterBlock.new unless parsed.is_a?(Hash)
|
|
29
|
-
|
|
30
|
-
schema = parsed['$schema']
|
|
31
|
-
data = parsed.except('$schema')
|
|
32
|
-
FrontmatterBlock.new(schema: schema&.to_s, data: data)
|
|
33
|
-
rescue YAML::SyntaxError, Psych::DisallowedClass
|
|
36
|
+
build_from_loaded(load_yaml(yaml_text))
|
|
37
|
+
rescue YAML::SyntaxError, Psych::DisallowedClass => e
|
|
38
|
+
Coradoc::Logger.warn("frontmatter parse failed: #{e.message}")
|
|
34
39
|
FrontmatterBlock.new
|
|
35
40
|
end
|
|
36
41
|
|
|
42
|
+
# Build a FrontmatterBlock from a Ruby hash with native-typed
|
|
43
|
+
# values (String, Integer, Date, …). Returns an empty block
|
|
44
|
+
# for non-Hash input.
|
|
45
|
+
def from_hash(hash)
|
|
46
|
+
return FrontmatterBlock.new unless hash.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
build_from_loaded(hash)
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
# Serialize a FrontmatterBlock to canonical YAML text.
|
|
38
|
-
# Does NOT include leading/trailing
|
|
39
|
-
# wraps the output.
|
|
52
|
+
# Does NOT include leading/trailing +---+ delimiters; the
|
|
53
|
+
# caller wraps the output. Returns +''+ for empty blocks.
|
|
40
54
|
def to_yaml(block)
|
|
41
55
|
return '' unless block.is_a?(FrontmatterBlock)
|
|
42
56
|
|
|
57
|
+
payload = flat_tree(block)
|
|
58
|
+
return '' if payload.empty?
|
|
59
|
+
|
|
60
|
+
YAML.dump(payload).delete_prefix("---\n").delete_suffix("\n...")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Return the frontmatter as a native-typed Ruby hash.
|
|
64
|
+
# +$schema+ is included when present.
|
|
65
|
+
def to_hash(block)
|
|
66
|
+
return {} unless block.is_a?(FrontmatterBlock)
|
|
67
|
+
|
|
68
|
+
flat_tree(block)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def load_yaml(yaml_text)
|
|
74
|
+
YAML.safe_load(
|
|
75
|
+
yaml_text,
|
|
76
|
+
permitted_classes: PERMITTED_CLASSES,
|
|
77
|
+
aliases: true
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def build_from_loaded(loaded)
|
|
82
|
+
return FrontmatterBlock.new unless loaded.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
schema = loaded['$schema']
|
|
85
|
+
FrontmatterBlock.new(
|
|
86
|
+
schema: schema&.to_s,
|
|
87
|
+
data: loaded.except('$schema')
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def flat_tree(block)
|
|
43
92
|
tree = {}
|
|
44
93
|
tree['$schema'] = block.schema if block.schema
|
|
45
94
|
tree.merge!(block.data || {})
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
YAML.dump(tree).delete_prefix("---\n").delete_suffix("\n...")
|
|
95
|
+
tree
|
|
49
96
|
end
|
|
50
97
|
end
|
|
51
98
|
end
|
|
@@ -50,6 +50,10 @@ module Coradoc
|
|
|
50
50
|
schema.nil? && (data.nil? || data.empty?)
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
def body_content?
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
|
|
53
57
|
# Sub-namespaces (Codec, SchemaResolver, FieldTransform, TextSplitter)
|
|
54
58
|
# live under FrontmatterBlock and autoload lazily.
|
|
55
59
|
autoload :Codec, "#{__dir__}/frontmatter/codec"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module CoreModel
|
|
5
|
+
# Operations on mixed arrays of inline content (String /
|
|
6
|
+
# InlineElement / CoreModel::Base). Single source of truth for text
|
|
7
|
+
# extraction and edge cleanup, replacing parallel implementations
|
|
8
|
+
# that previously lived in the HTML converters.
|
|
9
|
+
#
|
|
10
|
+
# No method here mutates its inputs — InlineElements are duplicated
|
|
11
|
+
# via #with_content, Strings are replaced with new instances.
|
|
12
|
+
module InlineContent
|
|
13
|
+
class << self
|
|
14
|
+
# Extract plain text from a mixed content value.
|
|
15
|
+
#
|
|
16
|
+
# nil → '' / String → itself / Array → text_of mapped + joined /
|
|
17
|
+
# InlineElement → #content.to_s / StructuralElement → recurse on
|
|
18
|
+
# #children / other Base → #content if String else #title.to_s /
|
|
19
|
+
# anything else → #to_s.
|
|
20
|
+
def text_of(content)
|
|
21
|
+
return '' if content.nil?
|
|
22
|
+
return content if content.is_a?(String)
|
|
23
|
+
return text_of_one(content) unless content.is_a?(Array)
|
|
24
|
+
|
|
25
|
+
content.map { |item| text_of_one(item) }.join
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Return a new array with leading whitespace stripped from the
|
|
29
|
+
# first text-carrying item and trailing whitespace stripped from
|
|
30
|
+
# the last. Inputs are not mutated. Non-Array inputs return
|
|
31
|
+
# unchanged. If no item carries text, returns the input array
|
|
32
|
+
# unchanged.
|
|
33
|
+
def strip_edges(content)
|
|
34
|
+
return content unless content.is_a?(Array)
|
|
35
|
+
return content if content.empty?
|
|
36
|
+
|
|
37
|
+
first_idx = content.index { |i| text_carrier?(i) }
|
|
38
|
+
return content if first_idx.nil?
|
|
39
|
+
last_idx = content.rindex { |i| text_carrier?(i) }
|
|
40
|
+
|
|
41
|
+
content.map.with_index do |item, idx|
|
|
42
|
+
next item unless text_carrier?(item)
|
|
43
|
+
|
|
44
|
+
stripped = item_text(item)
|
|
45
|
+
stripped = stripped.lstrip if idx == first_idx
|
|
46
|
+
stripped = stripped.rstrip if idx == last_idx
|
|
47
|
+
item.is_a?(String) ? stripped : item.with_content(stripped)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def text_of_one(item)
|
|
54
|
+
case item
|
|
55
|
+
when String then item
|
|
56
|
+
when CoreModel::InlineElement then item.content.to_s
|
|
57
|
+
when CoreModel::StructuralElement then text_of(Array(item.children))
|
|
58
|
+
when CoreModel::Block
|
|
59
|
+
item.children.is_a?(Array) && !item.children.empty? ? text_of(item.children) : item.content.to_s
|
|
60
|
+
when CoreModel::Base
|
|
61
|
+
item.content.is_a?(String) ? item.content : item.title.to_s
|
|
62
|
+
else
|
|
63
|
+
item.to_s
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def text_carrier?(item)
|
|
68
|
+
item.is_a?(String) || item.is_a?(CoreModel::InlineElement)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def item_text(item)
|
|
72
|
+
item.is_a?(String) ? item : item.content.to_s
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -24,13 +24,24 @@ module Coradoc
|
|
|
24
24
|
self.class.format_type || format_type
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
# Return a duplicate of this element with its content replaced.
|
|
28
|
+
# Used by InlineContent.strip_edges (and other edge-cleanup
|
|
29
|
+
# callers) so that mutations never leak through the public API.
|
|
30
|
+
def with_content(new_content)
|
|
31
|
+
dup.tap { |copy| copy.content = new_content }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Polymorphic classification used by LinkRewriter::Visitor. Returns
|
|
35
|
+
# :link / :xref when this node carries a rewrite-able target, nil
|
|
36
|
+
# otherwise. Generic InlineElement instances defer to their
|
|
37
|
+
# resolved format_type; typed subclasses override with a literal.
|
|
38
|
+
# Keeps the visitor free of class-keyed case/when (OCP).
|
|
39
|
+
def link_kind
|
|
40
|
+
case resolve_format_type
|
|
41
|
+
when 'link' then :link
|
|
42
|
+
when 'xref' then :xref
|
|
43
|
+
end
|
|
44
|
+
end
|
|
34
45
|
|
|
35
46
|
attribute :format_type, :string
|
|
36
47
|
attribute :content, :string
|
|
@@ -99,12 +110,20 @@ module Coradoc
|
|
|
99
110
|
def self.format_type
|
|
100
111
|
'link'
|
|
101
112
|
end
|
|
113
|
+
|
|
114
|
+
def link_kind
|
|
115
|
+
:link
|
|
116
|
+
end
|
|
102
117
|
end
|
|
103
118
|
|
|
104
119
|
class CrossReferenceElement < InlineElement
|
|
105
120
|
def self.format_type
|
|
106
121
|
'xref'
|
|
107
122
|
end
|
|
123
|
+
|
|
124
|
+
def link_kind
|
|
125
|
+
:xref
|
|
126
|
+
end
|
|
108
127
|
end
|
|
109
128
|
|
|
110
129
|
class StemElement < InlineElement
|
|
@@ -149,24 +168,31 @@ module Coradoc
|
|
|
149
168
|
end
|
|
150
169
|
end
|
|
151
170
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
# Wire-name table: bidirectional string ↔ class index. Single source
|
|
172
|
+
# of truth for serialization names and runtime dispatch. Reopened
|
|
173
|
+
# onto InlineElement so the subclasses above are defined first.
|
|
174
|
+
class InlineElement
|
|
175
|
+
FORMAT_TYPE_CLASS_MAP = {
|
|
176
|
+
'bold' => BoldElement,
|
|
177
|
+
'italic' => ItalicElement,
|
|
178
|
+
'monospace' => MonospaceElement,
|
|
179
|
+
'underline' => UnderlineElement,
|
|
180
|
+
'strikethrough' => StrikethroughElement,
|
|
181
|
+
'subscript' => SubscriptElement,
|
|
182
|
+
'superscript' => SuperscriptElement,
|
|
183
|
+
'highlight' => HighlightElement,
|
|
184
|
+
'link' => LinkElement,
|
|
185
|
+
'xref' => CrossReferenceElement,
|
|
186
|
+
'stem' => StemElement,
|
|
187
|
+
'footnote' => FootnoteElement,
|
|
188
|
+
'hard_line_break' => HardLineBreakElement,
|
|
189
|
+
'text' => TextElement,
|
|
190
|
+
'span' => SpanElement,
|
|
191
|
+
'term' => TermElement,
|
|
192
|
+
'line_break' => LineBreakElement
|
|
193
|
+
}.freeze
|
|
194
|
+
|
|
195
|
+
FORMAT_TYPES = FORMAT_TYPE_CLASS_MAP.keys.freeze
|
|
196
|
+
end
|
|
171
197
|
end
|
|
172
198
|
end
|
|
@@ -108,6 +108,23 @@ module Coradoc
|
|
|
108
108
|
# @return [Array<ListItem>] collection of list items
|
|
109
109
|
attribute :items, ListItem, collection: true
|
|
110
110
|
|
|
111
|
+
# -- Fluent construction helpers (paired with Base.build) --
|
|
112
|
+
|
|
113
|
+
# Append a new ListItem, built via ListItem.build. The block
|
|
114
|
+
# (if given) is yielded the new item so callers can chain
|
|
115
|
+
# +add_text+ / +add_link+ on it inline:
|
|
116
|
+
#
|
|
117
|
+
# ListBlock.build do |ul|
|
|
118
|
+
# children.each { |c| ul.add_item { |li| li.add_link(c[:slug], text: c[:title]) } }
|
|
119
|
+
# end
|
|
120
|
+
#
|
|
121
|
+
# Returns self for chaining at the list level.
|
|
122
|
+
def add_item(marker: self.marker_type == 'ordered' ? '.' : '*')
|
|
123
|
+
item = ListItem.build(marker: marker) { |li| yield li if block_given? }
|
|
124
|
+
self.items = Array(items) + [item]
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
|
|
111
128
|
private
|
|
112
129
|
|
|
113
130
|
# Attributes to compare for semantic equivalence
|
|
@@ -109,6 +109,27 @@ module Coradoc
|
|
|
109
109
|
true
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
# -- Fluent construction helpers (paired with Base.build) --
|
|
113
|
+
|
|
114
|
+
# Append a plain-text inline to this item's children. Returns
|
|
115
|
+
# self for chaining. Pairs naturally with ListItem.build:
|
|
116
|
+
#
|
|
117
|
+
# ListItem.build do |li|
|
|
118
|
+
# li.add_text("See ")
|
|
119
|
+
# li.add_link("foo.adoc", text: "Foo")
|
|
120
|
+
# end
|
|
121
|
+
def add_text(text)
|
|
122
|
+
self.children = Array(children) + [TextContent.new(text: text)]
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Append a +link:+ inline to this item's children. +text+ becomes
|
|
127
|
+
# the link's visible label; +target+ is the URL/anchor.
|
|
128
|
+
def add_link(target, text: nil)
|
|
129
|
+
self.children = Array(children) + [LinkElement.new(target: target, content: text)]
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
112
133
|
private
|
|
113
134
|
|
|
114
135
|
# Compare two list blocks for equivalence
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module CoreModel
|
|
5
|
+
# Output-side state object for host-system emitters (VitePress, Hugo,
|
|
6
|
+
# Astro, plain ERB, etc.).
|
|
7
|
+
#
|
|
8
|
+
# coradoc's source side has +IncludeResolver::Filesystem+ to resolve
|
|
9
|
+
# include targets without coupling to a storage layer. The output side
|
|
10
|
+
# has no analogous state object — until now. +OutputArtifact+ captures
|
|
11
|
+
# the three pieces of state coradoc genuinely needs to hand to a
|
|
12
|
+
# downstream emitter:
|
|
13
|
+
#
|
|
14
|
+
# - +output_key+ — site-relative key (e.g. "author/iso/ref/foo")
|
|
15
|
+
# - +frontmatter_block+ — parsed YAML frontmatter (may be empty)
|
|
16
|
+
# - +core_document+ — the canonical CoreModel document
|
|
17
|
+
#
|
|
18
|
+
# The consumer takes these and renders whatever wrapper it needs in
|
|
19
|
+
# its host system's native template language. coradoc does not know
|
|
20
|
+
# about VitePress, ERB, or Liquid. Symmetric with the source side:
|
|
21
|
+
# minimal protocol object, not an engine.
|
|
22
|
+
#
|
|
23
|
+
# A mirror-tree document is deliberately NOT bundled here. coradoc
|
|
24
|
+
# core has no runtime dependency on coradoc-mirror; consumers that
|
|
25
|
+
# target the mirror JSON pipeline pair an +OutputArtifact+ with a
|
|
26
|
+
# separately-computed +Coradoc::Mirror.transform(core)+ result.
|
|
27
|
+
class OutputArtifact < Base
|
|
28
|
+
# @!attribute output_key
|
|
29
|
+
# @return [String, nil] site-relative key with no leading slash
|
|
30
|
+
# and no trailing extension. SSGs map this to their URL space.
|
|
31
|
+
attribute :output_key, :string
|
|
32
|
+
|
|
33
|
+
# @!attribute frontmatter_block
|
|
34
|
+
# @return [FrontmatterBlock, nil] parsed YAML frontmatter
|
|
35
|
+
attribute :frontmatter_block, FrontmatterBlock
|
|
36
|
+
|
|
37
|
+
# @!attribute core_document
|
|
38
|
+
# @return [DocumentElement, nil] canonical CoreModel document
|
|
39
|
+
attribute :core_document, DocumentElement
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def comparable_attributes
|
|
44
|
+
%i[output_key frontmatter_block core_document]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module CoreModel
|
|
5
|
+
# STEM block — mathematical/scientific content authored in LaTeX,
|
|
6
|
+
# AsciiMath, or another STEM markup. Carries a +language+ attribute so
|
|
7
|
+
# downstream renderers know which interpreter to invoke.
|
|
8
|
+
#
|
|
9
|
+
# AsciiDoc surface forms:
|
|
10
|
+
# [stem]\n++++\nx^2\n++++ # language: "latex" (default)
|
|
11
|
+
# [latexmath]\n++++\nx^2\n++++ # language: "latex"
|
|
12
|
+
# [asciimath]\n++++\nx^2\n++++ # language: "asciimath"
|
|
13
|
+
class StemBlock < Block
|
|
14
|
+
attribute :language, :string, default: -> { 'latex' }
|
|
15
|
+
|
|
16
|
+
def self.semantic_type
|
|
17
|
+
:stem
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -52,6 +52,38 @@ module Coradoc
|
|
|
52
52
|
false
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Override in subclasses that carry document-title semantics.
|
|
56
|
+
# HeaderElement at level 0 represents the document title (`= Title`
|
|
57
|
+
# in AsciiDoc). Consumers that walk the body — TOC builders,
|
|
58
|
+
# section numbering — skip these so the title is not counted as
|
|
59
|
+
# "section 1". Polymorphic dispatch (vs. an is_a? guard at the
|
|
60
|
+
# call site) keeps the predicate open for future subclasses.
|
|
61
|
+
def document_title?
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Children that count as body content and aren't whitespace-only.
|
|
66
|
+
# Derived from per-node {#body_content?} and {#whitespace_only?}
|
|
67
|
+
# predicates — no central walker, no is_a? switch to maintain.
|
|
68
|
+
def visible_children
|
|
69
|
+
Array(children).select(&:body_content?).reject(&:whitespace_only?)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# True when the body has no visible content anywhere in its subtree.
|
|
73
|
+
# A document with only frontmatter + comments returns true; a
|
|
74
|
+
# document with one non-whitespace paragraph returns false.
|
|
75
|
+
def empty_body?
|
|
76
|
+
return true if children.nil? || children.empty?
|
|
77
|
+
|
|
78
|
+
children.all? do |child|
|
|
79
|
+
next true unless child.body_content?
|
|
80
|
+
next true if child.whitespace_only?
|
|
81
|
+
next child.empty_body? if child.is_a?(StructuralElement)
|
|
82
|
+
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
55
87
|
# Derived element_type string for backward compatibility with
|
|
56
88
|
# templates and legacy consumers. Subclasses override this.
|
|
57
89
|
def element_type
|
|
@@ -108,6 +140,15 @@ module Coradoc
|
|
|
108
140
|
true
|
|
109
141
|
end
|
|
110
142
|
|
|
143
|
+
# A level-0 HeaderElement represents the document title (the `= Title`
|
|
144
|
+
# line in AsciiDoc, the `<h1>` in HTML). It is structurally part of
|
|
145
|
+
# the body but semantically the document's title, not a section —
|
|
146
|
+
# section numbering, TOC builders, and other section-aware logic
|
|
147
|
+
# skip these so the title is not counted as "section 1".
|
|
148
|
+
def document_title?
|
|
149
|
+
level.to_i.zero?
|
|
150
|
+
end
|
|
151
|
+
|
|
111
152
|
def self.element_type_name
|
|
112
153
|
'header'
|
|
113
154
|
end
|
data/lib/coradoc/core_model.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Coradoc
|
|
|
13
13
|
autoload :Base, "#{__dir__}/core_model/base"
|
|
14
14
|
autoload :ChildrenContent, "#{__dir__}/core_model/children_content"
|
|
15
15
|
autoload :HasChildren, "#{__dir__}/core_model/has_children"
|
|
16
|
+
autoload :InlineContent, "#{__dir__}/core_model/inline_content"
|
|
16
17
|
autoload :Callout, "#{__dir__}/core_model/callout"
|
|
17
18
|
autoload :CalloutText, "#{__dir__}/core_model/callout_text"
|
|
18
19
|
autoload :Block, "#{__dir__}/core_model/block"
|
|
@@ -70,6 +71,7 @@ module Coradoc
|
|
|
70
71
|
autoload :SidebarBlock, "#{__dir__}/core_model/sidebar_block"
|
|
71
72
|
autoload :LiteralBlock, "#{__dir__}/core_model/literal_block"
|
|
72
73
|
autoload :PassBlock, "#{__dir__}/core_model/pass_block"
|
|
74
|
+
autoload :StemBlock, "#{__dir__}/core_model/stem_block"
|
|
73
75
|
autoload :ListingBlock, "#{__dir__}/core_model/listing_block"
|
|
74
76
|
autoload :OpenBlock, "#{__dir__}/core_model/open_block"
|
|
75
77
|
autoload :VerseBlock, "#{__dir__}/core_model/verse_block"
|
|
@@ -82,5 +84,6 @@ module Coradoc
|
|
|
82
84
|
autoload :Include, "#{__dir__}/core_model/include"
|
|
83
85
|
autoload :IncludeOptions, "#{__dir__}/core_model/include_options"
|
|
84
86
|
autoload :IncludeLevelOffset, "#{__dir__}/core_model/include_level_offset"
|
|
87
|
+
autoload :OutputArtifact, "#{__dir__}/core_model/output_artifact"
|
|
85
88
|
end
|
|
86
89
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
# Type-keyed handler dispatch.
|
|
5
|
+
#
|
|
6
|
+
# Replaces the bespoke `register/lookup` hashes that recur across gems with
|
|
7
|
+
# one cohesive registry. Each gem that needs type-keyed dispatch configures
|
|
8
|
+
# a single Dispatch instance and exposes its legacy DSL as thin delegates.
|
|
9
|
+
#
|
|
10
|
+
# Two resolution policies cover the common shapes:
|
|
11
|
+
#
|
|
12
|
+
# - `Coradoc::Dispatch.strict` — exact key match, raises on miss
|
|
13
|
+
# - `Coradoc::Dispatch.hierarchical` — walks ancestors on miss, returns nil
|
|
14
|
+
#
|
|
15
|
+
# Registries that need priority ordering, predicate matching, or lazy
|
|
16
|
+
# loading do not fit this shape and stay as they are; the friction there is
|
|
17
|
+
# genuine semantic difference, not duplicated mechanism.
|
|
18
|
+
class Dispatch
|
|
19
|
+
TERMINAL_ANCESTORS = [Object, BasicObject].freeze
|
|
20
|
+
private_constant :TERMINAL_ANCESTORS
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Exact key match; raises if no entry. Used by registries that map a
|
|
24
|
+
# concrete type to its sole handler (e.g. AsciiDoc ElementRegistry).
|
|
25
|
+
def strict = new(walk_ancestors: false)
|
|
26
|
+
|
|
27
|
+
# Walks the key's class ancestors on miss; returns nil if no entry.
|
|
28
|
+
# Used by registries that want base-class handlers to apply to all
|
|
29
|
+
# subclasses (e.g. Mirror HandlerRegistry).
|
|
30
|
+
def hierarchical = new(walk_ancestors: true, miss: :return_nil)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(walk_ancestors: false, miss: :raise, &default)
|
|
34
|
+
@walk_ancestors = walk_ancestors
|
|
35
|
+
@miss = miss
|
|
36
|
+
@default = default
|
|
37
|
+
@entries = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def register(key, handler)
|
|
41
|
+
@entries[key] = handler
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Replace the handler for an existing key. Returns the previous handler
|
|
45
|
+
# so wrappers can chain: original = dispatch.override(K, Wrapper.new(original))
|
|
46
|
+
def override(key, handler)
|
|
47
|
+
previous = @entries[key]
|
|
48
|
+
@entries[key] = handler
|
|
49
|
+
previous
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def unregister(key)
|
|
53
|
+
@entries.delete(key)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resolve the handler for a key. Returns nil if no handler matches
|
|
57
|
+
# unless the dispatch is configured to raise.
|
|
58
|
+
def lookup(key)
|
|
59
|
+
exact = @entries[key]
|
|
60
|
+
return exact if exact
|
|
61
|
+
return walk(key) if @walk_ancestors && key.is_a?(Class)
|
|
62
|
+
|
|
63
|
+
apply_default(key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Resolve the handler, raising Coradoc::Error on miss.
|
|
67
|
+
def lookup!(key)
|
|
68
|
+
lookup(key) || raise(Coradoc::Error, "no handler registered for #{key.inspect}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def registered?(key) = @entries.key?(key)
|
|
72
|
+
|
|
73
|
+
def registered_keys = @entries.keys
|
|
74
|
+
|
|
75
|
+
def clear! = @entries.clear
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def walk(klass)
|
|
80
|
+
klass.ancestors.each do |ancestor|
|
|
81
|
+
next if ancestor == klass
|
|
82
|
+
break if TERMINAL_ANCESTORS.include?(ancestor)
|
|
83
|
+
|
|
84
|
+
entry = @entries[ancestor]
|
|
85
|
+
return entry if entry
|
|
86
|
+
end
|
|
87
|
+
apply_default(klass)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def apply_default(key)
|
|
91
|
+
return @default.call(key) if @default
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|