Repository navigation
Avoid repeated token scans in liquid-html-parser - #1371
Merged
Merged
Conversation
Preserve the first-token-at-or-after semantics for forward and backward seeks while using sorted token offsets for bounded lookup work. Co-authored-by: River <river@shopify.com>
madmath
marked this pull request as ready for review
October 29, 2026 15:15
This comment was marked as resolved.
This comment was marked as resolved.
resliceTokensFrom runs once per raw Liquid tag, like {% raw %} or
{% comment %}, and still scanned every token: once for a token
straddling the end tag, and again, when none did, for the first token
after it. So parsing stayed quadratic in the number of those tags:
30,000 {% comment %}x{% endcomment %} took 6 s, and now take 34 ms.
Tokens are in source order and don't overlap, so the only token that
can straddle the end tag is the first one ending past it, and a binary
search finds it. When none straddles, the reslice seeks, which is the
binary search from the previous commit. Both are one helper now.
The two tests that parsed 2,000 comments or raw elements become a spec
case with four of them side by side. They passed before the fix too,
so what they checked was the tree, and the spec checks all of it, in
every parser that runs the specs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stray {{ or {% in a raw tag's body, as in {% raw %}{{ x{% endraw %},
leaves the tokenizer inside Liquid output at the end tag. The token
there runs past it, so resliceTokensFrom re-tokenizes from the end tag
on, and it did so through the end of the file, once per such tag.
3,000 of them took 1.5 to 22 s, depending on what follows, and now
take 9 to 59 ms.
The tokenizer now produces tokens as the parser reads them, and the
next straddling tag replaces them again, so each part of the file is
tokenized about once. For that:
- createTokenizer restarts at an offset and fills on demand, and
tokenize is that, run to the end;
- inside Liquid output, the tokenizer jumps to the next }} with
indexOf, and remembers where it found one, since restarts can land
inside the same unclosed {{ many times;
- the HTML context a reslice resumes in, an open quoted attribute or
tag, is folded over the kept tokens incrementally, instead of from
the first token each time.
Callers that read tokens by index go through hasToken and tokenAt,
which tokenize up to the index, and tokenCount is gone. The new test
counts the tokenizer's probes of the source across 200 straddling
reslices: about 4,000 now, 2.8 million before.
Whole files parse no slower: Dawn and Horizon take 0.96 to 1.0 times
as long. Across 350,000 generated documents with straddling raw tags,
200,000 fuzzed ones and the 333 theme files, the tokens, trees and
errors from all four parser entry points are the same as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clauderic
approved these changes
Oct 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What are you adding in this PR?
Files with many HTML comments,
<script>,<style>or<svg>elements, or raw Liquid tags like{% raw %}and{% comment %}, parsed in quadratic time: each of those nodes did work proportional to the whole file. There's one commit per cause:seekToSourceOffsetruns once per HTML comment and raw HTML element, and it scanned the tokens from the first one every time. Tokens are in source order, so it binary-searches them now.resliceTokensFromruns once per raw Liquid tag, and it still scanned every token: once for a token straddling the end tag, and again to seek when none did. Only the first token ending past the end tag can straddle it, so the same binary search finds both.{{or{%in a raw tag's body, as in{% raw %}{{ x{% endraw %}, runs past the end tag, and the reslice then re-tokenized the rest of the file, once per such tag. Now the tokenizer produces tokens as the parser reads them, so each part of the file is tokenized about once.}}withindexOfand remembers where it found it.hasTokenandtokenAt, which tokenize up to that index.tokenCountis gone.One parse of each document, Node 24.22.1:
<!--x--><script></script>{% comment %}x{% endcomment %}{% raw %}x{% endraw %}{% raw %}{{ x{% endraw %}{% comment %}{{ x{% endcomment %}{% raw %}{{ x{% endraw %}, then}}and 9,000<a>b</a><a href="{% raw %}{{ x{% endraw %}">{% raw %}{{ x{% endraw %}</a>, then}}and 9,000<a>b</a>Typical files parse at the same speed as on main. Dawn (98 files) takes 0.98× main's time and Horizon (231 files) 1.00×, and repeated runs vary by about ±2%.
Compatibility
The PR answers the open questions about backward seeks and first-token matching:
{{inside raw tags in every HTML context, with about 189,000 straddling reslices between them;Tests
The original description said that the added tests fail without the fix. That holds for the three tests that count work, and not for the others, which pin down behavior:
seekToSourceOffsetmoves to the first token at or after the offsetseekToSourceOffsetseeks backwardspecs/parser/html.ymlwith two HTML comments, a<script>and a<style>side by sideWhat's next? Any followup issues?
Nothing blocking. CI passed on 5877ab7: code quality, E2E, and the unit tests on Ubuntu and Windows with Node 22 and 24.
Tophatting
Run locally on Node 24.22.1:
pnpm build:tsandpnpm type-checkpass.pnpm exec vitest --run: 7,817 passed, 5 skipped.pnpm --dir packages/prettier-plugin-liquid test --runandtest:3 --run: 144 passed each.Before you deploy
changeset🤖 Generated with Claude Code