mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 10:42:45 +00:00
fix(app): link bare URLs with ports, colons and user info as one link (#7859)
MD4C's permissive URL autolink read the host, never a ":port", and accepted ":" as a link boundary. "http://127.0.0.1:8059/?token=abc" linked only "http://127.0.0.1". A colon in the path ended the link the same way. Port the upstream md4c fix into the dependency patch: - 3da1937: accept single-component hosts and a numeric port after an explicit scheme - f895c2b: scan the URL tail continuously and trim terminal punctuation and excess closing parentheses - 1645761: refuse an empty hostname - f9e5eb2: stop the tail rescan from going quadratic on long lines Also refuse the autolink when a colon after the host continues the URL ("user:pass@host"). Upstream does not parse user info, and the single-component host rule would link only "http://user". The text-link pattern links these URLs whole. Rebuild the web parser with emsdk 4.0.10, which reproduces the existing glue. That version has no GROWABLE_ARRAYBUFFERS setting, and the existing artifact was built without it. The fallback text-link pattern handles URLs MD4C rejects, like user@host and IPv6 hosts. It now stops at quotes and unbalanced parentheses and leaves trailing sentence punctuation outside the link. Closes #7440 Co-authored-by: Mariotaku <git@mariotaku.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Mariotaku
Claude Opus 5.5
parent
72e3316f4f
commit
4845e1da58
@@ -249,6 +249,35 @@ describe("bare URL sentence punctuation", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["http://127.0.0.1:8059/?token=t-1", ""],
|
||||
["http://example.com:8080/a", "."],
|
||||
["http://localhost:8059/?token=t-1", "."],
|
||||
["http://localhost:8059/?token=t-1", ")"],
|
||||
["http://user@127.0.0.1:8059/?token=t-1", '"'],
|
||||
["http://user:pass@127.0.0.1:8059/?token=t-1", ""],
|
||||
["http://localhost", ":"],
|
||||
["http://[::1]:8059/?token=t-1", "'"],
|
||||
["https://github.com/o/r/compare/main...user:repo:fix/branch", ""],
|
||||
["https://example.com/a:b/c", ":"],
|
||||
])("links %s whole and leaves %j outside", async (url, punctuation) => {
|
||||
const prepared = prepareMarkdownSource(`See ${url}${punctuation} now`);
|
||||
const tree = recognizeTextLinks(
|
||||
await parseMarkdown(prepared.render(), { latexMath: false }),
|
||||
prepared.linkRecognition.text,
|
||||
prepared.linkRecognition.inlineCode,
|
||||
);
|
||||
expect(tree.children?.[0]?.children).toEqual([
|
||||
{ type: "Text", content: "See " },
|
||||
{
|
||||
type: "Link",
|
||||
attributes: expect.objectContaining({ url }),
|
||||
children: [{ type: "Text", content: url }],
|
||||
},
|
||||
{ type: "Text", content: `${punctuation} now` },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(["[Authored](https://example.com/a.)", "<https://example.com/a.>"])(
|
||||
"preserves punctuation in explicit destinations: %s",
|
||||
async (source) => {
|
||||
|
||||
@@ -27,11 +27,17 @@ const PROSE_PATH = String.raw`(?:${PATH_PREFIX}${HAS_PATH_LETTER}${SEPARATED_PAT
|
||||
function createFilePattern(path: string): RegExp {
|
||||
// The native adapter mistakes a literal + in lookbehind for a quantifier; use its hex escape.
|
||||
return new RegExp(
|
||||
String.raw`(?:https?://[^\s<>]+|(?<![a-zA-Z0-9\u0080-\uFFFF_@.%\x2b~:/\\-])(?:${path}|${FILE_NAME})${LINE_SUFFIX}(?![a-zA-Z0-9\u0080-\uFFFF_@%+:/\\-]|\.${PATH_CHARACTER}))`,
|
||||
String.raw`(?:${BARE_URL}|(?<![a-zA-Z0-9\u0080-\uFFFF_@.%\x2b~:/\\-])(?:${path}|${FILE_NAME})${LINE_SUFFIX}(?![a-zA-Z0-9\u0080-\uFFFF_@%+:/\\-]|\.${PATH_CHARACTER}))`,
|
||||
"g",
|
||||
);
|
||||
}
|
||||
|
||||
// Matches bare URLs that MD4C does not autolink, like single-label or bracketed hosts.
|
||||
// Quotes and unbalanced closing parentheses end the URL. Trailing sentence punctuation stays outside.
|
||||
const URL_CHARACTER = String.raw`[^\s<>()"'\x60]`;
|
||||
const URL_GROUP = String.raw`\(${URL_CHARACTER}*\)`;
|
||||
const BARE_URL = String.raw`https?://(?:${URL_CHARACTER}|${URL_GROUP})*(?:[^\s<>()"'\x60.,;:!?]|${URL_GROUP})`;
|
||||
|
||||
const FILE_PATTERN = createFilePattern(PROSE_PATH);
|
||||
function linkMetadata(inlineCodePaths: ReadonlySet<string>) {
|
||||
// The engine accepts regexes on both platforms. Classify complete code spans
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user