From fc9bdcafa3959af65e77b3a529098e72d57214fb Mon Sep 17 00:00:00 2001 From: Evgeny Ponomarev Date: Fri, 21 Aug 2026 17:57:32 +0300 Subject: [PATCH] fix: support GFM protocol autolinks --- src/rules.ts | 10 +++++-- test/specs/gfm/gfm.0.29.json | 9 ++---- test/unit/Lexer.test.js | 53 ++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/rules.ts b/src/rules.ts index aeb880c597..a0bcdcd702 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -464,6 +464,10 @@ const reflinkSearch = edit('reflink|nolink(?!\\()', 'g') .getRegex(); const _caseInsensitiveProtocol = /[hH][tT][tT][pP][sS]?|[fF][tT][pP]/; +const _extendedEmail = /[A-Za-z0-9._+-]+@[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![\w-])/; +const _extendedEmailProtocol = edit(/(?:mailto:email|xmpp:email(?:\/[A-Za-z0-9@.]+)?)/) + .replace(/email/g, _extendedEmail) + .getRegex(); /** * Normal Inline Grammar @@ -522,14 +526,16 @@ const inlineGfm: Record = { emStrongLDelim: emStrongLDelimGfm, delLDelim, delRDelim, - url: edit(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/) + url: edit(/^emailProtocol|^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/) + .replace('emailProtocol', _extendedEmailProtocol) .replace('protocol', _caseInsensitiveProtocol) .replace('email', /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/) .getRegex(), _backpedal: /(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/, del: /^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/, - text: edit(/^(`+|~+|[^`~])(?:(?=[`~])|(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\mailto:foo@bar.baz

\n

mailto:a.b-c_d@a.b

\n

mailto:a.b-c_d@a.b.

\n

mailto:a.b-c_d@a.b/

\n

mailto:a.b-c_d@a.b-

\n

mailto:a.b-c_d@a.b_

\n

xmpp:foo@bar.baz

\n

xmpp:foo@bar.baz.

", "markdown": "mailto:foo@bar.baz\n\nmailto:a.b-c_d@a.b\n\nmailto:a.b-c_d@a.b.\n\nmailto:a.b-c_d@a.b/\n\nmailto:a.b-c_d@a.b-\n\nmailto:a.b-c_d@a.b_\n\nxmpp:foo@bar.baz\n\nxmpp:foo@bar.baz.", - "example": 633, - "shouldFail": true + "example": 633 }, { "section": "[extension] Autolinks", "html": "

xmpp:foo@bar.baz/txt

\n

xmpp:foo@bar.baz/txt@bin

\n

xmpp:foo@bar.baz/txt@bin.com

", "markdown": "xmpp:foo@bar.baz/txt\n\nxmpp:foo@bar.baz/txt@bin\n\nxmpp:foo@bar.baz/txt@bin.com", - "example": 634, - "shouldFail": true + "example": 634 }, { "section": "[extension] Autolinks", "html": "

xmpp:foo@bar.baz/txt/bin

", "markdown": "xmpp:foo@bar.baz/txt/bin", - "example": 635, - "shouldFail": true + "example": 635 }, { "section": "[extension] Disallowed Raw HTML", diff --git a/test/unit/Lexer.test.js b/test/unit/Lexer.test.js index 8064ff59b4..08d8760fbf 100644 --- a/test/unit/Lexer.test.js +++ b/test/unit/Lexer.test.js @@ -2118,6 +2118,59 @@ paragraph ], }); }); + + it('url email protocols', () => { + expectInlineTokens({ + md: 'Contact mailto:foo@bar.baz and (xmpp:bar@baz.qux/resource).', + options: { gfm: true }, + tokens: [ + { + type: 'text', + raw: 'Contact ', + text: 'Contact ', + escaped: false, + }, + { + type: 'link', + raw: 'mailto:foo@bar.baz', + text: 'mailto:foo@bar.baz', + href: 'mailto:foo@bar.baz', + tokens: [ + { + type: 'text', + raw: 'mailto:foo@bar.baz', + text: 'mailto:foo@bar.baz', + }, + ], + }, + { + type: 'text', + raw: ' and (', + text: ' and (', + escaped: false, + }, + { + type: 'link', + raw: 'xmpp:bar@baz.qux/resource', + text: 'xmpp:bar@baz.qux/resource', + href: 'xmpp:bar@baz.qux/resource', + tokens: [ + { + type: 'text', + raw: 'xmpp:bar@baz.qux/resource', + text: 'xmpp:bar@baz.qux/resource', + }, + ], + }, + { + type: 'text', + raw: ').', + text: ').', + escaped: false, + }, + ], + }); + }); }); it('text', () => {