in src/inlines/Emphasis.php [174:312]
private static function processEmphasis(
Context $context,
vec<Stack\Node> $stack,
): vec<Stack\Node> {
$position = 0;
$openers_bottom = dict[
'*' => vec[0, 0, 0], // indexed by number of chars in delimiter - [0] is unused, but convenient
'_' => vec[0, 0, 0],
];
while ($position < C\count($stack)) {
$closer_idx = self::findCloser($stack, $position);
if ($closer_idx === null) {
break;
}
$position = $closer_idx;
$closer = $stack[$closer_idx];
invariant(
$closer is Stack\DelimiterNode,
'closer must be a delimiter',
);
list($closer_text, $closer_flags) = tuple(
$closer->getText(),
$closer->getFlags(),
);
$char = $closer_text[0];
$opener = null;
$closer_len = Str\length($closer->getText()) % 3;
$bottom = $openers_bottom[$char][$closer_len];
for ($i = $position - 1; $i >= $bottom; $i--) {
$item = $stack[$i];
if (!$item is Stack\DelimiterNode) {
continue;
}
if (!($item->getFlags() & self::IS_START)) {
continue;
}
if ($item->getText()[0] !== $char) {
continue;
}
// intra-word delimiters must match exactly
// e.g.
// - `*foo**bar` is not emphasized
// - `**foo**bar` is emphasized
if (
(
($closer->getFlags() & self::IS_START)
|| ($item->getFlags() & self::IS_END)
)
&& (
(Str\length($closer->getText()) + Str\length($item->getText()))
% 3 === 0
)
) {
continue;
}
$opener = $item;
break;
}
$opener_idx = $i;
if ($opener === null) {
$openers_bottom[$char][$closer_len] = $position - 1;
if (!($closer_flags & self::IS_START)) {
$stack[$closer_idx] = new Stack\TextNode(
$closer_text,
$closer->getStartOffset(),
$closer->getEndOffset(),
);
}
++$position;
continue;
}
// Have an opener and closer pair
$opener_text = $opener->getText();
$strong = Str\length($opener_text) >= 2 && Str\length($closer_text) >= 2;
$chomp = $strong ? 2 : 1;
$opener_text = Str\slice($opener_text, $chomp);
$closer_text = Str\slice($closer_text, $chomp);
// We're going to throw away the existing opener, closer, and everything
// in between; we build up the replacements here.
$mid_nodes = vec[];
if ($opener_text !== '') {
// Remove the chars from the end of the delimiter as
// `**foo*` is `*<em>foo</em>`, not `<em>*foo</em>`
$mid_nodes[] = new Stack\DelimiterNode(
$opener_text,
$opener->getFlags(),
$opener->getStartOffset(),
$opener->getEndOffset() - $chomp,
);
} else {
// We just ate up the last of this delimiter, so there's now none.
// Adjust the stack offset, as we're effectively removing something
// earlier in the stack than the current position
$position--;
}
$first_content_idx = $opener_idx + 1;
$last_content_idx = $closer_idx - 1;
$content_length = ($last_content_idx - $first_content_idx) + 1;
$mid_nodes[] =
Vec\slice($stack, $first_content_idx, $content_length)
|> self::consumeStackSlice($context, $$)
|> new self($strong, $$)
|> new Stack\EmphasisNode(
$$,
$opener->getEndOffset() - $chomp,
$closer->getStartOffset() + $chomp,
);
$position -= $content_length;
if ($closer_text !== '') {
// Same as openers, however we take it from the start, as
// `*foo**` is `<em>foo</em>*`, not `<em>foo*</em>`
$mid_nodes[] = new Stack\DelimiterNode(
$closer_text,
$closer->getFlags(),
$closer->getStartOffset() + $chomp,
$closer->getEndOffset(),
);
}
$stack = Vec\concat(
Vec\take($stack, $opener_idx),
$mid_nodes,
Vec\drop($stack, $closer_idx + 1),
);
}
return $stack;
}