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.



84
85
86
87
88
# File 'lib/coradoc/asciidoc/parser/list.rb', line 84

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.



96
97
98
99
100
101
102
# File 'lib/coradoc/asciidoc/parser/list.rb', line 96

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



32
33
34
35
36
# File 'lib/coradoc/asciidoc/parser/list.rb', line 32

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

#dlist_definitionObject



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/coradoc/asciidoc/parser/list.rb', line 155

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



138
139
140
141
142
143
144
145
146
# File 'lib/coradoc/asciidoc/parser/list.rb', line 138

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

#dlist_item(_delimiter = nil) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/coradoc/asciidoc/parser/list.rb', line 249

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

  attached = (list_continuation.present? >>
               list_continuation >>
               (admonition_line | paragraph | block)
             ).repeat(0).as(:attached)
  item >>= attached.maybe

  item.as(:definition_list_item)
end

#dlist_term(_delimiter = nil) ⇒ Object



148
149
150
151
152
153
# File 'lib/coradoc/asciidoc/parser/list.rb', line 148

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_body_hard_break_marker?Boolean

Returns:

  • (Boolean)


234
235
236
237
# File 'lib/coradoc/asciidoc/parser/list.rb', line 234

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

#list_body_inline_except_hard_breakObject



201
202
203
# File 'lib/coradoc/asciidoc/parser/list.rb', line 201

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>.



243
244
245
246
247
# File 'lib/coradoc/asciidoc/parser/list.rb', line 243

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



196
197
198
199
# File 'lib/coradoc/asciidoc/parser/list.rb', line 196

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.



190
191
192
193
194
# File 'lib/coradoc/asciidoc/parser/list.rb', line 190

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

#list_body_text_unformattedObject



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

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_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.



134
135
136
# File 'lib/coradoc/asciidoc/parser/list.rb', line 134

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

#list_marker(nesting_level = 1) ⇒ Object



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

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

#olist_item(nesting_level = 1) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/coradoc/asciidoc/parser/list.rb', line 53

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)

  att = (list_continuation.present? >>
          list_continuation >>
          (admonition_line | paragraph | block)
        ).repeat(0).as(:attached)
  item >>= att.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



42
43
44
45
46
47
48
49
50
51
# File 'lib/coradoc/asciidoc/parser/list.rb', line 42

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



20
21
22
23
24
# File 'lib/coradoc/asciidoc/parser/list.rb', line 20

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

#ulist_item(nesting_level = 1) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/coradoc/asciidoc/parser/list.rb', line 104

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)

  att = (list_continuation.present? >>
          list_continuation >>
          (admonition_line | paragraph | block)
        ).repeat(0).as(:attached)
  item >>= att.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



72
73
74
75
76
77
78
79
# File 'lib/coradoc/asciidoc/parser/list.rb', line 72

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



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

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