coradoc-adoc 2.0.29 → 2.0.31
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/list/definition_item.rb +13 -0
- data/lib/coradoc/asciidoc/model/text_element.rb +19 -3
- data/lib/coradoc/asciidoc/parser/base.rb +8 -0
- data/lib/coradoc/asciidoc/parser/inline.rb +85 -76
- data/lib/coradoc/asciidoc/parser/list.rb +124 -22
- data/lib/coradoc/asciidoc/parser/rule_dispatcher.rb +4 -2
- data/lib/coradoc/asciidoc/parser.rb +1 -0
- data/lib/coradoc/asciidoc/serializer/serializers/document.rb +1 -3
- data/lib/coradoc/asciidoc/transform/element_transformers/block_transformer.rb +85 -73
- data/lib/coradoc/asciidoc/transform/element_transformers/document_transformer.rb +1 -1
- data/lib/coradoc/asciidoc/transform/element_transformers/inline_transformer.rb +27 -22
- data/lib/coradoc/asciidoc/transform/element_transformers/list_transformer.rb +61 -11
- data/lib/coradoc/asciidoc/transform/from_core_model.rb +27 -11
- data/lib/coradoc/asciidoc/transformer/inline_rules.rb +5 -5
- data/lib/coradoc/asciidoc/transformer/list_rules.rb +100 -30
- data/lib/coradoc/asciidoc/transformer/table_layout.rb +3 -1
- data/lib/coradoc/asciidoc/transformer.rb +73 -8
- data/lib/coradoc/asciidoc/typographic_quotes.rb +37 -0
- data/lib/coradoc/asciidoc/version.rb +1 -1
- data/lib/coradoc/asciidoc.rb +1 -0
- metadata +3 -1
|
@@ -169,19 +169,22 @@ module Coradoc
|
|
|
169
169
|
|
|
170
170
|
# Single admonition dispatch used by every block-form path
|
|
171
171
|
# (example / sidebar / quote / pass / open). Builds an
|
|
172
|
-
# AnnotationBlock
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
172
|
+
# AnnotationBlock by delegating to +transform_typed_block+
|
|
173
|
+
# with +annotation_type+ injected via +extra_attrs+. This
|
|
174
|
+
# preserves inline formatting (links, monospace, bold), list
|
|
175
|
+
# structure, and paragraph breaks — the typed-block pipeline
|
|
176
|
+
# already groups lines into ParagraphBlock children and
|
|
177
|
+
# dispatches block-level lines (lists, tables) through
|
|
178
|
+
# ToCoreModel.transform. Type is canonicalised via
|
|
179
|
+
# AdmonitionStyles so every code path (line admonition, block
|
|
180
|
+
# admonition, attribute-cast admonition) produces the same
|
|
181
|
+
# uppercase key.
|
|
177
182
|
def transform_admonition_block(block, type)
|
|
178
183
|
canonical = AdmonitionStyles.canonicalize(type) || type.to_s
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
title: ToCoreModel.extract_title_text(block.title),
|
|
184
|
-
source_line: block.source_line
|
|
184
|
+
transform_typed_block(
|
|
185
|
+
block,
|
|
186
|
+
Coradoc::CoreModel::AnnotationBlock,
|
|
187
|
+
annotation_type: canonical
|
|
185
188
|
)
|
|
186
189
|
end
|
|
187
190
|
|
|
@@ -214,76 +217,85 @@ module Coradoc
|
|
|
214
217
|
end
|
|
215
218
|
|
|
216
219
|
def transform_typed_block(block, klass, extra_attrs = {})
|
|
217
|
-
|
|
218
|
-
has_nested_blocks = raw_lines.any? { |line| block_level_child?(line) }
|
|
219
|
-
|
|
220
|
-
if has_nested_blocks
|
|
221
|
-
children = raw_lines.reject do |line|
|
|
222
|
-
line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
|
|
223
|
-
line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
|
|
224
|
-
end.filter_map do |line|
|
|
225
|
-
result = ToCoreModel.transform(line)
|
|
226
|
-
next nil if result.nil?
|
|
227
|
-
next result if result.is_a?(Coradoc::CoreModel::Base)
|
|
228
|
-
|
|
229
|
-
text = ToCoreModel.extract_text_content(result)
|
|
230
|
-
next nil if text.nil? || text.strip.empty?
|
|
231
|
-
|
|
232
|
-
Coradoc::CoreModel::TextContent.new(text: text)
|
|
233
|
-
end
|
|
234
|
-
klass.new(
|
|
235
|
-
id: block.id,
|
|
236
|
-
title: ToCoreModel.extract_title_text(block.title),
|
|
237
|
-
children: children,
|
|
238
|
-
language: ToCoreModel.extract_block_language(block),
|
|
239
|
-
source_line: block.source_line,
|
|
240
|
-
**extra_attrs
|
|
241
|
-
)
|
|
242
|
-
else
|
|
243
|
-
paragraph_groups = group_block_lines_into_paragraphs(raw_lines)
|
|
244
|
-
|
|
245
|
-
content_lines = paragraph_groups.map do |group|
|
|
246
|
-
ToCoreModel.extract_text_content(group)
|
|
247
|
-
end.join("\n\n")
|
|
248
|
-
|
|
249
|
-
children = paragraph_groups.map do |group|
|
|
250
|
-
inline = ToCoreModel.transform_inline_content(group)
|
|
251
|
-
inline = [Coradoc::CoreModel::TextContent.new(text: '')] if inline.empty?
|
|
252
|
-
Coradoc::CoreModel::ParagraphBlock.new(
|
|
253
|
-
content: ToCoreModel.extract_text_content(group),
|
|
254
|
-
children: Array(inline),
|
|
255
|
-
source_line: ToCoreModel.extract_source_line(group)
|
|
256
|
-
)
|
|
257
|
-
end
|
|
220
|
+
children, content = build_typed_block_children(block.lines)
|
|
258
221
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
end
|
|
222
|
+
klass.new(
|
|
223
|
+
id: block.id,
|
|
224
|
+
title: ToCoreModel.extract_title_text(block.title),
|
|
225
|
+
content: content,
|
|
226
|
+
children: children,
|
|
227
|
+
language: ToCoreModel.extract_block_language(block),
|
|
228
|
+
source_line: block.source_line,
|
|
229
|
+
**extra_attrs
|
|
230
|
+
)
|
|
269
231
|
end
|
|
270
232
|
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
233
|
+
# Walks block.lines in document order and emits the typed
|
|
234
|
+
# block's children alongside the prose-string view of its
|
|
235
|
+
# paragraphs.
|
|
236
|
+
#
|
|
237
|
+
# Consecutive inline lines coalesce into one ParagraphBlock
|
|
238
|
+
# (preserving AsciiDoc paragraph semantics: soft-wrapped
|
|
239
|
+
# source lines are one paragraph, blank lines split them).
|
|
240
|
+
# Block-level lines (lists, tables, nested blocks — anything
|
|
241
|
+
# whose AsciiDoc model returns true from +block_level?+)
|
|
242
|
+
# dispatch through +ToCoreModel.transform+ and slot in at
|
|
243
|
+
# their original document-order position. LineBreak /
|
|
244
|
+
# PageBreak lines flush the pending inline paragraph.
|
|
245
|
+
#
|
|
246
|
+
# Returns [+children+, +content+] where +content+ is the
|
|
247
|
+
# paragraph text joined with blank-line separators (nil when
|
|
248
|
+
# the block has no paragraphs, e.g. contains only nested
|
|
249
|
+
# blocks).
|
|
250
|
+
#
|
|
251
|
+
# Single source of truth for typed-block body construction.
|
|
252
|
+
# Used by every typed-block transformer (example, sidebar,
|
|
253
|
+
# quote, open, typed-cast, admonition) so they all preserve
|
|
254
|
+
# inline formatting, list structure, and paragraph breaks
|
|
255
|
+
# identically.
|
|
256
|
+
def build_typed_block_children(lines)
|
|
257
|
+
children = []
|
|
258
|
+
paragraph_texts = []
|
|
259
|
+
pending_inline = []
|
|
260
|
+
|
|
261
|
+
Array(lines).each do |line|
|
|
277
262
|
if line.is_a?(Coradoc::AsciiDoc::Model::LineBreak) ||
|
|
278
263
|
line.is_a?(Coradoc::AsciiDoc::Model::Break::PageBreak)
|
|
279
|
-
|
|
280
|
-
|
|
264
|
+
flush_inline_paragraph(pending_inline, children, paragraph_texts)
|
|
265
|
+
pending_inline.clear
|
|
266
|
+
elsif block_level_child?(line)
|
|
267
|
+
flush_inline_paragraph(pending_inline, children, paragraph_texts)
|
|
268
|
+
pending_inline.clear
|
|
269
|
+
dispatched = ToCoreModel.transform(line)
|
|
270
|
+
children << dispatched if dispatched
|
|
281
271
|
else
|
|
282
|
-
|
|
272
|
+
pending_inline << line
|
|
283
273
|
end
|
|
284
274
|
end
|
|
285
|
-
|
|
286
|
-
|
|
275
|
+
flush_inline_paragraph(pending_inline, children, paragraph_texts)
|
|
276
|
+
|
|
277
|
+
content = paragraph_texts.any? ? paragraph_texts.join("\n\n") : nil
|
|
278
|
+
[children, content]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Materialises +pending_inline+ into a ParagraphBlock and
|
|
282
|
+
# appends it to +children+, recording its flat text in
|
|
283
|
+
# +paragraph_texts+ for later content-string joining. No-op
|
|
284
|
+
# when +pending_inline+ is empty so callers can flush
|
|
285
|
+
# unconditionally at every boundary (blank line, block-level
|
|
286
|
+
# child, end-of-input) without producing empty paragraphs.
|
|
287
|
+
def flush_inline_paragraph(pending_inline, children, paragraph_texts)
|
|
288
|
+
return if pending_inline.empty?
|
|
289
|
+
|
|
290
|
+
inline = ToCoreModel.transform_inline_content(pending_inline)
|
|
291
|
+
inline = [Coradoc::CoreModel::TextContent.new(text: '')] if inline.empty?
|
|
292
|
+
text = ToCoreModel.extract_text_content(pending_inline)
|
|
293
|
+
paragraph_texts << text
|
|
294
|
+
children << Coradoc::CoreModel::ParagraphBlock.new(
|
|
295
|
+
content: text,
|
|
296
|
+
children: Array(inline),
|
|
297
|
+
source_line: ToCoreModel.extract_source_line(pending_inline)
|
|
298
|
+
)
|
|
287
299
|
end
|
|
288
300
|
|
|
289
301
|
# A line that should be emitted as a direct child of the
|
|
@@ -51,7 +51,7 @@ module Coradoc
|
|
|
51
51
|
return children if children.any? { |c| title_heading?(c) }
|
|
52
52
|
|
|
53
53
|
title_id = doc_id || Coradoc::CoreModel::IdGenerator
|
|
54
|
-
|
|
54
|
+
.generate_from_title(title_text)
|
|
55
55
|
title_heading = Coradoc::CoreModel::HeaderElement.new(
|
|
56
56
|
level: 0,
|
|
57
57
|
title: title_text,
|
|
@@ -4,6 +4,24 @@ module Coradoc
|
|
|
4
4
|
module AsciiDoc
|
|
5
5
|
module Transform
|
|
6
6
|
module ElementTransformers
|
|
7
|
+
# Owns the recursive re-parse that recognises nested inline marks
|
|
8
|
+
# (Bug 16B). The re-parse re-enters the full inline pipeline:
|
|
9
|
+
#
|
|
10
|
+
# transform_inline → parse_nested_inline_children
|
|
11
|
+
# → ToCoreModel.parse_and_transform_inline
|
|
12
|
+
# → InlineTransformVisitor.visit
|
|
13
|
+
# → ToCoreModel.transform
|
|
14
|
+
# → transform_inline (cycle)
|
|
15
|
+
#
|
|
16
|
+
# The cycle terminates when the mark's content has no further
|
|
17
|
+
# inline-mark characters — the parser produces only flat text,
|
|
18
|
+
# `parse_nested_inline_children` returns `[]`, and the mark
|
|
19
|
+
# keeps its flat content shape.
|
|
20
|
+
#
|
|
21
|
+
# Realistic nesting is 2–3 levels. Pathological input
|
|
22
|
+
# (`*****…*****` with hundreds of levels) raises
|
|
23
|
+
# `SystemStackError` naturally — that's Ruby's contract, not a
|
|
24
|
+
# bespoke guard.
|
|
7
25
|
class InlineTransformer
|
|
8
26
|
class << self
|
|
9
27
|
def transform_inline(inline, format_type)
|
|
@@ -11,36 +29,23 @@ module Coradoc
|
|
|
11
29
|
raw_content = ToCoreModel.extract_text_content(inline.content)
|
|
12
30
|
|
|
13
31
|
# Recursively parse the mark's content to recognise nested
|
|
14
|
-
# inline marks (Bug 16B).
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# produce ProseMirror's flat text-node-with-marks shape.
|
|
32
|
+
# inline marks (Bug 16B). Only populate `children` when the
|
|
33
|
+
# parser found real nested marks — flat marks keep children
|
|
34
|
+
# empty, and the Mirror handler's `build_simple_mark` falls
|
|
35
|
+
# back to the `content` string. Saves one TextContent
|
|
36
|
+
# allocation per flat mark (the common case).
|
|
20
37
|
children = parse_nested_inline_children(raw_content)
|
|
21
38
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
children: children,
|
|
26
|
-
source_line: inline.source_line
|
|
27
|
-
)
|
|
28
|
-
else
|
|
29
|
-
klass.new(
|
|
30
|
-
content: raw_content,
|
|
31
|
-
source_line: inline.source_line
|
|
32
|
-
)
|
|
33
|
-
end
|
|
39
|
+
kwargs = { content: raw_content, source_line: inline.source_line }
|
|
40
|
+
kwargs[:children] = children if children.any?
|
|
41
|
+
klass.new(**kwargs)
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
# Re-parse a mark's raw content string through the inline
|
|
37
45
|
# parser. Returns the list of CoreModel children when the
|
|
38
46
|
# content contains nested inline marks; returns [] when
|
|
39
47
|
# the content is plain text (no nested marks to preserve).
|
|
40
|
-
# The empty return is the recursion terminator
|
|
41
|
-
# mark's content has no further mark characters, the
|
|
42
|
-
# parser produces only TextContent nodes, which we drop
|
|
43
|
-
# in favour of the mark's flat content string.
|
|
48
|
+
# The empty return is the recursion terminator.
|
|
44
49
|
def parse_nested_inline_children(text)
|
|
45
50
|
return [] if text.nil? || text.to_s.empty?
|
|
46
51
|
|
|
@@ -32,24 +32,74 @@ module Coradoc
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
def transform_definition_item(item)
|
|
35
|
-
|
|
35
|
+
term_parts = Array(item.terms)
|
|
36
36
|
def_content = item.contents
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
# Process each term independently so multi-term `<dt>`'s
|
|
39
|
+
# (e.g. AsciiDoc `term1::\nterm2::\ndef`) preserve their
|
|
40
|
+
# distinct identities in CoreModel#terms. Each entry in
|
|
41
|
+
# `term_strings` is one term's plain text; `term_children`
|
|
42
|
+
# is the parallel array of typed inline children. The
|
|
43
|
+
# singular `term` accessor is set to the first term for
|
|
44
|
+
# backward compatibility with consumers that haven't
|
|
45
|
+
# migrated to the `terms` collection.
|
|
46
|
+
term_strings = term_parts.map do |part|
|
|
47
|
+
parsed = ToCoreModel.parse_inline_text(part)
|
|
48
|
+
ToCoreModel.extract_text_content(ToCoreModel.transform_inline_content(parsed))
|
|
41
49
|
end
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
# Inline children for the FIRST term only. The CoreModel
|
|
52
|
+
# `term_children` accessor is singular; carrying per-term
|
|
53
|
+
# children for multi-term `<dt>`'s would require a
|
|
54
|
+
# collection of arrays. The common case (single-term dt
|
|
55
|
+
# with inline markup) is fully supported; multi-term dt
|
|
56
|
+
# with inline markup on later terms degrades to text-only
|
|
57
|
+
# for those later terms (acceptable per asciidoctor's
|
|
58
|
+
# observed behaviour — multi-term inline markup is rare).
|
|
59
|
+
primary_term_children = if term_parts.any?
|
|
60
|
+
parsed = ToCoreModel.parse_inline_text(term_parts.first)
|
|
61
|
+
ToCoreModel.transform_inline_content(parsed)
|
|
62
|
+
else
|
|
63
|
+
[]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# contents is typed on DefinitionItem as Array<TextElement>
|
|
67
|
+
# (see model/list/definition_item.rb). Each TextElement's
|
|
68
|
+
# `to_s` handles the polymorphic content shape (String,
|
|
69
|
+
# Array, or nested Serializable). Join the per-line text
|
|
70
|
+
# elements into a single paragraph string so the inline
|
|
71
|
+
# parser sees the full multi-line dd content as one
|
|
72
|
+
# soft-wrapped paragraph.
|
|
73
|
+
#
|
|
74
|
+
# When dd has no inline definition (term-only item where
|
|
75
|
+
# the dd is populated entirely by `+`-continuation blocks),
|
|
76
|
+
# def_content is empty and we skip populating definitions
|
|
77
|
+
# entirely — avoids emitting an empty <text></text> node
|
|
78
|
+
# ahead of the attached blocks.
|
|
79
|
+
def_text = def_content.map(&:to_s).reject(&:empty?).join(' ')
|
|
80
|
+
has_definition = !def_text.empty?
|
|
81
|
+
|
|
82
|
+
parsed_defs = has_definition ? ToCoreModel.parse_inline_text(def_text) : []
|
|
83
|
+
def_children = has_definition ? ToCoreModel.transform_inline_content(parsed_defs) : []
|
|
84
|
+
|
|
85
|
+
# Transform `+`-continuation blocks (paragraphs, admonitions,
|
|
86
|
+
# delimited blocks) into their CoreModel equivalents. Each
|
|
87
|
+
# becomes a child of the dd, rendered after the definition
|
|
88
|
+
# paragraph. Passes through ToCoreModel.transform so any
|
|
89
|
+
# registered block transformer applies (paragraph, source,
|
|
90
|
+
# admonition, etc.).
|
|
91
|
+
attached_children = Array(item.attached).filter_map do |block|
|
|
92
|
+
ToCoreModel.transform(block)
|
|
93
|
+
end
|
|
47
94
|
|
|
95
|
+
primary_term = term_strings.first.to_s
|
|
48
96
|
di = Coradoc::CoreModel::DefinitionItem.new(
|
|
49
|
-
term:
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
term: primary_term,
|
|
98
|
+
terms: term_strings,
|
|
99
|
+
definitions: has_definition ? [ToCoreModel.extract_text_content(def_children)] : [],
|
|
100
|
+
term_children: primary_term_children,
|
|
52
101
|
definition_children: def_children,
|
|
102
|
+
attached_children: attached_children,
|
|
53
103
|
source_line: item.source_line
|
|
54
104
|
)
|
|
55
105
|
di.id = item.id if item.id
|
|
@@ -31,15 +31,15 @@ module Coradoc
|
|
|
31
31
|
without_title_heading, title_text = extract_title_heading(without_frontmatter, element.title)
|
|
32
32
|
|
|
33
33
|
header = if title_text
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
Coradoc::AsciiDoc::Model::Header.new(
|
|
35
|
+
title: Coradoc::AsciiDoc::Model::Title.new(
|
|
36
|
+
content: title_text,
|
|
37
|
+
level_int: 0
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
else
|
|
41
|
+
Coradoc::AsciiDoc::Model::Header.new(title: '')
|
|
42
|
+
end
|
|
43
43
|
|
|
44
44
|
Coradoc::AsciiDoc::Model::Document.new(
|
|
45
45
|
id: element.id,
|
|
@@ -369,12 +369,18 @@ module Coradoc
|
|
|
369
369
|
|
|
370
370
|
def transform_definition_item(item, depth = 1)
|
|
371
371
|
delimiter = ':' * (depth + 1)
|
|
372
|
-
term
|
|
372
|
+
# Multi-term `<dt>`: one Term per entry in `item.terms`.
|
|
373
|
+
# Falls back to `[item.term]` for items populated via
|
|
374
|
+
# legacy paths that only set the singular accessor.
|
|
375
|
+
term_strings = terms_for(item)
|
|
376
|
+
terms = term_strings.map do |text|
|
|
377
|
+
Coradoc::AsciiDoc::Model::Term.new(term: text.to_s)
|
|
378
|
+
end
|
|
373
379
|
contents = Array(item.definitions).map do |defn|
|
|
374
380
|
Coradoc::AsciiDoc::Model::TextElement.new(content: defn.to_s)
|
|
375
381
|
end
|
|
376
382
|
di = Coradoc::AsciiDoc::Model::List::DefinitionItem.new(
|
|
377
|
-
terms:
|
|
383
|
+
terms: terms,
|
|
378
384
|
contents: contents,
|
|
379
385
|
delimiter: delimiter
|
|
380
386
|
)
|
|
@@ -382,6 +388,16 @@ module Coradoc
|
|
|
382
388
|
di
|
|
383
389
|
end
|
|
384
390
|
|
|
391
|
+
# Source of truth for "the terms on this <dt>": the `terms`
|
|
392
|
+
# collection when populated, else `[term]` for legacy callers.
|
|
393
|
+
def terms_for(item)
|
|
394
|
+
collection = item.terms if item.is_a?(Coradoc::CoreModel::DefinitionItem)
|
|
395
|
+
return Array(collection) unless collection.nil? || collection.empty?
|
|
396
|
+
|
|
397
|
+
primary = item.term
|
|
398
|
+
primary.to_s.empty? ? [] : [primary.to_s]
|
|
399
|
+
end
|
|
400
|
+
|
|
385
401
|
def transform_toc(_toc)
|
|
386
402
|
Coradoc::AsciiDoc::Model::TextElement.new(
|
|
387
403
|
content: 'toc::[]'
|
|
@@ -71,13 +71,13 @@ module Coradoc
|
|
|
71
71
|
# Each 2-char pattern collapses to a single Unicode character
|
|
72
72
|
# at parse time so downstream consumers (HTML, Markdown) see
|
|
73
73
|
# the literal typographic char without any post-processing.
|
|
74
|
-
# The mapping
|
|
75
|
-
#
|
|
74
|
+
# The mapping lives in the shared TypographicQuotes module
|
|
75
|
+
# (not on Parser or Transformer) so both layers reference
|
|
76
|
+
# the same source of truth.
|
|
76
77
|
rule(typographic_quote: simple(:pattern)) do
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
Model::TextElement.new(
|
|
79
|
+
content: Coradoc::AsciiDoc::TypographicQuotes.char_for(pattern)
|
|
79
80
|
)
|
|
80
|
-
Model::TextElement.new(content: char)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
# Hard line break (` +\n` or `\\n`). Emitted as a dedicated
|
|
@@ -6,6 +6,19 @@ module Coradoc
|
|
|
6
6
|
# Module containing list transformation rules
|
|
7
7
|
module ListRules
|
|
8
8
|
class << self
|
|
9
|
+
# Build a nested DefinitionList tree from a flat list of items.
|
|
10
|
+
#
|
|
11
|
+
# AsciiDoc's dlist syntax uses delimiter length to express
|
|
12
|
+
# nesting depth (`::` = 1, `:::` = 2, `::::` = 3). Consecutive
|
|
13
|
+
# term-only items at the SAME depth are merged into a single
|
|
14
|
+
# multi-term `<dt>` sharing the next item's `<dd>`:
|
|
15
|
+
#
|
|
16
|
+
# term1:: → <dt>term1</dt>
|
|
17
|
+
# term2:: <dt>term2</dt>
|
|
18
|
+
# def <dd>def</dd>
|
|
19
|
+
#
|
|
20
|
+
# Stack-based walk in source order. Each item carries its own
|
|
21
|
+
# `delimiter`; depth is derived via {#dlist_depth}.
|
|
9
22
|
def build_dlist_tree(items)
|
|
10
23
|
root = Model::List::Definition.new(items: [])
|
|
11
24
|
stack = [[root, 0]]
|
|
@@ -14,16 +27,80 @@ module Coradoc
|
|
|
14
27
|
depth = dlist_depth(item.delimiter)
|
|
15
28
|
stack.pop while stack.last[1] >= depth
|
|
16
29
|
|
|
17
|
-
stack.last[0]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
30
|
+
parent_list = stack.last[0]
|
|
31
|
+
last = parent_list.items.last
|
|
32
|
+
|
|
33
|
+
if merge_with_predecessor?(last, item)
|
|
34
|
+
merge_term_only_item(last, item)
|
|
35
|
+
# Stack stays — deeper items still nest under `last`.
|
|
36
|
+
# Replace the stack top so deeper items land in `last`'s
|
|
37
|
+
# current nested list (already on the stack from when
|
|
38
|
+
# `last` was first added).
|
|
39
|
+
else
|
|
40
|
+
parent_list.items << item
|
|
41
|
+
item.nested << Model::List::Definition.new(items: [])
|
|
42
|
+
stack.push([item.nested.last, depth])
|
|
43
|
+
end
|
|
21
44
|
end
|
|
22
45
|
|
|
23
46
|
prune_empty_nested(root)
|
|
24
47
|
root
|
|
25
48
|
end
|
|
26
49
|
|
|
50
|
+
# Two consecutive items at the same depth become a multi-term
|
|
51
|
+
# `<dt>` sharing one `<dd>` when the PREVIOUS item is term-only
|
|
52
|
+
# (no def, no attached blocks). The previous item is the
|
|
53
|
+
# accumulating dt; the current item contributes either another
|
|
54
|
+
# term (if it's also term-only) or the terminal dt + the
|
|
55
|
+
# shared dd (if it has a def).
|
|
56
|
+
#
|
|
57
|
+
# Once an item has its own def/attached, it's the terminal dt
|
|
58
|
+
# of any in-progress multi-term group; the next same-depth
|
|
59
|
+
# item starts a fresh entry.
|
|
60
|
+
def merge_with_predecessor?(last, current)
|
|
61
|
+
return false unless last
|
|
62
|
+
return false unless last.delimiter == current.delimiter
|
|
63
|
+
return false unless term_only?(last)
|
|
64
|
+
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# "Term-only" means the item carries no dd content of its own
|
|
69
|
+
# (no inline def, no `+`-attached blocks, no nested child
|
|
70
|
+
# items). Such items are eligible to merge with a following
|
|
71
|
+
# same-depth item to form a multi-term `<dt>` sharing one dd.
|
|
72
|
+
# Items with nested children have already "claimed" their dd
|
|
73
|
+
# slot for the nested content and can't merge.
|
|
74
|
+
def term_only?(item)
|
|
75
|
+
return false if contents_present?(item)
|
|
76
|
+
return false if attached_present?(item)
|
|
77
|
+
return false if nested_has_items?(item)
|
|
78
|
+
|
|
79
|
+
true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def contents_present?(item)
|
|
83
|
+
!item.contents.nil? && !item.contents.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def attached_present?(item)
|
|
87
|
+
!item.attached.nil? && item.attached.any?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `item.nested` is an Array<Definition>; each Definition has
|
|
91
|
+
# its own `items` Array. An item with populated nested items
|
|
92
|
+
# (deeper dlist children) is not term-only.
|
|
93
|
+
def nested_has_items?(item)
|
|
94
|
+
Array(item.nested).any? { |list| list.is_a?(Model::List::Definition) && list.items.any? }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def merge_term_only_item(last, current)
|
|
98
|
+
last.terms.concat(current.terms)
|
|
99
|
+
# contents and attached are arrays; concat is safe (no-op for empty)
|
|
100
|
+
last.contents.concat(current.contents.to_a)
|
|
101
|
+
last.attached.concat(current.attached.to_a)
|
|
102
|
+
end
|
|
103
|
+
|
|
27
104
|
def dlist_depth(delimiter)
|
|
28
105
|
delim = delimiter.to_s
|
|
29
106
|
return 1 if delim == ';;' || delim.empty?
|
|
@@ -134,38 +211,30 @@ module Coradoc
|
|
|
134
211
|
end
|
|
135
212
|
end
|
|
136
213
|
|
|
137
|
-
# Definition list item
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
t.is_a?(Hash) ? t[:text].to_s : t.to_s
|
|
146
|
-
end
|
|
147
|
-
item_id = nil
|
|
148
|
-
item_delim = '::'
|
|
149
|
-
terms.each do |t|
|
|
150
|
-
next unless t.is_a?(Hash)
|
|
151
|
-
|
|
152
|
-
item_id = t[:id].to_s if t[:id]
|
|
153
|
-
item_delim = t[:delimiter].to_s if t[:delimiter]
|
|
154
|
-
end
|
|
155
|
-
Model::List::DefinitionItem.new(terms: term_strings, contents: contents,
|
|
156
|
-
id: item_id, delimiter: item_delim)
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# Definition list item with hash terms (single term case)
|
|
214
|
+
# Definition list item. The parser's dlist_definition rule
|
|
215
|
+
# always emits :lines (one entry per source line of the dd,
|
|
216
|
+
# including the single-line case as a one-element array).
|
|
217
|
+
# Join via the shared lines_to_text_elements helper used by
|
|
218
|
+
# ulist/olist — single source of truth for line joining.
|
|
219
|
+
# `attached:` carries any `+`-continuation blocks captured by
|
|
220
|
+
# the parser; pass through to the model so downstream stages
|
|
221
|
+
# can render them as additional dd children.
|
|
160
222
|
rule(
|
|
161
223
|
definition_list_item: subtree(:item_data)
|
|
162
224
|
) do
|
|
163
|
-
data = item_data.is_a?(Hash) ? item_data : { terms: Array(item_data),
|
|
225
|
+
data = item_data.is_a?(Hash) ? item_data : { terms: Array(item_data), lines: [] }
|
|
164
226
|
|
|
165
227
|
item_id = nil
|
|
166
228
|
item_delim = '::'
|
|
167
229
|
terms_data = data[:terms]
|
|
168
|
-
|
|
230
|
+
# Split lines on hard_line_break so each source line is one
|
|
231
|
+
# entry. Without this, text_any greedy-matches across
|
|
232
|
+
# newlines via hard_line_break, joining what should be
|
|
233
|
+
# separate source lines into one entry (and swallowing the
|
|
234
|
+
# `+` line-continuation marker).
|
|
235
|
+
split_lines = Transformer.split_lines_on_hard_break(data[:lines] || [])
|
|
236
|
+
definition = Transformer.lines_to_text_elements(split_lines)
|
|
237
|
+
attached = Array(data[:attached])
|
|
169
238
|
|
|
170
239
|
terms = Array(terms_data).map do |t|
|
|
171
240
|
case t
|
|
@@ -179,7 +248,8 @@ module Coradoc
|
|
|
179
248
|
end
|
|
180
249
|
|
|
181
250
|
Model::List::DefinitionItem.new(terms: terms, contents: definition,
|
|
182
|
-
id: item_id, delimiter: item_delim
|
|
251
|
+
id: item_id, delimiter: item_delim,
|
|
252
|
+
attached: attached)
|
|
183
253
|
end
|
|
184
254
|
|
|
185
255
|
rule(definition_list: sequence(:list_items)) do
|
|
@@ -114,7 +114,9 @@ module Coradoc
|
|
|
114
114
|
|
|
115
115
|
col_count = parse_cols_attribute(attrs)
|
|
116
116
|
if col_count.nil? && rows.first.is_a?(Model::TableRow) && rows.first.columns.any?
|
|
117
|
-
col_count = rows.first.columns.sum
|
|
117
|
+
col_count = rows.first.columns.sum do |c|
|
|
118
|
+
(c.colspan || 1).to_i
|
|
119
|
+
end
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
all_cells = rows.flat_map do |r|
|