Class: RedQuilt::Inline::EmphasisResolver
- Inherits:
-
Object
- Object
- RedQuilt::Inline::EmphasisResolver
- Defined in:
- lib/red_quilt/inline/emphasis_resolver.rb
Overview
CommonMark emphasis algorithm (spec 6.2). Phase 2 of inline parsing: given the delimiter stack the linear pass collected (provisional TEXT nodes for each ‘*` / `_` / `~` run), it pairs openers with closers and rebuilds the arena subtree into EMPHASIS / STRONG / STRIKETHROUGH nodes.
Kept separate from Builder because it is a closed algorithm with a narrow interface: it only needs the arena, the set of still-provisional nodes (so consumed delimiters can be unmarked), and whether source spans are tracked. Builder owns the linear pass and bracket handling; it hands this resolver a delimiter stack to collapse.
Defined Under Namespace
Classes: Delimiter
Instance Method Summary collapse
-
#initialize(arena, track_source:) ⇒ EmphasisResolver
constructor
A new instance of EmphasisResolver.
-
#resolve(stack, provisional_nodes) ⇒ Object
Collapses ‘stack` (an Array of Delimiter) in place, removing consumed entries from `provisional_nodes`.
Constructor Details
#initialize(arena, track_source:) ⇒ EmphasisResolver
Returns a new instance of EmphasisResolver.
22 23 24 25 |
# File 'lib/red_quilt/inline/emphasis_resolver.rb', line 22 def initialize(arena, track_source:) @arena = arena @track_source = track_source end |
Instance Method Details
#resolve(stack, provisional_nodes) ⇒ Object
Collapses ‘stack` (an Array of Delimiter) in place, removing consumed entries from `provisional_nodes`. Used both for the document-level stack and for the inner delimiters of a resolved link/image (see Builder#finalize_link).
31 32 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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 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 136 137 138 139 |
# File 'lib/red_quilt/inline/emphasis_resolver.rb', line 31 def resolve(stack, provisional_nodes) # NB: the CommonMark spec describes an `openers_bottom` # optimization keyed by closer character / length / flanking # flags. Implementing that correctly is subtle (a single # per-character bottom blocks valid matches like # `*foo**bar**baz*`), so the implementation here just walks # back to the start of the stack for every closer. This is # O(stack^2) in the worst case but stacks are tiny in practice. closer_idx = 0 while closer_idx < stack.length closer = stack[closer_idx] unless closer.can_close closer_idx += 1 next end opener_idx = closer_idx - 1 found = false while opener_idx >= 0 opener = stack[opener_idx] if opener.can_open && opener.char == closer.char skip = false if (opener.can_close || closer.can_open) && ((opener.count + closer.count) % 3).zero? && !((opener.count % 3).zero? && (closer.count % 3).zero?) skip = true end unless skip found = true break end end opener_idx -= 1 end unless found unless closer.can_open provisional_nodes.delete(closer.node_id) stack.delete_at(closer_idx) end closer_idx += 1 next end opener = stack[opener_idx] strength = [opener.count, closer.count].min >= 2 ? 2 : 1 if closer.char == "~" # GFM strikethrough only forms on `~~` runs. A single `~` # leaves the delimiter as text; advance the cursor so future # `~~` pairs can still match. if strength < 2 closer_idx += 1 next end kind = NodeType::STRIKETHROUGH else kind = strength == 2 ? NodeType::STRONG : NodeType::EMPHASIS end # CommonMark spec: any delimiters strictly between this opener and # closer can't open or close anything in this scope, so drop them # from the stack before we rebuild the tree. Their arena nodes # stay where they are (they'll be reparented into the new emphasis # alongside the surrounding content), but they must no longer be # candidates for future iterations. Without this, the next # iteration would try to pair stranded delimiters that have # already been moved into a different parent, which corrupts the # sibling chain (Arena#reparent walks into @parent[-1]). if closer_idx > opener_idx + 1 removed = stack.slice!((opener_idx + 1)...closer_idx) removed.each { |e| provisional_nodes.delete(e.node_id) } closer_idx = opener_idx + 1 closer = stack[closer_idx] end opener_node = opener.node_id closer_node = closer.node_id if @track_source opener_match_start = @arena.source_end(opener_node) - strength closer_match_end = @arena.source_start(closer_node) + strength else opener_match_start = -1 closer_match_end = 0 end emphasis_id = add_node(kind, opener_match_start, closer_match_end) first_inside = @arena.raw_next_sibling_id(opener_node) last_inside = @arena.raw_prev_sibling_id(closer_node) if first_inside != -1 && last_inside != -1 && first_inside != closer_node && last_inside != opener_node @arena.reparent(emphasis_id, first_inside, last_inside) end parent_id = @arena.raw_parent_id(opener_node) @arena.insert_before(parent_id, closer_node, emphasis_id) # Consume `strength` characters from the inner end of each # delimiter. The opener is trimmed on its right (trailing) end, # the closer on its left (leading) end; removing the opener from # the stack shifts the closer one slot left. closer_idx -= 1 if consume_delimiter(opener, opener_idx, stack, strength, provisional_nodes, from_start: false) consume_delimiter(closer, closer_idx, stack, strength, provisional_nodes, from_start: true) end stack.each { |e| provisional_nodes.delete(e.node_id) } stack.clear end |