securemark 0.260.2 → 0.260.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/design.md +4 -0
- package/dist/index.js +51 -39
- package/package.json +5 -5
- package/src/parser/inline/autolink/account.ts +2 -4
- package/src/parser/inline/autolink/anchor.ts +1 -1
- package/src/parser/inline/autolink/email.test.ts +1 -0
- package/src/parser/inline/autolink/email.ts +2 -2
- package/src/parser/inline/autolink/hashtag.ts +1 -1
- package/src/parser/inline/autolink.ts +3 -3
- package/src/parser/inline/link.test.ts +10 -5
- package/src/parser/inline/link.ts +45 -27
package/CHANGELOG.md
CHANGED
package/design.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! securemark v0.260.
|
|
1
|
+
/*! securemark v0.260.3 https://github.com/falsandtru/securemark | (c) 2017, falsandtru | UNLICENSED License */
|
|
2
2
|
(function webpackUniversalModuleDefinition(root, factory) {
|
|
3
3
|
if(typeof exports === 'object' && typeof module === 'object')
|
|
4
4
|
module.exports = factory(require("DOMPurify"), require("Prism"));
|
|
@@ -5944,15 +5944,15 @@ const source_1 = __webpack_require__(6743);
|
|
|
5944
5944
|
|
|
5945
5945
|
const util_1 = __webpack_require__(9437);
|
|
5946
5946
|
|
|
5947
|
-
exports.autolink = (0, combinator_1.fmap)((0, combinator_1.validate)(/^(?:[@#>0-
|
|
5947
|
+
exports.autolink = (0, combinator_1.fmap)((0, combinator_1.validate)(/^(?:[@#>0-9a-z]|\S[#>])/i, (0, combinator_1.constraint)(2
|
|
5948
5948
|
/* State.autolink */
|
|
5949
5949
|
, false, (0, combinator_1.syntax)(2
|
|
5950
5950
|
/* Syntax.autolink */
|
|
5951
5951
|
, 1, 1, 0
|
|
5952
5952
|
/* State.none */
|
|
5953
5953
|
, (0, combinator_1.some)((0, combinator_1.union)([url_1.url, email_1.email, // Escape unmatched email-like strings.
|
|
5954
|
-
(0, source_1.str)(/^[0-
|
|
5955
|
-
(0, source_1.str)(/^@+[0-
|
|
5954
|
+
(0, source_1.str)(/^[0-9a-z]+(?:[.+_-][0-9a-z]+)*(?:@(?:[0-9a-z]+(?:[.-][0-9a-z]+)*)?)*/i), channel_1.channel, account_1.account, // Escape unmatched account-like strings.
|
|
5955
|
+
(0, source_1.str)(/^@+[0-9a-z]*(?:-[0-9a-z]+)*/i), // Escape invalid leading characters.
|
|
5956
5956
|
(0, source_1.str)(new RegExp(/^(?:[^\p{C}\p{S}\p{P}\s]|emoji|['_])(?=#)/u.source.replace('emoji', hashtag_1.emoji), 'u')), hashtag_1.hashtag, hashnum_1.hashnum, // Escape unmatched hashtag-like strings.
|
|
5957
5957
|
(0, source_1.str)(new RegExp(/^#+(?:[^\p{C}\p{S}\p{P}\s]|emoji|['_])*/u.source.replace('emoji', hashtag_1.emoji), 'u')), anchor_1.anchor]))))), ns => ns.length === 1 ? ns : [(0, util_1.stringify)(ns)]);
|
|
5958
5958
|
|
|
@@ -5980,7 +5980,7 @@ const dom_1 = __webpack_require__(3252); // https://example/@user must be a user
|
|
|
5980
5980
|
|
|
5981
5981
|
exports.account = (0, combinator_1.lazy)(() => (0, combinator_1.fmap)((0, combinator_1.rewrite)((0, combinator_1.constraint)(1
|
|
5982
5982
|
/* State.shortcut */
|
|
5983
|
-
, false, (0, combinator_1.open)('@', (0, combinator_1.tails)([(0, combinator_1.verify)((0, source_1.str)(/^[0-
|
|
5983
|
+
, false, (0, combinator_1.open)('@', (0, combinator_1.tails)([(0, combinator_1.verify)((0, source_1.str)(/^[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*\//i), ([source]) => source.length <= 253 + 1), (0, source_1.str)(/^[a-z](?:-(?=[0-9a-z])|[0-9a-z]){0,63}/i)]))), (0, combinator_1.convert)(source => `[${source}]{ ${source.includes('/') ? `https://${source.slice(1).replace('/', '/@')}` : `/${source}`} }`, (0, combinator_1.union)([link_1.unsafelink]))), ([el]) => [(0, dom_1.define)(el, {
|
|
5984
5984
|
class: 'account'
|
|
5985
5985
|
})]));
|
|
5986
5986
|
|
|
@@ -6011,7 +6011,7 @@ const dom_1 = __webpack_require__(3252); // Timeline(pseudonym): user/tid
|
|
|
6011
6011
|
|
|
6012
6012
|
exports.anchor = (0, combinator_1.lazy)(() => (0, combinator_1.validate)('>>', (0, combinator_1.fmap)((0, combinator_1.constraint)(1
|
|
6013
6013
|
/* State.shortcut */
|
|
6014
|
-
, false, (0, combinator_1.focus)(/^>>(?:[
|
|
6014
|
+
, false, (0, combinator_1.focus)(/^>>(?:[a-z][0-9a-z]*(?:-[0-9a-z]+)*\/)?[0-9a-z]+(?:-[0-9a-z]+)*(?![0-9a-z@#:])/i, (0, combinator_1.convert)(source => `[${source}]{ ${source.includes('/') ? `/@${source.slice(2).replace('/', '/timeline/')}` : `?at=${source.slice(2)}`} }`, (0, combinator_1.union)([link_1.unsafelink])))), ([el]) => [(0, dom_1.define)(el, {
|
|
6015
6015
|
class: 'anchor'
|
|
6016
6016
|
})])));
|
|
6017
6017
|
|
|
@@ -6070,7 +6070,7 @@ const source_1 = __webpack_require__(6743);
|
|
|
6070
6070
|
const dom_1 = __webpack_require__(3252); // https://html.spec.whatwg.org/multipage/input.html
|
|
6071
6071
|
|
|
6072
6072
|
|
|
6073
|
-
exports.email = (0, combinator_1.creation)((0, combinator_1.rewrite)((0, combinator_1.verify)((0, source_1.str)(/^[0-
|
|
6073
|
+
exports.email = (0, combinator_1.creation)((0, combinator_1.rewrite)((0, combinator_1.verify)((0, source_1.str)(/^[0-9a-z](?:[.+_-](?=[^\W_])|[0-9a-z]){0,255}@[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*(?![0-9a-z])/i), ([source]) => source.length <= 255), ({
|
|
6074
6074
|
source
|
|
6075
6075
|
}) => [[(0, dom_1.html)('a', {
|
|
6076
6076
|
class: 'email',
|
|
@@ -6133,7 +6133,7 @@ const dom_1 = __webpack_require__(3252); // https://example/hashtags/a must be a
|
|
|
6133
6133
|
exports.emoji = String.raw`\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F`;
|
|
6134
6134
|
exports.hashtag = (0, combinator_1.lazy)(() => (0, combinator_1.fmap)((0, combinator_1.rewrite)((0, combinator_1.constraint)(1
|
|
6135
6135
|
/* State.shortcut */
|
|
6136
|
-
, false, (0, combinator_1.open)('#', (0, combinator_1.tails)([(0, combinator_1.verify)((0, source_1.str)(/^[0-
|
|
6136
|
+
, false, (0, combinator_1.open)('#', (0, combinator_1.tails)([(0, combinator_1.verify)((0, source_1.str)(/^[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*\//i), ([source]) => source.length <= 253 + 1), (0, combinator_1.verify)((0, source_1.str)(new RegExp([/^(?=[0-9]{0,127}_?(?:[^\d\p{C}\p{S}\p{P}\s]|emoji))/u.source, /(?:[^\p{C}\p{S}\p{P}\s]|emoji|_(?=[^\p{C}\p{S}\p{P}\s]|emoji)){1,128}/u.source, /(?!_?(?:[^\p{C}\p{S}\p{P}\s]|emoji)|')/u.source].join('').replace(/emoji/g, exports.emoji), 'u')), ([source]) => source.length <= 128)]))), (0, combinator_1.convert)(source => `[${source}]{ ${source.includes('/') ? `https://${source.slice(1).replace('/', '/hashtags/')}` : `/hashtags/${source.slice(1)}`} }`, (0, combinator_1.union)([link_1.unsafelink]))), ([el]) => [(0, dom_1.define)(el, {
|
|
6137
6137
|
class: 'hashtag'
|
|
6138
6138
|
}, el.innerText)]));
|
|
6139
6139
|
|
|
@@ -6931,17 +6931,6 @@ const textlink = (0, combinator_1.lazy)(() => (0, combinator_1.constraint)(8
|
|
|
6931
6931
|
, 2, 10, 254
|
|
6932
6932
|
/* State.linkable */
|
|
6933
6933
|
, (0, combinator_1.bind)((0, combinator_1.reverse)((0, combinator_1.tails)([(0, combinator_1.dup)((0, combinator_1.surround)('[', (0, combinator_1.some)((0, combinator_1.union)([inline_1.inline]), ']', [[/^\\?\n/, 9], [']', 2]]), ']', true)), (0, combinator_1.dup)((0, combinator_1.surround)(/^{(?![{}])/, (0, combinator_1.inits)([exports.uri, (0, combinator_1.some)(exports.option)]), /^[^\S\n]*}/))])), ([params, content = []], rest, context) => {
|
|
6934
|
-
if (content.length !== 0 && (0, visibility_1.trimNode)(content).length === 0) return;
|
|
6935
|
-
|
|
6936
|
-
for (let source = (0, util_1.stringify)(content); source;) {
|
|
6937
|
-
const result = autolink({
|
|
6938
|
-
source,
|
|
6939
|
-
context
|
|
6940
|
-
});
|
|
6941
|
-
if (typeof (0, parser_1.eval)(result, [])[0] === 'object') return;
|
|
6942
|
-
source = (0, parser_1.exec)(result, '');
|
|
6943
|
-
}
|
|
6944
|
-
|
|
6945
6934
|
return parse(content, params, rest, context);
|
|
6946
6935
|
}))));
|
|
6947
6936
|
const medialink = (0, combinator_1.lazy)(() => (0, combinator_1.constraint)(8
|
|
@@ -6965,8 +6954,37 @@ const autolink = (0, combinator_1.state)(2
|
|
|
6965
6954
|
, autolink_1.autolink));
|
|
6966
6955
|
|
|
6967
6956
|
function parse(content, params, rest, context) {
|
|
6957
|
+
if (content.length !== 0 && (0, visibility_1.trimNode)(content).length === 0) return;
|
|
6958
|
+
content = (0, dom_1.defrag)(content);
|
|
6959
|
+
|
|
6960
|
+
for (let source = (0, util_1.stringify)(content); source;) {
|
|
6961
|
+
if (/^[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*:\/\/[^/?#]/i.test(source)) return;
|
|
6962
|
+
const result = autolink({
|
|
6963
|
+
source,
|
|
6964
|
+
context
|
|
6965
|
+
});
|
|
6966
|
+
if (typeof (0, parser_1.eval)(result, [])[0] === 'object') return;
|
|
6967
|
+
source = (0, parser_1.exec)(result, '');
|
|
6968
|
+
}
|
|
6969
|
+
|
|
6968
6970
|
const INSECURE_URI = params.shift();
|
|
6969
|
-
const
|
|
6971
|
+
const uri = new url_1.ReadonlyURL(resolve(INSECURE_URI, context.host ?? global_1.location, context.url ?? context.host ?? global_1.location), context.host?.href || global_1.location.href);
|
|
6972
|
+
|
|
6973
|
+
switch (uri.protocol) {
|
|
6974
|
+
case 'tel:':
|
|
6975
|
+
{
|
|
6976
|
+
const tel = content.length === 0 ? INSECURE_URI : content[0];
|
|
6977
|
+
const pattern = /^(?:tel:)?(?:\+(?!0))?\d+(?:-\d+)*$/i;
|
|
6978
|
+
|
|
6979
|
+
if (content.length <= 1 && typeof tel === 'string' && pattern.test(tel) && pattern.test(INSECURE_URI) && tel.replace(/[^+\d]/g, '') === INSECURE_URI.replace(/[^+\d]/g, '')) {
|
|
6980
|
+
break;
|
|
6981
|
+
}
|
|
6982
|
+
|
|
6983
|
+
return;
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
|
|
6987
|
+
const el = elem(INSECURE_URI, content, uri, context.host?.origin || global_1.location.origin);
|
|
6970
6988
|
if (el.className === 'invalid') return [[el], rest];
|
|
6971
6989
|
return [[(0, dom_1.define)(el, (0, html_1.attributes)('link', [], optspec, params))], rest];
|
|
6972
6990
|
}
|
|
@@ -6991,23 +7009,10 @@ function elem(INSECURE_URI, content, uri, origin) {
|
|
|
6991
7009
|
}, content.length === 0 ? decode(INSECURE_URI) : content);
|
|
6992
7010
|
|
|
6993
7011
|
case 'tel:':
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
const pattern = /^(?:tel:)?(?:\+(?!0))?\d+(?:-\d+)*$/i;
|
|
6999
|
-
|
|
7000
|
-
switch (true) {
|
|
7001
|
-
case content.length === 1 && typeof content[0] === 'string' && pattern.test(INSECURE_URI) && pattern.test(content[0]) && INSECURE_URI.replace(/[^+\d]/g, '') === content[0].replace(/[^+\d]/g, ''):
|
|
7002
|
-
return (0, dom_1.html)('a', {
|
|
7003
|
-
class: 'tel',
|
|
7004
|
-
href: uri.source
|
|
7005
|
-
}, content);
|
|
7006
|
-
}
|
|
7007
|
-
|
|
7008
|
-
type = 'content';
|
|
7009
|
-
message = 'Invalid phone number';
|
|
7010
|
-
break;
|
|
7012
|
+
return (0, dom_1.html)('a', {
|
|
7013
|
+
class: 'tel',
|
|
7014
|
+
href: uri.source
|
|
7015
|
+
}, content.length === 0 ? [INSECURE_URI] : content);
|
|
7011
7016
|
}
|
|
7012
7017
|
|
|
7013
7018
|
return (0, dom_1.html)('a', {
|
|
@@ -7023,7 +7028,7 @@ function resolve(uri, host, source) {
|
|
|
7023
7028
|
case uri.slice(0, 2) === '^/':
|
|
7024
7029
|
const last = host.pathname.slice(host.pathname.lastIndexOf('/') + 1);
|
|
7025
7030
|
return last.includes('.') // isFile
|
|
7026
|
-
&& /^[0-9]*[
|
|
7031
|
+
&& /^[0-9]*[a-z][0-9a-z]*$/i.test(last.slice(last.lastIndexOf('.') + 1)) ? `${host.pathname.slice(0, -last.length)}${uri.slice(2)}` : `${host.pathname.replace(/\/?$/, '/')}${uri.slice(2)}`;
|
|
7027
7032
|
|
|
7028
7033
|
case host.origin === source.origin && host.pathname === source.pathname:
|
|
7029
7034
|
case uri.slice(0, 2) === '//':
|
|
@@ -7039,9 +7044,16 @@ exports.resolve = resolve;
|
|
|
7039
7044
|
|
|
7040
7045
|
function decode(uri) {
|
|
7041
7046
|
if (!uri.includes('%')) return uri;
|
|
7047
|
+
const origin = uri.match(/^[a-z](?:[-.](?=\w)|[0-9a-z])*:\/\/[^/?#]*/i)?.[0] ?? '';
|
|
7042
7048
|
|
|
7043
7049
|
try {
|
|
7044
|
-
|
|
7050
|
+
let path = (0, global_1.decodeURI)(uri.slice(origin.length));
|
|
7051
|
+
|
|
7052
|
+
if (!origin && /^[a-z](?:[-.](?=\w)|[0-9a-z])*:\/\/[^/?#]/i.test(path)) {
|
|
7053
|
+
path = uri.slice(origin.length);
|
|
7054
|
+
}
|
|
7055
|
+
|
|
7056
|
+
uri = origin + path;
|
|
7045
7057
|
} finally {
|
|
7046
7058
|
return uri.replace(/\s+/g, global_1.encodeURI);
|
|
7047
7059
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securemark",
|
|
3
|
-
"version": "0.260.
|
|
3
|
+
"version": "0.260.3",
|
|
4
4
|
"description": "Secure markdown renderer working on browsers for user input data.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"homepage": "https://github.com/falsandtru/securemark",
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"@types/mocha": "9.1.1",
|
|
35
35
|
"@types/power-assert": "1.5.8",
|
|
36
36
|
"@types/prismjs": "1.26.0",
|
|
37
|
-
"@typescript-eslint/parser": "^5.
|
|
37
|
+
"@typescript-eslint/parser": "^5.30.7",
|
|
38
38
|
"babel-loader": "^8.2.5",
|
|
39
39
|
"babel-plugin-unassert": "^3.2.0",
|
|
40
|
-
"concurrently": "^7.
|
|
41
|
-
"eslint": "^8.
|
|
40
|
+
"concurrently": "^7.3.0",
|
|
41
|
+
"eslint": "^8.20.0",
|
|
42
42
|
"eslint-plugin-redos": "^4.4.1",
|
|
43
43
|
"eslint-webpack-plugin": "^3.2.0",
|
|
44
44
|
"glob": "^8.0.3",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"karma-mocha": "^2.0.1",
|
|
50
50
|
"karma-power-assert": "^1.0.0",
|
|
51
51
|
"mocha": "^10.0.0",
|
|
52
|
-
"npm-check-updates": "^
|
|
52
|
+
"npm-check-updates": "^15.3.4",
|
|
53
53
|
"semver": "^7.3.7",
|
|
54
54
|
"spica": "0.0.573",
|
|
55
55
|
"ts-loader": "^9.3.1",
|
|
@@ -13,11 +13,9 @@ export const account: AutolinkParser.AccountParser = lazy(() => fmap(rewrite(
|
|
|
13
13
|
'@',
|
|
14
14
|
tails([
|
|
15
15
|
verify(
|
|
16
|
-
str(/^[0-
|
|
16
|
+
str(/^[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*\//i),
|
|
17
17
|
([source]) => source.length <= 253 + 1),
|
|
18
|
-
|
|
19
|
-
str(/^[A-Za-z][0-9A-Za-z]*(?:-[0-9A-Za-z]+)*/),
|
|
20
|
-
([source]) => source.length <= 64),
|
|
18
|
+
str(/^[a-z](?:-(?=[0-9a-z])|[0-9a-z]){0,63}/i),
|
|
21
19
|
]))),
|
|
22
20
|
convert(
|
|
23
21
|
source =>
|
|
@@ -16,7 +16,7 @@ import { define } from 'typed-dom/dom';
|
|
|
16
16
|
export const anchor: AutolinkParser.AnchorParser = lazy(() => validate('>>', fmap(
|
|
17
17
|
constraint(State.shortcut, false,
|
|
18
18
|
focus(
|
|
19
|
-
/^>>(?:[
|
|
19
|
+
/^>>(?:[a-z][0-9a-z]*(?:-[0-9a-z]+)*\/)?[0-9a-z]+(?:-[0-9a-z]+)*(?![0-9a-z@#:])/i,
|
|
20
20
|
convert(
|
|
21
21
|
source =>
|
|
22
22
|
`[${source}]{ ${
|
|
@@ -21,6 +21,7 @@ describe('Unit: parser/inline/autolink/email', () => {
|
|
|
21
21
|
assert.deepStrictEqual(inspect(parser('a@@')), [['a@@'], '']);
|
|
22
22
|
assert.deepStrictEqual(inspect(parser('a@@b')), [['a@@b'], '']);
|
|
23
23
|
assert.deepStrictEqual(inspect(parser('a+@b')), [['a'], '+@b']);
|
|
24
|
+
assert.deepStrictEqual(inspect(parser('a__b@c')), [['a'], '__b@c']);
|
|
24
25
|
assert.deepStrictEqual(inspect(parser('a..b@c')), [['a'], '..b@c']);
|
|
25
26
|
assert.deepStrictEqual(inspect(parser('a++b@c')), [['a'], '++b@c']);
|
|
26
27
|
assert.deepStrictEqual(inspect(parser(`a@${'b'.repeat(64)}`)), [[`a@${'b'.repeat(64)}`], '']);
|
|
@@ -6,6 +6,6 @@ import { html } from 'typed-dom/dom';
|
|
|
6
6
|
// https://html.spec.whatwg.org/multipage/input.html
|
|
7
7
|
|
|
8
8
|
export const email: AutolinkParser.EmailParser = creation(rewrite(verify(
|
|
9
|
-
str(/^[0-
|
|
10
|
-
([source]) => source.
|
|
9
|
+
str(/^[0-9a-z](?:[.+_-](?=[^\W_])|[0-9a-z]){0,255}@[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*(?![0-9a-z])/i),
|
|
10
|
+
([source]) => source.length <= 255),
|
|
11
11
|
({ source }) => [[html('a', { class: 'email', href: `mailto:${source}` }, source)], '']));
|
|
@@ -16,7 +16,7 @@ export const hashtag: AutolinkParser.HashtagParser = lazy(() => fmap(rewrite(
|
|
|
16
16
|
'#',
|
|
17
17
|
tails([
|
|
18
18
|
verify(
|
|
19
|
-
str(/^[0-
|
|
19
|
+
str(/^[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*\//i),
|
|
20
20
|
([source]) => source.length <= 253 + 1),
|
|
21
21
|
verify(
|
|
22
22
|
str(new RegExp([
|
|
@@ -12,18 +12,18 @@ import { Syntax, State } from '../context';
|
|
|
12
12
|
import { stringify } from '../util';
|
|
13
13
|
|
|
14
14
|
export const autolink: AutolinkParser = fmap(
|
|
15
|
-
validate(/^(?:[@#>0-
|
|
15
|
+
validate(/^(?:[@#>0-9a-z]|\S[#>])/i,
|
|
16
16
|
constraint(State.autolink, false,
|
|
17
17
|
syntax(Syntax.autolink, 1, 1, State.none,
|
|
18
18
|
some(union([
|
|
19
19
|
url,
|
|
20
20
|
email,
|
|
21
21
|
// Escape unmatched email-like strings.
|
|
22
|
-
str(/^[0-
|
|
22
|
+
str(/^[0-9a-z]+(?:[.+_-][0-9a-z]+)*(?:@(?:[0-9a-z]+(?:[.-][0-9a-z]+)*)?)*/i),
|
|
23
23
|
channel,
|
|
24
24
|
account,
|
|
25
25
|
// Escape unmatched account-like strings.
|
|
26
|
-
str(/^@+[0-
|
|
26
|
+
str(/^@+[0-9a-z]*(?:-[0-9a-z]+)*/i),
|
|
27
27
|
// Escape invalid leading characters.
|
|
28
28
|
str(new RegExp(/^(?:[^\p{C}\p{S}\p{P}\s]|emoji|['_])(?=#)/u.source.replace('emoji', emoji), 'u')),
|
|
29
29
|
hashtag,
|
|
@@ -25,11 +25,16 @@ describe('Unit: parser/inline/link', () => {
|
|
|
25
25
|
assert.deepStrictEqual(inspect(parser('[https://host]{http://host}')), undefined);
|
|
26
26
|
assert.deepStrictEqual(inspect(parser('[[]{http://host}.com]{http://host}')), undefined);
|
|
27
27
|
assert.deepStrictEqual(inspect(parser('[[]{http://host/a}b]{http://host/ab}')), undefined);
|
|
28
|
-
assert.deepStrictEqual(inspect(parser('
|
|
29
|
-
assert.deepStrictEqual(inspect(parser('
|
|
30
|
-
assert.deepStrictEqual(inspect(parser('[
|
|
31
|
-
assert.deepStrictEqual(inspect(parser('[
|
|
32
|
-
assert.deepStrictEqual(inspect(parser('[
|
|
28
|
+
assert.deepStrictEqual(inspect(parser('{http%73://host}')), [['<a class="url" href="http%73://host">http%73://host</a>'], '']);
|
|
29
|
+
assert.deepStrictEqual(inspect(parser('{http://a%C3%A1}')), [['<a class="url" href="http://a%C3%A1" target="_blank">http://a%C3%A1</a>'], '']);
|
|
30
|
+
assert.deepStrictEqual(inspect(parser('[http://á]{http://evil}')), undefined);
|
|
31
|
+
assert.deepStrictEqual(inspect(parser('[xxx://á]{http://evil}')), undefined);
|
|
32
|
+
assert.deepStrictEqual(inspect(parser('[.http://á]{http://evil}')), undefined);
|
|
33
|
+
assert.deepStrictEqual(inspect(parser('[0987654321]{tel:1234567890}')), undefined);
|
|
34
|
+
assert.deepStrictEqual(inspect(parser('[1234567890-]{tel:1234567890}')), undefined);
|
|
35
|
+
assert.deepStrictEqual(inspect(parser('[-1234567890]{tel:1234567890}')), undefined);
|
|
36
|
+
assert.deepStrictEqual(inspect(parser('[123456789a]{tel:1234567890}')), undefined);
|
|
37
|
+
assert.deepStrictEqual(inspect(parser('[1234567890]{tel:ttel:1234567890}')), undefined);
|
|
33
38
|
//assert.deepStrictEqual(inspect(parser('[#a]{b}')), undefined);
|
|
34
39
|
//assert.deepStrictEqual(inspect(parser('[\\#a]{b}')), undefined);
|
|
35
40
|
//assert.deepStrictEqual(inspect(parser('[c #a]{b}')), undefined);
|
|
@@ -36,12 +36,6 @@ const textlink: LinkParser.TextLinkParser = lazy(() =>
|
|
|
36
36
|
])),
|
|
37
37
|
([params, content = []]: [string[], (HTMLElement | string)[]], rest, context) => {
|
|
38
38
|
assert(!html('div', content).querySelector('a, .media, .annotation, .reference'));
|
|
39
|
-
if (content.length !== 0 && trimNode(content).length === 0) return;
|
|
40
|
-
for (let source = stringify(content); source;) {
|
|
41
|
-
const result = autolink({ source, context });
|
|
42
|
-
if (typeof eval(result, [])[0] === 'object') return;
|
|
43
|
-
source = exec(result, '');
|
|
44
|
-
}
|
|
45
39
|
return parse(content, params, rest, context);
|
|
46
40
|
}))));
|
|
47
41
|
|
|
@@ -91,15 +85,40 @@ function parse(
|
|
|
91
85
|
): Result<HTMLAnchorElement, MarkdownParser.Context> {
|
|
92
86
|
assert(params.length > 0);
|
|
93
87
|
assert(params.every(p => typeof p === 'string'));
|
|
88
|
+
if (content.length !== 0 && trimNode(content).length === 0) return;
|
|
89
|
+
content = defrag(content);
|
|
90
|
+
for (let source = stringify(content); source;) {
|
|
91
|
+
if (/^[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*:\/\/[^/?#]/i.test(source)) return;
|
|
92
|
+
const result = autolink({ source, context });
|
|
93
|
+
if (typeof eval(result, [])[0] === 'object') return;
|
|
94
|
+
source = exec(result, '');
|
|
95
|
+
}
|
|
94
96
|
const INSECURE_URI = params.shift()!;
|
|
95
97
|
assert(INSECURE_URI === INSECURE_URI.trim());
|
|
96
98
|
assert(!INSECURE_URI.match(/\s/));
|
|
99
|
+
const uri = new ReadonlyURL(
|
|
100
|
+
resolve(INSECURE_URI, context.host ?? location, context.url ?? context.host ?? location),
|
|
101
|
+
context.host?.href || location.href);
|
|
102
|
+
switch (uri.protocol) {
|
|
103
|
+
case 'tel:': {
|
|
104
|
+
const tel = content.length === 0
|
|
105
|
+
? INSECURE_URI
|
|
106
|
+
: content[0];
|
|
107
|
+
const pattern = /^(?:tel:)?(?:\+(?!0))?\d+(?:-\d+)*$/i;
|
|
108
|
+
if (content.length <= 1 &&
|
|
109
|
+
typeof tel === 'string' &&
|
|
110
|
+
pattern.test(tel) &&
|
|
111
|
+
pattern.test(INSECURE_URI) &&
|
|
112
|
+
tel.replace(/[^+\d]/g, '') === INSECURE_URI.replace(/[^+\d]/g, '')) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
97
118
|
const el = elem(
|
|
98
119
|
INSECURE_URI,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
resolve(INSECURE_URI, context.host ?? location, context.url ?? context.host ?? location),
|
|
102
|
-
context.host?.href || location.href),
|
|
120
|
+
content,
|
|
121
|
+
uri,
|
|
103
122
|
context.host?.origin || location.origin);
|
|
104
123
|
if (el.className === 'invalid') return [[el], rest];
|
|
105
124
|
return [[define(el, attributes('link', [], optspec, params))], rest];
|
|
@@ -137,21 +156,15 @@ function elem(
|
|
|
137
156
|
? decode(INSECURE_URI)
|
|
138
157
|
: content);
|
|
139
158
|
case 'tel:':
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
&& INSECURE_URI.replace(/[^+\d]/g, '') === content[0].replace(/[^+\d]/g, ''):
|
|
150
|
-
return html('a', { class: 'tel', href: uri.source }, content);
|
|
151
|
-
}
|
|
152
|
-
type = 'content';
|
|
153
|
-
message = 'Invalid phone number';
|
|
154
|
-
break;
|
|
159
|
+
assert(content.length <= 1);
|
|
160
|
+
return html('a',
|
|
161
|
+
{
|
|
162
|
+
class: 'tel',
|
|
163
|
+
href: uri.source,
|
|
164
|
+
},
|
|
165
|
+
content.length === 0
|
|
166
|
+
? [INSECURE_URI]
|
|
167
|
+
: content);
|
|
155
168
|
}
|
|
156
169
|
return html('a',
|
|
157
170
|
{
|
|
@@ -172,7 +185,7 @@ export function resolve(uri: string, host: URL | Location, source: URL | Locatio
|
|
|
172
185
|
case uri.slice(0, 2) === '^/':
|
|
173
186
|
const last = host.pathname.slice(host.pathname.lastIndexOf('/') + 1);
|
|
174
187
|
return last.includes('.') // isFile
|
|
175
|
-
&& /^[0-9]*[
|
|
188
|
+
&& /^[0-9]*[a-z][0-9a-z]*$/i.test(last.slice(last.lastIndexOf('.') + 1))
|
|
176
189
|
? `${host.pathname.slice(0, -last.length)}${uri.slice(2)}`
|
|
177
190
|
: `${host.pathname.replace(/\/?$/, '/')}${uri.slice(2)}`;
|
|
178
191
|
case host.origin === source.origin
|
|
@@ -189,8 +202,13 @@ export function resolve(uri: string, host: URL | Location, source: URL | Locatio
|
|
|
189
202
|
|
|
190
203
|
function decode(uri: string): string {
|
|
191
204
|
if (!uri.includes('%')) return uri;
|
|
205
|
+
const origin = uri.match(/^[a-z](?:[-.](?=\w)|[0-9a-z])*:\/\/[^/?#]*/i)?.[0] ?? '';
|
|
192
206
|
try {
|
|
193
|
-
|
|
207
|
+
let path = decodeURI(uri.slice(origin.length));
|
|
208
|
+
if (!origin && /^[a-z](?:[-.](?=\w)|[0-9a-z])*:\/\/[^/?#]/i.test(path)) {
|
|
209
|
+
path = uri.slice(origin.length);
|
|
210
|
+
}
|
|
211
|
+
uri = origin + path;
|
|
194
212
|
}
|
|
195
213
|
finally {
|
|
196
214
|
return uri.replace(/\s+/g, encodeURI);
|