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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.260.3
4
+
5
+ - Refactoring.
6
+
3
7
  ## 0.260.2
4
8
 
5
9
  - Refactoring.
package/design.md CHANGED
@@ -332,3 +332,7 @@ Data URIは保存および転送容量削減ならびにユーザーおよび管
332
332
  - \<small>: 法的表記を縮小表示すべきでないため削除。
333
333
  - \<sub>: \<small>の代わりに使用されないよう削除。他の構文との相性も悪い。
334
334
  - \<sup>: 同上。
335
+
336
+ ### _ emphasis
337
+
338
+ オートリンクと非常に相性が悪いため不採用。
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! securemark v0.260.2 https://github.com/falsandtru/securemark | (c) 2017, falsandtru | UNLICENSED License */
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-9A-Za-z]|\S[#>])/, (0, combinator_1.constraint)(2
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-9A-Za-z]+(?:[.+_-][0-9A-Za-z]+)*(?:@(?:[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?)*/), channel_1.channel, account_1.account, // Escape unmatched account-like strings.
5955
- (0, source_1.str)(/^@+[0-9A-Za-z]*(?:-[0-9A-Za-z]+)*/), // Escape invalid leading characters.
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-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*\//), ([source]) => source.length <= 253 + 1), (0, combinator_1.verify)((0, source_1.str)(/^[A-Za-z][0-9A-Za-z]*(?:-[0-9A-Za-z]+)*/), ([source]) => source.length <= 64)]))), (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, {
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)(/^>>(?:[A-Za-z][0-9A-Za-z]*(?:-[0-9A-Za-z]+)*\/)?[0-9A-Za-z]+(?:-[0-9A-Za-z]+)*(?![0-9A-Za-z@#:])/, (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, {
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-9A-Za-z]+(?:[.+_-][0-9A-Za-z]+)*@[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*(?![0-9A-Za-z])/), ([source]) => source.indexOf('@') <= 64 && source.length <= 255), ({
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-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*\//), ([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, {
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 el = elem(INSECURE_URI, (0, dom_1.defrag)(content), 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), context.host?.origin || global_1.location.origin);
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
- if (content.length === 0) {
6995
- content = [INSECURE_URI];
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]*[A-Za-z][0-9A-Za-z]*$/.test(last.slice(last.lastIndexOf('.') + 1)) ? `${host.pathname.slice(0, -last.length)}${uri.slice(2)}` : `${host.pathname.replace(/\/?$/, '/')}${uri.slice(2)}`;
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
- uri = (0, global_1.decodeURI)(uri);
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.2",
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.29.0",
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.2.2",
41
- "eslint": "^8.18.0",
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": "^14.1.1",
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-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*\//),
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
- verify(
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
- /^>>(?:[A-Za-z][0-9A-Za-z]*(?:-[0-9A-Za-z]+)*\/)?[0-9A-Za-z]+(?:-[0-9A-Za-z]+)*(?![0-9A-Za-z@#:])/,
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-9A-Za-z]+(?:[.+_-][0-9A-Za-z]+)*@[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*(?![0-9A-Za-z])/),
10
- ([source]) => source.indexOf('@') <= 64 && source.length <= 255),
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-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*\//),
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-9A-Za-z]|\S[#>])/,
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-9A-Za-z]+(?:[.+_-][0-9A-Za-z]+)*(?:@(?:[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?)*/),
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-9A-Za-z]*(?:-[0-9A-Za-z]+)*/),
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('[0987654321]{tel:1234567890}')), [['<a class="invalid">0987654321</a>'], '']);
29
- assert.deepStrictEqual(inspect(parser('[1234567890-]{tel:1234567890}')), [[`<a class="invalid">1234567890-</a>`], '']);
30
- assert.deepStrictEqual(inspect(parser('[-1234567890]{tel:1234567890}')), [[`<a class="invalid">-1234567890</a>`], '']);
31
- assert.deepStrictEqual(inspect(parser('[123456789a]{tel:1234567890}')), [['<a class="invalid">123456789a</a>'], '']);
32
- assert.deepStrictEqual(inspect(parser('[1234567890]{tel:ttel:1234567890}')), [['<a class="invalid">1234567890</a>'], '']);
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
- defrag(content),
100
- new ReadonlyURL(
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
- if (content.length === 0) {
141
- content = [INSECURE_URI];
142
- }
143
- const pattern = /^(?:tel:)?(?:\+(?!0))?\d+(?:-\d+)*$/i;
144
- switch (true) {
145
- case content.length === 1
146
- && typeof content[0] === 'string'
147
- && pattern.test(INSECURE_URI)
148
- && pattern.test(content[0])
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]*[A-Za-z][0-9A-Za-z]*$/.test(last.slice(last.lastIndexOf('.') + 1))
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
- uri = decodeURI(uri);
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);