|
1 | | -/*! showdown v 3.0.0-rc2 - 04-07-2026 */ |
| 1 | +/*! showdown v 3.0.0-rc2 - 05-07-2026 */ |
2 | 2 | const showdown = (function () { |
3 | 3 | // noinspection HtmlRequiredLangAttribute |
4 | 4 |
|
@@ -4227,6 +4227,10 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) { |
4227 | 4227 | const reEntity = /&(?:#[0-9]{1,7}|#[xX][0-9a-fA-F]{1,6}|[a-zA-Z][a-zA-Z0-9]*);/y; |
4228 | 4228 | // eslint-disable-next-line no-control-regex -- CommonMark autolinks exclude control chars (\x00-\x20) per spec |
4229 | 4229 | 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; |
4230 | 4234 | 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; |
4231 | 4235 | const reRawHtml = new RegExp('(?:' + showdown.helper.regexes.cmHTMLTagSource + ')', 'y'); |
4232 | 4236 |
|
@@ -4273,8 +4277,15 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) { |
4273 | 4277 | // An explicit scheme (http/https/ftp) does not require the host to contain a dot; |
4274 | 4278 | // a `www.` shortcut does (and is domain-validated below). |
4275 | 4279 | 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) { |
4277 | 4281 | 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 "<", 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) === '<') { return wholeMatch; } |
4278 | 4289 | // we now will start traversing the url from the front to back, looking for punctuation chars [_*~,;:.!?\)\]] |
4279 | 4290 | const len = url.length; |
4280 | 4291 | let suffix = ''; |
@@ -4957,6 +4968,24 @@ showdown.subParser('makehtml.cmInline', function (text, options, globals) { |
4957 | 4968 | } |
4958 | 4969 | return {html: showdown.helper._hashHTMLSpan('<a href="' + href + '">' + txt + '</a>', globals), end: i + email[0].length}; |
4959 | 4970 | } |
| 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, '&'); |
| 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 | + } |
4960 | 4989 | return null; |
4961 | 4990 | } |
4962 | 4991 |
|
@@ -12704,9 +12733,7 @@ showdown.subParser('makehtml.table', function (text, options, globals) { |
12704 | 12733 | headerText = headerText.trim(); |
12705 | 12734 | headerText = showdown.subParser('makehtml.spanGamut')(headerText, options, globals); |
12706 | 12735 |
|
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) { |
12710 | 12737 | attributes.id = headerText.replace(/ /g, '_').toLowerCase(); |
12711 | 12738 | } |
12712 | 12739 | return '<th' + showdown.helper._populateAttributes(attributes) + '>' + headerText + '</th>\n'; |
|
0 commit comments