Repository navigation
fix: avoid cubic backtracking on unicode whitespace in a link destination - #4106
Merged
Merged
Conversation
|
@spokodev is attempting to deploy a commit to the MarkedJS Team on Vercel. A member of the Team first needs to authorize it. |
spokodev
force-pushed
the
fix/link-gap-unicode-whitespace
branch
from
September 22, 2026 20:45
813f869 to
fb3532a
Compare
…tion The gap before a link destination was \s*, which matches unicode whitespace, while the destination class only excluded ASCII space, tab and newline. A run of U+00A0 or its relatives could therefore be divided between the two in n ways, and the anchored rule is retried at every offset, so the cost grows by roughly a factor of eight for every doubling of the run. CommonMark defines that gap as spaces or tabs plus up to one line ending, so narrowing it to [ \t\n]* is what the spec asks for. It also settles the split: the reference implementation treats unicode whitespace there as part of the destination, which is what marked now does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
spokodev
force-pushed
the
fix/link-gap-unicode-whitespace
branch
from
September 22, 2026 23:50
fb3532a to
dc20a38
Compare
UziTech
requested changes
Sep 24, 2026
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
github-actions Bot
pushed a commit
that referenced
this pull request
Oct 5, 2026
# [18.1.0](v18.0.14...v18.1.0) (2026-10-05) ### Bug Fixes * avoid cubic backtracking on unicode whitespace in a link destination ([#4106](#4106)) ([0d20220](0d20220)) * load CLI configs with top-level await ([#4100](#4100)) ([c61543e](c61543e)) * reject unbalanced parentheses in link destinations ([#4107](#4107)) ([3363c99](3363c99)) ### Features * add linkParenPossible to lexer state to fail fast for link token generation ([#4070](#4070)) ([4ee44f7](4ee44f7))
physics515
added a commit
to basic-automation/basicautomation.io
that referenced
this pull request
Oct 8, 2026
… buttons that speak and fail loudly, client-bundle audit, header hardening, 404s that say who asked (#15) * fix(deps): marked 18.1.0, which stops a link destination backtracking cubically marked 18.0.14 matched the gap before a link destination with `\s*`, which overlaps the destination class on unicode whitespace, so `[](` followed by a run of U+00A0 backtracked cubically (markedjs/marked#4106). Through this site's own renderMarkdown, 4 KB of it took 10.3 s, all on the server's one thread — one README edit in any of the seven repos could stall every page. On 18.1.0 the same input renders in 0.1 ms. All 87 documents the site renders today (7 READMEs, the 76 live posts, the 4 test fixtures) come out byte-identical on both versions. test/markdown-backtracking.test.ts renders 8 KB of three unicode spaces through both the README and post paths in under 500 ms; on 18.0.14 it does not finish inside 60 s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(a11y): axe-core 4.14, and its new default rule judged in the browser axe-core 4.14.0 turns `label-content-name-mismatch` on by default (WCAG 2.5.3, label in name). It compares a control's accessible name with its visible text, which jsdom cannot determine, so in `npm run a11y` it only ever came back undecided — on the copy buttons of every project page — and no pass judged it. It moves to the layout list: skipped by name in scripts/check-a11y.mjs, run in scripts/check-a11y-browser.mjs at 1280 and 390 px. Today's pages pass ("[copy]" sits at the start of "Copy …"). Proved it bites: with CodeBlock's label planted as "Download …", the browser pass fails with 12 violations, one per project page per width. Both passes clean over 95 pages, including the 76 live posts. README names the new rule beside the other browser-only ones. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(markdown): the README patterns run in linear time READMEs are fetched and prepared at request time on the server's one thread. Seven patterns in shared/markdown/readme.ts and slug.ts were quadratic on input that opens something and never closes it: a global regex is retried at every offset, and each of these could scan from one start past where the next one began. At 100 KB: absolutize, a run of `[` 5.2 s → 0.5 ms absolutize, a run of `` 1.0 s → 0.2 ms slugify, a run of `<` 5.0 s → 0.9 ms renderMarkdown, 100 KB heading 2.5 s → 34 ms A link label now stops at the next `[`, a tag at the next `<`, a URL may end at the end of the input, and the logo test moved out of the pattern into a callback. All 7 READMEs (prepared markdown and HTML) and the 76 live posts are byte-identical before and after. The edges that move are pinned in the test: `` is now read as text and a link, the way marked renders it, and a stray `<` before a tag stays out of it, which is what GitHub's slug of that heading is made from. test/markdown-backtracking.test.ts times each input: 11 cases, every one failing on the old patterns. ROADMAP gains the item, with the marked fix from the previous commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(a11y): a copy button announces that it copied Pressing [copy] changed the visible text to [copied], but the button's accessible name is its aria-label ("Copy to clipboard: …", "Copy <label>"), so a screen reader announced nothing (WCAG 4.1.3, status messages). app/components/CodeLine.vue and CodeBlock.vue each render an empty `role="status"` sr-only region on the server, so it exists before it changes, and it reads "Copied to clipboard" for the same 1.6 s as the visible label. CodeBlock's sits beside its figcaption, not in it, so it is never part of the figure's name. Checked in headless Chromium over CDP with clipboard permission: after activation the polite live region's text in the accessibility tree is "Copied to clipboard", the clipboard holds the command (and the config.toml block for CodeBlock), and both reset after 1.6 s. Pages are unchanged to the eye: the pixel difference against the previous build is in the masthead's animated glass only, and the same size as between two earlier builds. check, a11y and a11y:browser clean over 95 pages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(headers): drop x-powered-by, add Cross-Origin-Opener-Policy Every live response carried `x-powered-by: Nuxt`. Nuxt's renderer, payload and island handlers set it themselves after route rules apply, so no rule can take it off; server/plugins/powered-by.ts removes it in Nitro's `beforeResponse`, which runs after any handler. It tells a scanner which framework's advisories to try first and nothing else. `Cross-Origin-Opener-Policy: same-origin` joins the `/**` headers in nuxt.config.ts: no page keeps a handle on a window it opens, and a cross-origin page that opens this site gets none into it. scripts/check-site.mjs fails any response with x-powered-by and any page without the policy. Against the previous build: 94 failures for each. After: gone from pages, the 404 page, /healthz, /api, the XML routes, static files and HEAD; check, a11y and a11y:browser clean over 95 pages; routes unchanged; client navigation home → onyums → its blog in the browser with no console errors; screenshots differ only in the masthead's animated glass, as between any two builds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(copy): a refused clipboard selects the text and says so When `navigator.clipboard.writeText` was refused (denied permission, insecure context, a frame that forbids it) the copy buttons did nothing: still `[copy]`, nothing announced, and the visitor pasted whatever their clipboard held before. Found when the built-in browser pane refused it while the previous commit was being tested. app/composables/useCopy.ts is now the one copy of the logic CodeLine and CodeBlock each carried. On refusal it selects the code element's contents, so the platform's copy shortcut finishes the job, shows `[selected]` for 4 s and announces "Could not copy. The text is selected; copy it with your keyboard." in the status region. Checked in headless Chromium over CDP: - permission granted: `[copied]`, "Copied to clipboard", the clipboard holds the command / the config.toml block, reset after 1.6 s - permission denied (`NotAllowedError`): `[selected]`, the message above, the selection is exactly the command without its `$` sigil (and the whole config.toml block), reset after 4 s, no overflow at 390 px (screenshot in the run's scratch dir) check, a11y, a11y:browser clean over 95 pages; 226 tests; routes unchanged; page screenshots unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(roadmap): Nuxt 4.6, trialled and deliberately not landed yet Nuxt 4.6.0 was published an hour into tonight's run. A trial in a scratch worktree was clean on every gate (typecheck, 226 tests, build, audit:runtime, check, a11y, a11y:browser; 23 routes as on 4.5.2; byte-identical page sizes; one JSON log line per request) and changes nothing visible: one CSS line height rounds to 16 px instead of 15.98. It stays a `[ ]` with that evidence: a 420-commit minor with a CLI major, an hour old, should not go out in an unattended deploy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * fix(log): a failed request's line says who asked The request log has two writers: `afterResponse` for answered requests, and the `error` hook for requests that threw, every 404 and 500. Its comment promised "same shape as the line above", but the error line stopped at `via`: no `ip`, `ua` or `ref`. In fifteen hours of live log that was 627 lines, nearly all scanners probing `.env`, `.php` and `.git`, and the only lines with no address or user agent. server/plugins/request-log.ts builds both from one `requester(event)`. Checked on a built server with a forwarded address, user agent and referer: a 200, a page 404, an API 404 and a HEAD 404 all carry the three fields; an onion-marked request keeps `ip: null` and `via: onion`; each request is still exactly one line. Gate: typecheck, 226 tests, build, check, a11y, a11y:browser; routes and pages unchanged. README says the fields are on every line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * feat(audit): audit:runtime covers the client bundle too The runtime audit asked the registry about what the server ships and said plainly that the client bundle was not covered: it is built without sourcemaps (rightly; they would be served), so there was nothing to read its packages from. scripts/lib/client-packages.ts is a Vite plugin applied only to the client environment. In generateBundle it maps every module with rendered code to its package and that package's own version, and a Nitro `compiled` hook in nuxt.config.ts writes the list to .output/server/client-packages.json: inside the image, never under public/, and a 404 from the server at every spelling tried. scripts/audit-runtime.mjs adds those packages to its registry query and fails when the file is missing or empty, rather than passing with the browser half unasked. Today: 32 client packages, 14 of which the server audit never saw; 79 asked in all; clean. Proved it bites: a planted marked@4.0.9 fails with its two advisories; a missing and an empty inventory both fail. Gate: typecheck, 226 tests, build, check, a11y, a11y:browser (95 pages, both widths); routes and pages unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(readme): audit:runtime covers the client bundle Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(data): refresh the fallback snapshot npm run sync, authenticated, all 8 projects including splimes: issue counts and one push date moved. test/snapshot.test.ts passes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * docs(roadmap): the linear-patterns item counts the <a href> rule from the splimes merge Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch was successfully deployed
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.
Marked version: 18.0.14 (master, ef0704c)
Markdown flavor: CommonMark
Description
Follow-up to your note on #4070 — here's the fix.
The gap before a link destination was
\s*, which matches unicode whitespace. The destination class next to it,[^ \t\n\x00-\x1f]+, excludes only ASCII space, tab, newline and the C0 controls. A run of U+00A0 (or U+2003, U+202F, U+2028, U+FEFF …) matches both, so it can be divided between them in n ways, and the anchored rule is retried at every offset.marked.parse('[](' + ' '.repeat(n)), default options, Node 26. Each point is the minimum of several runs, taken with a fixed reference workload in the same process and accepted only within 8% of its idle baseline; a second sweep reproduced every row within 10%.×6.3, ×7.6, ×7.9, ×8.0, ×7.7 per doubling on master; flat here. 8.6 KB of ordinary prose parses in 0.14 ms on both builds.
test/specs/redos/quadratic_link_empty_href.cjsis'[](' + ' '.repeat(50000). The new guard is that input with the space replaced by U+00A0: master does not finish it inside 120 s, this branch renders it in 10 ms.The change
\s*→[ \t\n]*, both occurrences in thelinkrule. Changing only one of them takes the growth from ×7.2 to ×3.3 (leading gap) or ×3.9 (trailing gap) — better, but still super-linear. Both together make it flat.Narrowing the other side instead —
[^\s\x00-\x1f]+for the destination, which is what the neighbouringautolinkrule already uses — also flattens it (×1.6), but it makes[a](a b)stop being a link, and the reference implementation accepts that destination. So I left the destination alone.What it changes
Nothing unless unicode whitespace sits in one of the two gaps:
{},gfm,no-gfm,pedantic,breaks) — 0 rendered differencesWhere it does change,
commonmark@0.31.2never sides with master. Over the gap grid, 464 forms render differently; the reference agrees with this branch in 208 of them and with master in 0. The rest are cases where marked and the reference already disagreed for unrelated reasons, such as'escaping.The third is an honest "neither" case: all three differ, and this branch is the closer of the two to the reference.
Test suite
test:specs1863/1863,test:unit192/192,test:types,test:umd,test:cjs,eslintclean.Contributor
test/specs/redos/cubic_link_unicode_space.cjs