Module: RedQuilt::List

Defined in:
lib/red_quilt/list.rb

Overview

CommonMark spec 5.2 lists.

Module-level functions are stateless predicates used by BlockParser’s dispatch and paragraph-interruption logic. ‘List::Parser` holds a cached reference to its owning BlockParser (for parse_lines recursion and shared helpers) but no per-list state — a single Parser instance is reused for every list in the document, including nested ones, and the per-call state lives in method locals so reentrant calls are safe.

Defined Under Namespace

Classes: Parser

Class Method Summary collapse

Class Method Details

.build_match(leading, marker_width, marker, spaces_after, body, ordered:, start_number:) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/red_quilt/list.rb', line 111

def build_match(leading, marker_width, marker, spaces_after, body, ordered:, start_number:)
  if body.empty?
    # Marker followed by EOL: empty item content.
    content_indent = leading + marker_width + 1
    content = ""
  elsif spaces_after >= 5
    # Indented-code form: keep (spaces_after - 1) of the spaces in
    # the content so the body of the item is recognised as an
    # indented code block.
    content_indent = leading + marker_width + 1
    content = (" " * (spaces_after - 1)) + body
  else
    content_indent = leading + marker_width + spaces_after
    content = body
  end

  {
    ordered: ordered,
    start_number: start_number,
    marker: marker,
    content: content,
    content_start: leading + marker_width + 1,
    content_indent: content_indent,
  }
end

.column_width(whitespace, start_col) ⇒ Object

Returns the column width of ‘whitespace` if it begins at the absolute column `start_col`, expanding tabs to the next tab stop of 4. `whitespace` must contain only 0x20/0x09 bytes.



99
100
101
102
103
104
105
106
107
108
109
# File 'lib/red_quilt/list.rb', line 99

def column_width(whitespace, start_col)
  col = start_col
  whitespace.each_byte do |b|
    if b == 0x20
      col += 1
    elsif b == 0x09
      col = ((col / 4) + 1) * 4
    end
  end
  col - start_col
end

.interrupts_paragraph?(li_match) ⇒ Boolean

CommonMark spec: a list item can only interrupt an open paragraph if it has visible content, and (for ordered lists) only if the start number is 1.

Returns:

  • (Boolean)


89
90
91
92
93
94
# File 'lib/red_quilt/list.rb', line 89

def interrupts_paragraph?(li_match)
  return false if li_match[:content].empty?
  return false if li_match[:ordered] && li_match[:start_number] != 1

  true
end

.match(text) ⇒ Object

Recognises the start of a list item per CommonMark spec section 5.2.

Returns nil if ‘text` is not a list-item start, otherwise a Hash:

ordered:        true (1.  / 1)) or false (- / + / *)
start_number:   Integer (0 for unordered)
marker:         String, the marker character (".", ")", "-", "+", "*")
content:        String, the body of the line as it should appear
                inside the item (may include leading whitespace
                when the marker was followed by 5+ spaces -- that
                is the indented-code form).
content_start:  Integer, byte offset into `text` where `content`
                begins. Always (leading + marker_width + 1) in
                absolute terms, regardless of spec form.
content_indent: Integer, the spec's N -- the indent level all
                subsequent continuation lines must reach to stay
                inside this item.


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/red_quilt/list.rb', line 33

def match(text)
  # Fast reject before touching the regex engine: a list item is at
  # most 3 leading spaces followed by a bullet (`* + -`) or a digit.
  # This runs on every line, so bailing here avoids a MatchData (plus
  # a `rest` substring and two marker-regex attempts) for the common
  # non-list line.
  i = 0
  i += 1 while i < 3 && text.getbyte(i) == 0x20
  c = text.getbyte(i)
  return nil if c.nil?
  return nil unless c == 0x2A || c == 0x2B || c == 0x2D || (c >= 0x30 && c <= 0x39)

  m = /\A( {0,3})/.match(text)
  leading = m[1].length
  rest = text[leading..]

  if (bm = /\A([*+-])(?:([ \t]+)(.*)|([ \t]*)\z)/.match(rest))
    marker = bm[1]
    if bm[2]
      # `spaces_after` is column width, not byte length, so a tab
      # after the marker is billed as the number of cols needed to
      # reach the next tab stop (CommonMark Tabs section).
      spaces_after = column_width(bm[2], leading + 1)
      body = bm[3]
    else
      spaces_after = 0
      body = ""
    end
    return build_match(leading, 1, marker, spaces_after, body,
                       ordered: false, start_number: 0)
  end

  if (om = /\A(\d{1,9})([.)])(?:([ \t]+)(.*)|([ \t]*)\z)/.match(rest))
    digits = om[1]
    marker = om[2]
    if om[3]
      spaces_after = column_width(om[3], leading + digits.length + 1)
      body = om[4]
    else
      spaces_after = 0
      body = ""
    end
    return build_match(leading, digits.length + 1, marker, spaces_after, body,
                       ordered: true, start_number: digits.to_i)
  end

  nil
end

.same_group?(expected, actual) ⇒ Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/red_quilt/list.rb', line 82

def same_group?(expected, actual)
  expected[:ordered] == actual[:ordered] && expected[:marker] == actual[:marker]
end