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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
# Format registry, detection, and capability introspection. Single
|
|
5
|
+
# source of truth for "what formats exist and what can they do?",
|
|
6
|
+
# extracted from the top-level Coradoc façade. Public API on
|
|
7
|
+
# +Coradoc+ delegates here.
|
|
8
|
+
module FormatCatalog
|
|
9
|
+
class << self
|
|
10
|
+
def registry
|
|
11
|
+
@registry ||= Registry.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def register_format(format_name, format_module, **options)
|
|
15
|
+
format_module.extend(FormatModule::Interface) unless format_module.is_a?(FormatModule::Interface)
|
|
16
|
+
registry.register(format_name, format_module, options)
|
|
17
|
+
FormatModule.validate!(format_module, format_name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get_format(format_name)
|
|
21
|
+
registry.get(format_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def registered_formats
|
|
25
|
+
registry.list
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def detect_format(filename)
|
|
29
|
+
ext = File.extname(filename).downcase
|
|
30
|
+
registry.each_key do |name|
|
|
31
|
+
opts = registry.options_for(name)
|
|
32
|
+
return name if opts[:extensions]&.include?(ext)
|
|
33
|
+
end
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def binary_format?(format)
|
|
38
|
+
opts = registry.options_for(format)
|
|
39
|
+
opts&.fetch(:binary, false) == true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize_format(name)
|
|
43
|
+
return nil unless name
|
|
44
|
+
|
|
45
|
+
key = name.to_s.downcase
|
|
46
|
+
registry.each_key do |fmt_name|
|
|
47
|
+
opts = registry.options_for(fmt_name)
|
|
48
|
+
return fmt_name if opts[:aliases]&.include?(key)
|
|
49
|
+
end
|
|
50
|
+
key.to_sym
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def serialize_format?(format)
|
|
54
|
+
mod = get_format(format)
|
|
55
|
+
return false unless mod
|
|
56
|
+
|
|
57
|
+
mod.serialize?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_format?(format)
|
|
61
|
+
mod = get_format(format)
|
|
62
|
+
return false unless mod
|
|
63
|
+
|
|
64
|
+
mod.public_methods.include?(:parse_to_core) || mod.public_methods.include?(:parse)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def capabilities
|
|
68
|
+
registered_formats.each_with_object({}) do |name, caps|
|
|
69
|
+
caps[name] = {
|
|
70
|
+
parse: parse_format?(name),
|
|
71
|
+
serialize: serialize_format?(name)
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resolve_output_format(output_file, default: :html)
|
|
77
|
+
return default unless output_file
|
|
78
|
+
|
|
79
|
+
detect_format(output_file) || default
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Introspection
|
|
5
|
+
# Visitor that walks a document and counts each CoreModel node by
|
|
6
|
+
# its type key. Used by Introspection.count_element_types to back
|
|
7
|
+
# the +Coradoc.document_stats+ API.
|
|
8
|
+
#
|
|
9
|
+
# Typed StructuralElement / Block nodes are counted under their
|
|
10
|
+
# +element_type+ (semantic identity). Other nodes fall back to a
|
|
11
|
+
# snake_case rendering of their class name.
|
|
12
|
+
class ElementCounter < Visitor::Base
|
|
13
|
+
def initialize
|
|
14
|
+
@counts = Hash.new(0)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :counts
|
|
18
|
+
|
|
19
|
+
def visit(element)
|
|
20
|
+
return super(element) unless element.is_a?(CoreModel::Base)
|
|
21
|
+
|
|
22
|
+
@counts[type_key_for(element)] += 1
|
|
23
|
+
super(element)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def type_key_for(element)
|
|
29
|
+
if typed_node?(element) && element.element_type
|
|
30
|
+
element.element_type
|
|
31
|
+
else
|
|
32
|
+
snake_case(element.class.name)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def typed_node?(element)
|
|
37
|
+
element.is_a?(CoreModel::StructuralElement) || element.is_a?(CoreModel::Block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def snake_case(class_name)
|
|
41
|
+
class_name.split('::').last
|
|
42
|
+
.gsub(/([A-Z])/, '_\1')
|
|
43
|
+
.downcase
|
|
44
|
+
.sub(/^_/, '')
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
# Document introspection: file metadata, validation, stats. Extracted
|
|
5
|
+
# from the top-level Coradoc façade so the document-counting visitor
|
|
6
|
+
# and file-metadata helpers have their own home. Public API on
|
|
7
|
+
# +Coradoc+ delegates here.
|
|
8
|
+
module Introspection
|
|
9
|
+
autoload :ElementCounter, "#{__dir__}/introspection/element_counter"
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def file_info(path)
|
|
13
|
+
fmt = FormatCatalog.detect_format(path)
|
|
14
|
+
info = { size: File.size(path), format: fmt }
|
|
15
|
+
info[:lines] = File.foreach(path).count unless FormatCatalog.binary_format?(fmt)
|
|
16
|
+
info
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def validate_file(path, format: nil)
|
|
20
|
+
doc = Pipeline.parse_file(path, format: format)
|
|
21
|
+
|
|
22
|
+
schema = Validation::SchemaGenerator.generate(doc.class)
|
|
23
|
+
return schema.validate(doc) if schema
|
|
24
|
+
|
|
25
|
+
Validation::Result.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def document_stats(doc)
|
|
29
|
+
stats = {}
|
|
30
|
+
stats[:title] = doc.title if doc.title
|
|
31
|
+
|
|
32
|
+
if doc.is_a?(CoreModel::StructuralElement)
|
|
33
|
+
stats[:child_count] = count_elements(doc)
|
|
34
|
+
stats[:element_counts] = count_element_types(doc)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
stats
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def describe_element(elem)
|
|
41
|
+
return elem.to_s unless elem.is_a?(CoreModel::Base)
|
|
42
|
+
|
|
43
|
+
type = elem.class.name.split('::').last
|
|
44
|
+
if elem.title
|
|
45
|
+
"#{type}: #{elem.title}"
|
|
46
|
+
elsif elem.is_a?(CoreModel::Block) && elem.content
|
|
47
|
+
preview = elem.content.to_s[0..50]
|
|
48
|
+
preview += '...' if elem.content.to_s.length > 50
|
|
49
|
+
"#{type}: #{preview}"
|
|
50
|
+
else
|
|
51
|
+
type
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def count_elements(doc)
|
|
58
|
+
return 0 unless doc.is_a?(CoreModel::StructuralElement)
|
|
59
|
+
|
|
60
|
+
doc.children.sum do |child|
|
|
61
|
+
1 + (child.is_a?(CoreModel::StructuralElement) ? count_elements(child) : 0)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def count_element_types(doc)
|
|
66
|
+
counter = ElementCounter.new
|
|
67
|
+
counter.visit(doc)
|
|
68
|
+
counter.counts.reject { |_, v| v.zero? }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module LinkRewriter
|
|
5
|
+
# Default no-op rewriter. Returns every target unchanged. Used when
|
|
6
|
+
# the caller only wants the visitor's immutable-copy guarantee.
|
|
7
|
+
class Identity
|
|
8
|
+
def call(target:, **)
|
|
9
|
+
target
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module LinkRewriter
|
|
5
|
+
# Focused rewriting visitor for the CoreModel tree.
|
|
6
|
+
#
|
|
7
|
+
# The set of node types that carry link/xref targets is closed and
|
|
8
|
+
# small: +LinkElement+, +CrossReferenceElement+, plus generic
|
|
9
|
+
# +InlineElement+ instances whose +format_type+ is +'link'+ or
|
|
10
|
+
# +'xref'+. That classification is owned polymorphically by
|
|
11
|
+
# +InlineElement#link_kind+ — the visitor just calls it. Adding a
|
|
12
|
+
# new link-bearing subclass means overriding +link_kind+ on it,
|
|
13
|
+
# not editing a case/when here (OCP).
|
|
14
|
+
#
|
|
15
|
+
# Verbatim block types are also closed: +SourceBlock+, +ListingBlock+,
|
|
16
|
+
# +LiteralBlock+, +PassBlock+, +StemBlock+. The visitor returns them
|
|
17
|
+
# unchanged so the rewriter never sees link-shaped text that is, in
|
|
18
|
+
# fact, raw code/math.
|
|
19
|
+
#
|
|
20
|
+
# Dispatch is class-based (no +respond_to?+ duck-typing). Unrecognized
|
|
21
|
+
# classes are returned unchanged — the visitor is closed by design.
|
|
22
|
+
class Visitor
|
|
23
|
+
# Verbatim block classes — content is raw, no link semantics.
|
|
24
|
+
VERBATIM_TYPES = [
|
|
25
|
+
Coradoc::CoreModel::SourceBlock,
|
|
26
|
+
Coradoc::CoreModel::ListingBlock,
|
|
27
|
+
Coradoc::CoreModel::LiteralBlock,
|
|
28
|
+
Coradoc::CoreModel::PassBlock,
|
|
29
|
+
Coradoc::CoreModel::StemBlock
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# Structural/container classes that own a child collection. Each
|
|
33
|
+
# entry maps the class to the reader method that exposes its
|
|
34
|
+
# children. MECE — every "recurse into the children" case lands
|
|
35
|
+
# in this table. Exposed as a public constant so spec helpers
|
|
36
|
+
# and other visitors can mirror the same paths without restating
|
|
37
|
+
# the dispatch (DRY).
|
|
38
|
+
CONTAINER_TYPES = {
|
|
39
|
+
Coradoc::CoreModel::DocumentElement => :children,
|
|
40
|
+
Coradoc::CoreModel::SectionElement => :children,
|
|
41
|
+
Coradoc::CoreModel::PreambleElement => :children,
|
|
42
|
+
Coradoc::CoreModel::HeaderElement => :children,
|
|
43
|
+
Coradoc::CoreModel::Block => :children,
|
|
44
|
+
Coradoc::CoreModel::ListBlock => :items,
|
|
45
|
+
Coradoc::CoreModel::ListItem => :children,
|
|
46
|
+
Coradoc::CoreModel::Table => :rows,
|
|
47
|
+
Coradoc::CoreModel::TableRow => :cells,
|
|
48
|
+
Coradoc::CoreModel::TableCell => :children,
|
|
49
|
+
Coradoc::CoreModel::DefinitionList => :items,
|
|
50
|
+
Coradoc::CoreModel::Toc => :entries,
|
|
51
|
+
Coradoc::CoreModel::Bibliography => :entries,
|
|
52
|
+
Coradoc::CoreModel::AnnotationBlock => :children
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
def initialize(rewriter)
|
|
56
|
+
@rewriter = rewriter
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Entry point. Always returns a NEW root node — even Identity
|
|
60
|
+
# callers can rely on object identity to confirm the rewrite ran.
|
|
61
|
+
def visit_document(document)
|
|
62
|
+
return document unless document.is_a?(Coradoc::CoreModel::Base)
|
|
63
|
+
|
|
64
|
+
result = visit_subtree(document)
|
|
65
|
+
result.equal?(document) ? document.dup : result
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def visit_subtree(node)
|
|
71
|
+
return node if VERBATIM_TYPES.any? { |type| node.is_a?(type) }
|
|
72
|
+
return rewrite_inline(node) if node.is_a?(Coradoc::CoreModel::InlineElement)
|
|
73
|
+
|
|
74
|
+
reader = reader_for(node)
|
|
75
|
+
return node unless reader
|
|
76
|
+
|
|
77
|
+
rewrite_collection(node, reader)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Look up the children-reader method for +node+. Returns nil for
|
|
81
|
+
# unrecognized classes (no duck-typing — the CONTAINER_TYPES
|
|
82
|
+
# table is the single source of truth).
|
|
83
|
+
def reader_for(node)
|
|
84
|
+
CONTAINER_TYPES.each do |klass, reader|
|
|
85
|
+
return reader if node.is_a?(klass)
|
|
86
|
+
end
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def rewrite_collection(node, attr_name)
|
|
91
|
+
original = node.public_send(attr_name)
|
|
92
|
+
return node if original.nil? || original.empty?
|
|
93
|
+
|
|
94
|
+
rewritten = original.map { |child| visit_subtree(child) }
|
|
95
|
+
return node if unchanged?(rewritten, original)
|
|
96
|
+
|
|
97
|
+
rebuild_with(node, attr_name => rewritten)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Inline dispatch. Typed LinkElement / CrossReferenceElement are
|
|
101
|
+
# always candidates; generic InlineElement instances must declare a
|
|
102
|
+
# matching format_type. Other typed subclasses (Bold, Italic, …)
|
|
103
|
+
# are walked for nested inlines instead of being rewritten.
|
|
104
|
+
def rewrite_inline(inline)
|
|
105
|
+
kind = inline.link_kind
|
|
106
|
+
|
|
107
|
+
rewritten_target = rewrite_target(inline, kind)
|
|
108
|
+
rewritten_nested = rewrite_nested(inline)
|
|
109
|
+
|
|
110
|
+
return inline if rewritten_target.nil? && rewritten_nested.nil?
|
|
111
|
+
|
|
112
|
+
overrides = {}
|
|
113
|
+
overrides[:target] = rewritten_target unless rewritten_target.nil?
|
|
114
|
+
overrides[:nested_elements] = rewritten_nested unless rewritten_nested.nil?
|
|
115
|
+
rebuild_with(inline, overrides)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def rewrite_target(inline, kind)
|
|
119
|
+
return nil unless kind
|
|
120
|
+
|
|
121
|
+
target = inline.target
|
|
122
|
+
return nil if target.nil? || target.empty?
|
|
123
|
+
|
|
124
|
+
new_target = @rewriter.call(
|
|
125
|
+
target: target,
|
|
126
|
+
kind: kind,
|
|
127
|
+
context: { in_verbatim: false }
|
|
128
|
+
)
|
|
129
|
+
return nil if new_target == target
|
|
130
|
+
|
|
131
|
+
new_target
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def rewrite_nested(inline)
|
|
135
|
+
nested = inline.nested_elements
|
|
136
|
+
return nil if nested.nil? || nested.empty?
|
|
137
|
+
|
|
138
|
+
rewritten = nested.map { |child| visit_subtree(child) }
|
|
139
|
+
return nil if unchanged?(rewritten, nested)
|
|
140
|
+
|
|
141
|
+
rewritten
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def rebuild_with(node, overrides)
|
|
145
|
+
duplicate = node.dup
|
|
146
|
+
overrides.each { |key, value| duplicate.public_send("#{key}=", value) }
|
|
147
|
+
duplicate
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def unchanged?(rewritten, original)
|
|
151
|
+
return false unless rewritten.length == original.length
|
|
152
|
+
|
|
153
|
+
rewritten.each_with_index.all? { |node, i| node.equal?(original[i]) }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
# Post-parse link/xref rewriting.
|
|
5
|
+
#
|
|
6
|
+
# Consumers that need to canonicalize link and xref targets (snake→kebab,
|
|
7
|
+
# strip +.adoc+, redirect maps, dialect translation) get a single
|
|
8
|
+
# immutable entry point: +Coradoc.rewrite_links(doc, rewriter:, &)+.
|
|
9
|
+
# The visitor walks the parsed CoreModel, invokes the supplied rewriter
|
|
10
|
+
# for every link/xref target, and returns a NEW document. Verbatim
|
|
11
|
+
# blocks (source, listing, literal, pass, stem) are skipped entirely —
|
|
12
|
+
# coradoc owns the parse and guarantees those bodies never reach the
|
|
13
|
+
# rewriter, removing the "track parser state to avoid verbatim bodies"
|
|
14
|
+
# footgun that plagues regex-based rewriting.
|
|
15
|
+
#
|
|
16
|
+
# Two-step API mirrors +Coradoc.resolve_includes+: parse produces the
|
|
17
|
+
# document, rewrite is a separate explicit step the caller controls.
|
|
18
|
+
module LinkRewriter
|
|
19
|
+
autoload :Identity, "#{__dir__}/link_rewriter/identity"
|
|
20
|
+
autoload :Visitor, "#{__dir__}/link_rewriter/visitor"
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
# Rewrite every link/xref target in +doc+.
|
|
24
|
+
#
|
|
25
|
+
# +rewriter+ responds to +#call(target:, kind:, context:)+ and returns
|
|
26
|
+
# the new target String. If a block is given it is used as the
|
|
27
|
+
# rewriter. Omitting both falls back to {Identity} (no-op) — useful
|
|
28
|
+
# for "give me a structurally identical copy" cases.
|
|
29
|
+
#
|
|
30
|
+
# Returns a NEW document; the input is never mutated.
|
|
31
|
+
def rewrite(doc, rewriter: nil, &block)
|
|
32
|
+
callable = rewriter || block || Identity.new
|
|
33
|
+
Visitor.new(callable).visit_document(doc)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
# Parse / serialize / convert pipeline. Single source of truth for
|
|
5
|
+
# the document transformation flow, extracted from the top-level
|
|
6
|
+
# Coradoc façade so pipeline logic has its own home and its own
|
|
7
|
+
# spec surface. Public API on +Coradoc+ delegates here.
|
|
8
|
+
module Pipeline
|
|
9
|
+
class << self
|
|
10
|
+
# Parse text to a document model. Graph mode: +include::+
|
|
11
|
+
# directives survive as +CoreModel::Include+ link nodes — no
|
|
12
|
+
# file I/O happens during parse. Splicing included content is
|
|
13
|
+
# a separate, explicit step (see +Coradoc.resolve_includes+).
|
|
14
|
+
def parse(text, format:)
|
|
15
|
+
format_module = FormatCatalog.get_format(format)
|
|
16
|
+
unless format_module
|
|
17
|
+
raise UnsupportedFormatError,
|
|
18
|
+
"Format '#{format}' is not registered. " \
|
|
19
|
+
"Available formats: #{FormatCatalog.registered_formats.join(', ')}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
text = Hooks.invoke(:before_parse, text, format: format)
|
|
23
|
+
result = format_module.parse_to_core(text)
|
|
24
|
+
Hooks.invoke(:after_parse, result, format: format)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def resolve_includes(document, base_dir:,
|
|
28
|
+
missing_include: :error,
|
|
29
|
+
max_depth: Coradoc::ResolveIncludes::DEFAULT_MAX_DEPTH,
|
|
30
|
+
allow_unsafe: false,
|
|
31
|
+
resolver: nil)
|
|
32
|
+
resolver = Coradoc::IncludeResolver.coerce(
|
|
33
|
+
resolver,
|
|
34
|
+
base_dir: base_dir,
|
|
35
|
+
allow_unsafe: allow_unsafe
|
|
36
|
+
)
|
|
37
|
+
Coradoc::ResolveIncludes.call(
|
|
38
|
+
document,
|
|
39
|
+
resolver: resolver,
|
|
40
|
+
base_dir: base_dir,
|
|
41
|
+
missing_include: missing_include,
|
|
42
|
+
max_depth: max_depth
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def rewrite_links(document, rewriter: nil, &block)
|
|
47
|
+
Coradoc::LinkRewriter.rewrite(document, rewriter: rewriter, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def convert(text, from:, to:, **)
|
|
51
|
+
core = parse(text, format: from)
|
|
52
|
+
serialize(core, to: to, **)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_core(model)
|
|
56
|
+
return model if model.is_a?(CoreModel::Base)
|
|
57
|
+
|
|
58
|
+
FormatCatalog.registry.each_value do |format_module|
|
|
59
|
+
next unless format_module.handles_model?(model)
|
|
60
|
+
|
|
61
|
+
return format_module.to_core(model)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
raise TransformationError, "No transformer found for #{model.class}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def serialize(model, to:, **)
|
|
68
|
+
format_module = FormatCatalog.get_format(to)
|
|
69
|
+
raise UnsupportedFormatError, "Format '#{to}' is not registered" unless format_module
|
|
70
|
+
|
|
71
|
+
model = Hooks.invoke(:before_serialize, model, format: to)
|
|
72
|
+
result = format_module.serialize(model, **)
|
|
73
|
+
Hooks.invoke(:after_serialize, result, format: to)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build(&block)
|
|
77
|
+
CoreModel::DocumentElement.build(children: [], &block)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parse_file(path, format: nil)
|
|
81
|
+
raise FileNotFoundError, path unless File.exist?(path)
|
|
82
|
+
|
|
83
|
+
source_format = format || FormatCatalog.detect_format(path)
|
|
84
|
+
raise UnsupportedFormatError, "Could not detect format for: #{path}" unless source_format
|
|
85
|
+
|
|
86
|
+
format_module = FormatCatalog.get_format(source_format)
|
|
87
|
+
raise UnsupportedFormatError, "Format '#{source_format}' is not registered" unless format_module
|
|
88
|
+
|
|
89
|
+
if FormatCatalog.binary_format?(source_format)
|
|
90
|
+
format_module.parse_to_core(path)
|
|
91
|
+
else
|
|
92
|
+
content = File.read(path)
|
|
93
|
+
content = Hooks.invoke(:before_parse, content, format: source_format)
|
|
94
|
+
result = format_module.parse_file_to_core(path, content)
|
|
95
|
+
Hooks.invoke(:after_parse, result, format: source_format)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def convert_file(path, to:, from: nil, **)
|
|
100
|
+
source_format = from || FormatCatalog.detect_format(path)
|
|
101
|
+
raise UnsupportedFormatError, "Could not detect format for: #{path}" unless source_format
|
|
102
|
+
|
|
103
|
+
core = parse_file(path, format: source_format)
|
|
104
|
+
serialize(core, to: to, **)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
# Pure-function module for relative-path arithmetic across output keys.
|
|
5
|
+
#
|
|
6
|
+
# Every SSG wrapper (VitePress, Hugo, Astro) needs to compute "how many
|
|
7
|
+
# directories up do I walk to reach the site root from this output?".
|
|
8
|
+
# The answer is the segment count of the source output_key. Everything
|
|
9
|
+
# else (template, imports) is host-system-specific; this module owns
|
|
10
|
+
# only the one piece of arithmetic that is genuinely shared.
|
|
11
|
+
#
|
|
12
|
+
# No state. No class. No knowledge of any specific SSG.
|
|
13
|
+
#
|
|
14
|
+
# @example Compute a VitePress import path
|
|
15
|
+
# Coradoc::RelativePath.from("author/iso/ref/foo", to: ".vitepress/theme")
|
|
16
|
+
# # => "../../../.vitepress/theme"
|
|
17
|
+
module RelativePath
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Compute a relative path from an output_key to a site-root-relative
|
|
21
|
+
# target.
|
|
22
|
+
#
|
|
23
|
+
# @param output_key [String, nil] site-relative key for the source
|
|
24
|
+
# page (e.g. "author/iso/ref/foo"). No leading slash, no extension.
|
|
25
|
+
# @param to [String] destination path relative to the site root.
|
|
26
|
+
# @return [String] the composed relative path.
|
|
27
|
+
def from(output_key, to:)
|
|
28
|
+
depth = output_key.to_s.count('/')
|
|
29
|
+
('../' * depth) + to.to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/coradoc/version.rb
CHANGED
data/lib/coradoc.rb
CHANGED
|
@@ -20,18 +20,13 @@
|
|
|
20
20
|
#
|
|
21
21
|
# @example Manipulating documents
|
|
22
22
|
# doc = Coradoc.parse(text, format: :asciidoc)
|
|
23
|
-
# html = Coradoc.
|
|
24
|
-
# .transform_text(&:upcase)
|
|
25
|
-
# .add_toc
|
|
26
|
-
# .to_html
|
|
23
|
+
# html = Coradoc.serialize(doc, to: :html)
|
|
27
24
|
#
|
|
28
25
|
# @example Building documents programmatically
|
|
29
|
-
# doc = Coradoc.build do
|
|
30
|
-
# title "My Document"
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# end
|
|
34
|
-
# end.to_core
|
|
26
|
+
# doc = Coradoc.build do |d|
|
|
27
|
+
# d.title = "My Document"
|
|
28
|
+
# d.children << Coradoc::CoreModel::ParagraphBlock.new(content: "Hello")
|
|
29
|
+
# end
|
|
35
30
|
# Coradoc.serialize(doc, to: :html)
|
|
36
31
|
|
|
37
32
|
require_relative 'coradoc/coradoc'
|
|
@@ -39,8 +34,7 @@ require_relative 'coradoc/version'
|
|
|
39
34
|
|
|
40
35
|
module Coradoc
|
|
41
36
|
autoload :CLI, 'coradoc/cli'
|
|
42
|
-
autoload :DocumentBuilder, 'coradoc/document_builder'
|
|
43
|
-
autoload :DocumentManipulator, 'coradoc/document_manipulator'
|
|
44
37
|
autoload :Visitor, 'coradoc/visitor'
|
|
45
|
-
autoload :
|
|
38
|
+
autoload :LinkRewriter, 'coradoc/link_rewriter'
|
|
39
|
+
autoload :RelativePath, 'coradoc/relative_path'
|
|
46
40
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: coradoc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -83,6 +83,7 @@ files:
|
|
|
83
83
|
- lib/coradoc/core_model/include.rb
|
|
84
84
|
- lib/coradoc/core_model/include_level_offset.rb
|
|
85
85
|
- lib/coradoc/core_model/include_options.rb
|
|
86
|
+
- lib/coradoc/core_model/inline_content.rb
|
|
86
87
|
- lib/coradoc/core_model/inline_element.rb
|
|
87
88
|
- lib/coradoc/core_model/list_block.rb
|
|
88
89
|
- lib/coradoc/core_model/list_item.rb
|
|
@@ -90,6 +91,7 @@ files:
|
|
|
90
91
|
- lib/coradoc/core_model/literal_block.rb
|
|
91
92
|
- lib/coradoc/core_model/metadata.rb
|
|
92
93
|
- lib/coradoc/core_model/open_block.rb
|
|
94
|
+
- lib/coradoc/core_model/output_artifact.rb
|
|
93
95
|
- lib/coradoc/core_model/paragraph_block.rb
|
|
94
96
|
- lib/coradoc/core_model/pass_block.rb
|
|
95
97
|
- lib/coradoc/core_model/quote_block.rb
|
|
@@ -97,6 +99,7 @@ files:
|
|
|
97
99
|
- lib/coradoc/core_model/reviewer_block.rb
|
|
98
100
|
- lib/coradoc/core_model/sidebar_block.rb
|
|
99
101
|
- lib/coradoc/core_model/source_block.rb
|
|
102
|
+
- lib/coradoc/core_model/stem_block.rb
|
|
100
103
|
- lib/coradoc/core_model/structural_element.rb
|
|
101
104
|
- lib/coradoc/core_model/table.rb
|
|
102
105
|
- lib/coradoc/core_model/term.rb
|
|
@@ -104,9 +107,9 @@ files:
|
|
|
104
107
|
- lib/coradoc/core_model/toc.rb
|
|
105
108
|
- lib/coradoc/core_model/toc_generator.rb
|
|
106
109
|
- lib/coradoc/core_model/verse_block.rb
|
|
107
|
-
- lib/coradoc/
|
|
108
|
-
- lib/coradoc/document_manipulator.rb
|
|
110
|
+
- lib/coradoc/dispatch.rb
|
|
109
111
|
- lib/coradoc/errors.rb
|
|
112
|
+
- lib/coradoc/format_catalog.rb
|
|
110
113
|
- lib/coradoc/format_module.rb
|
|
111
114
|
- lib/coradoc/hooks.rb
|
|
112
115
|
- lib/coradoc/include_resolver.rb
|
|
@@ -116,17 +119,18 @@ files:
|
|
|
116
119
|
- lib/coradoc/include_selectors/level_offset.rb
|
|
117
120
|
- lib/coradoc/include_selectors/lines.rb
|
|
118
121
|
- lib/coradoc/include_selectors/tags.rb
|
|
119
|
-
- lib/coradoc/
|
|
122
|
+
- lib/coradoc/introspection.rb
|
|
123
|
+
- lib/coradoc/introspection/element_counter.rb
|
|
124
|
+
- lib/coradoc/link_rewriter.rb
|
|
125
|
+
- lib/coradoc/link_rewriter/identity.rb
|
|
126
|
+
- lib/coradoc/link_rewriter/visitor.rb
|
|
120
127
|
- lib/coradoc/logger.rb
|
|
121
|
-
- lib/coradoc/output.rb
|
|
122
128
|
- lib/coradoc/performance_regression.rb
|
|
123
|
-
- lib/coradoc/
|
|
129
|
+
- lib/coradoc/pipeline.rb
|
|
124
130
|
- lib/coradoc/query.rb
|
|
125
131
|
- lib/coradoc/registry.rb
|
|
132
|
+
- lib/coradoc/relative_path.rb
|
|
126
133
|
- lib/coradoc/resolve_includes.rb
|
|
127
|
-
- lib/coradoc/serializer/registry.rb
|
|
128
|
-
- lib/coradoc/transform.rb
|
|
129
|
-
- lib/coradoc/transform/base.rb
|
|
130
134
|
- lib/coradoc/validation.rb
|
|
131
135
|
- lib/coradoc/version.rb
|
|
132
136
|
- lib/coradoc/visitor.rb
|