Skip to content

fix: detect query string / fragment directly after a domain#557

Open
abhu85 wants to merge 1 commit into
nfrasser:mainfrom
abhu85:fix/query-fragment-after-domain
Open

fix: detect query string / fragment directly after a domain#557
abhu85 wants to merge 1 commit into
nfrasser:mainfrom
abhu85:fix/query-fragment-after-domain

Conversation

@abhu85

@abhu85 abhu85 commented Jun 22, 2026

Copy link
Copy Markdown

Problem

Fixes #516.

A query string or fragment that comes directly after the domain, with no path slash, is dropped:

import { find } from 'linkifyjs';

find('example.com/?foo=bar')[0].href; // http://example.com/?foo=bar  ✅
find('example.com?foo=bar')[0].href;  // http://example.com           ❌ (query dropped)
find('example.com#frag')[0].href;     // http://example.com           ❌ (fragment dropped)

Cause

In the parser state machine, the DomainDotTld (e.g. example.com) and DomainDotTldColonPort (e.g. example.com:8080) states only transition into the query-accepting Url state on tk.SLASH:

tt(DomainDotTld, tk.SLASH, Url);
tt(DomainDotTldColonPort, tk.SLASH, Url);

There's no transition on ? (QUERY) or # (POUND), so a ?/# without a preceding / ends the match at the bare domain. The scheme-prefixed path already handles this (tt(SchemeColon, tk.QUERY, Url)), which is why http://example.com?x=1 works but example.com?x=1 doesn't.

Fix

Add the missing transitions, mirroring how the Url state already treats these tokens:

tt(DomainDotTld, tk.QUERY, UrlNonaccept);  // `?` is non-accepting -> trailing `?` stays trimmed
tt(DomainDotTld, tk.POUND, Url);           // `#` is accepting
tt(DomainDotTldColonPort, tk.QUERY, UrlNonaccept);
tt(DomainDotTldColonPort, tk.POUND, Url);

Because QUERY routes to UrlNonaccept (consistent with qsNonAccepting), sentence punctuation like have you seen example.com? is not over-matched — the trailing ? is still trimmed.

Test plan

  • Added parser cases: example.com?foo=bar, ?a=1&b=2, :8080?x=1, #section are single Url tokens; have you seen example.com? keeps the ? as trailing text.
  • Updated one existing case (google.com?q=asda) whose expectation encoded the old drop-the-query behavior — it's now a single Url, same token types.
  • New cases fail on main (5 failing) and pass with the fix.
  • Full suite 394 passing; eslint clean.

`example.com?foo=bar` and `example.com#frag` dropped the query/fragment
(the href became `http://example.com`), while `example.com/?foo=bar`
worked. The DomainDotTld / DomainDotTldColonPort states only transitioned
to the Url state on SLASH, so a `?` or `#` with no preceding path slash
ended the match early.

Add those transitions, mirroring how the Url state already treats the
tokens: `?` (QUERY) is non-accepting so a trailing `?` is still trimmed,
`#` (POUND) is accepting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

example.com?foo=bar detected as http://example.com instead of http://example.com/?foo=bar

1 participant