coradoc-adoc 2.0.9 → 2.0.10
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 +3 -22
- 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/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/element_transformers/block_transformer.rb +10 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +15 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +3 -1
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +33 -3
- 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 +1 -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 +10 -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)
|
|
@@ -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,28 @@ 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 = prepend_frontmatter(children, doc.frontmatter)
|
|
14
|
+
|
|
12
15
|
Coradoc::CoreModel::DocumentElement.new(
|
|
13
16
|
id: doc.id,
|
|
14
17
|
title: title_text,
|
|
15
18
|
attributes: attributes,
|
|
16
|
-
children:
|
|
19
|
+
children: children
|
|
17
20
|
)
|
|
18
21
|
end
|
|
19
22
|
|
|
23
|
+
# If the AsciiDoc document carried raw frontmatter text, parse
|
|
24
|
+
# it via Codec into a typed FrontmatterBlock and prepend it so
|
|
25
|
+
# it participates in the standard block pipeline. Codec is the
|
|
26
|
+
# single source of truth for YAML parsing (DRY/MECE).
|
|
27
|
+
def prepend_frontmatter(children, frontmatter_text)
|
|
28
|
+
return children if frontmatter_text.nil? || frontmatter_text.strip.empty?
|
|
29
|
+
|
|
30
|
+
block = Coradoc::CoreModel::FrontmatterBlock::Codec.from_yaml(frontmatter_text)
|
|
31
|
+
[block, *children]
|
|
32
|
+
end
|
|
33
|
+
|
|
20
34
|
def transform_section(section, parent_id: nil)
|
|
21
35
|
title_text = ToCoreModel.extract_title_text(section.title)
|
|
22
36
|
section_id = section.id || Coradoc::CoreModel::IdGenerator.generate_from_title(
|
|
@@ -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,10 +40,13 @@ 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: transform(
|
|
48
|
+
sections: transform(sections),
|
|
49
|
+
frontmatter: frontmatter
|
|
39
50
|
)
|
|
40
51
|
when CoreModel::SectionElement
|
|
41
52
|
Coradoc::AsciiDoc::Model::Section.new(
|
|
@@ -77,6 +88,7 @@ module Coradoc
|
|
|
77
88
|
Coradoc::AsciiDoc::Model::Block::SourceCode.new(
|
|
78
89
|
id: block.id,
|
|
79
90
|
title: block.title,
|
|
91
|
+
lang: block.language,
|
|
80
92
|
lines: content_text.split("\n"),
|
|
81
93
|
attributes: build_attributes(block)
|
|
82
94
|
)
|
|
@@ -335,8 +347,26 @@ module Coradoc
|
|
|
335
347
|
)
|
|
336
348
|
end
|
|
337
349
|
|
|
350
|
+
def transform_comment_line(comment)
|
|
351
|
+
Coradoc::AsciiDoc::Model::CommentLine.new(
|
|
352
|
+
text: comment.text.to_s
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
|
|
338
356
|
private
|
|
339
357
|
|
|
358
|
+
# If the first CoreModel child is a FrontmatterBlock, serialize
|
|
359
|
+
# it to YAML text via Codec (single source of truth) and pop it
|
|
360
|
+
# from the children list. Returns [remaining_children,
|
|
361
|
+
# frontmatter_text].
|
|
362
|
+
def extract_frontmatter(children)
|
|
363
|
+
first = children.first
|
|
364
|
+
return [children, nil] unless first.is_a?(CoreModel::FrontmatterBlock)
|
|
365
|
+
|
|
366
|
+
yaml = CoreModel::FrontmatterBlock::Codec.to_yaml(first)
|
|
367
|
+
[children.drop(1), yaml.nil? || yaml.empty? ? nil : yaml]
|
|
368
|
+
end
|
|
369
|
+
|
|
340
370
|
def resolve_semantic_type(block)
|
|
341
371
|
semantic = block.resolve_semantic_type
|
|
342
372
|
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!
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module AsciiDoc
|
|
5
|
+
module Transform
|
|
6
|
+
# OCP opt-in bridge mapping AsciiDoc document attributes
|
|
7
|
+
# (`:author:`, `:revdate:`, etc.) to/from FrontmatterBlock data.
|
|
8
|
+
#
|
|
9
|
+
# NOT auto-registered. Users opt in by invoking the bridge methods
|
|
10
|
+
# from their pipeline (e.g., from a custom transformer extension
|
|
11
|
+
# or rake task). This honors OCP: core conversion never silently
|
|
12
|
+
# rewrites data; opt-in extensions add behavior explicitly.
|
|
13
|
+
#
|
|
14
|
+
# MECE: lives in its own file, dispatches on `attribute_name` /
|
|
15
|
+
# `frontmatter_key` pairs. Does not touch FrontmatterBlock::Codec
|
|
16
|
+
# (YAML I/O) or SchemaResolver (validation).
|
|
17
|
+
#
|
|
18
|
+
# Mappings (bidirectional):
|
|
19
|
+
#
|
|
20
|
+
# | Frontmatter key | AsciiDoc attribute | Notes |
|
|
21
|
+
# |-----------------|--------------------|--------------------|
|
|
22
|
+
# | author | author | |
|
|
23
|
+
# | date | revdate | |
|
|
24
|
+
# | tags | tags | Array <-> space str|
|
|
25
|
+
# | categories | categories | Array <-> space str|
|
|
26
|
+
module FrontmatterAttributeMap
|
|
27
|
+
# Single source of truth for the attribute <-> frontmatter
|
|
28
|
+
# mapping. Each tuple: [attribute_name(String), front_key(String),
|
|
29
|
+
# kind(:scalar | :array)]
|
|
30
|
+
MAPPINGS = [
|
|
31
|
+
['author', 'author', :scalar],
|
|
32
|
+
['revdate', 'date', :scalar],
|
|
33
|
+
['tags', 'tags', :array],
|
|
34
|
+
['categories', 'categories', :array]
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Build a frontmatter data hash from an AsciiDoc document
|
|
39
|
+
# attributes Hash. Skips unknown keys and empty values.
|
|
40
|
+
#
|
|
41
|
+
# @param attributes [Hash{String=>Object}] AsciiDoc document
|
|
42
|
+
# attributes (e.g., from DocumentAttributes#to_hash)
|
|
43
|
+
# @return [Hash{String=>Object}] frontmatter data hash
|
|
44
|
+
def entries_from_attributes(attributes)
|
|
45
|
+
attributes = normalize_hash(attributes)
|
|
46
|
+
MAPPINGS.each_with_object({}) do |(attr_name, front_key, kind), h|
|
|
47
|
+
raw = attributes[attr_name]
|
|
48
|
+
next if raw.nil? || raw.to_s.strip.empty?
|
|
49
|
+
|
|
50
|
+
h[front_key] = build_value(raw, kind)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Reverse: walk a FrontmatterBlock's data and produce a Hash
|
|
55
|
+
# of AsciiDoc document attributes. Unknown entry keys are
|
|
56
|
+
# dropped (only mapped keys are translated).
|
|
57
|
+
#
|
|
58
|
+
# @param block [Coradoc::CoreModel::FrontmatterBlock]
|
|
59
|
+
# @return [Hash{String=>String}]
|
|
60
|
+
def attributes_from_block(block)
|
|
61
|
+
result = {}
|
|
62
|
+
return result unless block&.data
|
|
63
|
+
|
|
64
|
+
MAPPINGS.each do |(_, front_key, _)|
|
|
65
|
+
value = block.data[front_key]
|
|
66
|
+
next if value.nil?
|
|
67
|
+
|
|
68
|
+
attr_name = front_key_to_attribute(front_key)
|
|
69
|
+
next unless attr_name
|
|
70
|
+
|
|
71
|
+
result[attr_name] = serialize_value(value)
|
|
72
|
+
end
|
|
73
|
+
result
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def normalize_hash(attributes)
|
|
79
|
+
return {} unless attributes.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
attributes.each_with_object({}) do |(k, v), h|
|
|
82
|
+
h[k.to_s] = v
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_value(raw, kind)
|
|
87
|
+
case kind
|
|
88
|
+
when :array
|
|
89
|
+
raw.to_s.split(/\s+/).reject(&:empty?)
|
|
90
|
+
else
|
|
91
|
+
raw.to_s
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def serialize_value(value)
|
|
96
|
+
case value
|
|
97
|
+
when Array
|
|
98
|
+
value.map(&:to_s).join(' ')
|
|
99
|
+
else
|
|
100
|
+
value.to_s
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def front_key_to_attribute(front_key)
|
|
105
|
+
mapping = MAPPINGS.find { |_, fk, _| fk == front_key.to_s }
|
|
106
|
+
mapping&.first
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -74,8 +74,33 @@ module Coradoc
|
|
|
74
74
|
model.alt || model.src || ''
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
def visit_inline_span(model)
|
|
78
|
+
model.text.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visit_list(model)
|
|
82
|
+
visit_array(model.items)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def visit_block(model)
|
|
86
|
+
visit_array(model.lines)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def visit_core_model_list(model)
|
|
90
|
+
model.items.map { |i| visit(i.content) }.join(' ')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def visit_definition_item(model)
|
|
94
|
+
terms_text = visit(model.terms)
|
|
95
|
+
contents_text = visit(model.contents)
|
|
96
|
+
[terms_text, contents_text].reject(&:empty?).join(': ')
|
|
97
|
+
end
|
|
98
|
+
|
|
77
99
|
def visit_base_model(model)
|
|
78
|
-
|
|
100
|
+
content = model.content
|
|
101
|
+
content ? visit(content) : ''
|
|
102
|
+
rescue NoMethodError
|
|
103
|
+
''
|
|
79
104
|
end
|
|
80
105
|
|
|
81
106
|
def visit_core_model_inline(model)
|
|
@@ -115,6 +140,13 @@ module Coradoc
|
|
|
115
140
|
when Model::Inline::Footnote then visit_footnote(model)
|
|
116
141
|
when Model::Inline::AttributeReference then visit_attribute_reference(model)
|
|
117
142
|
when Model::Image::Core then visit_adoc_image(model)
|
|
143
|
+
when CoreModel::ListBlock then visit_core_model_list(model)
|
|
144
|
+
when Model::Inline::Span then visit_inline_span(model)
|
|
145
|
+
when Model::List::Core then visit_list(model)
|
|
146
|
+
when Model::List::Definition then visit_list(model)
|
|
147
|
+
when Model::List::DefinitionItem then visit_definition_item(model)
|
|
148
|
+
when Model::Block::Core then visit_block(model)
|
|
149
|
+
when Model::LineBreak, Model::CommentLine, Model::CommentBlock then ''
|
|
118
150
|
when Model::Base then visit_base_model(model)
|
|
119
151
|
else
|
|
120
152
|
model.class.name.start_with?('Parslet::') ? model.to_s : ''
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'to_core_model_registrations'
|
|
4
|
-
|
|
5
3
|
module Coradoc
|
|
6
4
|
module AsciiDoc
|
|
7
5
|
module Transform
|
|
8
6
|
class ToCoreModel
|
|
9
7
|
include Coradoc::Transform::Base
|
|
10
8
|
|
|
9
|
+
@registered = false
|
|
10
|
+
|
|
11
11
|
class << self
|
|
12
|
+
def register!
|
|
13
|
+
return if @registered
|
|
14
|
+
|
|
15
|
+
Transform::ToCoreModelRegistrations.register_all!
|
|
16
|
+
@registered = true
|
|
17
|
+
end
|
|
18
|
+
|
|
12
19
|
def transform(model)
|
|
20
|
+
register!
|
|
13
21
|
return model.filter_map { |item| transform(item) } if model.is_a?(Array)
|
|
14
22
|
return model unless model.is_a?(Coradoc::AsciiDoc::Model::Base)
|
|
15
23
|
|
|
@@ -75,6 +75,15 @@ module Coradoc
|
|
|
75
75
|
)
|
|
76
76
|
}
|
|
77
77
|
)
|
|
78
|
+
|
|
79
|
+
Registry.register(
|
|
80
|
+
Coradoc::AsciiDoc::Model::CommentLine,
|
|
81
|
+
lambda { |model|
|
|
82
|
+
Coradoc::CoreModel::CommentLine.new(
|
|
83
|
+
text: model.text.to_s
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
)
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
def register_list_transformers!
|
|
@@ -179,6 +188,11 @@ module Coradoc
|
|
|
179
188
|
->(model) { Oth.transform_image(model) }
|
|
180
189
|
)
|
|
181
190
|
|
|
191
|
+
Registry.register(
|
|
192
|
+
Coradoc::AsciiDoc::Model::Image::InlineImage,
|
|
193
|
+
->(model) { Oth.transform_image(model) }
|
|
194
|
+
)
|
|
195
|
+
|
|
182
196
|
Registry.register(
|
|
183
197
|
Coradoc::AsciiDoc::Model::Bibliography,
|
|
184
198
|
->(model) { Oth.transform_bibliography(model) }
|
|
@@ -190,17 +204,11 @@ module Coradoc
|
|
|
190
204
|
)
|
|
191
205
|
|
|
192
206
|
[
|
|
193
|
-
Coradoc::AsciiDoc::Model::TextElement,
|
|
194
207
|
Coradoc::AsciiDoc::Model::Include,
|
|
195
208
|
Coradoc::AsciiDoc::Model::Audio,
|
|
196
209
|
Coradoc::AsciiDoc::Model::Video,
|
|
197
210
|
Coradoc::AsciiDoc::Model::ContentList,
|
|
198
|
-
Coradoc::AsciiDoc::Model::Tag
|
|
199
|
-
].each do |klass|
|
|
200
|
-
Registry.register(klass, ->(model) { model })
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
[
|
|
211
|
+
Coradoc::AsciiDoc::Model::Tag,
|
|
204
212
|
Coradoc::AsciiDoc::Model::LineBreak,
|
|
205
213
|
Coradoc::AsciiDoc::Model::Break::PageBreak
|
|
206
214
|
].each do |klass|
|
|
@@ -212,6 +220,3 @@ module Coradoc
|
|
|
212
220
|
end
|
|
213
221
|
end
|
|
214
222
|
end
|
|
215
|
-
|
|
216
|
-
# Auto-register when this file is loaded
|
|
217
|
-
Coradoc::AsciiDoc::Transform::ToCoreModelRegistrations.register_all!
|
|
@@ -12,6 +12,7 @@ module Coradoc
|
|
|
12
12
|
autoload :TextExtractVisitor, "#{__dir__}/transform/text_extract_visitor"
|
|
13
13
|
autoload :InlineTransformVisitor, "#{__dir__}/transform/inline_transform_visitor"
|
|
14
14
|
autoload :ElementTransformers, "#{__dir__}/transform/element_transformers"
|
|
15
|
+
autoload :FrontmatterAttributeMap, "#{__dir__}/transform/frontmatter_attribute_map"
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|