From f049c85efaea55aec4845a98376c5030aff80123 Mon Sep 17 00:00:00 2001 From: ocavue Date: Wed, 17 Jun 2026 06:41:10 +1000 Subject: [PATCH 1/4] feat: add common-TLD allowlist helper --- .../core/src/extensions/common-tlds.test.ts | 81 +++++++++++++++++++ packages/core/src/extensions/common-tlds.ts | 39 +++++++++ 2 files changed, 120 insertions(+) create mode 100644 packages/core/src/extensions/common-tlds.test.ts create mode 100644 packages/core/src/extensions/common-tlds.ts diff --git a/packages/core/src/extensions/common-tlds.test.ts b/packages/core/src/extensions/common-tlds.test.ts new file mode 100644 index 0000000..c0ff144 --- /dev/null +++ b/packages/core/src/extensions/common-tlds.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest' + +import { COMMON_TLDS, extractTld, hasAllowedTld } from './common-tlds.ts' + +describe('extractTld', () => { + it.each([ + ['https://example.com', 'com'], + ['http://example.com', 'com'], + ['www.example.com', 'com'], + ['me@example.com', 'com'], + ['mailto:me@example.com', 'com'], + ['xmpp:user@host.org', 'org'], + ['https://a.b.example.co.uk', 'uk'], + ['https://example.com:8080', 'com'], + ['https://example.com/a/b?x=1#y', 'com'], + ['https://user:pass@example.com', 'com'], + ['first.last@mail.example.com', 'com'], + ['https://EXAMPLE.COM', 'com'], + ['https://example.рф', 'рф'], + ['https://example.com.', 'com'], + ['http://192.168.1.1', '1'], + ])('extracts the TLD of %s as %s', (input, expected) => { + expect(extractTld(input)).toBe(expected) + }) + + it.each([['https://localhost'], ['mailto:foo'], ['nodots']])( + 'returns undefined for %s (no dotted host)', + (input) => { + expect(extractTld(input)).toBeUndefined() + }, + ) +}) + +describe('hasAllowedTld', () => { + it.each([ + 'https://example.com', + 'https://example.org', + 'https://example.io', + 'https://example.co', + 'https://a.example.co.uk', + 'https://example.dev', + 'https://example.app', + 'https://example.xyz', + 'https://example.рф', + 'me@example.com', + ])('allows %s', (input) => { + expect(hasAllowedTld(input)).toBe(true) + }) + + it.each([ + 'https://example.zzz', + 'https://example.invalidtld', + 'https://example.museum', + 'https://example.guru', + 'https://example.ninja', + 'https://example.123', + 'http://192.168.1.1', + 'https://example.c', + 'https://localhost', + ])('rejects %s', (input) => { + expect(hasAllowedTld(input)).toBe(false) + }) +}) + +describe('COMMON_TLDS', () => { + it('contains representative common TLDs', () => { + for (const tld of ['com', 'net', 'org', 'io', 'uk', 'co', 'om', 'qa', 'dev', 'рф']) { + expect(COMMON_TLDS.has(tld)).toBe(true) + } + }) + + it('excludes uncommon and invalid TLDs', () => { + for (const tld of ['zzz', 'museum', 'guru', 'invalidtld', '123']) { + expect(COMMON_TLDS.has(tld)).toBe(false) + } + }) + + it('pins the set size', () => { + expect(COMMON_TLDS.size).toMatchInlineSnapshot(`285`) + }) +}) diff --git a/packages/core/src/extensions/common-tlds.ts b/packages/core/src/extensions/common-tlds.ts new file mode 100644 index 0000000..908ad94 --- /dev/null +++ b/packages/core/src/extensions/common-tlds.ts @@ -0,0 +1,39 @@ +const GENERIC_TLDS = + 'com net org xyz info online top biz shop site icu store cyou club vip live рф app buzz tech space fun dev pro mobi life website cloud click work art asia tokyo blog link one world africa bar gov' + +const COUNTRY_TLDS = + 'ac ad ae af ag ai al am an ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bv bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr ga gb gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sj sk sl sm sn so sr st su sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tv tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw' + +/** + * The TLDs a bare GFM literal autolink is allowed to use. Common generic + * TLDs plus every two-letter country code, mirroring reflect-editor's + * curated "popular TLD" list. Explicit `<...>` autolinks and `[](...)` + * links are not subject to this set. + */ +export const COMMON_TLDS: ReadonlySet = new Set( + `${GENERIC_TLDS} ${COUNTRY_TLDS}`.split(' '), +) + +/** + * Pull the TLD (last domain label) out of an autolink's visible text. + * Handles a leading `scheme:`, protocol-relative `//`, userinfo or an + * email local part (`@`), a port, a path/query/hash, and a trailing dot. + * Returns the lowercased TLD, or `undefined` when there is no dotted host. + */ +export function extractTld(urlText: string): string | undefined { + let host = urlText.replace(/^[a-z][a-z0-9+.-]*:/i, '') + host = host.replace(/^\/\//, '') + const atIndex = host.lastIndexOf('@') + if (atIndex >= 0) host = host.slice(atIndex + 1) + host = host.split(/[/?#:]/)[0] + host = host.replace(/\.+$/, '') + const dotIndex = host.lastIndexOf('.') + if (dotIndex < 0) return undefined + return host.slice(dotIndex + 1).toLowerCase() +} + +/** Whether an autolink's visible text ends in an allowed common TLD. */ +export function hasAllowedTld(urlText: string): boolean { + const tld = extractTld(urlText) + return tld != null && COMMON_TLDS.has(tld) +} From 28f2689c96dd60bbe3a12de3a1ee28b5b3b29ddd Mon Sep 17 00:00:00 2001 From: ocavue Date: Wed, 17 Jun 2026 06:41:10 +1000 Subject: [PATCH 2/4] feat: restrict bare autolinks to common TLDs --- .../inline-text-to-mark-chunks.test.ts | 157 ++++++++++++++++++ .../extensions/inline-text-to-mark-chunks.ts | 30 +++- 2 files changed, 178 insertions(+), 9 deletions(-) diff --git a/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts b/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts index 77d97e0..49e5977 100644 --- a/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts +++ b/packages/core/src/extensions/inline-text-to-mark-chunks.test.ts @@ -254,6 +254,163 @@ describe('inlineTextToMarkChunks', () => { `) }) + // --- autolink TLD allowlist ---------------------------------------------- + + it('autolinks a two-letter ccTLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'https://example.io') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-18: mdLinkText(href=https://example.io) + " + `) + }) + + it('autolinks a multi-label host ending in a ccTLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'https://a.b.example.co.uk') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-25: mdLinkText(href=https://a.b.example.co.uk) + " + `) + }) + + it('autolinks an uppercase host with a lowercase scheme', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'https://EXAMPLE.COM') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-19: mdLinkText(href=https://EXAMPLE.COM) + " + `) + }) + + it('autolinks a URL with port, path, query and hash', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'https://example.com:8080/p?q=1#h') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-32: mdLinkText(href=https://example.com:8080/p?q=1#h) + " + `) + }) + + it('does not link a bare URL with an uncommon TLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'https://example.zzz') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-19: - + " + `) + }) + + it('does not link a bare URL with an uncommon TLD in a sentence', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'visit https://example.zzz now') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-29: - + " + `) + }) + + it('does not link a www URL with an uncommon TLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'see www.example.zzz here') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-24: - + " + `) + }) + + it('does not link an email with an uncommon TLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'mail me@example.zzz ok') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-22: - + " + `) + }) + + it('does not link a real but uncommon TLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'https://example.museum') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-22: - + " + `) + }) + + it('does not link a numeric TLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'https://example.123') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-19: - + " + `) + }) + + it('does not link a bare IP-literal host', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'http://192.168.1.1/path') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-23: - + " + `) + }) + + it('keeps an angle-bracket autolink with an uncommon TLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, '') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-1: mdMark + 1-20: mdLinkText(href=https://example.zzz) + 20-21: mdMark + " + `) + }) + + it('keeps an angle-bracket non-http scheme with an uncommon TLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, '') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-1: mdMark + 1-18: mdLinkText(href=ssh://example.zzz) + 18-19: mdMark + " + `) + }) + + it('keeps an explicit link with an uncommon TLD', () => { + const chunks = inlineTextToMarkChunks(markBuilders, '[text](https://example.zzz)') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-1: mdLinkText(href=https://example.zzz) + mdMark + 1-5: mdLinkText(href=https://example.zzz) + 5-7: mdMark + 7-26: mdLinkUri + 26-27: mdMark + " + `) + }) + + it('does not link a bare uncommon-TLD URL nested in emphasis', () => { + const chunks = inlineTextToMarkChunks(markBuilders, '*https://example.zzz*') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-1: mdEm + mdMark + 1-20: mdEm + 20-21: mdEm + mdMark + " + `) + }) + + it('links only the common-TLD URL when two autolinks share a block', () => { + const chunks = inlineTextToMarkChunks(markBuilders, 'https://a.com and https://b.zzz') + expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` + " + 0-13: mdLinkText(href=https://a.com) + 13-31: - + " + `) + }) + it('nested emphasis inside strong (***foo***)', () => { const chunks = inlineTextToMarkChunks(markBuilders, '***foo***') expect(foramtMarkChunks(chunks)).toMatchInlineSnapshot(` diff --git a/packages/core/src/extensions/inline-text-to-mark-chunks.ts b/packages/core/src/extensions/inline-text-to-mark-chunks.ts index 1cbb44c..4a7076e 100644 --- a/packages/core/src/extensions/inline-text-to-mark-chunks.ts +++ b/packages/core/src/extensions/inline-text-to-mark-chunks.ts @@ -4,6 +4,7 @@ import type { InlineElement } from '../lezer/inline.ts' import { parseInline } from '../lezer/inline.ts' import { LEZER_NODE_IDS } from '../lezer/node-ids.ts' +import { hasAllowedTld } from './common-tlds.ts' import type { MarkName } from './inline-marks.ts' import type { MarkChunk } from './mark-chunk.ts' import { marksEqual } from './marks-equal.ts' @@ -48,7 +49,7 @@ export function inlineTextToMarkChunks( ): MarkChunk[] { const elements = parseInline(text) const out: MarkChunk[] = [] - walk(elements, [], 0, text.length, text, marks, out) + walk(elements, [], 0, text.length, text, marks, out, false) return out } @@ -75,6 +76,7 @@ function walk( text: string, marks: TypedMarkBuilders, out: MarkChunk[], + insideAutolink: boolean, ): void { let pos = rangeStart for (const node of nodes) { @@ -82,14 +84,22 @@ function walk( emit(out, pos, node.from, parentMarks) } if (node.type === LEZER_NODE_IDS.Link || node.type === LEZER_NODE_IDS.Image) { - walkLink(node, parentMarks, text, marks, out) + walkLink(node, parentMarks, text, marks, out, insideAutolink) } else if (node.type === LEZER_NODE_IDS.URL) { // A standalone `URL` node is a GFM autolink (the address part of a real - // `[text](url)` is handled inside `walkLink`, not here). Linkify the - // shapes we recognize; anything else keeps the muted `mdLinkUri`. - const href = getAutolinkHref(text.slice(node.from, node.to)) - const mark = href ? marks.mdLinkText.create({ href }) : marks.mdLinkUri.create() - emit(out, node.from, node.to, [...parentMarks, mark]) + // `[text](url)` is handled inside `walkLink`, not here). Explicit `<...>` + // autolinks bypass the TLD allowlist; a bare literal autolink must end in + // a common TLD or it stays plain body text. An unrecognized URL shape + // keeps the muted `mdLinkUri`. + const urlText = text.slice(node.from, node.to) + const href = getAutolinkHref(urlText) + if (href && (insideAutolink || hasAllowedTld(urlText))) { + emit(out, node.from, node.to, [...parentMarks, marks.mdLinkText.create({ href })]) + } else if (href) { + emit(out, node.from, node.to, parentMarks) + } else { + emit(out, node.from, node.to, [...parentMarks, marks.mdLinkUri.create()]) + } } else { const maybeMarkName = MARK_NAME_BY_TYPE_ID.get(node.type) const childMarks = maybeMarkName @@ -98,7 +108,8 @@ function walk( if (node.children.length === 0) { emit(out, node.from, node.to, childMarks) } else { - walk(node.children, childMarks, node.from, node.to, text, marks, out) + const childInsideAutolink = insideAutolink || node.type === LEZER_NODE_IDS.Autolink + walk(node.children, childMarks, node.from, node.to, text, marks, out, childInsideAutolink) } } pos = node.to @@ -131,6 +142,7 @@ function walkLink( text: string, marks: TypedMarkBuilders, out: MarkChunk[], + insideAutolink: boolean, ): void { let labelEnd = -1 let urlNode: InlineElement | null = null @@ -161,7 +173,7 @@ function walkLink( if (child.children.length === 0) { emit(out, child.from, child.to, childMarks) } else { - walk(child.children, childMarks, child.from, child.to, text, marks, out) + walk(child.children, childMarks, child.from, child.to, text, marks, out, insideAutolink) } pos = child.to } From 78f0de8458a775a0dc4380aab301a7160f6aefbd Mon Sep 17 00:00:00 2001 From: ocavue Date: Wed, 17 Jun 2026 06:41:10 +1000 Subject: [PATCH 3/4] test: cover TLD-restricted autolinks across layers --- .../core/src/converters/roundtrip.test.ts | 6 ++ .../src/extensions/inline-mark-plugin.test.ts | 102 ++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/packages/core/src/converters/roundtrip.test.ts b/packages/core/src/converters/roundtrip.test.ts index b80f2c8..68e5966 100644 --- a/packages/core/src/converters/roundtrip.test.ts +++ b/packages/core/src/converters/roundtrip.test.ts @@ -75,6 +75,12 @@ describe('markdown round-trip is byte-identical', () => { 'mail me@example.com ok', 'a b', 'end https://example.com.', + // Uncommon-TLD autolinks render as plain text but must serialize unchanged + 'visit https://example.zzz now', + 'see www.foo.invalidtld here', + 'mail me@foo.zzz ok', + 'a b', + '[text](https://example.zzz)', // Embeds stay literal `![](url)` text; the embed renders as a decoration only '![](https://www.youtube.com/watch?v=dQw4w9WgXcQ)', '![](https://twitter.com/jack/status/20)', diff --git a/packages/core/src/extensions/inline-mark-plugin.test.ts b/packages/core/src/extensions/inline-mark-plugin.test.ts index 20a3f0a..f87c2e0 100644 --- a/packages/core/src/extensions/inline-mark-plugin.test.ts +++ b/packages/core/src/extensions/inline-mark-plugin.test.ts @@ -66,6 +66,108 @@ describe('inlineMarkPlugin', () => { expect(linkText!.attrs.href).toBe('https://example.com') }) + it('does not link a bare autolink with an uncommon TLD', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.paragraph('visit https://example.zzz now')) + fixture.set(doc) + + const pos = findText(fixture.doc, 'https://example.zzz') + expect(marksAt(fixture.doc, pos + 1)).toEqual([]) + }) + + it('does not link a www autolink with an uncommon TLD', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.paragraph('see www.foo.zzz here')) + fixture.set(doc) + + const pos = findText(fixture.doc, 'www.foo.zzz') + expect(marksAt(fixture.doc, pos + 1)).toEqual([]) + }) + + it('does not link an email autolink with an uncommon TLD', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.paragraph('mail me@foo.zzz ok')) + fixture.set(doc) + + const pos = findText(fixture.doc, 'me@foo.zzz') + expect(marksAt(fixture.doc, pos + 1)).toEqual([]) + }) + + it('keeps an angle-bracket autolink despite an uncommon TLD', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.paragraph('a b')) + fixture.set(doc) + + const pos = findText(fixture.doc, 'https://example.zzz') + const $pos = fixture.doc.resolve(pos + 1) + const linkText = $pos.marks().find((m) => m.type.name === 'mdLinkText') + expect(linkText?.attrs.href).toBe('https://example.zzz') + }) + + it('keeps an explicit link despite an uncommon TLD', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.paragraph('see [docs](https://example.zzz)')) + fixture.set(doc) + + const pos = findText(fixture.doc, 'docs') + const $pos = fixture.doc.resolve(pos + 1) + const linkText = $pos.marks().find((m) => m.type.name === 'mdLinkText') + expect(linkText?.attrs.href).toBe('https://example.zzz') + }) + + it('does not link an uncommon-TLD autolink inside a heading', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.heading({ level: 2 }, 'see https://x.zzz')) + fixture.set(doc) + + const pos = findText(fixture.doc, 'https://x.zzz') + expect(marksAt(fixture.doc, pos + 1)).toEqual([]) + }) + + it('links a common-TLD autolink inside a table cell', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.table(n.tableRow(n.tableCell(n.paragraph('go https://x.com'))))) + fixture.set(doc) + + const pos = findText(fixture.doc, 'https://x.com') + expect(marksAt(fixture.doc, pos + 1)).toEqual(['mdLinkText']) + }) + + it('drops mdLinkText when the TLD is edited to an uncommon one', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.paragraph('visit https://example.com now')) + fixture.set(doc) + + const url = findText(fixture.doc, 'https://example.com') + expect(marksAt(fixture.doc, url + 1)).toEqual(['mdLinkText']) + const com = findText(fixture.doc, 'com') + fixture.view.dispatch(fixture.state.tr.insertText('zzz', com, com + 3)) + const after = findText(fixture.doc, 'https://example.zzz') + expect(marksAt(fixture.doc, after + 1)).toEqual([]) + }) + + it('adds mdLinkText when the TLD is edited to a common one', () => { + using fixture = setupFixture() + const { n } = fixture + const doc = n.doc(n.paragraph('visit https://example.zzz now')) + fixture.set(doc) + + const url = findText(fixture.doc, 'https://example.zzz') + expect(marksAt(fixture.doc, url + 1)).toEqual([]) + const zzz = findText(fixture.doc, 'zzz') + fixture.view.dispatch(fixture.state.tr.insertText('com', zzz, zzz + 3)) + const after = findText(fixture.doc, 'https://example.com') + expect(marksAt(fixture.doc, after + 1)).toEqual(['mdLinkText']) + }) + it('marks `*foo*` inside headings as well', () => { using fixture = setupFixture() const { n } = fixture From 3b340d4e35a9cde8ec1aafbb0f833188d9a91e1c Mon Sep 17 00:00:00 2001 From: ocavue Date: Wed, 17 Jun 2026 07:01:59 +1000 Subject: [PATCH 4/4] fix: add missing de, pm and tt ccTLDs --- packages/core/src/extensions/common-tlds.test.ts | 2 +- packages/core/src/extensions/common-tlds.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/extensions/common-tlds.test.ts b/packages/core/src/extensions/common-tlds.test.ts index c0ff144..5613933 100644 --- a/packages/core/src/extensions/common-tlds.test.ts +++ b/packages/core/src/extensions/common-tlds.test.ts @@ -76,6 +76,6 @@ describe('COMMON_TLDS', () => { }) it('pins the set size', () => { - expect(COMMON_TLDS.size).toMatchInlineSnapshot(`285`) + expect(COMMON_TLDS.size).toMatchInlineSnapshot(`288`) }) }) diff --git a/packages/core/src/extensions/common-tlds.ts b/packages/core/src/extensions/common-tlds.ts index 908ad94..9125366 100644 --- a/packages/core/src/extensions/common-tlds.ts +++ b/packages/core/src/extensions/common-tlds.ts @@ -2,7 +2,7 @@ const GENERIC_TLDS = 'com net org xyz info online top biz shop site icu store cyou club vip live рф app buzz tech space fun dev pro mobi life website cloud click work art asia tokyo blog link one world africa bar gov' const COUNTRY_TLDS = - 'ac ad ae af ag ai al am an ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bv bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr ga gb gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sj sk sl sm sn so sr st su sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tv tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw' + 'ac ad ae af ag ai al am an ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bv bw by bz ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr ga gb gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sj sk sl sm sn so sr st su sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tt tv tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw' /** * The TLDs a bare GFM literal autolink is allowed to use. Common generic