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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coradoc/asciidoc/model/bibliography_entry.rb +18 -0
  3. data/lib/coradoc/asciidoc/model/document.rb +9 -0
  4. data/lib/coradoc/asciidoc/model/glossaries.rb +1 -1
  5. data/lib/coradoc/asciidoc/model/list/base.rb +41 -0
  6. data/lib/coradoc/asciidoc/model/list/core.rb +4 -24
  7. data/lib/coradoc/asciidoc/model/list/definition.rb +7 -0
  8. data/lib/coradoc/asciidoc/model/list/definition_item.rb +1 -1
  9. data/lib/coradoc/asciidoc/model/list/item.rb +1 -1
  10. data/lib/coradoc/asciidoc/model/list/nestable.rb +7 -3
  11. data/lib/coradoc/asciidoc/model/list.rb +4 -2
  12. data/lib/coradoc/asciidoc/parser/base.rb +10 -70
  13. data/lib/coradoc/asciidoc/parser/block.rb +3 -22
  14. data/lib/coradoc/asciidoc/parser/block_assembler.rb +37 -100
  15. data/lib/coradoc/asciidoc/parser/block_header.rb +55 -0
  16. data/lib/coradoc/asciidoc/parser/frontmatter_parser.rb +24 -0
  17. data/lib/coradoc/asciidoc/parser/paragraph.rb +1 -3
  18. data/lib/coradoc/asciidoc/parser/rule_dispatcher.rb +158 -0
  19. data/lib/coradoc/asciidoc/parser/section.rb +1 -3
  20. data/lib/coradoc/asciidoc/parser/table.rb +1 -4
  21. data/lib/coradoc/asciidoc/parser/text.rb +1 -3
  22. data/lib/coradoc/asciidoc/parser.rb +1 -0
  23. data/lib/coradoc/asciidoc/serializer/serializers/base.rb +1 -1
  24. data/lib/coradoc/asciidoc/serializer/serializers/document.rb +7 -0
  25. data/lib/coradoc/asciidoc/serializer/serializers/list/definition.rb +3 -1
  26. data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +10 -1
  27. data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +15 -1
  28. data/lib/coradoc/asciidoc/transform/element_transformers/other_transformer.rb +3 -1
  29. data/lib/coradoc/asciidoc/transform/from_core_model.rb +33 -3
  30. data/lib/coradoc/asciidoc/transform/from_core_model_registrations.rb +5 -1
  31. data/lib/coradoc/asciidoc/transform/frontmatter_attribute_map.rb +112 -0
  32. data/lib/coradoc/asciidoc/transform/text_extract_visitor.rb +33 -1
  33. data/lib/coradoc/asciidoc/transform/to_core_model.rb +10 -2
  34. data/lib/coradoc/asciidoc/transform/to_core_model_registrations.rb +15 -10
  35. data/lib/coradoc/asciidoc/transform.rb +1 -0
  36. data/lib/coradoc/asciidoc/transformer/attribute_list_normalizer.rb +69 -0
  37. data/lib/coradoc/asciidoc/transformer/block_rules.rb +4 -42
  38. data/lib/coradoc/asciidoc/transformer/block_type_classifier.rb +56 -0
  39. data/lib/coradoc/asciidoc/transformer/header_rules.rb +15 -53
  40. data/lib/coradoc/asciidoc/transformer/inline_rules.rb +39 -57
  41. data/lib/coradoc/asciidoc/transformer/misc_rules.rb +1 -24
  42. data/lib/coradoc/asciidoc/transformer/structural_rules.rb +18 -81
  43. data/lib/coradoc/asciidoc/transformer/table_cell_builder.rb +161 -0
  44. data/lib/coradoc/asciidoc/transformer/table_layout.rb +135 -0
  45. data/lib/coradoc/asciidoc/transformer/text_rules.rb +1 -25
  46. data/lib/coradoc/asciidoc/transformer.rb +38 -294
  47. data/lib/coradoc/asciidoc/version.rb +1 -1
  48. data/lib/coradoc/asciidoc.rb +6 -3
  49. metadata +10 -1
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ class Transformer < Parslet::Transform
6
+ # Pure-function module for normalizing raw parser `:attribute_list`
7
+ # values into a single canonical Model::AttributeList.
8
+ #
9
+ # The parser's `block_header` rule captures every consecutive `[...]`
10
+ # block before a structural element as a Parslet sequence under
11
+ # `:attribute_list`. Real-world AsciiDoc often stacks multiple lists
12
+ # before a single delimiter:
13
+ #
14
+ # [role=quote]
15
+ # [source, ruby]
16
+ # ----
17
+ # code
18
+ # ----
19
+ #
20
+ # This module is the single source of truth for converting any of those
21
+ # shapes (nil, single list, array of lists, array of hashes) into one
22
+ # canonical AttributeList that downstream model constructors can use.
23
+ module AttributeListNormalizer
24
+ module_function
25
+
26
+ # @param value [Object, nil] Raw parser value bound to :attribute_list
27
+ # @return [Model::AttributeList, nil]
28
+ def coerce(value)
29
+ case value
30
+ when nil then nil
31
+ when Model::AttributeList then value
32
+ when Array
33
+ lists = value.map { |entry| unwrap(entry) }.compact
34
+ return nil if lists.empty?
35
+ return lists.first if lists.size == 1
36
+
37
+ merge(lists)
38
+ else
39
+ value
40
+ end
41
+ end
42
+
43
+ # Merge several AttributeLists into one, preserving positional order
44
+ # and concatenating named keys in input order.
45
+ # @param lists [Array<Model::AttributeList>]
46
+ # @return [Model::AttributeList]
47
+ def merge(lists)
48
+ merged = Model::AttributeList.new
49
+ lists.each do |list|
50
+ next unless list.is_a?(Model::AttributeList)
51
+
52
+ list.positional.each { |p| merged.add_positional(p.value) }
53
+ list.named.each { |n| merged.add_named(n.name, n.value) }
54
+ end
55
+ merged
56
+ end
57
+
58
+ # Unwrap a single entry of the parser's :attribute_list sequence.
59
+ # @param entry [Object]
60
+ # @return [Model::AttributeList, nil]
61
+ def unwrap(entry)
62
+ return entry if entry.is_a?(Model::AttributeList)
63
+
64
+ entry[:attribute_list] if entry.is_a?(Hash) && entry.key?(:attribute_list)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -11,12 +11,11 @@ module Coradoc
11
11
  rule(block: subtree(:block)) do
12
12
  id = block[:id]
13
13
  title = block[:title]
14
- attribute_list = block[:attribute_list]
14
+ attribute_list = AttributeListNormalizer.coerce(block[:attribute_list])
15
15
  delimiter = block[:delimiter].to_s
16
- delimiter_c = delimiter[0]
17
16
  lines = block[:lines]
18
17
  ordering = block.keys.select do |k|
19
- %i[id title attribute_list attribute_list2].include?(k)
18
+ %i[id title attribute_list].include?(k)
20
19
  end
21
20
 
22
21
  opts = {
@@ -26,44 +25,7 @@ module Coradoc
26
25
  lines: lines,
27
26
  ordering: ordering
28
27
  }
29
- opts[:attributes] = attribute_list if attribute_list
30
- delimiter_len = opts[:delimiter_len]
31
-
32
- if delimiter_c == '*'
33
- if attribute_list
34
- if attribute_list.positional == [] &&
35
- attribute_list.named.first&.name == 'reviewer'
36
- Model::Block::ReviewerComment.new(
37
- id:,
38
- title:,
39
- lines:,
40
- delimiter_len:,
41
- attributes: attribute_list
42
- )
43
- else
44
- Model::Block::Side.new(id:, title:, lines:, delimiter_len:,
45
- attributes: attribute_list)
46
- end
47
- else
48
- Model::Block::Side.new(id:, title:, lines:, delimiter_len:,
49
- attributes: attribute_list)
50
- end
51
- elsif delimiter_c == '='
52
- Model::Block::Example.new(id:, title:, lines:, delimiter_len:,
53
- attributes: attribute_list)
54
- elsif delimiter_c == '+'
55
- Model::Block::Pass.new(id:, title:, lines:, delimiter_len:,
56
- attributes: attribute_list)
57
- elsif delimiter_c == '-' && delimiter.size == 2
58
- Model::Block::Open.new(id:, title:, lines:, delimiter_len:,
59
- attributes: attribute_list)
60
- elsif delimiter_c == '-' && delimiter.size >= 4
61
- Model::Block::SourceCode.new(id:, title:, lines:, delimiter_len:,
62
- attributes: attribute_list)
63
- elsif delimiter_c == '_'
64
- Model::Block::Quote.new(id:, title:, lines:, delimiter_len:,
65
- attributes: attribute_list)
66
- end
28
+ BlockTypeClassifier.classify(delimiter, opts, attribute_list)
67
29
  end
68
30
 
69
31
  # Example
@@ -84,7 +46,7 @@ module Coradoc
84
46
  id = block_image[:id]
85
47
  title = block_image[:title]
86
48
  path = block_image[:path]
87
- attrs = block_image[:attribute_list]
49
+ attrs = AttributeListNormalizer.coerce(block_image[:attribute_list])
88
50
  Model::Image::BlockImage.new(
89
51
  title: title,
90
52
  id: id,
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ class Transformer < Parslet::Transform
6
+ # Single source of truth for "which delimiter maps to which block model".
7
+ #
8
+ # The block rule in BlockRules delegates here to convert a parser
9
+ # delimiter string (e.g., `----`, `****`, `--`) into the appropriate
10
+ # Model::Block::* subclass instance. Adding a new block type means
11
+ # appending one entry to DELIMITER_CLASSIFICATIONS — no edits to the
12
+ # block rule itself. (Open/Closed Principle.)
13
+ module BlockTypeClassifier
14
+ # Each entry is [char, min_length, max_length, factory].
15
+ # The factory is a callable taking (opts, attribute_list) and
16
+ # returning a Model::Block::* instance. `max_length` nil means
17
+ # unbounded.
18
+ DELIMITER_CLASSIFICATIONS = [
19
+ ['*', 4, nil, ->(opts, attrs) {
20
+ if attrs && attrs.positional == [] && attrs.named.first&.name == 'reviewer'
21
+ Model::Block::ReviewerComment.new(**opts.merge(attributes: attrs))
22
+ else
23
+ Model::Block::Side.new(**opts.merge(attributes: attrs))
24
+ end
25
+ }],
26
+ ['=', 4, nil, ->(opts, attrs) { Model::Block::Example.new(**opts.merge(attributes: attrs)) }],
27
+ ['+', 4, nil, ->(opts, attrs) { Model::Block::Pass.new(**opts.merge(attributes: attrs)) }],
28
+ ['_', 4, nil, ->(opts, attrs) { Model::Block::Quote.new(**opts.merge(attributes: attrs)) }],
29
+ ['-', 4, nil, ->(opts, attrs) { Model::Block::SourceCode.new(**opts.merge(attributes: attrs)) }],
30
+ ['-', 2, 2, ->(opts, attrs) { Model::Block::Open.new(**opts.merge(attributes: attrs)) }]
31
+ ].freeze
32
+
33
+ module_function
34
+
35
+ # @param delimiter [String] e.g., "----", "**", "--"
36
+ # @param opts [Hash] Constructor options (id, title, lines, delimiter_len, ordering)
37
+ # @param attrs [Model::AttributeList, nil]
38
+ # @return [Model::Block::Base, nil]
39
+ def classify(delimiter, opts, attrs)
40
+ char = delimiter[0]
41
+ len = delimiter.size
42
+ entry = DELIMITER_CLASSIFICATIONS.find do |c, min_len, max_len, _|
43
+ next false unless c == char
44
+ next false unless len >= min_len
45
+ next false if max_len && len > max_len
46
+
47
+ true
48
+ end
49
+ return nil unless entry
50
+
51
+ entry.last.call(opts, attrs)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -7,40 +7,25 @@ module Coradoc
7
7
  module HeaderRules
8
8
  def self.apply(transformer_class)
9
9
  transformer_class.class_eval do
10
- # Header with author and revision
11
- rule(
12
- title: simple(:title),
13
- author: simple(:author),
14
- revision: simple(:revision)
15
- ) do
16
- id = title.is_a?(Model::Title) ? title.id : nil
17
- Model::Header.new(id:, title:, author:, revision:)
18
- end
10
+ # Header single canonical rule covering all combinations of
11
+ # optional :author and :revision slots. The previous design had
12
+ # four explicit rules (one per combination); this version reads
13
+ # the same data with one `subtree` match.
14
+ rule(header: subtree(:header)) do
15
+ title = header[:title]
16
+ author = header[:author]
17
+ revision = header[:revision]
19
18
 
20
- # Header with author only
21
- rule(
22
- title: simple(:title),
23
- author: simple(:author)
24
- ) do
25
- id = title.is_a?(Model::Title) ? title.id : nil
26
- Model::Header.new(id:, title:, author:, revision: nil)
27
- end
19
+ id = header[:id]
20
+ id = title.id if title.is_a?(Model::Title) && title.id && !id
21
+ id = id.to_s unless id.nil?
22
+ id = nil if id && id.empty?
28
23
 
29
- # Header with revision only
30
- rule(
31
- title: simple(:title),
32
- revision: simple(:revision)
33
- ) do
34
- id = title.is_a?(Model::Title) ? title.id : nil
35
- Model::Header.new(id:, title:, author: nil, revision:)
24
+ Model::Header.new(id:, title:, author:, revision:)
36
25
  end
37
26
 
38
- # Header with title only
39
- rule(
40
- title: simple(:title)
41
- ) do
42
- id = title.is_a?(Model::Title) ? title.id : nil
43
- Model::Header.new(id:, title:, author: nil, revision: nil)
27
+ rule(header: simple(:header)) do
28
+ header
44
29
  end
45
30
 
46
31
  # Author
@@ -60,29 +45,6 @@ module Coradoc
60
45
  ) do
61
46
  Model::Revision.new(number:, date:, remark:)
62
47
  end
63
-
64
- # Unwrap header hash - handles cases where header wasn't transformed yet
65
- rule(header: subtree(:header)) do
66
- if header.is_a?(Hash) && header.key?(:title)
67
- id = header[:id]
68
- id = id.to_s unless id.nil?
69
- id = nil if id && id.empty?
70
-
71
- title = header[:title]
72
- author = header[:author]
73
- revision = header[:revision]
74
-
75
- id = title.id if title.is_a?(Model::Title) && title.id && !id
76
-
77
- Model::Header.new(id:, title:, author:, revision:)
78
- else
79
- header
80
- end
81
- end
82
-
83
- rule(header: simple(:header)) do
84
- header
85
- end
86
48
  end
87
49
  end
88
50
  end
@@ -5,6 +5,17 @@ module Coradoc
5
5
  class Transformer < Parslet::Transform
6
6
  # Module containing inline element transformation rules
7
7
  module InlineRules
8
+ # Inline formatting variants that share the same rule shape:
9
+ # constrained and unconstrained forms of the same model class.
10
+ # `span` is excluded because it carries `text:` + `attributes:`
11
+ # rather than `content:`, so it gets its own pair of rules.
12
+ FORMATTING_VARIANTS = [
13
+ %i[bold Bold],
14
+ %i[italic Italic],
15
+ %i[highlight Highlight],
16
+ %i[monospace Monospace]
17
+ ].freeze
18
+
8
19
  def self.apply(transformer_class)
9
20
  transformer_class.class_eval do
10
21
  # Link
@@ -82,64 +93,23 @@ module Coradoc
82
93
  href_arg.to_s
83
94
  end
84
95
 
85
- # Bold (constrained)
86
- rule(bold_constrained: subtree(:bold)) do
87
- content = Transformer.extract_inline_content(bold)
88
- Model::Inline::Bold.new(content: content, unconstrained: false)
89
- end
90
-
91
- # Bold (unconstrained)
92
- rule(bold_unconstrained: subtree(:bold)) do
93
- content = Transformer.extract_inline_content(bold)
94
- Model::Inline::Bold.new(content: content, unconstrained: true)
95
- end
96
-
97
- # Italic (constrained)
98
- rule(italic_constrained: subtree(:italic)) do
99
- content = Transformer.extract_inline_content(italic)
100
- Model::Inline::Italic.new(content: content, unconstrained: false)
101
- end
102
-
103
- # Italic (unconstrained)
104
- rule(italic_unconstrained: subtree(:italic)) do
105
- content = Transformer.extract_inline_content(italic)
106
- Model::Inline::Italic.new(content: content, unconstrained: true)
107
- end
108
-
109
- # Highlight (constrained)
110
- rule(highlight_constrained: subtree(:highlight)) do
111
- content = Transformer.extract_inline_content(highlight)
112
- Model::Inline::Highlight.new(content: content, unconstrained: false)
113
- end
114
-
115
- # Highlight (unconstrained)
116
- rule(highlight_unconstrained: subtree(:highlight)) do
117
- content = Transformer.extract_inline_content(highlight)
118
- Model::Inline::Highlight.new(content: content, unconstrained: true)
119
- end
120
-
121
- # Monospace (constrained)
122
- rule(monospace_constrained: subtree(:monospace)) do
123
- content = Transformer.extract_inline_content(monospace)
124
- Model::Inline::Monospace.new(content: content, unconstrained: false)
125
- end
126
-
127
- # Monospace (unconstrained)
128
- rule(monospace_unconstrained: subtree(:monospace)) do
129
- content = Transformer.extract_inline_content(monospace)
130
- Model::Inline::Monospace.new(content: content, unconstrained: true)
131
- end
132
-
133
- # Superscript
134
- rule(superscript: subtree(:superscript)) do
135
- content = Transformer.extract_simple_inline_content(superscript)
136
- Model::Inline::Superscript.new(content:)
137
- end
96
+ # Inline formatting rules generated from a single registry.
97
+ # See InlineRules::FORMATTING_VARIANTS. `span` is special
98
+ # because it carries `text:` + `attributes:` rather than
99
+ # `content:`, so it stays inline below.
100
+ InlineRules::FORMATTING_VARIANTS.each do |prefix, class_name|
101
+ klass = Model::Inline.const_get(class_name)
102
+ constrained_key = :"#{prefix}_constrained"
103
+ unconstrained_key = :"#{prefix}_unconstrained"
138
104
 
139
- # Subscript
140
- rule(subscript: subtree(:subscript)) do
141
- content = Transformer.extract_simple_inline_content(subscript)
142
- Model::Inline::Subscript.new(content:)
105
+ rule(constrained_key => subtree(:subtree)) do
106
+ content = Transformer.extract_inline_content(subtree)
107
+ klass.new(content: content, unconstrained: false)
108
+ end
109
+ rule(unconstrained_key => subtree(:subtree)) do
110
+ content = Transformer.extract_inline_content(subtree)
111
+ klass.new(content: content, unconstrained: true)
112
+ end
143
113
  end
144
114
 
145
115
  # Span (constrained)
@@ -160,6 +130,18 @@ module Coradoc
160
130
  )
161
131
  end
162
132
 
133
+ # Superscript
134
+ rule(superscript: subtree(:superscript)) do
135
+ content = Transformer.extract_simple_inline_content(superscript)
136
+ Model::Inline::Superscript.new(content:)
137
+ end
138
+
139
+ # Subscript
140
+ rule(subscript: subtree(:subscript)) do
141
+ content = Transformer.extract_simple_inline_content(subscript)
142
+ Model::Inline::Subscript.new(content:)
143
+ end
144
+
163
145
  # Highlight (simple)
164
146
  rule(highlight: simple(:text)) do
165
147
  Model::Highlight.new(content: text)
@@ -157,35 +157,12 @@ module Coradoc
157
157
  end
158
158
  end
159
159
 
160
- content = lines.map do |line|
161
- if line.is_a?(Hash) && line.key?(:text)
162
- text_content = line[:text]
163
- line_break = line[:line_break]
164
-
165
- transformed_text = if text_content.is_a?(Array)
166
- text_content.map do |item|
167
- if item.is_a?(Hash)
168
- Transformer.new.apply(item)
169
- else
170
- item
171
- end
172
- end
173
- else
174
- text_content
175
- end
176
-
177
- Model::TextElement.new(content: transformed_text, line_break: line_break)
178
- else
179
- line
180
- end
181
- end
182
-
183
160
  Model::ReviewerNote.new(
184
161
  reviewer: attrs[:reviewer],
185
162
  date: attrs[:date],
186
163
  from: attrs[:from],
187
164
  to: attrs[:to],
188
- content: content
165
+ content: Transformer.lines_to_text_elements(lines)
189
166
  )
190
167
  end
191
168
  end
@@ -60,81 +60,19 @@ module Coradoc
60
60
  table
61
61
  end
62
62
 
63
- # Table with rows (new parser output - rows captured explicitly)
64
- rule(
65
- delim_char: simple(:delim_char),
66
- rows: sequence(:rows)
67
- ) do
68
- Model::Table.new(rows: Transformer.regroup_table_rows(rows))
69
- end
70
-
71
- # Table with rows and title
72
- rule(
73
- title: simple(:title),
74
- delim_char: simple(:delim_char),
75
- rows: sequence(:rows)
76
- ) do
77
- Model::Table.new(title: title.to_s, rows: Transformer.regroup_table_rows(rows))
78
- end
79
-
80
- # Table with rows and id
81
- rule(
82
- id: simple(:id),
83
- delim_char: simple(:delim_char),
84
- rows: sequence(:rows)
85
- ) do
86
- Model::Table.new(id: id.to_s, rows: Transformer.regroup_table_rows(rows))
87
- end
88
-
89
- # Table with rows, id, and attributes
90
- rule(
91
- id: simple(:id),
92
- attribute_list: simple(:attrs),
93
- delim_char: simple(:delim_char),
94
- rows: sequence(:rows)
95
- ) do
96
- Model::Table.new(id: id.to_s, rows: Transformer.regroup_table_rows(rows, attrs), attrs: attrs)
97
- end
98
-
99
- # Table with rows, title, and attributes
100
- rule(
101
- title: simple(:title),
102
- attribute_list: simple(:attrs),
103
- delim_char: simple(:delim_char),
104
- rows: sequence(:rows)
105
- ) do
106
- Model::Table.new(title: title.to_s, rows: Transformer.regroup_table_rows(rows, attrs), attrs: attrs)
107
- end
108
-
109
- # Table with rows and attributes only
110
- rule(
111
- attribute_list: simple(:attrs),
112
- delim_char: simple(:delim_char),
113
- rows: sequence(:rows)
114
- ) do
115
- Model::Table.new(rows: Transformer.regroup_table_rows(rows, attrs), attrs: attrs)
116
- end
117
-
118
- # Table with rows, id, title, and attributes (full set)
119
- rule(
120
- id: simple(:id),
121
- title: simple(:title),
122
- attribute_list: simple(:attrs),
123
- delim_char: simple(:delim_char),
124
- rows: sequence(:rows)
125
- ) do
126
- Model::Table.new(id: id.to_s, title: title.to_s, rows: Transformer.regroup_table_rows(rows, attrs),
127
- attrs: attrs)
128
- end
129
-
130
- # Table with id and title (no attributes)
131
- rule(
132
- id: simple(:id),
133
- title: simple(:title),
134
- delim_char: simple(:delim_char),
135
- rows: sequence(:rows)
136
- ) do
137
- Model::Table.new(id: id.to_s, title: title.to_s, rows: Transformer.regroup_table_rows(rows))
63
+ # Unified Table rule. Every variant (with or without title, id,
64
+ # attributes) flows through here. Parser::BlockHeader always
65
+ # captures attribute_lists as a sequence, so we funnel through
66
+ # coerce_attribute_list before constructing the model.
67
+ rule(table: subtree(:table)) do
68
+ id = table[:id]&.to_s
69
+ title = table[:title]&.to_s
70
+ attrs = AttributeListNormalizer.coerce(table[:attribute_list])
71
+ rows = table[:rows]
72
+ opts = { rows: Transformer.regroup_table_rows(rows, attrs), attrs: attrs }
73
+ opts[:id] = id if id
74
+ opts[:title] = title unless title.nil? || title.empty?
75
+ Model::Table.new(**opts)
138
76
  end
139
77
 
140
78
  # Title
@@ -171,7 +109,7 @@ module Coradoc
171
109
 
172
110
  id = title.id if title.is_a?(Model::Title) && title.id && !id
173
111
 
174
- attribute_list = section[:attribute_list] || nil
112
+ attribute_list = AttributeListNormalizer.coerce(section[:attribute_list])
175
113
  contents = section[:contents] || []
176
114
  sections = section[:sections]
177
115
  Model::Section.new(
@@ -200,12 +138,11 @@ module Coradoc
200
138
 
201
139
  # Bibliography entry
202
140
  rule(bibliography_entry: subtree(:bib_entry)) do
203
- anchor_name = bib_entry[:anchor_name]
204
- document_id = bib_entry[:document_id]
205
- ref_text = bib_entry[:ref_text]
206
- line_break = bib_entry[:line_break]
207
141
  Model::BibliographyEntry.new(
208
- anchor_name:, document_id:, ref_text:, line_break:
142
+ anchor_name: bib_entry[:anchor_name],
143
+ document_id: bib_entry[:document_id],
144
+ ref_text: Model::BibliographyEntry.coerce_ref_text(bib_entry[:ref_text]),
145
+ line_break: bib_entry[:line_break]
209
146
  )
210
147
  end
211
148
  end