Class: RedQuilt::List::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/red_quilt/list.rb

Overview

Cached collaborator for BlockParser. A single instance is created in BlockParser#initialize and reused for every list (including nested ones) — the per-call state lives in method locals so reentrant ‘#parse` calls are safe.

Instance Method Summary collapse

Constructor Details

#initialize(block_parser) ⇒ Parser

Returns a new instance of Parser.



142
143
144
145
# File 'lib/red_quilt/list.rb', line 142

def initialize(block_parser)
  @block_parser = block_parser
  @arena = block_parser.arena
end

Instance Method Details

#parse(parent_id, lines, index) ⇒ Object



147
148
149
150
151
152
153
154
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
181
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
208
209
210
211
212
213
214
# File 'lib/red_quilt/list.rb', line 147

def parse(parent_id, lines, index)
  first_match = List.match(lines[index].content)
  list_id = @arena.add_node(NodeType::LIST,
                            source_start: lines[index].start_byte,
                            source_len: 0,
                            int1: first_match[:ordered] ? 1 : 0,
                            int2: first_match[:start_number],
                            int3: 1,
                            str1: first_match[:marker])
  @arena.append_child(parent_id, list_id)
  start_byte = lines[index].start_byte
  end_byte = lines[index].end_byte
  loose = false

  while index < lines.length
    # Thematic break beats list-item continuation per CommonMark:
    # a line like `* * *` ends the list and starts an <hr />.
    break if @block_parser.thematic_break?(lines[index].content)

    match = List.match(lines[index].content)
    break unless match
    break unless List.same_group?(first_match, match)

    item_lines, index = collect_item(lines, index, match)
    end_byte = item_lines.last.end_byte
    item_id = @arena.add_node(NodeType::LIST_ITEM,
                              source_start: item_lines.first.start_byte,
                              source_len: item_lines.last.end_byte - item_lines.first.start_byte)
    @arena.append_child(list_id, item_id)
    # CommonMark: an item is loose when "two block-level elements
    # with a blank line between them" appear at its top level.
    # parse_lines reports that directly — a blank line followed by
    # ANY block-level construct it processed at this scope. That
    # captures cases the arena walk would miss (a ref-def after a
    # blank line consumes a line but emits no arena child).
    #
    # NB: must NOT collapse into `loose ||= parse_lines(...)` — if
    # `loose` is already true from a previous iteration, `||=`
    # would skip the call and the item would never receive its
    # children.
    item_blank_between_blocks = @block_parser.parse_lines(item_id, item_lines, transformed: true)
    loose = true if item_blank_between_blocks

    blank_count = 0
    while index < lines.length && lines[index].blank
      blank_count += 1
      index += 1
    end

    next unless blank_count.positive?

    next_match = index < lines.length ? List.match(lines[index].content) : nil
    if next_match && List.same_group?(first_match, next_match)
      loose = true
    else
      # Rewind so the caller's parse_lines sees the blank line.
      # When this parse was itself processing an item's
      # continuation lines, the caller needs the blank to detect
      # "blank between block-level elements" → loose-makes-item.
      index -= blank_count
      break
    end
  end

  @arena.update_span(list_id, start_byte, end_byte)
  @arena.update_int3(list_id, loose ? 0 : 1)
  index
end