Skip to content

Commit 3aaed6d

Browse files
authored
Optimize parser and renderer performance (~25-30% faster) (#47)
* Optimize parser and renderer performance (~25-30% faster)
1 parent b654a70 commit 3aaed6d

File tree

4 files changed

+140
-79
lines changed

4 files changed

+140
-79
lines changed

src/Parser/BlockParser.php

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,31 +1910,31 @@ protected function tryParseDjotDefinitionList(Node $parent, array $lines, int $s
19101910
$def = new DefinitionDescription();
19111911
$defAttributes = [];
19121912
if ($defLines !== []) {
1913-
// Remove leading blank lines
1914-
while ($defLines !== [] && $defLines[0] === '') {
1915-
array_shift($defLines);
1913+
// Skip leading blank lines using index (avoid O(n) array_shift)
1914+
$defStart = 0;
1915+
$defLineCount = count($defLines);
1916+
while ($defStart < $defLineCount && $defLines[$defStart] === '') {
1917+
$defStart++;
19161918
}
19171919
// Remove trailing blank lines (but preserve potential attribute line)
1918-
$defLineCount = count($defLines);
1919-
while ($defLineCount > 1 && $defLines[$defLineCount - 1] === '') {
1920-
array_pop($defLines);
1921-
$defLineCount--;
1920+
$defEnd = $defLineCount;
1921+
while ($defEnd > $defStart + 1 && $defLines[$defEnd - 1] === '') {
1922+
$defEnd--;
19221923
}
19231924

19241925
// Check if last line is a standalone attribute block for the dd
1925-
$defLineCount = count($defLines);
1926-
if ($defLineCount > 0 && preg_match('/^\{([^{}]+)\}\s*$/', $defLines[$defLineCount - 1], $attrMatch)) {
1926+
if ($defEnd > $defStart && preg_match('/^\{([^{}]+)\}\s*$/', $defLines[$defEnd - 1], $attrMatch)) {
19271927
$defAttributes = AttributeParser::parse($attrMatch[1]);
1928-
array_pop($defLines);
1928+
$defEnd--;
19291929
// Remove any trailing blank lines before the attribute
1930-
$defLineCount = count($defLines);
1931-
while ($defLineCount > 0 && $defLines[$defLineCount - 1] === '') {
1932-
array_pop($defLines);
1933-
$defLineCount--;
1930+
while ($defEnd > $defStart && $defLines[$defEnd - 1] === '') {
1931+
$defEnd--;
19341932
}
19351933
}
19361934

1937-
$this->parseBlocks($def, $defLines, 0);
1935+
if ($defEnd > $defStart) {
1936+
$this->parseBlocks($def, array_slice($defLines, $defStart, $defEnd - $defStart), 0);
1937+
}
19381938
}
19391939
// Apply definition attributes
19401940
if ($defAttributes !== []) {
@@ -1971,12 +1971,15 @@ protected function splitByBlankLines(array $lines): array
19711971
$blocks = [];
19721972
$current = [];
19731973

1974-
// Remove leading blank lines
1975-
while ($lines !== [] && $lines[0] === '') {
1976-
array_shift($lines);
1974+
// Skip leading blank lines using index (avoid O(n) array_shift)
1975+
$start = 0;
1976+
$count = count($lines);
1977+
while ($start < $count && $lines[$start] === '') {
1978+
$start++;
19771979
}
19781980

1979-
foreach ($lines as $line) {
1981+
for ($i = $start; $i < $count; $i++) {
1982+
$line = $lines[$i];
19801983
if ($line === '') {
19811984
if ($current !== []) {
19821985
$blocks[] = $current;

src/Parser/InlineParser.php

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ class InlineParser
5252
*/
5353
protected array $customPatterns = [];
5454

55+
/**
56+
* Cached abbreviation regex pattern (built once per document)
57+
*/
58+
protected ?string $abbreviationPattern = null;
59+
60+
/**
61+
* Cached abbreviation keys for the current pattern
62+
*
63+
* @var array<string, string>|null
64+
*/
65+
protected ?array $cachedAbbreviations = null;
66+
5567
public function __construct(protected BlockParser $blockParser)
5668
{
5769
}
@@ -449,16 +461,23 @@ protected function flushText(Node $parent, string $text): void
449461
*/
450462
protected function flushTextWithAbbreviations(Node $parent, string $text, array $abbreviations): void
451463
{
452-
// Sort abbreviations by length (longest first) to match longer abbreviations first
453-
$abbrKeys = array_keys($abbreviations);
454-
usort($abbrKeys, fn ($a, $b) => strlen($b) - strlen($a));
464+
// Cache the regex pattern for abbreviations (built once per document)
465+
if ($this->cachedAbbreviations !== $abbreviations) {
466+
// Sort abbreviations by length (longest first) to match longer abbreviations first
467+
$abbrKeys = array_keys($abbreviations);
468+
usort($abbrKeys, fn ($a, $b) => strlen($b) - strlen($a));
455469

456-
// Build a regex pattern that matches any abbreviation at word boundaries
457-
// We need to escape special regex characters in abbreviation keys
458-
$escapedKeys = array_map(fn ($key) => preg_quote($key, '/'), $abbrKeys);
459-
$pattern = '/\b(' . implode('|', $escapedKeys) . ')\b/u';
470+
// Build a regex pattern that matches any abbreviation at word boundaries
471+
// We need to escape special regex characters in abbreviation keys
472+
$escapedKeys = array_map(fn ($key) => preg_quote($key, '/'), $abbrKeys);
473+
$this->abbreviationPattern = '/\b(' . implode('|', $escapedKeys) . ')\b/u';
474+
$this->cachedAbbreviations = $abbreviations;
475+
}
460476

461477
// Split text by abbreviation matches, keeping the delimiters
478+
// Pattern is guaranteed to be set at this point
479+
/** @var string $pattern */
480+
$pattern = $this->abbreviationPattern;
462481
$parts = preg_split($pattern, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
463482

464483
if ($parts === false) {

src/Renderer/HtmlRenderer.php

Lines changed: 84 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,67 @@ class HtmlRenderer implements RendererInterface
101101
*/
102102
protected array $collectedFootnotes = [];
103103

104+
/**
105+
* Dispatch table mapping node class names to render method names
106+
*
107+
* @var array<class-string<\Djot\Node\Node>, string>
108+
*/
109+
protected array $nodeRenderers = [];
110+
104111
public function __construct(protected bool $xhtml = false)
105112
{
113+
$this->initNodeRenderers();
114+
}
115+
116+
/**
117+
* Initialize the node renderer dispatch table
118+
*
119+
* Maps node class names to render method names for O(1) lookup.
120+
*/
121+
protected function initNodeRenderers(): void
122+
{
123+
$this->nodeRenderers = [
124+
Document::class => 'renderChildren',
125+
Paragraph::class => 'renderParagraph',
126+
Heading::class => 'renderHeading',
127+
CodeBlock::class => 'renderCodeBlock',
128+
Comment::class => '',
129+
RawBlock::class => 'renderRawBlock',
130+
BlockQuote::class => 'renderBlockQuote',
131+
DefinitionList::class => 'renderDefinitionList',
132+
DefinitionTerm::class => 'renderDefinitionTerm',
133+
DefinitionDescription::class => 'renderDefinitionDescription',
134+
ListBlock::class => 'renderList',
135+
ListItem::class => 'renderListItem',
136+
ThematicBreak::class => 'renderThematicBreak',
137+
Div::class => 'renderDiv',
138+
Figure::class => 'renderFigure',
139+
Caption::class => 'renderCaption',
140+
Table::class => 'renderTable',
141+
TableRow::class => 'renderTableRow',
142+
TableCell::class => 'renderTableCell',
143+
LineBlock::class => 'renderLineBlock',
144+
Footnote::class => 'renderFootnote',
145+
Text::class => 'renderText',
146+
Emphasis::class => 'renderEmphasis',
147+
Strong::class => 'renderStrong',
148+
Link::class => 'renderLink',
149+
Image::class => 'renderImage',
150+
Code::class => 'renderCode',
151+
RawInline::class => 'renderRawInline',
152+
Math::class => 'renderMath',
153+
Symbol::class => 'renderSymbol',
154+
FootnoteRef::class => 'renderFootnoteRef',
155+
SoftBreak::class => 'renderSoftBreak',
156+
HardBreak::class => 'renderHardBreak',
157+
Span::class => 'renderSpan',
158+
Highlight::class => 'renderHighlight',
159+
Superscript::class => 'renderSuperscript',
160+
Subscript::class => 'renderSubscript',
161+
Insert::class => 'renderInsert',
162+
Delete::class => 'renderDelete',
163+
Abbreviation::class => 'renderAbbreviation',
164+
];
106165
}
107166

108167
/**
@@ -362,64 +421,36 @@ protected function renderAttributesExcluding(Node $node, array $exclude): string
362421

363422
protected function renderNode(Node $node): string
364423
{
365-
// Dispatch render event
366-
$eventName = 'render.' . $node->getType();
367-
$event = new RenderEvent($node);
424+
// Only dispatch events if listeners are registered (avoid object allocation)
425+
if ($this->hasAnyListeners()) {
426+
$eventName = 'render.' . $node->getType();
427+
$event = new RenderEvent($node);
368428

369-
// Call specific listeners
370-
$this->dispatchEvent($eventName, $event);
429+
// Call specific listeners
430+
$this->dispatchEvent($eventName, $event);
371431

372-
// Call wildcard listeners
373-
$this->dispatchEvent('render.*', $event);
432+
// Call wildcard listeners
433+
$this->dispatchEvent('render.*', $event);
374434

375-
// If listener provided custom HTML, use it
376-
if ($event->isDefaultPrevented()) {
377-
return $event->getHtml() ?? '';
435+
// If listener provided custom HTML, use it
436+
if ($event->isDefaultPrevented()) {
437+
return $event->getHtml() ?? '';
438+
}
378439
}
379440

380-
return match (true) {
381-
$node instanceof Document => $this->renderChildren($node),
382-
$node instanceof Paragraph => $this->renderParagraph($node),
383-
$node instanceof Heading => $this->renderHeading($node),
384-
$node instanceof CodeBlock => $this->renderCodeBlock($node),
385-
$node instanceof Comment => '', // Comments are stripped from output
386-
$node instanceof RawBlock => $this->renderRawBlock($node),
387-
$node instanceof BlockQuote => $this->renderBlockQuote($node),
388-
$node instanceof DefinitionList => $this->renderDefinitionList($node),
389-
$node instanceof DefinitionTerm => $this->renderDefinitionTerm($node),
390-
$node instanceof DefinitionDescription => $this->renderDefinitionDescription($node),
391-
$node instanceof ListBlock => $this->renderList($node),
392-
$node instanceof ListItem => $this->renderListItem($node),
393-
$node instanceof ThematicBreak => $this->renderThematicBreak($node),
394-
$node instanceof Div => $this->renderDiv($node),
395-
$node instanceof Figure => $this->renderFigure($node),
396-
$node instanceof Caption => $this->renderCaption($node),
397-
$node instanceof Table => $this->renderTable($node),
398-
$node instanceof TableRow => $this->renderTableRow($node),
399-
$node instanceof TableCell => $this->renderTableCell($node),
400-
$node instanceof LineBlock => $this->renderLineBlock($node),
401-
$node instanceof Footnote => $this->renderFootnote($node),
402-
$node instanceof Text => $this->renderText($node),
403-
$node instanceof Emphasis => $this->renderEmphasis($node),
404-
$node instanceof Strong => $this->renderStrong($node),
405-
$node instanceof Link => $this->renderLink($node),
406-
$node instanceof Image => $this->renderImage($node),
407-
$node instanceof Code => $this->renderCode($node),
408-
$node instanceof RawInline => $this->renderRawInline($node),
409-
$node instanceof Math => $this->renderMath($node),
410-
$node instanceof Symbol => $this->renderSymbol($node),
411-
$node instanceof FootnoteRef => $this->renderFootnoteRef($node),
412-
$node instanceof SoftBreak => $this->renderSoftBreak(),
413-
$node instanceof HardBreak => $this->renderHardBreak(),
414-
$node instanceof Span => $this->renderSpan($node),
415-
$node instanceof Highlight => $this->renderHighlight($node),
416-
$node instanceof Superscript => $this->renderSuperscript($node),
417-
$node instanceof Subscript => $this->renderSubscript($node),
418-
$node instanceof Insert => $this->renderInsert($node),
419-
$node instanceof Delete => $this->renderDelete($node),
420-
$node instanceof Abbreviation => $this->renderAbbreviation($node),
421-
default => $this->renderChildren($node),
422-
};
441+
// Use dispatch table for O(1) lookup instead of instanceof chain
442+
$class = $node::class;
443+
if (isset($this->nodeRenderers[$class])) {
444+
$method = $this->nodeRenderers[$class];
445+
if ($method === '') {
446+
return ''; // Comment nodes
447+
}
448+
449+
/** @var string */
450+
return $this->$method($node);
451+
}
452+
453+
return $this->renderChildren($node);
423454
}
424455

425456
protected function renderChildren(Node $node): string

src/Renderer/Utility/EventDispatcherTrait.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ public function off(?string $event = null): void
5151
}
5252
}
5353

54+
/**
55+
* Check if there are any listeners registered for any render event
56+
*/
57+
protected function hasAnyListeners(): bool
58+
{
59+
return $this->listeners !== [];
60+
}
61+
5462
/**
5563
* Dispatch an event to all registered listeners
5664
*

0 commit comments

Comments
 (0)