Module: Coradoc::AsciiDoc::Parser::List

Defined in:
lib/coradoc/asciidoc/parser/list.rb

Constant Summary collapse

LIST_BODY_INLINE_RULE_NAMES =

Single source of truth for "inline rules usable inside a list item body line". Mirrors INLINE_RULE_ORDER minus hard_line_break. If INLINE_RULE_ORDER changes, update this list (the list_body_inline_rule_names spec catches drift).

%i[
  typographic_quote
  bold_unconstrained bold_constrained
  span_unconstrained span_constrained
  italic_unconstrained italic_constrained
  highlight_unconstrained highlight_constrained
  monospace_unconstrained monospace_constrained
  superscript subscript
  attribute_reference
  escaped_xref cross_reference
  term_inline term_inline2
  footnote stem
  link inline_image
  inline_passthrough
  underline small
].freeze

Instance Method Summary collapse

Instance Method Details

#asterisk_marker(nesting_level) ⇒ Object

AsciiDoc standard bullet: *, **, ***, ... matching the nesting level. Excludes table delimiters (|===) and deeper asterisk runs that belong to a sibling level.



115
116
117
118
119
# File 'lib/coradoc/asciidoc/parser/list.rb', line 115

def asterisk_marker(nesting_level)
  str('*' * nesting_level) >>
    str('*').absent? >>
    str('===').absent?
end

#dash_marker(nesting_level) ⇒ Object

Markdown-style dash bullet: -. Accepted only at the top level because Markdown nests via indentation rather than multi-char markers — deeper levels stay on the AsciiDoc * form. Guards exclude em-dashes (--), delimited-block fences (----), and negative-number runs (-1, -42) which are not list markers in any common dialect.



127
128
129
130
131
132
133
# File 'lib/coradoc/asciidoc/parser/list.rb', line 127

def dash_marker(nesting_level)
  return match('').absent? unless nesting_level == 1

  str('-') >>
    str('-').absent? >>
    match('[0-9]').absent?
end

#definition_list(_delimiter = nil) ⇒ Object



67
68
69
70
71
# File 'lib/coradoc/asciidoc/parser/list.rb', line 67

def definition_list(_delimiter = nil)
  (attribute_list >> newline).maybe >>
    (list_item_separator >> dlist_item).repeat(1).as(:definition_list) >>
    dlist_item.absent?
end

#dlist_definitionObject



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/coradoc/asciidoc/parser/list.rb', line 182

def dlist_definition
  # AsciiDoc convention: the definition body is indented relative
  # to the term. That leading whitespace is structural (marks
  # the line as a continuation of the dlist item), not content.
  # Consume it without capturing so downstream CoreModel text
  # doesn't carry the source indentation into HTML/Markdown.
  #
  # Multi-line definitions: AsciiDoc joins consecutive non-blank
  # lines into a single paragraph within the dd. We capture each
  # source line so the transformer can join them.
  #
  # Two load-bearing guards:
  #   1. `line_not_text?` — prevents matching a `+` (list
  #      continuation marker) or `[example]` (block attribute)
  #      as the dd's text content.
  #   2. `dlist_definition_line` (custom text_line variant) —
  #      text_line's text_any is greedy and matches across
  #      newlines via hard_line_break, swallowing the next
  #      source line's content (e.g. the `+` marker on the
  #      line after a hard break). The custom matcher excludes
  #      hard_line_break from the inline set so each `:lines`
  #      entry corresponds to exactly one source line.
  (line_not_text? >> match('[ \t]').repeat(0) >>
    (list_body_text_line >> list_item_continuation_lines).as(:lines)
  ) >> empty_line.repeat(0)
end

#dlist_delimiterObject



165
166
167
168
169
170
171
172
173
# File 'lib/coradoc/asciidoc/parser/list.rb', line 165

def dlist_delimiter
  (
    (str(':::::') >> match(':').absent?) |
    (str('::::') >> match(':').absent?) |
    (str(':::') >> match(':').absent?) |
    (str('::') >> match(':').absent?) |
    str(';;')
  ).as(:delimiter)
end

#dlist_item(_delimiter = nil) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/coradoc/asciidoc/parser/list.rb', line 276

def dlist_item(_delimiter = nil)
  # AST shape: { terms: [single dlist_term], delimiter: <delim>,
  #              lines: <def or nil>, attached: [...] }
  #
  # ONE term per dlist_item. Multi-term `<dt>`'s and multi-level
  # nesting are both handled by the transformer's `build_dlist_tree`,
  # which inspects each item's `delimiter` to infer depth and
  # merges consecutive same-depth term-only items into multi-term
  # items. Keeping the parser's term-emission at one-per-item lets
  # `delimiter` carry the nesting signal unambiguously — a parser
  # that greedily groups `parent::` and `child:::` into one item
  # loses the depth change between them.
  #
  # `repeat(1, 1).as(:terms)` ensures `:terms` is captured as a
  # single-element Array (matching the shape transformer's `Array()`
  # wrap expects) rather than a bare Hash — bare Hashes confuse
  # `Array(hash)` into nested-pair conversion.
  #
  # Two forms:
  #   1. multi-line: term on its own line, optional def on next line(s)
  #   2. inline:     term + def on same line
  multi_line = dlist_term.repeat(1, 1).as(:terms) >> line_ending >>
               empty_line.repeat(0) >> dlist_definition.maybe
  inline = dlist_term.repeat(1, 1).as(:terms) >> space >> dlist_definition

  item = multi_line | inline

  item >>= list_item_attached.maybe

  # A `+` line directly before a continuing dlist run is a
  # list-continuation marker (asciidoctor semantics): consume it
  # so the following items stay in the current list and nest by
  # delimiter depth. Without this, the `+` dangles and the run
  # splits off as a detached sibling list.
  item >>= (list_continuation >> dlist_item.present?).repeat(0)

  item.as(:definition_list_item)
end

#dlist_term(_delimiter = nil) ⇒ Object



175
176
177
178
179
180
# File 'lib/coradoc/asciidoc/parser/list.rb', line 175

def dlist_term(_delimiter = nil)
  term_chars =
    (dlist_delimiter.absent? >> match("[^\n]")).repeat(1)
                                               .as(:text)
  (element_id_inline.maybe >> term_chars).as(:dlist_term) >> dlist_delimiter
end

#list(nesting_level = 1) ⇒ Object



8
9
10
11
12
13
14
# File 'lib/coradoc/asciidoc/parser/list.rb', line 8

def list(nesting_level = 1)
  (
  unordered_list(nesting_level) |
     ordered_list(nesting_level) |
     definition_list
).as(:list)
end

#list_attached_blockObject

Single source of truth for "what block can attach to a list item via a + continuation marker". The + line is consumed by #list_item_attached; this rule expresses only the block that follows. Definition lists are intentionally excluded — + before a continuing dlist run is handled inside dlist_item as a continuation marker so the run nests by delimiter depth (see there).



38
39
40
# File 'lib/coradoc/asciidoc/parser/list.rb', line 38

def list_attached_block
  admonition_line | unordered_list(1) | ordered_list(1) | paragraph | block
end

#list_body_hard_break_marker?Boolean

Returns:

  • (Boolean)


261
262
263
264
# File 'lib/coradoc/asciidoc/parser/list.rb', line 261

def list_body_hard_break_marker?
  (str(' +') >> str("\n")).present? |
    (str('\\') >> str("\n")).present?
end

#list_body_inline_except_hard_breakObject



228
229
230
# File 'lib/coradoc/asciidoc/parser/list.rb', line 228

def list_body_inline_except_hard_break
  LIST_BODY_INLINE_RULE_NAMES.map { |name| public_send(name) }.reduce(:|)
end

#list_body_line_endingObject

Recognizes line endings for a list body source line, including the +\n hard-break form. The hard_break marker is preserved as a :hard_line_break capture so downstream can render it as <br>.



270
271
272
273
274
# File 'lib/coradoc/asciidoc/parser/list.rb', line 270

def list_body_line_ending
  (str(' +').as(:hard_line_break) >> line_ending.as(:line_break)) |
    line_ending.as(:line_break) |
    eof?
end

#list_body_text_anyObject



223
224
225
226
# File 'lib/coradoc/asciidoc/parser/list.rb', line 223

def list_body_text_any
  (list_body_inline_except_hard_break |
    list_body_text_unformatted.as(:text)).repeat(1)
end

#list_body_text_lineObject

Single source line of text for any list item body (dlist dd, ulist item, olist item). Like text_line(false, unguarded: true) but uses LIST_BODY_INLINE_RULE_NAMES (excludes hard_line_break) so hard_break's +\n consumption doesn't greedy-pull the next source line's content (e.g. the + line-continuation marker).

Shared across list types — single source of truth for the "text inside a list item" grammar.



217
218
219
220
221
# File 'lib/coradoc/asciidoc/parser/list.rb', line 217

def list_body_text_line
  literal_space? >>
    list_body_text_any.as(:text) >>
    list_body_line_ending
end

#list_body_text_unformattedObject



253
254
255
256
257
258
259
# File 'lib/coradoc/asciidoc/parser/list.rb', line 253

def list_body_text_unformatted
  (str('\\<<').absent? >>
    list_body_inline_except_hard_break.absent? >>
    list_body_hard_break_marker?.absent? >>
    match("[^\n]")
  ).repeat(1)
end

#list_continuationObject



16
17
18
# File 'lib/coradoc/asciidoc/parser/list.rb', line 16

def list_continuation
  line_start? >> str("+\n")
end

#list_item_attachedObject

Shared +-continuation attachment for every list item type (ulist, olist, dlist). A + line directly followed by an attached block becomes a child of the current item. The list_continuation.present? lookahead prevents greedy over-match when the + actually belongs to surrounding context.



48
49
50
51
52
53
# File 'lib/coradoc/asciidoc/parser/list.rb', line 48

def list_item_attached
  (list_continuation.present? >>
     list_continuation >>
     list_attached_block
  ).repeat(0).as(:attached)
end

#list_item_continuation_linesObject

Continuation lines of a list item's first paragraph. AsciiDoc joins consecutive non-blank lines into one paragraph within the item, but the lines must not start a sibling construct (another list marker, block delimiter, attribute list, section, element id, table boundary, list continuation, or list prefix). line_not_text? (from Paragraph) is that exact lookahead.

Uses list_body_text_line (not raw text_line) so a hard break at end of one source line doesn't greedy-pull the next line's content via text_any — same fix as dlist.



161
162
163
# File 'lib/coradoc/asciidoc/parser/list.rb', line 161

def list_item_continuation_lines
  (line_not_text? >> list_body_text_line).repeat(0)
end

#list_item_separatorObject

Single source of truth for "what can appear between consecutive list items without ending the list". Asciidoctor treats blank lines and // comment lines as non-terminating inside every list type. Comments are consumed without capture — they are structurally invisible inside a list (no CommentLine node is emitted). Tag directives (// tag::) keep their separate handling via comment_line_content's tag.absent? guard.



27
28
29
# File 'lib/coradoc/asciidoc/parser/list.rb', line 27

def list_item_separator
  (comment_line_content | empty_line).repeat(0)
end

#list_marker(nesting_level = 1) ⇒ Object



73
74
75
# File 'lib/coradoc/asciidoc/parser/list.rb', line 73

def list_marker(nesting_level = 1)
  olist_marker(nesting_level) | ulist_marker(nesting_level)
end

#olist_item(nesting_level = 1) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/coradoc/asciidoc/parser/list.rb', line 88

def olist_item(nesting_level = 1)
  item = olist_marker(nesting_level).as(:marker) >>
         match("\n").absent? >> space >>
         (list_body_text_line >>
          list_item_continuation_lines).as(:lines)

  item >>= list_item_attached.maybe

  if nesting_level <= 4
    item >>= (list_marker(nesting_level + 1).present? >>
           list(nesting_level + 1)).repeat(0).as(:nested)
  end
  olist_marker(nesting_level).present? >> item.as(:list_item)
end

#olist_marker(nesting_level = 1) ⇒ Object



77
78
79
80
81
82
83
84
85
86
# File 'lib/coradoc/asciidoc/parser/list.rb', line 77

def olist_marker(nesting_level = 1)
  # Don't match table cell format specs like ".2+^.^|"
  line_start? >>
    (nesting_level > 1 ? literal_space.maybe : str('')) >>
    str('.' * nesting_level) >>
    str('.').absent? >>
    (
      (match['0-9.<>^'] | str('+')).repeat(0, 3) >> str('|')
    ).absent?
end

#ordered_list(nesting_level = 1) ⇒ Object



55
56
57
58
59
# File 'lib/coradoc/asciidoc/parser/list.rb', line 55

def ordered_list(nesting_level = 1)
  attrs = (attribute_list >> newline).maybe
  r = olist_item(nesting_level)
  attrs >> (list_item_separator >> r).repeat(1).as(:ordered)
end

#ulist_item(nesting_level = 1) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/coradoc/asciidoc/parser/list.rb', line 135

def ulist_item(nesting_level = 1)
  item = ulist_marker(nesting_level).as(:marker) >>
         str(' [[[').absent? >>
         match("\n").absent? >> space >>
         (list_body_text_line >>
          list_item_continuation_lines).as(:lines)

  item >>= list_item_attached.maybe

  if nesting_level <= 4
    item >>= (list_marker(nesting_level + 1).present? >>
           list(nesting_level + 1)).repeat(0).as(:nested)
  end
  ulist_marker(nesting_level).present? >> item.as(:list_item)
end

#ulist_marker(nesting_level = 1) ⇒ Object



103
104
105
106
107
108
109
110
# File 'lib/coradoc/asciidoc/parser/list.rb', line 103

def ulist_marker(nesting_level = 1)
  line_start? >>
    (nesting_level > 1 ? literal_space.maybe : str('')) >>
    (
      asterisk_marker(nesting_level) |
      dash_marker(nesting_level)
    )
end

#unordered_list(nesting_level = 1) ⇒ Object



61
62
63
64
65
# File 'lib/coradoc/asciidoc/parser/list.rb', line 61

def unordered_list(nesting_level = 1)
  attrs = (attribute_list >> newline).maybe
  r = ulist_item(nesting_level)
  attrs >> (list_item_separator >> r).repeat(1).as(:unordered)
end