coradoc-adoc 2.0.9 → 2.0.11
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/asciidoc/model/bibliography_entry.rb +18 -0
- data/lib/coradoc/asciidoc/model/document.rb +9 -0
- data/lib/coradoc/asciidoc/model/glossaries.rb +1 -1
- data/lib/coradoc/asciidoc/model/list/base.rb +41 -0
- data/lib/coradoc/asciidoc/model/list/core.rb +4 -24
- data/lib/coradoc/asciidoc/model/list/definition.rb +7 -0
- data/lib/coradoc/asciidoc/model/list/definition_item.rb +1 -1
- data/lib/coradoc/asciidoc/model/list/item.rb +1 -1
- data/lib/coradoc/asciidoc/model/list/nestable.rb +7 -3
- data/lib/coradoc/asciidoc/model/list.rb +4 -2
- data/lib/coradoc/asciidoc/parser/base.rb +10 -70
- data/lib/coradoc/asciidoc/parser/block.rb +19 -27
- data/lib/coradoc/asciidoc/parser/block_assembler.rb +37 -100
- data/lib/coradoc/asciidoc/parser/block_header.rb +55 -0
- data/lib/coradoc/asciidoc/parser/content.rb +10 -1
- data/lib/coradoc/asciidoc/parser/frontmatter_parser.rb +24 -0
- data/lib/coradoc/asciidoc/parser/paragraph.rb +1 -3
- data/lib/coradoc/asciidoc/parser/rule_dispatcher.rb +158 -0
- data/lib/coradoc/asciidoc/parser/section.rb +1 -3
- data/lib/coradoc/asciidoc/parser/table.rb +1 -4
- data/lib/coradoc/asciidoc/parser/text.rb +1 -3
- data/lib/coradoc/asciidoc/parser.rb +1 -0
- data/lib/coradoc/asciidoc/serializer/serializers/base.rb +1 -1
- data/lib/coradoc/asciidoc/serializer/serializers/document.rb +7 -0
- data/lib/coradoc/asciidoc/serializer/serializers/list/definition.rb +3 -1
- data/lib/coradoc/asciidoc/transform/callout_merger.rb +94 -0
- data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +10 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +17 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +3 -1
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +70 -5
- data/lib/coradoc/asciidoc/transform/from_core_model_registrations.rb +5 -1
- data/lib/coradoc/asciidoc/transform/frontmatter_attribute_map.rb +112 -0
- data/lib/coradoc/asciidoc/transform/text_extract_visitor.rb +33 -1
- data/lib/coradoc/asciidoc/transform/to_core_model.rb +10 -2
- data/lib/coradoc/asciidoc/transform/to_core_model_registrations.rb +15 -10
- data/lib/coradoc/asciidoc/transform.rb +2 -0
- data/lib/coradoc/asciidoc/transformer/attribute_list_normalizer.rb +69 -0
- data/lib/coradoc/asciidoc/transformer/block_rules.rb +4 -42
- data/lib/coradoc/asciidoc/transformer/block_type_classifier.rb +56 -0
- data/lib/coradoc/asciidoc/transformer/header_rules.rb +15 -53
- data/lib/coradoc/asciidoc/transformer/inline_rules.rb +39 -57
- data/lib/coradoc/asciidoc/transformer/misc_rules.rb +1 -24
- data/lib/coradoc/asciidoc/transformer/structural_rules.rb +18 -81
- data/lib/coradoc/asciidoc/transformer/table_cell_builder.rb +161 -0
- data/lib/coradoc/asciidoc/transformer/table_layout.rb +135 -0
- data/lib/coradoc/asciidoc/transformer/text_rules.rb +1 -25
- data/lib/coradoc/asciidoc/transformer.rb +38 -294
- data/lib/coradoc/asciidoc/version.rb +1 -1
- data/lib/coradoc/asciidoc.rb +6 -3
- metadata +11 -1
|
@@ -24,7 +24,7 @@ module Coradoc
|
|
|
24
24
|
# :zero :one :many
|
|
25
25
|
def text_line(many_breaks = false, unguarded: false, verbatim: false)
|
|
26
26
|
tl = if verbatim
|
|
27
|
-
|
|
27
|
+
raw_verbatim_line.as(:text)
|
|
28
28
|
elsif unguarded
|
|
29
29
|
literal_space? >> text_any.as(:text)
|
|
30
30
|
else
|
|
@@ -38,6 +38,15 @@ module Coradoc
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
# A verbatim source/listing line: capture every character up to the
|
|
42
|
+
# next newline without applying any inline substitutions. Source and
|
|
43
|
+
# listing blocks must round-trip their text untouched — otherwise
|
|
44
|
+
# sequences like Liquid `{{ var }}` collide with AsciiDoc attribute
|
|
45
|
+
# references and get rewritten.
|
|
46
|
+
def raw_verbatim_line
|
|
47
|
+
(line_ending.absent? >> any).repeat(1)
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def asciidoc_char
|
|
42
51
|
line_start? >> match['*_:+=\-']
|
|
43
52
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
module Parser
|
|
6
|
+
# AsciiDoc frontmatter extractor.
|
|
7
|
+
#
|
|
8
|
+
# Delegates to the shared +Coradoc::CoreModel::FrontmatterBlock::TextSplitter+
|
|
9
|
+
# — the single source of truth for the `---\n...\n---\n` convention
|
|
10
|
+
# (DRY). AsciiDoc retains a local parser module for discoverability
|
|
11
|
+
# and as the seam for any format-specific extensions should they arise
|
|
12
|
+
# (e.g., recognizing AsciiDoc-style front matter variants).
|
|
13
|
+
module FrontmatterParser
|
|
14
|
+
class << self
|
|
15
|
+
def call(text)
|
|
16
|
+
Coradoc::CoreModel::FrontmatterBlock::TextSplitter.call(text)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Result = Coradoc::CoreModel::FrontmatterBlock::TextSplitter::Result
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -40,9 +40,7 @@ module Coradoc
|
|
|
40
40
|
# rubocop:enable Style/OptionalBooleanParameter, Style/NumericPredicate
|
|
41
41
|
|
|
42
42
|
def paragraph
|
|
43
|
-
(
|
|
44
|
-
block_title.maybe >>
|
|
45
|
-
(attribute_list >> newline).maybe >>
|
|
43
|
+
(block_header >>
|
|
46
44
|
((paragraph_text_line(0).repeat(1, 1) >>
|
|
47
45
|
(newline.repeat(1).as(:line_break) | eof?)) |
|
|
48
46
|
(paragraph_text_line(false).repeat(1) >>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
module Parser
|
|
6
|
+
# Wraps every parser rule for Parslet memoization.
|
|
7
|
+
#
|
|
8
|
+
# Parameterized rules (e.g., `block_style(n_deep, delimiter, repeater)`)
|
|
9
|
+
# cannot be memoized by Parslet directly because their result depends
|
|
10
|
+
# on the args. This module aliases each rule to a per-args dispatch
|
|
11
|
+
# rule so Parslet sees a memoizable, parameterless rule for every
|
|
12
|
+
# (rule_name, args) combination. Parameterless rules get a single
|
|
13
|
+
# memoized alias.
|
|
14
|
+
#
|
|
15
|
+
# Also warns when a parser method is defined in more than one parser
|
|
16
|
+
# module — duplicate definitions are usually a refactoring mistake
|
|
17
|
+
# and produce confusing "last one wins" behavior at include time.
|
|
18
|
+
#
|
|
19
|
+
# Invoked once at Parser::Base load time.
|
|
20
|
+
module RuleDispatcher
|
|
21
|
+
DISPATCH_CONFIG = {
|
|
22
|
+
add_dispatch: true,
|
|
23
|
+
with_params: true
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# @param parser_class [Class] Parser::Base or a subclass
|
|
28
|
+
def apply(parser_class)
|
|
29
|
+
parser_methods = collect_rule_names
|
|
30
|
+
warn_on_duplicates(parser_methods)
|
|
31
|
+
wrap_rules(parser_class, parser_methods)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Per-instance dispatch invoked from the parameterized rule wrappers.
|
|
35
|
+
# @param parser_instance [Parser::Base]
|
|
36
|
+
# @param alias_name [Symbol] The alias created at wrap time
|
|
37
|
+
# @param args [Array]
|
|
38
|
+
# @param kwargs [Hash]
|
|
39
|
+
def dispatch(parser_instance, alias_name, *args, **kwargs)
|
|
40
|
+
cache = dispatch_cache(parser_instance)
|
|
41
|
+
key = dispatch_key(alias_name, args, kwargs)
|
|
42
|
+
unless cache.key?(key)
|
|
43
|
+
rule_name = dispatch_rule_name(alias_name, key)
|
|
44
|
+
unless parser_instance.respond_to?(rule_name)
|
|
45
|
+
build_dispatch_rule(parser_instance.class, alias_name,
|
|
46
|
+
rule_name, args, kwargs)
|
|
47
|
+
end
|
|
48
|
+
cache[key] = rule_name
|
|
49
|
+
end
|
|
50
|
+
parser_instance.public_send(cache[key])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Walk every parser module and collect rule-method names.
|
|
56
|
+
# `instance_methods(false)` returns only methods defined in the
|
|
57
|
+
# module itself, not inherited ones — without `false` we would
|
|
58
|
+
# also wrap Object#send, Kernel#__send__, Module#class, etc.
|
|
59
|
+
# Wrapping `__send__` in particular causes infinite recursion.
|
|
60
|
+
# @return [Hash{Symbol => Array<Module>}]
|
|
61
|
+
def collect_rule_names
|
|
62
|
+
parser_constants = Parser.constants - %i[Base Cache FixFiles RuleDispatcher]
|
|
63
|
+
parser_constants.each_with_object({}) do |const, acc|
|
|
64
|
+
Parser.const_get(const).instance_methods(false).each do |name|
|
|
65
|
+
acc[name] ||= []
|
|
66
|
+
acc[name] << const
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def warn_on_duplicates(parser_methods)
|
|
72
|
+
parser_methods.each do |name, sites|
|
|
73
|
+
next unless sites.size > 1
|
|
74
|
+
|
|
75
|
+
modules = sites.map { |c| Parser.const_get(c) }
|
|
76
|
+
Coradoc::Logger.warn(
|
|
77
|
+
"Parser method '#{name}' is defined #{sites.size} times in #{modules.join(', ')}"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def wrap_rules(parser_class, parser_methods)
|
|
83
|
+
parser_methods.each_key do |rule_name|
|
|
84
|
+
params = parser_class.instance_method(rule_name).parameters
|
|
85
|
+
if dispatch? && params.empty?
|
|
86
|
+
wrap_nondispatch(parser_class, rule_name)
|
|
87
|
+
elsif dispatch? && with_params?
|
|
88
|
+
wrap_dispatch(parser_class, rule_name)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def wrap_nondispatch(parser_class, rule_name)
|
|
94
|
+
alias_name = :"alias_nondispatch_#{rule_name}"
|
|
95
|
+
guard_name = :"alias_nondispatch_rule_guard_#{rule_name}"
|
|
96
|
+
return if parser_class.method_defined?(guard_name)
|
|
97
|
+
|
|
98
|
+
parser_class.class_eval do
|
|
99
|
+
alias_method alias_name, rule_name
|
|
100
|
+
define_method(guard_name) {}
|
|
101
|
+
rule(rule_name) do
|
|
102
|
+
public_send(alias_name)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def wrap_dispatch(parser_class, rule_name)
|
|
108
|
+
alias_name = :"alias_dispatch_#{rule_name}"
|
|
109
|
+
guard_name = :"alias_dispatch_rule_guard_#{rule_name}"
|
|
110
|
+
return if parser_class.method_defined?(guard_name)
|
|
111
|
+
|
|
112
|
+
parser_class.class_eval do
|
|
113
|
+
alias_method alias_name, rule_name
|
|
114
|
+
define_method(guard_name) {}
|
|
115
|
+
define_method(rule_name) do |*args, **kwargs|
|
|
116
|
+
RuleDispatcher.dispatch(self, alias_name, *args, **kwargs)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def dispatch_cache(parser_instance)
|
|
122
|
+
parser_instance.instance_variable_get(:@_rule_dispatch_cache) ||
|
|
123
|
+
parser_instance.instance_variable_set(:@_rule_dispatch_cache, {})
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def dispatch?
|
|
127
|
+
DISPATCH_CONFIG[:add_dispatch]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def with_params?
|
|
131
|
+
DISPATCH_CONFIG[:with_params]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def dispatch_key(alias_name, args, kwargs)
|
|
135
|
+
[alias_name, args, kwargs.to_a.sort].hash.abs
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def dispatch_rule_name(alias_name, key)
|
|
139
|
+
:"#{alias_name}_#{key}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Build a Parslet memoizable rule that closes over the captured
|
|
143
|
+
# args and forwards to the original aliased rule. Using Parslet's
|
|
144
|
+
# class-level `rule()` (not define_method on singleton_class) is
|
|
145
|
+
# essential — Parslet's memoization, `as()`, and tree building
|
|
146
|
+
# depend on the rule going through the standard Parslet machinery.
|
|
147
|
+
def build_dispatch_rule(parser_class, original_alias, rule_name, args, kwargs)
|
|
148
|
+
parser_class.class_eval do
|
|
149
|
+
rule(rule_name) do
|
|
150
|
+
public_send(original_alias, *args, **kwargs)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -27,9 +27,7 @@ module Coradoc
|
|
|
27
27
|
def section_block(level = 2)
|
|
28
28
|
return nil if level > 8
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
element_id.maybe >>
|
|
32
|
-
(attribute_list >> newline).maybe >>
|
|
30
|
+
block_header >>
|
|
33
31
|
section_title(level).as(:title) >>
|
|
34
32
|
contents.as(:contents).maybe
|
|
35
33
|
end
|
|
@@ -41,10 +41,7 @@ module Coradoc
|
|
|
41
41
|
# - "^e|" centered cell with emphasis style
|
|
42
42
|
|
|
43
43
|
def table
|
|
44
|
-
|
|
45
|
-
(attribute_list >> newline).maybe >>
|
|
46
|
-
block_title.maybe >>
|
|
47
|
-
(attribute_list >> newline).maybe >>
|
|
44
|
+
block_header >>
|
|
48
45
|
table_start.capture(:table_delim) >>
|
|
49
46
|
line_ending >>
|
|
50
47
|
table_rows.as(:rows) >>
|
|
@@ -113,9 +113,7 @@ module Coradoc
|
|
|
113
113
|
# inline_image is defined in inline.rb for inline images (image:)
|
|
114
114
|
|
|
115
115
|
def block_image
|
|
116
|
-
(
|
|
117
|
-
block_title.maybe >>
|
|
118
|
-
(attribute_list >> newline).maybe >>
|
|
116
|
+
(block_header >>
|
|
119
117
|
match('^i') >> str('mage::') >>
|
|
120
118
|
file_path.as(:path) >>
|
|
121
119
|
attribute_list(:attribute_list_macro) >>
|
|
@@ -21,6 +21,13 @@ module Coradoc
|
|
|
21
21
|
|
|
22
22
|
def serialize_to_adoc
|
|
23
23
|
parts = []
|
|
24
|
+
# Frontmatter prefix, if present. The raw YAML text is
|
|
25
|
+
# already canonical (single source of truth is Codec), so we
|
|
26
|
+
# only wrap it with `---` delimiters.
|
|
27
|
+
if @model.frontmatter && !@model.frontmatter.strip.empty?
|
|
28
|
+
parts << "---\n#{@model.frontmatter.chomp}\n---\n\n"
|
|
29
|
+
end
|
|
30
|
+
|
|
24
31
|
# Only add leading newline if we have sections but no header
|
|
25
32
|
parts << "\n" if @model.sections && !@model.sections.empty? && !@model.header
|
|
26
33
|
|
|
@@ -8,7 +8,9 @@ module Coradoc
|
|
|
8
8
|
class Definition < Base
|
|
9
9
|
def to_adoc(model, _options = {})
|
|
10
10
|
@model = model
|
|
11
|
-
|
|
11
|
+
_attrs = @model.attrs.to_adoc(show_empty: false).to_s
|
|
12
|
+
prefix = _attrs.empty? ? '' : "#{_attrs}\n"
|
|
13
|
+
content = "\n#{prefix}"
|
|
12
14
|
@model.items.each do |item|
|
|
13
15
|
# Pass delimiter to item serialization
|
|
14
16
|
serialized = serialize_child_with_options(item, delimiter: @model.delimiter)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
module Transform
|
|
6
|
+
# Post-processing pass that merges AsciiDoc callout annotation
|
|
7
|
+
# paragraphs into the verbatim block they annotate.
|
|
8
|
+
#
|
|
9
|
+
# AsciiDoc callouts look like:
|
|
10
|
+
#
|
|
11
|
+
# [source,ruby]
|
|
12
|
+
# ----
|
|
13
|
+
# get '/hi' do <1>
|
|
14
|
+
# ----
|
|
15
|
+
# <1> Returns hello world
|
|
16
|
+
#
|
|
17
|
+
# The parser emits the source block and the annotation as two
|
|
18
|
+
# independent children. The CoreModel representation should attach
|
|
19
|
+
# the annotation to the block as a typed Callout, so downstream
|
|
20
|
+
# serializers can render them appropriately for each format.
|
|
21
|
+
#
|
|
22
|
+
# Single responsibility: take a flat list of transformed CoreModel
|
|
23
|
+
# children, return a flat list with `<N>` paragraphs adjacent to a
|
|
24
|
+
# SourceBlock / ListingBlock folded into that block's `callouts`.
|
|
25
|
+
# Anything else is passed through untouched.
|
|
26
|
+
class CalloutMerger
|
|
27
|
+
ANNOTATION_LINE = /<(\d+)>\s*(.*?)\s*\z/
|
|
28
|
+
ANNOTATION_SPLIT = /(?=<\d+>)/
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def call(children)
|
|
32
|
+
new.merge(Array(children))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Walks the input children left-to-right. When a paragraph whose
|
|
37
|
+
# content is composed entirely of `<N> text` lines follows a
|
|
38
|
+
# verbatim block (SourceBlock or ListingBlock), each `<N>` line
|
|
39
|
+
# becomes a Callout attached to that block instead of a separate
|
|
40
|
+
# paragraph.
|
|
41
|
+
#
|
|
42
|
+
# Annotations that do not follow a verbatim block are preserved
|
|
43
|
+
# verbatim — they may be legitimate prose.
|
|
44
|
+
def merge(children)
|
|
45
|
+
children.each.with_object([]) do |child, result|
|
|
46
|
+
annotations = extract_annotations(child)
|
|
47
|
+
target = annotations && preceding_verbatim_block(result)
|
|
48
|
+
if target
|
|
49
|
+
target.callouts.concat(annotations)
|
|
50
|
+
else
|
|
51
|
+
result << child
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Returns an Array of Callout if the paragraph is entirely
|
|
59
|
+
# callout annotations, otherwise nil.
|
|
60
|
+
def extract_annotations(child)
|
|
61
|
+
return nil unless child.is_a?(Coradoc::CoreModel::ParagraphBlock)
|
|
62
|
+
|
|
63
|
+
lines = split_annotation_lines(child.flat_text)
|
|
64
|
+
return nil if lines.empty?
|
|
65
|
+
|
|
66
|
+
callouts = lines.map do |line|
|
|
67
|
+
match = line.match(ANNOTATION_LINE)
|
|
68
|
+
match ? Coradoc::CoreModel::Callout.new(index: match[1].to_i, content: match[2]) : nil
|
|
69
|
+
end
|
|
70
|
+
return nil if callouts.any?(&:nil?)
|
|
71
|
+
|
|
72
|
+
callouts
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Splits a paragraph into candidate annotation lines. Returns []
|
|
76
|
+
# if any non-blank segment fails to look like an annotation.
|
|
77
|
+
def split_annotation_lines(text)
|
|
78
|
+
return [] if text.nil? || text.strip.empty?
|
|
79
|
+
|
|
80
|
+
chunks = text.strip.split(ANNOTATION_SPLIT)
|
|
81
|
+
chunks.map(&:strip).reject(&:empty?)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def preceding_verbatim_block(result)
|
|
85
|
+
last = result.last
|
|
86
|
+
return nil unless last.is_a?(Coradoc::CoreModel::SourceBlock) ||
|
|
87
|
+
last.is_a?(Coradoc::CoreModel::ListingBlock)
|
|
88
|
+
|
|
89
|
+
last
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -62,7 +62,16 @@ module Coradoc
|
|
|
62
62
|
has_nested_blocks = lines.any?(Coradoc::AsciiDoc::Model::Block::Core)
|
|
63
63
|
|
|
64
64
|
if has_nested_blocks
|
|
65
|
-
children = lines.
|
|
65
|
+
children = lines.filter_map do |line|
|
|
66
|
+
result = ToCoreModel.transform(line)
|
|
67
|
+
next nil if result.nil?
|
|
68
|
+
next result if result.is_a?(Coradoc::CoreModel::Base)
|
|
69
|
+
|
|
70
|
+
text = ToCoreModel.extract_text_content(result)
|
|
71
|
+
next nil if text.nil? || text.strip.empty?
|
|
72
|
+
|
|
73
|
+
Coradoc::CoreModel::TextContent.new(text: text)
|
|
74
|
+
end
|
|
66
75
|
klass.new(
|
|
67
76
|
id: block.id,
|
|
68
77
|
title: ToCoreModel.extract_title_text(block.title),
|
|
@@ -9,14 +9,29 @@ module Coradoc
|
|
|
9
9
|
def transform_document(doc)
|
|
10
10
|
title_text = ToCoreModel.extract_title_text(doc.header&.title)
|
|
11
11
|
attributes = ToCoreModel.extract_document_attributes(doc)
|
|
12
|
+
children = ToCoreModel.transform(doc.sections || doc.contents || [])
|
|
13
|
+
children = CalloutMerger.call(children)
|
|
14
|
+
children = prepend_frontmatter(children, doc.frontmatter)
|
|
15
|
+
|
|
12
16
|
Coradoc::CoreModel::DocumentElement.new(
|
|
13
17
|
id: doc.id,
|
|
14
18
|
title: title_text,
|
|
15
19
|
attributes: attributes,
|
|
16
|
-
children:
|
|
20
|
+
children: children
|
|
17
21
|
)
|
|
18
22
|
end
|
|
19
23
|
|
|
24
|
+
# If the AsciiDoc document carried raw frontmatter text, parse
|
|
25
|
+
# it via Codec into a typed FrontmatterBlock and prepend it so
|
|
26
|
+
# it participates in the standard block pipeline. Codec is the
|
|
27
|
+
# single source of truth for YAML parsing (DRY/MECE).
|
|
28
|
+
def prepend_frontmatter(children, frontmatter_text)
|
|
29
|
+
return children if frontmatter_text.nil? || frontmatter_text.strip.empty?
|
|
30
|
+
|
|
31
|
+
block = Coradoc::CoreModel::FrontmatterBlock::Codec.from_yaml(frontmatter_text)
|
|
32
|
+
[block, *children]
|
|
33
|
+
end
|
|
34
|
+
|
|
20
35
|
def transform_section(section, parent_id: nil)
|
|
21
36
|
title_text = ToCoreModel.extract_title_text(section.title)
|
|
22
37
|
section_id = section.id || Coradoc::CoreModel::IdGenerator.generate_from_title(
|
|
@@ -24,6 +39,7 @@ module Coradoc
|
|
|
24
39
|
)
|
|
25
40
|
|
|
26
41
|
content_children = ToCoreModel.transform(section.contents || [])
|
|
42
|
+
content_children = CalloutMerger.call(content_children)
|
|
27
43
|
nested_sections = (section.sections || []).map do |child|
|
|
28
44
|
transform_section(child, parent_id: section_id)
|
|
29
45
|
end
|
|
@@ -25,8 +25,10 @@ module Coradoc
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def transform_image(image)
|
|
28
|
+
src = image.src.to_s
|
|
29
|
+
src = src[1..] if src.start_with?(':')
|
|
28
30
|
Coradoc::CoreModel::Image.new(
|
|
29
|
-
src:
|
|
31
|
+
src: src,
|
|
30
32
|
alt: image.title&.to_s,
|
|
31
33
|
width: image.attributes&.[]('width'),
|
|
32
34
|
height: image.attributes&.[]('height')
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'from_core_model_registrations'
|
|
4
|
-
|
|
5
3
|
module Coradoc
|
|
6
4
|
module AsciiDoc
|
|
7
5
|
module Transform
|
|
@@ -9,8 +7,18 @@ module Coradoc
|
|
|
9
7
|
class FromCoreModel
|
|
10
8
|
include Coradoc::Transform::Base
|
|
11
9
|
|
|
10
|
+
@registered = false
|
|
11
|
+
|
|
12
12
|
class << self
|
|
13
|
+
def register!
|
|
14
|
+
return if @registered
|
|
15
|
+
|
|
16
|
+
Transform::FromCoreModelRegistrations.register_all!
|
|
17
|
+
@registered = true
|
|
18
|
+
end
|
|
19
|
+
|
|
13
20
|
def transform(model)
|
|
21
|
+
register!
|
|
14
22
|
return model.map { |item| transform(item) } if model.is_a?(Array)
|
|
15
23
|
return model unless model.is_a?(Coradoc::CoreModel::Base)
|
|
16
24
|
|
|
@@ -32,27 +40,42 @@ module Coradoc
|
|
|
32
40
|
Coradoc::AsciiDoc::Model::Header.new(title: '')
|
|
33
41
|
end
|
|
34
42
|
|
|
43
|
+
sections, frontmatter = extract_frontmatter(Array(element.children))
|
|
44
|
+
|
|
35
45
|
Coradoc::AsciiDoc::Model::Document.new(
|
|
36
46
|
id: element.id,
|
|
37
47
|
header: header,
|
|
38
|
-
sections:
|
|
48
|
+
sections: flatten_children(sections),
|
|
49
|
+
frontmatter: frontmatter
|
|
39
50
|
)
|
|
40
51
|
when CoreModel::SectionElement
|
|
41
52
|
Coradoc::AsciiDoc::Model::Section.new(
|
|
42
53
|
id: element.id,
|
|
43
54
|
level: element.level,
|
|
44
55
|
title: create_title(element.title, element.level),
|
|
45
|
-
contents:
|
|
56
|
+
contents: flatten_children(element.children)
|
|
46
57
|
)
|
|
47
58
|
else
|
|
48
59
|
Coradoc::AsciiDoc::Model::Section.new(
|
|
49
60
|
id: element.id,
|
|
50
61
|
title: create_title(element.title, 1),
|
|
51
|
-
contents:
|
|
62
|
+
contents: flatten_children(element.children)
|
|
52
63
|
)
|
|
53
64
|
end
|
|
54
65
|
end
|
|
55
66
|
|
|
67
|
+
# Transforms each CoreModel child and flattens one level so a
|
|
68
|
+
# transform that returns multiple siblings (e.g. a source block
|
|
69
|
+
# followed by its re-expanded callout paragraphs) stays in
|
|
70
|
+
# document order.
|
|
71
|
+
def flatten_children(children)
|
|
72
|
+
Array(children).flat_map { |child| flatten_one(transform(child)) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def flatten_one(result)
|
|
76
|
+
result.is_a?(Array) ? result : [result]
|
|
77
|
+
end
|
|
78
|
+
|
|
56
79
|
def transform_block(block)
|
|
57
80
|
content = block.renderable_content
|
|
58
81
|
|
|
@@ -71,12 +94,19 @@ module Coradoc
|
|
|
71
94
|
end
|
|
72
95
|
|
|
73
96
|
content_text = safe_content_to_string(content)
|
|
97
|
+
result = build_verbatim_block(semantic, block, content_text)
|
|
98
|
+
return result unless verbatim_with_callouts?(semantic, block)
|
|
74
99
|
|
|
100
|
+
[result, *build_callout_paragraphs(block.callouts)]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def build_verbatim_block(semantic, block, content_text)
|
|
75
104
|
case semantic
|
|
76
105
|
when :source_code
|
|
77
106
|
Coradoc::AsciiDoc::Model::Block::SourceCode.new(
|
|
78
107
|
id: block.id,
|
|
79
108
|
title: block.title,
|
|
109
|
+
lang: block.language,
|
|
80
110
|
lines: content_text.split("\n"),
|
|
81
111
|
attributes: build_attributes(block)
|
|
82
112
|
)
|
|
@@ -151,6 +181,23 @@ module Coradoc
|
|
|
151
181
|
end
|
|
152
182
|
end
|
|
153
183
|
|
|
184
|
+
def verbatim_with_callouts?(semantic, block)
|
|
185
|
+
return false unless %i[source_code listing].include?(semantic)
|
|
186
|
+
return false if block.callouts.nil? || block.callouts.empty?
|
|
187
|
+
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Re-expands typed Callouts back into the AsciiDoc `<N> text`
|
|
192
|
+
# paragraph form so the round-trip is faithful.
|
|
193
|
+
def build_callout_paragraphs(callouts)
|
|
194
|
+
callouts.sort_by { |c| c.index || Float::INFINITY }.map do |callout|
|
|
195
|
+
Coradoc::AsciiDoc::Model::Paragraph.new(
|
|
196
|
+
content: create_text_elements("<#{callout.index}> #{callout.content}")
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
154
201
|
def transform_table(table)
|
|
155
202
|
rows = Array(table.rows).map do |row|
|
|
156
203
|
columns = Array(row.cells).map do |cell|
|
|
@@ -335,8 +382,26 @@ module Coradoc
|
|
|
335
382
|
)
|
|
336
383
|
end
|
|
337
384
|
|
|
385
|
+
def transform_comment_line(comment)
|
|
386
|
+
Coradoc::AsciiDoc::Model::CommentLine.new(
|
|
387
|
+
text: comment.text.to_s
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
338
391
|
private
|
|
339
392
|
|
|
393
|
+
# If the first CoreModel child is a FrontmatterBlock, serialize
|
|
394
|
+
# it to YAML text via Codec (single source of truth) and pop it
|
|
395
|
+
# from the children list. Returns [remaining_children,
|
|
396
|
+
# frontmatter_text].
|
|
397
|
+
def extract_frontmatter(children)
|
|
398
|
+
first = children.first
|
|
399
|
+
return [children, nil] unless first.is_a?(CoreModel::FrontmatterBlock)
|
|
400
|
+
|
|
401
|
+
yaml = CoreModel::FrontmatterBlock::Codec.to_yaml(first)
|
|
402
|
+
[children.drop(1), yaml.nil? || yaml.empty? ? nil : yaml]
|
|
403
|
+
end
|
|
404
|
+
|
|
340
405
|
def resolve_semantic_type(block)
|
|
341
406
|
semantic = block.resolve_semantic_type
|
|
342
407
|
return semantic if semantic
|
|
@@ -115,6 +115,11 @@ module Coradoc
|
|
|
115
115
|
Coradoc::CoreModel::BibliographyEntry,
|
|
116
116
|
->(model) { FromCoreModel.transform_bibliography_entry(model) }
|
|
117
117
|
)
|
|
118
|
+
|
|
119
|
+
Registry.register(
|
|
120
|
+
Coradoc::CoreModel::CommentLine,
|
|
121
|
+
->(model) { FromCoreModel.transform_comment_line(model) }
|
|
122
|
+
)
|
|
118
123
|
end
|
|
119
124
|
end
|
|
120
125
|
end
|
|
@@ -123,4 +128,3 @@ module Coradoc
|
|
|
123
128
|
end
|
|
124
129
|
|
|
125
130
|
# Auto-register when this file is loaded
|
|
126
|
-
Coradoc::AsciiDoc::Transform::FromCoreModelRegistrations.register_all!
|