coradoc-adoc 2.0.34 → 2.0.36

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/grammar/coradoc-adoc.artifact.json +1 -0
  3. data/grammar/coradoc-adoc.parg +396 -0
  4. data/lib/coradoc/asciidoc/formatter.rb +75 -0
  5. data/lib/coradoc/asciidoc/lint/document_title.rb +26 -0
  6. data/lib/coradoc/asciidoc/lint/duplicate_ids.rb +36 -0
  7. data/lib/coradoc/asciidoc/lint/empty_section.rb +45 -0
  8. data/lib/coradoc/asciidoc/lint/heading_levels.rb +44 -0
  9. data/lib/coradoc/asciidoc/lint.rb +27 -0
  10. data/lib/coradoc/asciidoc/model/text_element.rb +18 -5
  11. data/lib/coradoc/asciidoc/parser/base.rb +28 -11
  12. data/lib/coradoc/asciidoc/parser/citation.rb +10 -0
  13. data/lib/coradoc/asciidoc/parser/document_attributes.rb +6 -1
  14. data/lib/coradoc/asciidoc/parser/inline.rb +26 -19
  15. data/lib/coradoc/asciidoc/parser/list.rb +25 -8
  16. data/lib/coradoc/asciidoc/parser/paragraph.rb +4 -2
  17. data/lib/coradoc/asciidoc/parser/positioned_string.rb +40 -0
  18. data/lib/coradoc/asciidoc/parser/rule_dispatcher.rb +3 -2
  19. data/lib/coradoc/asciidoc/parser/tree_materializer.rb +28 -0
  20. data/lib/coradoc/asciidoc/parser.rb +2 -0
  21. data/lib/coradoc/asciidoc/serializer/serializers/block/source_code.rb +3 -1
  22. data/lib/coradoc/asciidoc/serializer/serializers/list/item.rb +4 -0
  23. data/lib/coradoc/asciidoc/serializer/serializers/table.rb +8 -2
  24. data/lib/coradoc/asciidoc/serializer/serializers/text_element.rb +3 -1
  25. data/lib/coradoc/asciidoc/transform/from_core_model/list_building.rb +86 -0
  26. data/lib/coradoc/asciidoc/transform/from_core_model.rb +100 -29
  27. data/lib/coradoc/asciidoc/transform/frontmatter_attribute_map.rb +3 -2
  28. data/lib/coradoc/asciidoc/transform/text_extract_visitor.rb +5 -2
  29. data/lib/coradoc/asciidoc/transform/to_core_model.rb +12 -2
  30. data/lib/coradoc/asciidoc/transform.rb +1 -0
  31. data/lib/coradoc/asciidoc/transformer/source_line_extractor.rb +1 -1
  32. data/lib/coradoc/asciidoc/transformer/table_cell_builder.rb +18 -7
  33. data/lib/coradoc/asciidoc/version.rb +1 -1
  34. data/lib/coradoc/asciidoc.rb +12 -0
  35. data/lib/coradoc/util/asciidoc.rb +56 -0
  36. metadata +16 -5
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ # AsciiDoc lint rules (#165). Each rule checks the AsciiDoc model
6
+ # tree in a model-driven way — no text scanning. Rules self-register
7
+ # into Coradoc::Lint.registry when this file is required; adding a
8
+ # rule is adding one file under lint/ plus its require here.
9
+ #
10
+ # NOTE: a "list starts deeper than level 1" rule is structurally
11
+ # unobservable here — the parser canonicalizes depth markers
12
+ # (#285), so a lone '**' list already reads as level 1 on the
13
+ # model tree. That concern lives in the parser/serializer, not
14
+ # in a lint.
15
+ module Lint
16
+ require_relative 'lint/heading_levels'
17
+ require_relative 'lint/document_title'
18
+ require_relative 'lint/empty_section'
19
+ require_relative 'lint/duplicate_ids'
20
+
21
+ RULES = [HeadingLevels, DocumentTitle, EmptySection,
22
+ DuplicateIds].freeze
23
+
24
+ RULES.each { |rule| Coradoc::Lint.registry.register(rule) }
25
+ end
26
+ end
27
+ end
@@ -40,14 +40,27 @@ module Coradoc
40
40
  line_break.to_s.empty?
41
41
  end
42
42
 
43
+ # Escape whole runs of '*'/'_' that touch whitespace on either
44
+ # side. Run-aware two-pass scan — the lookaround + alternation
45
+ # + quantifier regex is modeled as polynomial-redos by CodeQL.
43
46
  def self.escape_keychars(string)
44
47
  subs = { '*' => '\*', '_' => '\_' }
45
- string
46
- .gsub(/((?<=\s)[*_]+)|[*_]+(?=\s)/) do |n|
47
- n.chars.map do |char|
48
- subs[char]
49
- end.join
48
+ chars = string.chars
49
+ escaped = Array.new(chars.length, false)
50
+ i = 0
51
+ while i < chars.length
52
+ unless subs.key?(chars[i])
53
+ i += 1
54
+ next
55
+ end
56
+ j = i
57
+ j += 1 while j < chars.length && subs.key?(chars[j])
58
+ touches = (i.positive? && chars[i - 1].match?(/\s/)) ||
59
+ (j < chars.length && chars[j].match?(/\s/))
60
+ (i...j).each { |k| escaped[k] = true } if touches
61
+ i = j
50
62
  end
63
+ chars.each_with_index.map { |ch, k| escaped[k] ? subs[ch] : ch }.join
51
64
  end
52
65
 
53
66
  # Get text content as a string. Handles the polymorphic shape of
@@ -85,17 +85,16 @@ module Coradoc
85
85
  warn e.parse_failure_cause.ascii_tree
86
86
  end
87
87
 
88
- # The Ruby engine is the required surface for this grammar.
89
- # parsanol 1.3.44 fixes capture WRITES across the bridge
90
- # (parsanol-ruby#80 — verified with a continuation-style repro),
91
- # but the native tree still diverges from the Ruby-engine tree
92
- # on attribute-list constructs: 69 suite failures natively from
93
- # transform rules receiving raw Hashes where models are
94
- # expected (e.g. Image::AttributeExtractor.promote_positional).
95
- # Drop the override when native/Ruby tree parity holds for this
96
- # grammar.
97
- def parse(string, **options)
98
- super(string, **options, mode: options.fetch(:mode, :ruby))
88
+ # The compiled PARG artifact (grammar/coradoc-adoc.parg, the
89
+ # single source of truth) is the parse backend; the Ruby-DSL
90
+ # grammar defined by this class remains the parity reference
91
+ # (parg_parity_spec + engine_differential_spec assert the two
92
+ # produce identical trees). The artifact routes through
93
+ # parsanol's compiled-program fast lane (parsanol-ruby#162,
94
+ # 1.3.81) and parses native by default; the historical
95
+ # ruby-mode artifact divergence is fixed (#185, 1.3.82).
96
+ def parse(string, **)
97
+ GrammarBackend.parse(string, **)
99
98
  end
100
99
 
101
100
  def rule_dispatch(rule_name, *, **)
@@ -111,6 +110,24 @@ module Coradoc
111
110
  end
112
111
  end
113
112
 
113
+ # Lazily-loaded compiled-grammar backend (the shipped artifact).
114
+ module GrammarBackend
115
+ class << self
116
+ def parse(string, **)
117
+ TreeMaterializer.convert(artifact.parse('document', string, **))
118
+ end
119
+
120
+ private
121
+
122
+ def artifact
123
+ require 'parsanol/parg'
124
+ @artifact ||= Parsanol::PARG::Artifact.load(
125
+ File.expand_path('../../../../grammar/coradoc-adoc.artifact.json', __dir__)
126
+ )
127
+ end
128
+ end
129
+ end
130
+
114
131
  # Wrap every parser rule for Parsanol memoization. Must run after all
115
132
  # parser modules are included in Base so that instance_method(rule_name)
116
133
  # finds the methods defined by every module.
@@ -22,6 +22,16 @@ module Coradoc
22
22
  def escaped_xref
23
23
  str('\\') >> str('<<').as(:text)
24
24
  end
25
+
26
+ # General AsciiDoc escape: \X (X punctuation, minus < for
27
+ # escaped_xref and \ itself) yields the literal character X
28
+ # without firing any inline rule (#281).
29
+ def escape_char
30
+ # Explicit class (all ASCII punctuation except < for
31
+ # escaped_xref and \ itself): the native engine does not
32
+ # compile && class intersections.
33
+ str('\\') >> match("[!\"#$%&'()*+,\\-./:;=?@\\[\\]^_`{|}~]").as(:text)
34
+ end
25
35
  end
26
36
  end
27
37
  end
@@ -19,7 +19,12 @@ module Coradoc
19
19
 
20
20
  def document_attribute
21
21
  str(':') >> attribute_name.as(:key) >> str(':') >>
22
- space? >> (attribute_value | str('')).as(:value) >> (line_ending | eof?)
22
+ # attribute_value itself already matches empty (its
23
+ # zero-width branch), so the former `| str('')` fallback was
24
+ # dead — ordered choice commits to attribute_value's empty
25
+ # match first — and the VM's shadowed-alternative lint
26
+ # (parsanol 1.3.55+) correctly rejects it.
27
+ space? >> attribute_value.as(:value) >> (line_ending | eof?)
23
28
  end
24
29
  end
25
30
  end
@@ -46,10 +46,14 @@ module Coradoc
46
46
  # highlight_unconstrained was previously inconsistent (excluded
47
47
  # newlines). All four are now uniform.
48
48
 
49
+ # Constrained markers delimit at boundaries only (#281): the
50
+ # opening marker must not follow a word char, the closing one
51
+ # must not precede one — `foo_bar_baz` stays plain text.
49
52
  def constrained_mark(marker, reject_paragraph_break: false, content: nil)
50
- open_guard = str(marker) >> str(marker).absent?
53
+ open_guard = Parsanol::Atoms::Lookbehind.regex('[[:word:]]', positive: false) >>
54
+ str(marker) >> str(marker).absent?
51
55
  content_rule = content || default_constrained_content(marker)
52
- close_guard = str(marker) >> str(marker).absent?
56
+ close_guard = str(marker) >> str(marker).absent? >> match('[[:word:]]').absent?
53
57
  sequence = open_guard >> content_rule >> close_guard
54
58
  sequence >>= str("\n\n").absent? if reject_paragraph_break
55
59
  sequence
@@ -138,7 +142,7 @@ module Coradoc
138
142
  end
139
143
 
140
144
  def link
141
- ((str('http').present? | str('https').present? | str('ftp').present?) >>
145
+ ((str('http') | str('https') | str('ftp')).present? >>
142
146
  match('[A-Za-z0-9_.\\-:/&?=+,%#~;]+').repeat(1).as(:path) >>
143
147
  (str('[') >> match('[^\\]]').repeat(1).as(:text) >> str(']')).maybe
144
148
  ).as(:link) |
@@ -201,19 +205,22 @@ module Coradoc
201
205
  end
202
206
 
203
207
  def inline_chars?
204
- match('[\[*#_{<^~`]').present? |
205
- typographic_quote.present? |
206
- str('http').present? |
207
- str('https').present? |
208
- str('link:').present? |
209
- str('image:').present? |
210
- str('+++').present? |
211
- str('pass:').present? |
212
- term_type.present? |
213
- str('footnote').present? |
214
- stem_type.present? |
215
- str('\\<<').present? |
216
- hard_line_break_marker?
208
+ # Single lookahead over the whole dispatch alternation; the former
209
+ # per-branch .present? chain put zero-width branches in every
210
+ # non-last slot (shadowed-alternative lint, parsanol 1.3.55+).
211
+ (match('[\\\\\[*#_{<^~`]') |
212
+ typographic_quote |
213
+ str('http') |
214
+ str('https') |
215
+ str('link:') |
216
+ str('image:') |
217
+ str('+++') |
218
+ str('pass:') |
219
+ term_type |
220
+ str('footnote') |
221
+ stem_type |
222
+ str('\\<<') |
223
+ hard_line_break_marker?).present?
217
224
  end
218
225
 
219
226
  # AsciiDoc hard line break: a space followed by `+` at end of line,
@@ -221,8 +228,8 @@ module Coradoc
221
228
  # the enclosing paragraph/verse. Recognised ahead of `text_unformatted`
222
229
  # so the marker isn't swallowed as plain text.
223
230
  def hard_line_break_marker?
224
- (str(' +') >> str("\n")).present? |
225
- (str('\\') >> str("\n")).present?
231
+ ((str(' +') >> str("\n")) |
232
+ (str('\\') >> str("\n"))).present?
226
233
  end
227
234
 
228
235
  def hard_line_break
@@ -244,7 +251,7 @@ module Coradoc
244
251
  monospace_unconstrained monospace_constrained
245
252
  superscript subscript
246
253
  attribute_reference
247
- escaped_xref cross_reference
254
+ escaped_xref escape_char cross_reference
248
255
  term_inline term_inline2
249
256
  footnote stem
250
257
  link inline_image
@@ -7,10 +7,20 @@ module Coradoc
7
7
  module List
8
8
  def list(nesting_level = 1)
9
9
  (
10
- unordered_list(nesting_level) |
11
- ordered_list(nesting_level) |
12
- definition_list
13
- ).as(:list)
10
+ unordered_list(nesting_level) |
11
+ ordered_list(nesting_level) |
12
+ definition_list
13
+ ).as(:list)
14
+ end
15
+
16
+ # A list item at ANY depth starts a list construct: a nested
17
+ # list (`* a` followed by `** b`) must stop the parent item's
18
+ # continuation text and attach as `nested` instead. Full items
19
+ # (not bare markers) so `**un**constrained` — no space after
20
+ # the marker — stays ordinary text.
21
+ def list_start
22
+ (1..5).map { |level| unordered_list(level) | ordered_list(level) }
23
+ .reduce(:|)
14
24
  end
15
25
 
16
26
  def list_continuation
@@ -133,6 +143,13 @@ module Coradoc
133
143
  end
134
144
 
135
145
  def ulist_item(nesting_level = 1)
146
+ # A bare marker line (`*` then newline) is an empty list
147
+ # item — without this arm it fell through to a continuation
148
+ # text line and corrupted the preceding item (#139).
149
+ empty_item = ulist_marker(nesting_level).as(:marker) >>
150
+ str(' [[[').absent? >>
151
+ line_ending.as(:lines)
152
+
136
153
  item = ulist_marker(nesting_level).as(:marker) >>
137
154
  str(' [[[').absent? >>
138
155
  match("\n").absent? >> space >>
@@ -145,7 +162,7 @@ module Coradoc
145
162
  item >>= (list_marker(nesting_level + 1).present? >>
146
163
  list(nesting_level + 1)).repeat(0).as(:nested)
147
164
  end
148
- ulist_marker(nesting_level).present? >> item.as(:list_item)
165
+ (empty_item | item).as(:list_item)
149
166
  end
150
167
 
151
168
  # Continuation lines of a list item's first paragraph. AsciiDoc
@@ -242,7 +259,7 @@ module Coradoc
242
259
  monospace_unconstrained monospace_constrained
243
260
  superscript subscript
244
261
  attribute_reference
245
- escaped_xref cross_reference
262
+ escaped_xref escape_char cross_reference
246
263
  term_inline term_inline2
247
264
  footnote stem
248
265
  link inline_image
@@ -259,8 +276,8 @@ module Coradoc
259
276
  end
260
277
 
261
278
  def list_body_hard_break_marker?
262
- (str(' +') >> str("\n")).present? |
263
- (str('\\') >> str("\n")).present?
279
+ ((str(' +') >> str("\n")) |
280
+ (str('\\') >> str("\n"))).present?
264
281
  end
265
282
 
266
283
  # Recognizes line endings for a list body source line, including
@@ -9,7 +9,7 @@ module Coradoc
9
9
  (attribute_list >> newline).absent? >>
10
10
  block_delimiter.absent? >>
11
11
  (str('|===') >> newline).absent? >>
12
- list.absent? >>
12
+ list_start.absent? >>
13
13
  list_prefix.absent? >>
14
14
  list_continuation.absent? >>
15
15
  element_id.absent? >>
@@ -49,7 +49,9 @@ module Coradoc
49
49
  (paragraph_text_line(true).repeat(1, 1) >>
50
50
  (newline.repeat(1).as(:line_break) | eof?)).repeat(0, 1))
51
51
  ).as(:lines) >>
52
- (newline.repeat(0) | eof?)
52
+ # the former "| eof?" was dead: newline.repeat(0) already
53
+ # matches empty (shadowed-alternative lint, parsanol 1.3.55+)
54
+ newline.repeat(0)
53
55
  ).as(:paragraph)
54
56
  end
55
57
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parsanol'
4
+
5
+ module Coradoc
6
+ module AsciiDoc
7
+ module Parser
8
+ # An immutable String carrying its byte offset and source input.
9
+ #
10
+ # The native engine hands pooled, mutable Parsanol::Slice objects
11
+ # back in the parse tree; storing those in document models is
12
+ # unsafe (the pool resets them) and they fail serializer type
13
+ # checks. TreeMaterializer converts every Slice into a
14
+ # PositionedString at the GrammarBackend boundary, so models only
15
+ # ever see Strings — while SourceLineExtractor can still recover
16
+ # positions (line_and_column delegates the computation to a
17
+ # throwaway Slice).
18
+ class PositionedString < String
19
+ attr_reader :byte_position, :source_input
20
+
21
+ def initialize(content, byte_position: 0, source_input: nil)
22
+ super(content)
23
+ @byte_position = byte_position
24
+ @source_input = source_input
25
+ end
26
+
27
+ def self.from_slice(slice)
28
+ new(slice.to_s, byte_position: slice.offset, source_input: slice.input)
29
+ end
30
+
31
+ def line_and_column
32
+ raise ArgumentError, 'Line/column requires input' unless @source_input
33
+
34
+ @line_and_column ||= Parsanol::Slice.new(@byte_position, to_s,
35
+ @source_input).line_and_column
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -47,7 +47,7 @@ module Coradoc
47
47
  end
48
48
  cache[key] = rule_name
49
49
  end
50
- parser_instance.public_send(cache[key])
50
+ TreeMaterializer.convert(parser_instance.public_send(cache[key]))
51
51
  end
52
52
 
53
53
  private
@@ -59,7 +59,8 @@ module Coradoc
59
59
  # Wrapping `__send__` in particular causes infinite recursion.
60
60
  # @return [Hash{Symbol => Array<Module>}]
61
61
  def collect_rule_names
62
- parser_constants = Parser.constants - %i[Base Cache FixFiles RuleDispatcher]
62
+ parser_constants = Parser.constants - %i[Base Cache FixFiles RuleDispatcher
63
+ PositionedString TreeMaterializer]
63
64
  parser_constants.each_with_object({}) do |const, acc|
64
65
  Parser.const_get(const).instance_methods(false).each do |name|
65
66
  acc[name] ||= []
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parsanol'
4
+
5
+ module Coradoc
6
+ module AsciiDoc
7
+ module Parser
8
+ # Converts a native-engine parse tree: every
9
+ # Parsanol::Slice becomes an immutable PositionedString. Runs at
10
+ # the GrammarBackend boundary so the transformer and document
11
+ # models never see pooled slices, without losing source
12
+ # positions.
13
+ module TreeMaterializer
14
+ module_function
15
+
16
+ def convert(node)
17
+ case node
18
+ when Parsanol::Slice then PositionedString.from_slice(node)
19
+ when Hash then node.transform_values { |value| convert(value) }
20
+ when Array then node.map { |value| convert(value) }
21
+ else
22
+ node
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -7,6 +7,8 @@ module Coradoc
7
7
  autoload :Cache, "#{__dir__}/parser/cache"
8
8
  autoload :FrontmatterParser, "#{__dir__}/parser/frontmatter_parser"
9
9
  autoload :Inline, "#{__dir__}/parser/inline"
10
+ autoload :PositionedString, "#{__dir__}/parser/positioned_string"
11
+ autoload :TreeMaterializer, "#{__dir__}/parser/tree_materializer"
10
12
  end
11
13
  end
12
14
  end
@@ -8,7 +8,9 @@ module Coradoc
8
8
  class SourceCode < Core
9
9
  def to_adoc(model, _options = {})
10
10
  @model = model
11
- "\n\n#{gen_anchor}[source,#{model.lang}]\n#{gen_delimiter}\n" <<
11
+ lang = model.lang.to_s.strip
12
+ header = lang.empty? ? '[source]' : "[source,#{lang}]"
13
+ "\n\n#{gen_anchor}#{header}\n#{gen_delimiter}\n" <<
12
14
  gen_lines << "\n#{gen_delimiter}\n\n"
13
15
  end
14
16
  end
@@ -87,6 +87,10 @@ module Coradoc
87
87
  "+\n#{elem.to_adoc}"
88
88
  end.join
89
89
  nest = model.nested.nil? || (model.nested.is_a?(Array) && model.nested.empty?) ? '' : model.nested.to_adoc
90
+ # The nested list serializer opens with a block separator;
91
+ # adjacent to the item line it would read as a detached
92
+ # sibling list on re-parse.
93
+ nest = nest.sub(/\A\n+/, '')
90
94
  out = " #{_anchor}#{out}#{model.line_break}"
91
95
  out + attach + nest
92
96
  end
@@ -5,14 +5,20 @@ module Coradoc
5
5
  module Serializer
6
6
  module Serializers
7
7
  class Table < Base
8
- def to_adoc(model, _options = {})
8
+ def to_adoc(model, options_or_context = {})
9
9
  @model = model
10
+ context = normalize_context(options_or_context)
10
11
  _anchor = @model.anchor.nil? ? '' : "#{serialize_child(@model.anchor)}\n"
11
12
  _attrs = @model.attrs.to_s.empty? ? '' : "#{serialize_child(@model.attrs)}\n"
12
13
  _title = @model.title.nil? || @model.title.empty? ? '' : ".#{serialize_child(@model.title)}\n"
13
14
  _content = @model.rows.map { |row| serialize_child(row) }.join
14
15
 
15
- "\n\n#{_anchor}#{_attrs}#{_title}|===\n#{_content}|===\n"
16
+ # Blank-line separation after the table so a following
17
+ # block never glues onto the closer (#151) — same
18
+ # convention as the paragraph serializer, suppressed for
19
+ # the document's last element.
20
+ separator = context.last_element ? '' : "\n"
21
+ "\n\n#{_anchor}#{_attrs}#{_title}|===\n#{_content}|===\n#{separator}"
16
22
  end
17
23
  end
18
24
  end
@@ -9,7 +9,9 @@ module Coradoc
9
9
  def to_adoc(model, _options = {})
10
10
  result = ''
11
11
  result += "[[#{model.id}]] " if model.id
12
- result += serialize_content(model.content)
12
+ content = serialize_content(model.content)
13
+ content = Coradoc::Util::AsciiDoc.escape_text_delimiters(content) if content.is_a?(String)
14
+ result += content
13
15
  result += model.line_break.to_s
14
16
  result
15
17
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module AsciiDoc
5
+ module Transform
6
+ # List construction for the AsciiDoc view: nested lists flatten
7
+ # into the item stream with depth markers so AsciiDoc reads them
8
+ # as nested rather than sibling lists (#68) — a marker-type
9
+ # change alone signals nesting; same-type nesting goes one level
10
+ # deeper.
11
+ module ListBuilding
12
+ def transform_list(list, depth = 1)
13
+ items = []
14
+ Array(list.items).each do |item|
15
+ nested_blocks = [item.nested_list,
16
+ *Array(item.children).grep(CoreModel::ListBlock)].compact
17
+ text = inline_text(item)
18
+ attached = attached_blocks(item)
19
+ if text.empty? && attached.empty? && nested_blocks.any?
20
+ # A wrapper item whose only content is its nested list
21
+ # cannot render as a deeper list in AsciiDoc (lists start
22
+ # at level 1, #68), so splice the nested list at this depth.
23
+ nested_blocks.each do |child|
24
+ child_depth = child.marker_type == list.marker_type ? depth : 1
25
+ items.concat(transform_list(child, child_depth).items)
26
+ end
27
+ next
28
+ end
29
+ items << Coradoc::AsciiDoc::Model::List::Item.new(
30
+ content: text,
31
+ marker: item.marker || (default_marker(list.marker_type) * depth),
32
+ attached: attached
33
+ )
34
+ nested_blocks.each do |child|
35
+ child_depth = child.marker_type == list.marker_type ? depth + 1 : 1
36
+ items.concat(transform_list(child, child_depth).items)
37
+ end
38
+ end
39
+
40
+ case list.marker_type
41
+ when 'ordered'
42
+ Coradoc::AsciiDoc::Model::List::Ordered.new(items: items)
43
+ when 'definition'
44
+ Coradoc::AsciiDoc::Model::List::Definition.new(items: items)
45
+ else
46
+ Coradoc::AsciiDoc::Model::List::Unordered.new(items: items)
47
+ end
48
+ end
49
+
50
+ # Inline children render on the item line; CoreModel::Block
51
+ # children become attached blocks (`+` continuations) instead of
52
+ # being glued into the item text.
53
+ def inline_text(item)
54
+ case (rc = item.renderable_content)
55
+ when String then rc
56
+ when Array
57
+ rc.each_with_object(+'') do |child, out|
58
+ next if child.is_a?(CoreModel::Block) || child.is_a?(CoreModel::ListBlock)
59
+
60
+ out << case child
61
+ when CoreModel::TextContent then child.text.to_s
62
+ when String then child
63
+ when CoreModel::Base then child.flat_text.to_s
64
+ end
65
+ end
66
+ else
67
+ ''
68
+ end
69
+ end
70
+
71
+ def attached_blocks(item)
72
+ Array(item.children)
73
+ .grep(CoreModel::Block)
74
+ .flat_map { |block| FromCoreModel.flatten_children([block]) }
75
+ end
76
+
77
+ def default_marker(marker_type)
78
+ case marker_type
79
+ when 'ordered' then '.'
80
+ else '*'
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end