Skip to content

Commit e5cab1e

Browse files
committed
Merge branch 'develop' into master
2 parents 35a67a4 + c987270 commit e5cab1e

12 files changed

Lines changed: 377 additions & 195 deletions

File tree

dist/showdown.esm.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*! showdown v 3.0.0-rc2 - 04-07-2026 */
1+
/*! showdown v 3.0.0-rc2 - 05-07-2026 */
22
const showdown = (function () {
33
// noinspection HtmlRequiredLangAttribute
44

@@ -4227,6 +4227,10 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
42274227
const reEntity = /&(?:#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]*);/y;
42284228
// eslint-disable-next-line no-control-regex -- CommonMark autolinks exclude control chars (\x00-\x20) per spec
42294229
const reAutoUri = /<[A-Za-z][A-Za-z0-9+.-]{1,31}:[^<>\x00-\x20]*>/y;
4230+
// Showdown extension: <www...> angle autolinks (cmark-gfm does not autolink these, but the
4231+
// explicit <> is unambiguous user intent). Single variable class → linear / ReDoS-safe.
4232+
// eslint-disable-next-line no-control-regex -- same control-char exclusion as reAutoUri
4233+
const reAutoWww = /<www\.[^<>\x00-\x20]+>/y;
42304234
const reAutoEmail = /<[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>/y;
42314235
const reRawHtml = new RegExp('(?:' + showdown.helper.regexes.cmHTMLTagSource + ')', 'y');
42324236

@@ -4273,8 +4277,15 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
42734277
// An explicit scheme (http/https/ftp) does not require the host to contain a dot;
42744278
// a `www.` shortcut does (and is domain-validated below).
42754279
let nakedUrlRegex = /([_*~]*?)((?:(?:https?|ftp):\/\/[^\s<>"'`´]+|www\.[^\s<>"'`´.-][^\s<>"'`´]*?\.[a-z\d.]+[^\s<>"']*))\1/gi;
4276-
text = text.replace(nakedUrlRegex, function (wholeMatch, leadingMDChars, url) {
4280+
text = text.replace(nakedUrlRegex, function (wholeMatch, leadingMDChars, url, offset, fullText) {
42774281
let isWww = /^www\./i.test(url);
4282+
// GFM boundary rule: a "www." autolink (unlike a scheme URL) is not recognized when
4283+
// preceded by "<". By this pass "<" has been escaped to "&lt;", so a www match sitting
4284+
// right after it (e.g. the interior of a malformed <www.x.com foo> the angle recognizer
4285+
// could not consume) is left untouched — matching cmark-gfm, which links <https://x bim>
4286+
// but not <www.x bim>.
4287+
let urlStart = offset + leadingMDChars.length;
4288+
if (isWww && fullText.substring(urlStart - 4, urlStart) === '&lt;') { return wholeMatch; }
42784289
// we now will start traversing the url from the front to back, looking for punctuation chars [_*~,;:.!?\)\]]
42794290
const len = url.length;
42804291
let suffix = '';
@@ -4957,6 +4968,24 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
49574968
}
49584969
return {html: showdown.helper._hashHTMLSpan('<a href="' + href + '">' + txt + '</a>', globals), end: i + email[0].length};
49594970
}
4971+
// Showdown extension: <www...> angle autolink. Applies the GFM www rule (http(s)://
4972+
// prepend + domain validation) to the explicitly delimited form. Active whenever
4973+
// cmSpec is on (the early `!options.cmSpec` guard above already scopes this).
4974+
reAutoWww.lastIndex = i;
4975+
let www = reAutoWww.exec(str);
4976+
if (www) {
4977+
let raw = www[0].slice(1, -1),
4978+
host = raw.split(/[/?#]/)[0];
4979+
// GFM www rule: the domain after "www." must contain a period, plus the shared host
4980+
// validation (>= 2 labels, last two labels have no "_").
4981+
if (host.slice(4).indexOf('.') !== -1 && validAutolinkHost(raw, true)) {
4982+
let full = (options.httpsAutoLinks ? 'https://' : 'http://') + raw,
4983+
href = showdown.helper.cmEncodeURI(full).replace(/&/g, '&amp;');
4984+
// safeMode: neutralize dangerous schemes but keep the visible text
4985+
if (options.safeMode && !showdown.helper.isSafeUrl(full)) { href = ''; }
4986+
return {html: showdown.helper._hashHTMLSpan('<a href="' + href + '">' + escapeAngles(raw) + '</a>', globals), end: i + www[0].length};
4987+
}
4988+
}
49604989
return null;
49614990
}
49624991

@@ -12704,9 +12733,7 @@ showdown.subParser('makehtml.table', function (text, options, globals) {
1270412733
headerText = headerText.trim();
1270512734
headerText = showdown.subParser('makehtml.spanGamut')(headerText, options, globals);
1270612735

12707-
// support both tablesHeaderId and tableHeaderId due to error in documentation so we don't break backwards compatibility
12708-
// TODO think about this option!!! and remove backwards compatibility
12709-
if (options.tablesHeaderId || options.tableHeaderId) {
12736+
if (options.tablesHeaderId) {
1271012737
attributes.id = headerText.replace(/ /g, '_').toLowerCase();
1271112738
}
1271212739
return '<th' + showdown.helper._populateAttributes(attributes) + '>' + headerText + '</th>\n';

dist/showdown.esm.min.js

Lines changed: 79 additions & 79 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/showdown.esm.min.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/showdown.js

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
;/*! showdown v 3.0.0-rc2 - 04-07-2026 */
1+
;/*! showdown v 3.0.0-rc2 - 05-07-2026 */
22
(function(){
33
// noinspection HtmlRequiredLangAttribute
44

@@ -4227,6 +4227,10 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
42274227
const reEntity = /&(?:#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]*);/y;
42284228
// eslint-disable-next-line no-control-regex -- CommonMark autolinks exclude control chars (\x00-\x20) per spec
42294229
const reAutoUri = /<[A-Za-z][A-Za-z0-9+.-]{1,31}:[^<>\x00-\x20]*>/y;
4230+
// Showdown extension: <www...> angle autolinks (cmark-gfm does not autolink these, but the
4231+
// explicit <> is unambiguous user intent). Single variable class → linear / ReDoS-safe.
4232+
// eslint-disable-next-line no-control-regex -- same control-char exclusion as reAutoUri
4233+
const reAutoWww = /<www\.[^<>\x00-\x20]+>/y;
42304234
const reAutoEmail = /<[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>/y;
42314235
const reRawHtml = new RegExp('(?:' + showdown.helper.regexes.cmHTMLTagSource + ')', 'y');
42324236

@@ -4273,8 +4277,15 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
42734277
// An explicit scheme (http/https/ftp) does not require the host to contain a dot;
42744278
// a `www.` shortcut does (and is domain-validated below).
42754279
let nakedUrlRegex = /([_*~]*?)((?:(?:https?|ftp):\/\/[^\s<>"'`´]+|www\.[^\s<>"'`´.-][^\s<>"'`´]*?\.[a-z\d.]+[^\s<>"']*))\1/gi;
4276-
text = text.replace(nakedUrlRegex, function (wholeMatch, leadingMDChars, url) {
4280+
text = text.replace(nakedUrlRegex, function (wholeMatch, leadingMDChars, url, offset, fullText) {
42774281
let isWww = /^www\./i.test(url);
4282+
// GFM boundary rule: a "www." autolink (unlike a scheme URL) is not recognized when
4283+
// preceded by "<". By this pass "<" has been escaped to "&lt;", so a www match sitting
4284+
// right after it (e.g. the interior of a malformed <www.x.com foo> the angle recognizer
4285+
// could not consume) is left untouched — matching cmark-gfm, which links <https://x bim>
4286+
// but not <www.x bim>.
4287+
let urlStart = offset + leadingMDChars.length;
4288+
if (isWww && fullText.substring(urlStart - 4, urlStart) === '&lt;') { return wholeMatch; }
42784289
// we now will start traversing the url from the front to back, looking for punctuation chars [_*~,;:.!?\)\]]
42794290
const len = url.length;
42804291
let suffix = '';
@@ -4957,6 +4968,24 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
49574968
}
49584969
return {html: showdown.helper._hashHTMLSpan('<a href="' + href + '">' + txt + '</a>', globals), end: i + email[0].length};
49594970
}
4971+
// Showdown extension: <www...> angle autolink. Applies the GFM www rule (http(s)://
4972+
// prepend + domain validation) to the explicitly delimited form. Active whenever
4973+
// cmSpec is on (the early `!options.cmSpec` guard above already scopes this).
4974+
reAutoWww.lastIndex = i;
4975+
let www = reAutoWww.exec(str);
4976+
if (www) {
4977+
let raw = www[0].slice(1, -1),
4978+
host = raw.split(/[/?#]/)[0];
4979+
// GFM www rule: the domain after "www." must contain a period, plus the shared host
4980+
// validation (>= 2 labels, last two labels have no "_").
4981+
if (host.slice(4).indexOf('.') !== -1 && validAutolinkHost(raw, true)) {
4982+
let full = (options.httpsAutoLinks ? 'https://' : 'http://') + raw,
4983+
href = showdown.helper.cmEncodeURI(full).replace(/&/g, '&amp;');
4984+
// safeMode: neutralize dangerous schemes but keep the visible text
4985+
if (options.safeMode && !showdown.helper.isSafeUrl(full)) { href = ''; }
4986+
return {html: showdown.helper._hashHTMLSpan('<a href="' + href + '">' + escapeAngles(raw) + '</a>', globals), end: i + www[0].length};
4987+
}
4988+
}
49604989
return null;
49614990
}
49624991

@@ -12704,9 +12733,7 @@ showdown.subParser('makehtml.table', function (text, options, globals) {
1270412733
headerText = headerText.trim();
1270512734
headerText = showdown.subParser('makehtml.spanGamut')(headerText, options, globals);
1270612735

12707-
// support both tablesHeaderId and tableHeaderId due to error in documentation so we don't break backwards compatibility
12708-
// TODO think about this option!!! and remove backwards compatibility
12709-
if (options.tablesHeaderId || options.tableHeaderId) {
12736+
if (options.tablesHeaderId) {
1271012737
attributes.id = headerText.replace(/ /g, '_').toLowerCase();
1271112738
}
1271212739
return '<th' + showdown.helper._populateAttributes(attributes) + '>' + headerText + '</th>\n';

dist/showdown.min.js

Lines changed: 79 additions & 79 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/showdown.min.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/commonmark.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ CommonMark spec:
4343
| Area | What it does |
4444
|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
4545
| Emphasis | Parse emphasis / strong emphasis with the CommonMark delimiter-run (flanking) algorithm. |
46-
| Autolinks | Recognize CommonMark autolinks `<scheme:uri>` and `<email>` without entity-encoding. |
46+
| Autolinks | Recognize CommonMark autolinks `<scheme:uri>` and `<email>` without entity-encoding, plus `<www.…>` (a Showdown extension — see [Known differences](#known-differences)). |
4747
| Links & images | Parse links, images and link reference definitions per the spec (balanced-paren and `<...>` destinations, backslash escapes, in-URL entity decoding, alt-text flattening). |
4848
| Inline raw HTML | Recognize inline raw HTML with the strict CommonMark grammar; malformed tags are escaped instead of passed through. |
4949
| HTML blocks | Recognize HTML blocks using the 7 CommonMark block types instead of Showdown's balanced-tag matching. |
@@ -141,6 +141,34 @@ Showdown emits both the bare language name and the `language-`-prefixed class.
141141
</code></pre>
142142
```
143143

144+
### Angle-bracket `www.` autolinks
145+
146+
CommonMark only autolinks `<…>` content that is a full URI with a scheme (e.g.
147+
`<https://example.com>`) or an email address. `<www.example.com>` has no scheme, so the spec leaves
148+
it as literal text. Showdown instead treats the explicit angle brackets as unambiguous user intent
149+
and links it, applying the GFM `www.` autolink rule (`http://` prefix, `https://` with
150+
[`httpsAutoLinks`](options.md#httpsautolinks), plus domain validation). This also matches Showdown's
151+
default (legacy) flavor, which has always linked `<www.…>`. Note this is a deliberate divergence from
152+
**cmark-gfm** too, which likewise leaves `<www.…>` as text.
153+
154+
=== "input"
155+
156+
```md
157+
<www.example.com>
158+
```
159+
160+
=== "Showdown output"
161+
162+
```html
163+
<p><a href="http://www.example.com">www.example.com</a></p>
164+
```
165+
166+
=== "CommonMark output"
167+
168+
```html
169+
<p>&lt;www.example.com&gt;</p>
170+
```
171+
144172
### Other intentional deviations
145173

146174
A few spec examples are intentionally not followed because they are self-contradictory, malformed,

docs/gfm.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ task lists, strikethrough, autolink literals, `@`-mentions, emoji, [footnotes](o
3636
* **Tables** — pipe tables with per-column alignment.
3737
* **Task lists**`- [ ]` / `- [x]` checkboxes.
3838
* **Strikethrough**`~~text~~`.
39-
* **Autolink literals** — bare URLs become links without `<>`.
39+
* **Autolink literals** — bare URLs become links without `<>`. Showdown additionally links
40+
`<www.…>` inside angle brackets (a deviation from the spec and cmark-gfm — see
41+
[Known differences](commonmark.md#known-differences)).
4042
* **`@`-mentions** and **emoji** (`:smile:`).
4143
* **[Footnotes](options.md#footnotes)**`[^id]` references and definitions (also reversible via
4244
[`makeMarkdown`](html-to-markdown.md#feature-options-matching-makehtml)).
@@ -45,7 +47,8 @@ task lists, strikethrough, autolink literals, `@`-mentions, emoji, [footnotes](o
4547

4648
Because the flavor uses the CommonMark base, **every
4749
[CommonMark known difference](commonmark.md#known-differences) also applies to `gfm`** — empty ATX
48-
headings, the dual fenced-code language classes, and the malformed thematic-break examples included.
50+
headings, the dual fenced-code language classes, angle-bracket `<www.…>` autolinks, and the malformed
51+
thematic-break examples included.
4952
The items below are the additional, intentional deviations specific to the GFM table extension.
5053

5154
### Table column alignment

src/subParsers/makehtml/cmInline.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
3434
const reEntity = /&(?:#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]*);/y;
3535
// eslint-disable-next-line no-control-regex -- CommonMark autolinks exclude control chars (\x00-\x20) per spec
3636
const reAutoUri = /<[A-Za-z][A-Za-z0-9+.-]{1,31}:[^<>\x00-\x20]*>/y;
37+
// Showdown extension: <www...> angle autolinks (cmark-gfm does not autolink these, but the
38+
// explicit <> is unambiguous user intent). Single variable class → linear / ReDoS-safe.
39+
// eslint-disable-next-line no-control-regex -- same control-char exclusion as reAutoUri
40+
const reAutoWww = /<www\.[^<>\x00-\x20]+>/y;
3741
const reAutoEmail = /<[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*>/y;
3842
const reRawHtml = new RegExp('(?:' + showdown.helper.regexes.cmHTMLTagSource + ')', 'y');
3943

@@ -80,8 +84,15 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
8084
// An explicit scheme (http/https/ftp) does not require the host to contain a dot;
8185
// a `www.` shortcut does (and is domain-validated below).
8286
let nakedUrlRegex = /([_*~]*?)((?:(?:https?|ftp):\/\/[^\s<>"'`´]+|www\.[^\s<>"'`´.-][^\s<>"'`´]*?\.[a-z\d.]+[^\s<>"']*))\1/gi;
83-
text = text.replace(nakedUrlRegex, function (wholeMatch, leadingMDChars, url) {
87+
text = text.replace(nakedUrlRegex, function (wholeMatch, leadingMDChars, url, offset, fullText) {
8488
let isWww = /^www\./i.test(url);
89+
// GFM boundary rule: a "www." autolink (unlike a scheme URL) is not recognized when
90+
// preceded by "<". By this pass "<" has been escaped to "&lt;", so a www match sitting
91+
// right after it (e.g. the interior of a malformed <www.x.com foo> the angle recognizer
92+
// could not consume) is left untouched — matching cmark-gfm, which links <https://x bim>
93+
// but not <www.x bim>.
94+
let urlStart = offset + leadingMDChars.length;
95+
if (isWww && fullText.substring(urlStart - 4, urlStart) === '&lt;') { return wholeMatch; }
8596
// we now will start traversing the url from the front to back, looking for punctuation chars [_*~,;:.!?\)\]]
8697
const len = url.length;
8798
let suffix = '';
@@ -764,6 +775,24 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) {
764775
}
765776
return {html: showdown.helper._hashHTMLSpan('<a href="' + href + '">' + txt + '</a>', globals), end: i + email[0].length};
766777
}
778+
// Showdown extension: <www...> angle autolink. Applies the GFM www rule (http(s)://
779+
// prepend + domain validation) to the explicitly delimited form. Active whenever
780+
// cmSpec is on (the early `!options.cmSpec` guard above already scopes this).
781+
reAutoWww.lastIndex = i;
782+
let www = reAutoWww.exec(str);
783+
if (www) {
784+
let raw = www[0].slice(1, -1),
785+
host = raw.split(/[/?#]/)[0];
786+
// GFM www rule: the domain after "www." must contain a period, plus the shared host
787+
// validation (>= 2 labels, last two labels have no "_").
788+
if (host.slice(4).indexOf('.') !== -1 && validAutolinkHost(raw, true)) {
789+
let full = (options.httpsAutoLinks ? 'https://' : 'http://') + raw,
790+
href = showdown.helper.cmEncodeURI(full).replace(/&/g, '&amp;');
791+
// safeMode: neutralize dangerous schemes but keep the visible text
792+
if (options.safeMode && !showdown.helper.isSafeUrl(full)) { href = ''; }
793+
return {html: showdown.helper._hashHTMLSpan('<a href="' + href + '">' + escapeAngles(raw) + '</a>', globals), end: i + www[0].length};
794+
}
795+
}
767796
return null;
768797
}
769798

src/subParsers/makehtml/table.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,7 @@ showdown.subParser('makehtml.table', function (text, options, globals) {
340340
headerText = headerText.trim();
341341
headerText = showdown.subParser('makehtml.spanGamut')(headerText, options, globals);
342342

343-
// support both tablesHeaderId and tableHeaderId due to error in documentation so we don't break backwards compatibility
344-
// TODO think about this option!!! and remove backwards compatibility
345-
if (options.tablesHeaderId || options.tableHeaderId) {
343+
if (options.tablesHeaderId) {
346344
attributes.id = headerText.replace(/ /g, '_').toLowerCase();
347345
}
348346
return '<th' + showdown.helper._populateAttributes(attributes) + '>' + headerText + '</th>\n';

0 commit comments

Comments
 (0)