securemark 0.294.5 → 0.294.7

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.294.7
4
+
5
+ - Refactoring.
6
+
7
+ ## 0.294.6
8
+
9
+ - Refactoring.
10
+
3
11
  ## 0.294.5
4
12
 
5
13
  - Refactoring.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! securemark v0.294.5 https://github.com/falsandtru/securemark | (c) 2017, falsandtru | UNLICENSED License */
1
+ /*! securemark v0.294.7 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("Prism"), require("DOMPurify"));
@@ -2894,9 +2894,7 @@ function indent(opener, parser = false, separation = false) {
2894
2894
  } = context;
2895
2895
  context.position = source.length;
2896
2896
  return new parser_1.List([new parser_1.Data(source.slice(position))]);
2897
- }))), ([indent]) => indent.length * 2 + -(indent[0] === ' '), [], 2 ** 4 - 1)), separation), (lines, context) => {
2898
- return parser((0, parser_1.subinput)(trimBlockEnd(lines.foldl((acc, node) => acc + node.value, '')), context));
2899
- }));
2897
+ }))), ([indent]) => indent.length * 2 + -(indent[0] === ' '), [], 2 ** 4 - 1)), separation), (lines, context) => parser((0, parser_1.subinput)(trimBlockEnd(lines.foldl((acc, node) => acc + node.value, '')), context))));
2900
2898
  }
2901
2899
  exports.indent = indent;
2902
2900
  function trimBlockEnd(block) {
@@ -3094,6 +3092,11 @@ function surround(opener, parser, closer, optional = false, f, g, backtracks = [
3094
3092
  case 'object':
3095
3093
  opener = (0, combinator_1.clear)((0, combinator_1.matcher)(opener, true));
3096
3094
  }
3095
+ switch (typeof parser) {
3096
+ case 'string':
3097
+ case 'object':
3098
+ parser = (0, combinator_1.clear)((0, combinator_1.matcher)(parser, true));
3099
+ }
3097
3100
  switch (typeof closer) {
3098
3101
  case 'string':
3099
3102
  case 'object':
@@ -3124,16 +3127,14 @@ function surround(opener, parser, closer, optional = false, f, g, backtracks = [
3124
3127
  if (!nodesM && !optional) {
3125
3128
  setBacktrack(context, backtracks, position);
3126
3129
  const result = g?.([nodesO, nodesM], context);
3127
- revert(context, linebreak);
3128
- return result;
3130
+ return result || void revert(context, linebreak);
3129
3131
  }
3130
3132
  const nodesC = nodesM || optional ? closer(input) : undefined;
3131
3133
  context.range = context.position - position;
3132
3134
  if (!nodesC) {
3133
3135
  setBacktrack(context, backtracks, position);
3134
3136
  const result = g?.([nodesO, nodesM], context);
3135
- revert(context, linebreak);
3136
- return result;
3137
+ return result || void revert(context, linebreak);
3137
3138
  }
3138
3139
  if (context.position === position) {
3139
3140
  return void revert(context, linebreak);
@@ -3142,10 +3143,8 @@ function surround(opener, parser, closer, optional = false, f, g, backtracks = [
3142
3143
  const result = f ? f([nodesO, nodesM, nodesC], context) : nodesO.import(nodesM ?? new parser_1.List()).import(nodesC);
3143
3144
  if (result) {
3144
3145
  context.linebreak ||= linebreak;
3145
- } else {
3146
- revert(context, linebreak);
3147
3146
  }
3148
- return result;
3147
+ return result || void revert(context, linebreak);
3149
3148
  });
3150
3149
  }
3151
3150
  exports.surround = surround;
@@ -3183,7 +3182,7 @@ function isBacktrack(context, backtracks, position = context.position, length =
3183
3182
  }
3184
3183
  exports.isBacktrack = isBacktrack;
3185
3184
  function setBacktrack(context, backtracks, position, length = 1) {
3186
- // 以降バックトラックの可能性がなく記録不要の場合もあるが判別が面倒なので省略
3185
+ // バックトラックの可能性がなく記録不要の場合もあるが判別が面倒なので省略
3187
3186
  const {
3188
3187
  source
3189
3188
  } = context;
@@ -4396,15 +4395,16 @@ exports.normalize = normalize;
4396
4395
  function format(source) {
4397
4396
  return source.replace(/\r\n?/g, '\n');
4398
4397
  }
4398
+ const invalid = new RegExp([/(?![\t\r\n])[\x00-\x1F\x7F]/g.source, /(?!\u200D)[\u2006\u200B-\u200F\u202A-\u202F\u2060\uFEFF]|(?<![\u1820\u1821])\u180E/g.source, /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g.source].join('|'), 'g');
4399
4399
  function sanitize(source) {
4400
- return source.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]|[\u2006\u200B-\u200F\u202A-\u202F\u2060\uFEFF]|(?<![\u1820\u1821])\u180E/g, UNICODE_REPLACEMENT_CHARACTER).replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/g, char => char.length === 1 ? UNICODE_REPLACEMENT_CHARACTER : char);
4400
+ return source.replace(invalid, UNICODE_REPLACEMENT_CHARACTER);
4401
4401
  }
4402
4402
  // https://dev.w3.org/html5/html-author/charref
4403
4403
  // https://en.wikipedia.org/wiki/Whitespace_character
4404
4404
  exports.invisibleHTMLEntityNames = ['Tab', 'NewLine', 'NonBreakingSpace', 'nbsp', 'shy', 'ensp', 'emsp', 'emsp13', 'emsp14', 'numsp', 'puncsp', 'ThinSpace', 'thinsp', 'VeryThinSpace', 'hairsp', 'ZeroWidthSpace', 'NegativeVeryThinSpace', 'NegativeThinSpace', 'NegativeMediumSpace', 'NegativeThickSpace', 'zwj', 'zwnj', 'lrm', 'rlm', 'MediumSpace', 'NoBreak', 'ApplyFunction', 'af', 'InvisibleTimes', 'it', 'InvisibleComma', 'ic'];
4405
- const unreadableHTMLEntityNames = exports.invisibleHTMLEntityNames.slice(2);
4406
- const unreadableEscapableCharacters = unreadableHTMLEntityNames.map(name => (0, htmlentity_1.unsafehtmlentity)((0, parser_1.input)(`&${name};`, {})).head.value);
4407
- const unreadableEscapableCharacter = new RegExp(`[${unreadableEscapableCharacters.join('')}]`, 'g');
4405
+ const unreadableEscapeHTMLEntityNames = exports.invisibleHTMLEntityNames.filter(name => !['Tab', 'NewLine', 'NonBreakingSpace', 'nbsp', 'zwj', 'zwnj'].includes(name));
4406
+ const unreadableEscapeCharacters = unreadableEscapeHTMLEntityNames.map(name => (0, htmlentity_1.unsafehtmlentity)((0, parser_1.input)(`&${name};`, {})).head.value);
4407
+ const unreadableEscapeCharacter = new RegExp(`[${unreadableEscapeCharacters.join('')}]`, 'g');
4408
4408
  // https://www.pandanoir.info/entry/2018/03/11/193000
4409
4409
  // http://anti.rosx.net/etc/memo/002_space.html
4410
4410
  // http://nicowiki.com/%E7%A9%BA%E7%99%BD%E3%83%BB%E7%89%B9%E6%AE%8A%E8%A8%98%E5%8F%B7.html
@@ -4416,7 +4416,7 @@ const unreadableSpecialCharacters = (/* unused pure expression or super */ null
4416
4416
  // ZERO WIDTH NON-JOINER
4417
4417
  '\u200C',
4418
4418
  // ZERO WIDTH JOINER
4419
- '\u200D',
4419
+ //'\u200D',
4420
4420
  // LEFT-TO-RIGHT MARK
4421
4421
  '\u200E',
4422
4422
  // RIGHT-TO-LEFT MARK
@@ -4439,7 +4439,7 @@ const unreadableSpecialCharacters = (/* unused pure expression or super */ null
4439
4439
  '\uFEFF']));
4440
4440
  // 特殊不可視文字はエディタおよびソースビューアでは等幅および強調表示により可視化する
4441
4441
  function escape(source) {
4442
- return source.replace(unreadableEscapableCharacter, char => `&${unreadableHTMLEntityNames[unreadableEscapableCharacters.indexOf(char)]};`);
4442
+ return source.replace(unreadableEscapeCharacter, char => `&${unreadableEscapeHTMLEntityNames[unreadableEscapeCharacters.indexOf(char)]};`);
4443
4443
  }
4444
4444
  exports.escape = escape;
4445
4445
 
@@ -6041,6 +6041,7 @@ const reference_1 = __webpack_require__(9047);
6041
6041
  const template_1 = __webpack_require__(4510);
6042
6042
  const remark_1 = __webpack_require__(8948);
6043
6043
  const extension_1 = __webpack_require__(2743);
6044
+ const label_1 = __webpack_require__(2178);
6044
6045
  const link_1 = __webpack_require__(3628);
6045
6046
  const ruby_1 = __webpack_require__(7304);
6046
6047
  const html_1 = __webpack_require__(5013);
@@ -6096,7 +6097,7 @@ exports.inline = (0, combinator_1.lazy)(() => (0, combinator_1.union)([input =>
6096
6097
  return (0, html_1.html)(input);
6097
6098
  case '$':
6098
6099
  if (source[position + 1] === '{') return (0, math_1.math)(input);
6099
- return (0, extension_1.extension)(input) || (0, math_1.math)(input);
6100
+ return (0, label_1.label)(input) || (0, math_1.math)(input);
6100
6101
  case '+':
6101
6102
  if (source[position + 1] === '+') return (0, insertion_1.insertion)(input);
6102
6103
  break;
@@ -6208,16 +6209,45 @@ const account_1 = __webpack_require__(4107);
6208
6209
  const hashtag_1 = __webpack_require__(5764);
6209
6210
  const hashnum_1 = __webpack_require__(8684);
6210
6211
  const anchor_1 = __webpack_require__(8535);
6211
- exports.autolink = (0, combinator_1.lazy)(() => (0, combinator_1.validate)(new RegExp([/(?<![0-9a-z])@/yi.source, /(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yiu.source, /(?<![0-9a-z])>>/yi.source, /(?<![0-9a-z][.+-]?|[@#])!?[0-9a-z]/yi.source].join('|').replace(/emoji/g, hashtag_1.emoji), 'yiu'), (0, combinator_1.state)(~1 /* State.autolink */, (0, combinator_1.union)([url_1.lineurl, url_1.url, email_1.email,
6212
- // Escape unmatched email-like strings.
6213
- //str(/[0-9a-z]+(?:[_.+-][0-9a-z]+[:@]?|:|@(?=@))*/yi),
6214
- channel_1.channel, account_1.account,
6215
- // Escape unmatched account-like strings.
6216
- //str(/@+(?:[0-9a-z]+(?:[_.+-][0-9a-z]+)*)?/yi),
6217
- hashtag_1.hashtag, hashnum_1.hashnum,
6218
- // Escape unmatched hashtag-like strings.
6219
- //str(new RegExp(/#+(?:(?:[^\p{C}\p{S}\p{P}\s]|emoji)+(?:['_.+-](?:[^\p{C}\p{S}\p{P}\s]|emoji)+)*)?/yu.source.replace(/emoji/g, emoji), 'yu')),
6220
- anchor_1.anchor]))));
6212
+ const text_1 = __webpack_require__(5655);
6213
+ exports.autolink = (0, combinator_1.lazy)(() => (0, combinator_1.state)(~1 /* State.autolink */, input => {
6214
+ const {
6215
+ context: {
6216
+ source,
6217
+ position
6218
+ }
6219
+ } = input;
6220
+ if (position === source.length) return;
6221
+ const fst = source[position];
6222
+ switch (fst) {
6223
+ case '@':
6224
+ return (0, channel_1.channel)(input) || (0, account_1.account)(input);
6225
+ case '#':
6226
+ return (0, hashtag_1.hashtag)(input) || (0, hashnum_1.hashnum)(input);
6227
+ case '>':
6228
+ return (0, anchor_1.anchor)(input);
6229
+ case '!':
6230
+ if (!source.startsWith('http', position + 1)) break;
6231
+ if (position === 0) return (0, url_1.lineurl)(input);
6232
+ switch (source[position - 1]) {
6233
+ case '\r':
6234
+ case '\n':
6235
+ return (0, url_1.lineurl)(input);
6236
+ }
6237
+ break;
6238
+ case 'h':
6239
+ if (!source.startsWith('http', position)) return;
6240
+ if (position === 0) return (0, url_1.lineurl)(input) || (0, url_1.url)(input) || (0, email_1.email)(input);
6241
+ switch (source[position - 1]) {
6242
+ case '\r':
6243
+ case '\n':
6244
+ return (0, url_1.lineurl)(input) || (0, url_1.url)(input) || (0, email_1.email)(input);
6245
+ }
6246
+ return (0, url_1.url)(input) || (0, email_1.email)(input);
6247
+ default:
6248
+ if ((0, text_1.isAlphanumeric)(fst)) return (0, email_1.email)(input);
6249
+ }
6250
+ }));
6221
6251
 
6222
6252
  /***/ },
6223
6253
 
@@ -6234,10 +6264,9 @@ exports.account = void 0;
6234
6264
  const parser_1 = __webpack_require__(605);
6235
6265
  const combinator_1 = __webpack_require__(3484);
6236
6266
  const link_1 = __webpack_require__(3628);
6237
- const source_1 = __webpack_require__(8745);
6238
6267
  const dom_1 = __webpack_require__(394);
6239
6268
  // https://example/@user must be a user page or a redirect page going there.
6240
- exports.account = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.surround)(/(?<![0-9a-z])@/yi, (0, source_1.str)(/[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//yi), (0, source_1.str)(/[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*(?![-.]?[0-9a-z@#]|>>|:\S)/yi), true, undefined, undefined, [3 | 0 /* Backtrack.autolink */]), (0, combinator_1.constraint)(1 /* State.autolink */, (0, combinator_1.state)(1 /* State.autolink */, (0, combinator_1.fmap)((0, combinator_1.convert)(source => `[${source}]{ ${source.includes('/') ? `https://${source.slice(1).replace('/', '/@')}` : `/${source}`} }`, (0, combinator_1.union)([link_1.unsafelink]), false), ([{
6269
+ exports.account = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.surround)(/(?<![0-9a-z])@/yi, /[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//yi, /[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*(?![-.]?[0-9a-z@#]|>>|:\S)/yi, true, undefined, undefined, [3 | 0 /* Backtrack.autolink */]), (0, combinator_1.constraint)(1 /* State.autolink */, (0, combinator_1.state)(1 /* State.autolink */, (0, combinator_1.fmap)((0, combinator_1.convert)(source => `[${source}]{ ${source.includes('/') ? `https://${source.slice(1).replace('/', '/@')}` : `/${source}`} }`, (0, combinator_1.union)([link_1.unsafelink]), false), ([{
6241
6270
  value
6242
6271
  }]) => new parser_1.List([new parser_1.Data((0, dom_1.define)(value, {
6243
6272
  class: 'account'
@@ -6258,7 +6287,6 @@ exports.anchor = void 0;
6258
6287
  const parser_1 = __webpack_require__(605);
6259
6288
  const combinator_1 = __webpack_require__(3484);
6260
6289
  const link_1 = __webpack_require__(3628);
6261
- const source_1 = __webpack_require__(8745);
6262
6290
  const dom_1 = __webpack_require__(394);
6263
6291
  // Timeline(pseudonym): user/tid
6264
6292
  // Thread(anonymous): cid
@@ -6267,7 +6295,7 @@ const dom_1 = __webpack_require__(394);
6267
6295
  // cid: YYYY-MMDD-HHMM-SSmmm
6268
6296
  // 内部表現はUnixTimeに統一する(時系列順)
6269
6297
  // 外部表現は投稿ごとに投稿者の投稿時のタイムゾーンに統一する(非時系列順)
6270
- exports.anchor = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.open)(/(?<![0-9a-z])>>/yi, (0, source_1.str)(/(?:[a-z][0-9a-z]*(?:-[0-9a-z]+)*\/)?[0-9a-z]+(?:-[0-9a-z]+)*(?!-?[0-9a-z@#]|>>|:\S)/yi), false, [3 | 0 /* Backtrack.autolink */]), (0, combinator_1.constraint)(1 /* State.autolink */, (0, combinator_1.state)(1 /* State.autolink */, (0, combinator_1.fmap)((0, combinator_1.convert)(source => `[${source}]{ ${source.includes('/') ? `/@${source.slice(2).replace('/', '/timeline?at=')}` : `?at=${source.slice(2)}`} }`, (0, combinator_1.union)([link_1.unsafelink]), false), ([{
6298
+ exports.anchor = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.open)(/(?<![0-9a-z])>>/yi, /(?:[a-z][0-9a-z]*(?:-[0-9a-z]+)*\/)?[0-9a-z]+(?:-[0-9a-z]+)*(?!-?[0-9a-z@#]|>>|:\S)/yi, false, [3 | 0 /* Backtrack.autolink */]), (0, combinator_1.constraint)(1 /* State.autolink */, (0, combinator_1.state)(1 /* State.autolink */, (0, combinator_1.fmap)((0, combinator_1.convert)(source => `[${source}]{ ${source.includes('/') ? `/@${source.slice(2).replace('/', '/timeline?at=')}` : `?at=${source.slice(2)}`} }`, (0, combinator_1.union)([link_1.unsafelink]), false), ([{
6271
6299
  value
6272
6300
  }]) => new parser_1.List([new parser_1.Data((0, dom_1.define)(value, {
6273
6301
  class: 'anchor'
@@ -6292,7 +6320,7 @@ const hashtag_1 = __webpack_require__(5764);
6292
6320
  const source_1 = __webpack_require__(8745);
6293
6321
  const dom_1 = __webpack_require__(394);
6294
6322
  // https://example/@user?ch=a+b must be a user channel page or a redirect page going there.
6295
- exports.channel = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.sequence)([(0, combinator_1.surround)(/(?<![0-9a-z])@/yi, (0, source_1.str)(/[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//yi), (0, source_1.str)(/[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*(?![-.]?[0-9a-z@]|>>|:\S)/yi), true, undefined, undefined, [3 | 0 /* Backtrack.autolink */]), (0, combinator_1.some)((0, combinator_1.verify)((0, combinator_1.surround)('#', (0, source_1.str)(new RegExp([/(?!['_])(?:[^\p{C}\p{S}\p{P}\s]|emoji|'(?=[0-9A-Za-z])|_(?=[^\p{C}\p{S}\p{P}\s]|emoji))+/yu.source].join('').replace(/emoji/g, hashtag_1.emoji), 'yu')), (0, source_1.str)(new RegExp([/(?![0-9a-z@]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source].join('').replace(/emoji/g, hashtag_1.emoji), 'yu')), false, undefined, undefined, [3 | 0 /* Backtrack.autolink */]), ([{
6323
+ exports.channel = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.sequence)([(0, combinator_1.surround)(/(?<![0-9a-z])@/yi, /[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//yi, /[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*(?![-.]?[0-9a-z@]|>>|:\S)/yi, true, undefined, undefined, [3 | 0 /* Backtrack.autolink */]), (0, combinator_1.some)((0, combinator_1.verify)((0, combinator_1.surround)('#', (0, source_1.str)(new RegExp([/(?!['_])(?:[^\p{C}\p{S}\p{P}\s]|emoji|'(?=[0-9A-Za-z])|_(?=[^\p{C}\p{S}\p{P}\s]|emoji))+/yu.source].join('|').replace(/emoji/g, hashtag_1.emoji.source), 'yu')), new RegExp([/(?![0-9a-z@]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source].join('|').replace(/emoji/g, hashtag_1.emoji.source), 'yu'), false, undefined, undefined, [3 | 0 /* Backtrack.autolink */]), ([{
6296
6324
  value
6297
6325
  }]) => !/^[0-9]{1,4}$|^[0-9]{5}/.test(value)))]), (0, combinator_1.constraint)(1 /* State.autolink */, (0, combinator_1.state)(1 /* State.autolink */, (0, combinator_1.fmap)((0, combinator_1.convert)(source => `[${source}]{ ${source.includes('/') ? `https://${source.slice(1, source.indexOf('#')).replace('/', '/@')}` : `/${source.slice(0, source.indexOf('#'))}`} }`, (0, combinator_1.union)([link_1.unsafelink]), false), ([{
6298
6326
  value: el
@@ -6353,9 +6381,8 @@ const parser_1 = __webpack_require__(605);
6353
6381
  const combinator_1 = __webpack_require__(3484);
6354
6382
  const link_1 = __webpack_require__(3628);
6355
6383
  const hashtag_1 = __webpack_require__(5764);
6356
- const source_1 = __webpack_require__(8745);
6357
6384
  const dom_1 = __webpack_require__(394);
6358
- exports.hashnum = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.open)(new RegExp([/(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yiu.source].join('').replace(/emoji/g, hashtag_1.emoji), 'yu'), (0, source_1.str)(new RegExp([/[0-9]{1,9}(?![0-9a-z@#]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source].join('').replace(/emoji/g, hashtag_1.emoji), 'yu')), false, [1 | 0 /* Backtrack.autolink */]), (0, combinator_1.constraint)(1 /* State.autolink */, (0, combinator_1.state)(1 /* State.autolink */, (0, combinator_1.fmap)((0, combinator_1.convert)(source => `[${source}]{ ${source.slice(1)} }`, (0, combinator_1.union)([link_1.unsafelink]), false), ([{
6385
+ exports.hashnum = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.open)(new RegExp([/(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yu.source].join('|').replace(/emoji/g, hashtag_1.emoji.source), 'yu'), new RegExp([/[0-9]{1,9}(?![0-9a-z@#]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source].join('|').replace(/emoji/g, hashtag_1.emoji.source), 'yu'), false, [1 | 0 /* Backtrack.autolink */]), (0, combinator_1.constraint)(1 /* State.autolink */, (0, combinator_1.state)(1 /* State.autolink */, (0, combinator_1.fmap)((0, combinator_1.convert)(source => `[${source}]{ ${source.slice(1)} }`, (0, combinator_1.union)([link_1.unsafelink]), false), ([{
6359
6386
  value
6360
6387
  }]) => new parser_1.List([new parser_1.Data((0, dom_1.define)(value, {
6361
6388
  class: 'hashnum',
@@ -6381,8 +6408,8 @@ const source_1 = __webpack_require__(8745);
6381
6408
  const dom_1 = __webpack_require__(394);
6382
6409
  // https://example/hashtags/a must be a hashtag page or a redirect page going there.
6383
6410
  // https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji
6384
- exports.emoji = String.raw`\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F`;
6385
- exports.hashtag = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.verify)((0, combinator_1.surround)(new RegExp([/(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yiu.source].join('').replace(/emoji/g, exports.emoji), 'yu'), (0, source_1.str)(new RegExp([/(?!['_])(?:[^\p{C}\p{S}\p{P}\s]|emoji|'(?=[0-9A-Za-z])|_(?=[^\p{C}\p{S}\p{P}\s]|emoji))+/yu.source].join('').replace(/emoji/g, exports.emoji), 'yu')), (0, source_1.str)(new RegExp([/(?![0-9a-z@#]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source].join('').replace(/emoji/g, exports.emoji), 'yu')), false, undefined, undefined, [3 | 0 /* Backtrack.autolink */]), ([{
6411
+ exports.emoji = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F|\u200D/u;
6412
+ exports.hashtag = (0, combinator_1.lazy)(() => (0, combinator_1.rewrite)((0, combinator_1.verify)((0, combinator_1.surround)(new RegExp([/(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yu.source].join('|').replace(/emoji/g, exports.emoji.source), 'yu'), (0, source_1.str)(new RegExp([/(?!['_])(?:[^\p{C}\p{S}\p{P}\s]|emoji|'(?=[0-9A-Za-z])|_(?=[^\p{C}\p{S}\p{P}\s]|emoji))+/yu.source].join('|').replace(/emoji/g, exports.emoji.source), 'yu')), new RegExp([/(?![0-9a-z@#]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source].join('|').replace(/emoji/g, exports.emoji.source), 'yu'), false, undefined, undefined, [3 | 0 /* Backtrack.autolink */]), ([{
6386
6413
  value
6387
6414
  }]) => !/^[0-9]{1,4}$|^[0-9]{5}/.test(value)), (0, combinator_1.constraint)(1 /* State.autolink */, (0, combinator_1.state)(1 /* State.autolink */, (0, combinator_1.fmap)((0, combinator_1.convert)(source => `[${source}]{ ${`/hashtags/${source.slice(1)}`} }`, (0, combinator_1.union)([link_1.unsafelink]), false), ([{
6388
6415
  value
@@ -7065,7 +7092,7 @@ Object.setPrototypeOf(attrspecs, null);
7065
7092
  Object.values(attrspecs).forEach(o => Object.setPrototypeOf(o, null));
7066
7093
  exports.html = (0, combinator_1.lazy)(() => (0, combinator_1.validate)(/<[a-z]+(?=[ >])/yi, (0, combinator_1.union)([(0, combinator_1.surround)(
7067
7094
  // https://html.spec.whatwg.org/multipage/syntax.html#void-elements
7068
- (0, source_1.str)(/<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[ >])/yi), (0, combinator_1.some)((0, combinator_1.union)([exports.attribute])), (0, combinator_1.open)((0, source_1.str)(/ ?/y), (0, source_1.str)('>'), true), true, ([as, bs = new parser_1.List(), cs], context) => new parser_1.List([new parser_1.Data(elem(as.head.value.slice(1), false, [...(0, util_1.unwrap)(as.import(bs).import(cs))], new parser_1.List(), new parser_1.List(), context))]), ([as, bs = new parser_1.List()], context) => new parser_1.List([new parser_1.Data(elem(as.head.value.slice(1), false, [...(0, util_1.unwrap)(as.import(bs))], new parser_1.List(), new parser_1.List(), context))])), (0, combinator_1.match)(new RegExp(String.raw`<(${TAGS.join('|')})(?=[ >])`, 'y'), (0, memoize_1.memoize)(([, tag]) => (0, combinator_1.surround)((0, combinator_1.surround)((0, source_1.str)(`<${tag}`), (0, combinator_1.some)(exports.attribute), (0, combinator_1.open)((0, source_1.str)(/ ?/y), (0, source_1.str)('>'), true), true, ([as, bs = new parser_1.List(), cs]) => as.import(bs).import(cs), ([as, bs = new parser_1.List()]) => as.import(bs)),
7095
+ (0, source_1.str)(/<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[ >])/y), (0, combinator_1.some)((0, combinator_1.union)([exports.attribute])), (0, combinator_1.open)((0, source_1.str)(/ ?/y), (0, source_1.str)('>'), true), true, ([as, bs = new parser_1.List(), cs], context) => new parser_1.List([new parser_1.Data(elem(as.head.value.slice(1), false, [...(0, util_1.unwrap)(as.import(bs).import(cs))], new parser_1.List(), new parser_1.List(), context))]), ([as, bs = new parser_1.List()], context) => new parser_1.List([new parser_1.Data(elem(as.head.value.slice(1), false, [...(0, util_1.unwrap)(as.import(bs))], new parser_1.List(), new parser_1.List(), context))])), (0, combinator_1.match)(new RegExp(String.raw`<(${TAGS.join('|')})(?=[ >])`, 'y'), (0, memoize_1.memoize)(([, tag]) => (0, combinator_1.surround)((0, combinator_1.surround)((0, source_1.str)(`<${tag}`), (0, combinator_1.some)(exports.attribute), (0, combinator_1.open)((0, source_1.str)(/ ?/y), (0, source_1.str)('>'), true), true, ([as, bs = new parser_1.List(), cs]) => as.import(bs).import(cs), ([as, bs = new parser_1.List()]) => as.import(bs)),
7069
7096
  // 不可視のHTML構造が可視構造を変化させるべきでない。
7070
7097
  // 可視のHTMLは優先度変更を検討する。
7071
7098
  // このため<>は将来的に共通構造を変化させる可能性があり
@@ -7140,19 +7167,10 @@ Object.defineProperty(exports, "__esModule", ({
7140
7167
  exports.htmlentity = exports.unsafehtmlentity = void 0;
7141
7168
  const parser_1 = __webpack_require__(605);
7142
7169
  const combinator_1 = __webpack_require__(3484);
7170
+ const source_1 = __webpack_require__(8745);
7143
7171
  const util_1 = __webpack_require__(4992);
7144
7172
  const dom_1 = __webpack_require__(394);
7145
- exports.unsafehtmlentity = (0, combinator_1.focus)(/&(?:[0-9A-Za-z]+;?)?/y,
7146
- //({ source }) => [[parser(source) ?? `${Command.Error}${source}`], '']));
7147
- ({
7148
- context
7149
- }) => {
7150
- const {
7151
- source
7152
- } = context;
7153
- context.position += source.length;
7154
- return source.length > 1 && source.at(-1) === ';' ? new parser_1.List([new parser_1.Data(parser(source) ?? source)]) : new parser_1.List([new parser_1.Data(source)]);
7155
- });
7173
+ exports.unsafehtmlentity = (0, combinator_1.surround)((0, source_1.str)('&'), (0, source_1.str)(/[0-9A-Za-z]+/y), (0, source_1.str)(';'), false, ([as, bs, cs]) => new parser_1.List([new parser_1.Data(parser(as.head.value + bs.head.value + cs.head.value))]), ([as, bs]) => new parser_1.List([new parser_1.Data(as.head.value + (bs?.head?.value ?? ''))]), [3 | 64 /* Backtrack.bracket */]);
7156
7174
  exports.htmlentity = (0, combinator_1.fmap)((0, combinator_1.union)([exports.unsafehtmlentity]), ([{
7157
7175
  value
7158
7176
  }]) => new parser_1.List([length === 1 || value.at(-1) !== ';' ? new parser_1.Data(value) : new parser_1.Data((0, dom_1.html)('span', {
@@ -7162,8 +7180,7 @@ exports.htmlentity = (0, combinator_1.fmap)((0, combinator_1.union)([exports.uns
7162
7180
  const parser = (el => entity => {
7163
7181
  if (entity === '&NewLine;') return ' ';
7164
7182
  el.innerHTML = entity;
7165
- const text = el.textContent;
7166
- return entity === text ? undefined : text;
7183
+ return el.textContent;
7167
7184
  })((0, dom_1.html)('span'));
7168
7185
 
7169
7186
  /***/ },
@@ -7772,6 +7789,7 @@ exports.ruby = (0, combinator_1.lazy)(() => (0, combinator_1.bind)((0, combinato
7772
7789
  }, acc) => value + ' ' + acc, '').trim())), new parser_1.Data((0, dom_1.html)('rp', ')'))])))))]);
7773
7790
  }
7774
7791
  }));
7792
+ const delimiter = /[$"`\[\](){}<>()[]{}]|\\?\n/y;
7775
7793
  const text = input => {
7776
7794
  const {
7777
7795
  context
@@ -7785,11 +7803,12 @@ const text = input => {
7785
7803
  for (let {
7786
7804
  position
7787
7805
  } = context; position < source.length; position = context.position) {
7788
- if (/[$"`\[\](){}<>()[]{}]|\\?\n/yi.test(source.slice(position, position + 2))) break;
7806
+ delimiter.lastIndex = position;
7807
+ if (delimiter.test(source)) break;
7789
7808
  switch (source[position]) {
7790
7809
  case '&':
7791
7810
  {
7792
- const result = (0, htmlentity_1.unsafehtmlentity)(input) ?? (0, source_1.txt)(input);
7811
+ const result = source[position + 1] !== ' ' ? (0, htmlentity_1.unsafehtmlentity)(input) ?? (0, source_1.txt)(input) : (0, source_1.txt)(input);
7793
7812
  acc.last.value += result.head.value;
7794
7813
  continue;
7795
7814
  }
@@ -8517,7 +8536,7 @@ exports.strs = strs;
8517
8536
  Object.defineProperty(exports, "__esModule", ({
8518
8537
  value: true
8519
8538
  }));
8520
- exports.backToEmailHead = exports.backToUrlHead = exports.backToWhitespace = exports.next = exports.canSkip = exports.linebreak = exports.txt = exports.text = exports.nonWhitespace = void 0;
8539
+ exports.isAlphanumeric = exports.backToEmailHead = exports.backToUrlHead = exports.backToWhitespace = exports.next = exports.canSkip = exports.linebreak = exports.txt = exports.text = exports.nonWhitespace = void 0;
8521
8540
  const parser_1 = __webpack_require__(605);
8522
8541
  const combinator_1 = __webpack_require__(3484);
8523
8542
  const dom_1 = __webpack_require__(394);
@@ -8678,8 +8697,9 @@ function backToEmailHead(source, position, index) {
8678
8697
  exports.backToEmailHead = backToEmailHead;
8679
8698
  function isAlphanumeric(char) {
8680
8699
  if (char < '0' || '\x7F' < char) return false;
8681
- return '0' <= char && char <= '9' || 'a' <= char && char <= 'z' || 'A' <= char && char <= 'Z';
8700
+ return '0' <= char && char <= '9' || 'A' <= char && char <= 'Z' || 'a' <= char && char <= 'z';
8682
8701
  }
8702
+ exports.isAlphanumeric = isAlphanumeric;
8683
8703
  //const dict = new class {
8684
8704
  // constructor() {
8685
8705
  // [
@@ -8724,7 +8744,6 @@ function seek(source, position) {
8724
8744
  case '@':
8725
8745
  case '#':
8726
8746
  case '$':
8727
- case '&':
8728
8747
  case '"':
8729
8748
  case '`':
8730
8749
  case '[':
@@ -8760,6 +8779,9 @@ function seek(source, position) {
8760
8779
  case ':':
8761
8780
  if (source[i + 1] === '/' && source[i + 2] === '/') return i;
8762
8781
  continue;
8782
+ case '&':
8783
+ if (source[i + 1] !== ' ') return i;
8784
+ continue;
8763
8785
  case ' ':
8764
8786
  case '\t':
8765
8787
  case ' ':
package/markdown.d.ts CHANGED
@@ -1057,7 +1057,9 @@ export namespace MarkdownParser {
1057
1057
  export interface UnsafeHTMLEntityParser extends
1058
1058
  // &copy;
1059
1059
  Inline<'unsafehtmlentity'>,
1060
- Parser<string, Context, []> {
1060
+ Parser<string, Context, [
1061
+ SourceParser.StrParser,
1062
+ ]> {
1061
1063
  }
1062
1064
  export interface ShortMediaParser extends
1063
1065
  // !https://host
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.294.5",
3
+ "version": "0.294.7",
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",
@@ -16,7 +16,7 @@ export function indent<N>(opener: RegExp | Parser<N>, parser: Parser<N> | boolea
16
16
  opener = / {1,4}|\t{1,2}/y;
17
17
  }
18
18
  assert(!opener.flags.match(/[gm]/) && opener.sticky && !opener.source.startsWith('^'));
19
- assert(parser);
19
+ assert(parser = parser as Parser<N>);
20
20
  return failsafe(bind(block(match(
21
21
  opener,
22
22
  memoize(
@@ -27,10 +27,8 @@ export function indent<N>(opener: RegExp | Parser<N>, parser: Parser<N> | boolea
27
27
  return new List([new Data(source.slice(position))]);
28
28
  }))),
29
29
  ([indent]) => indent.length * 2 + -(indent[0] === ' '), [], 2 ** 4 - 1)), separation),
30
- (lines, context) => {
31
- assert(parser = parser as Parser<N>);
32
- return parser(subinput(trimBlockEnd(lines.foldl((acc, node) => acc + node.value, '')), context));
33
- }));
30
+ (lines, context) =>
31
+ parser(subinput(trimBlockEnd(lines.foldl((acc, node) => acc + node.value, '')), context))));
34
32
  }
35
33
 
36
34
  function trimBlockEnd(block: string): string {
@@ -29,8 +29,22 @@ export function surround<P extends Parser<unknown>, S = string>(
29
29
  g?: (rss: [List<Data<S>>, List<Data<Node<P>>> | undefined], context: Context<P>) => Result<Node<P>, Context<P>, SubParsers<P>>,
30
30
  backtracks?: readonly number[],
31
31
  ): P;
32
+ export function surround<P extends Parser<string>, S = string>(
33
+ opener: string | RegExp | Parser<S, Context<P>>, parser: string | RegExp | P, closer: string | RegExp | Parser<S, Context<P>>,
34
+ optional?: false,
35
+ f?: (rss: [List<Data<S>>, List<Data<Node<P>>>, List<Data<S>>], context: Context<P>) => Result<Node<P>, Context<P>, SubParsers<P>>,
36
+ g?: (rss: [List<Data<S>>, List<Data<Node<P>>> | undefined], context: Context<P>) => Result<Node<P>, Context<P>, SubParsers<P>>,
37
+ backtracks?: readonly number[],
38
+ ): P;
39
+ export function surround<P extends Parser<string>, S = string>(
40
+ opener: string | RegExp | Parser<S, Context<P>>, parser: string | RegExp | P, closer: string | RegExp | Parser<S, Context<P>>,
41
+ optional?: boolean,
42
+ f?: (rss: [List<Data<S>>, List<Data<Node<P>>> | undefined, List<Data<S>>], context: Context<P>) => Result<Node<P>, Context<P>, SubParsers<P>>,
43
+ g?: (rss: [List<Data<S>>, List<Data<Node<P>>> | undefined], context: Context<P>) => Result<Node<P>, Context<P>, SubParsers<P>>,
44
+ backtracks?: readonly number[],
45
+ ): P;
32
46
  export function surround<N>(
33
- opener: string | RegExp | Parser<N>, parser: Parser<N>, closer: string | RegExp | Parser<N>,
47
+ opener: string | RegExp | Parser<N>, parser: string | RegExp | Parser<N>, closer: string | RegExp | Parser<N>,
34
48
  optional: boolean = false,
35
49
  f?: (rss: [List<Data<N>>, List<Data<N>>, List<Data<N>>], context: Ctx) => Result<N>,
36
50
  g?: (rss: [List<Data<N>>, List<Data<N>> | undefined], context: Ctx) => Result<N>,
@@ -41,11 +55,19 @@ export function surround<N>(
41
55
  case 'object':
42
56
  opener = clear(matcher(opener, true));
43
57
  }
58
+ assert(opener);
59
+ switch (typeof parser) {
60
+ case 'string':
61
+ case 'object':
62
+ parser = clear(matcher(parser, true));
63
+ }
64
+ assert(parser);
44
65
  switch (typeof closer) {
45
66
  case 'string':
46
67
  case 'object':
47
68
  closer = clear(matcher(closer, true));
48
69
  }
70
+ assert(closer);
49
71
  return failsafe(input => {
50
72
  const { context } = input;
51
73
  const { source, position } = context;
@@ -66,8 +88,7 @@ export function surround<N>(
66
88
  if (!nodesM && !optional) {
67
89
  setBacktrack(context, backtracks, position);
68
90
  const result = g?.([nodesO, nodesM], context);
69
- revert(context, linebreak);
70
- return result;
91
+ return result || void revert(context, linebreak);
71
92
  }
72
93
  const nodesC = nodesM || optional ? closer(input) : undefined;
73
94
  assert(context.position >= position);
@@ -75,8 +96,7 @@ export function surround<N>(
75
96
  if (!nodesC) {
76
97
  setBacktrack(context, backtracks, position);
77
98
  const result = g?.([nodesO, nodesM], context);
78
- revert(context, linebreak);
79
- return result;
99
+ return result || void revert(context, linebreak);
80
100
  }
81
101
  if (context.position === position) {
82
102
  return void revert(context, linebreak);
@@ -88,10 +108,7 @@ export function surround<N>(
88
108
  if (result) {
89
109
  context.linebreak ||= linebreak;
90
110
  }
91
- else {
92
- revert(context, linebreak);
93
- }
94
- return result;
111
+ return result || void revert(context, linebreak);
95
112
  });
96
113
  }
97
114
  export function open<P extends Parser<unknown>>(
@@ -100,13 +117,19 @@ export function open<P extends Parser<unknown>>(
100
117
  optional?: boolean,
101
118
  backtracks?: readonly number[],
102
119
  ): P;
120
+ export function open<P extends Parser<string>>(
121
+ opener: string | RegExp | Parser<Node<P>, Context<P>>,
122
+ parser: string | RegExp | P,
123
+ optional?: boolean,
124
+ backtracks?: readonly number[],
125
+ ): P;
103
126
  export function open<N>(
104
127
  opener: string | RegExp | Parser<N, Ctx>,
105
- parser: Parser<N>,
128
+ parser: string | RegExp | Parser<N>,
106
129
  optional?: boolean,
107
130
  backtracks?: readonly number[],
108
131
  ): Parser<N> {
109
- return surround(opener, parser, '', optional, undefined, undefined, backtracks);
132
+ return surround(opener, parser as Parser<N>, '', optional, undefined, undefined, backtracks);
110
133
  }
111
134
  export function close<P extends Parser<unknown>>(
112
135
  parser: P,
@@ -114,13 +137,19 @@ export function close<P extends Parser<unknown>>(
114
137
  optional?: boolean,
115
138
  backtracks?: readonly number[],
116
139
  ): P;
140
+ export function close<P extends Parser<string>>(
141
+ parser: string | RegExp | P,
142
+ closer: string | RegExp | Parser<Node<P>, Context<P>>,
143
+ optional?: boolean,
144
+ backtracks?: readonly number[],
145
+ ): P;
117
146
  export function close<N>(
118
- parser: Parser<N>,
147
+ parser: string | RegExp | Parser<N>,
119
148
  closer: string | RegExp | Parser<N, Ctx>,
120
149
  optional?: boolean,
121
150
  backtracks?: readonly number[],
122
151
  ): Parser<N> {
123
- return surround('', parser, closer, optional, undefined, undefined, backtracks);
152
+ return surround('', parser as Parser<N>, closer, optional, undefined, undefined, backtracks);
124
153
  }
125
154
 
126
155
  const statesize = 2;
@@ -153,7 +182,7 @@ export function setBacktrack(
153
182
  position: number,
154
183
  length: number = 1,
155
184
  ): void {
156
- // 以降バックトラックの可能性がなく記録不要の場合もあるが判別が面倒なので省略
185
+ // バックトラックの可能性がなく記録不要の場合もあるが判別が面倒なので省略
157
186
  const { source } = context;
158
187
  if (position === source.length) return;
159
188
  if (length === 0) return;
@@ -39,8 +39,11 @@ export interface CtxOptions {
39
39
  // 区間別テーブルは固定サイズであるためプールして再使用できる。
40
40
  // 従って分割時のデータ構造は区間ごとに探索木を動的に生成しデータ数に応じてテーブルに移行するのが最も効率的である。
41
41
  // これにより最悪時間計算量線形化に要する最悪空間計算量が+1nに局限される。
42
+ // またはテーブルの参照が高速なら変換せず併用してもよい。
42
43
  // 木とテーブルいずれにおいてもバックトラックデータとオーバーヘッドを合わせた追加データサイズの最大値は
43
44
  // セグメントサイズに制約されるため入力サイズに対する最大追加データサイズの平均比率はかなり小さくなる。
45
+ // 必要なテーブルの最大サイズは最大セグメントサイズであるため最大追加データサイズは入力サイズにかかわらず
46
+ // 10KB*並列数に留まり最大数百文字以下の短文ならば数百byte*並列数となる。
44
47
  //
45
48
  // 1. データ数が規定数を超えたら区間テーブルを生成しデータを振り分ける。
46
49
  // - 子ノードのポインタだけ保持するとしても1ノード複数データ保持で圧縮できるかは微妙。
@@ -67,12 +67,20 @@ describe('Unit: parser/normalize', () => {
67
67
  assert(normalize('\x01---\na: b\x01\n---\n\n!> \x01---\na: b\x01\n---') === '\uFFFD---\na: b\uFFFD\n---\n\n!> \uFFFD---\na: b\uFFFD\n---');
68
68
  });
69
69
 
70
+ it('emoji', () => {
71
+ assert(normalize('😀') === '😀');
72
+ assert(normalize('🤚🏽') === '🤚🏽');
73
+ assert(normalize('👨‍👩‍👧') === '👨‍👩‍👧');
74
+ assert(normalize('🇺🇳') === '🇺🇳');
75
+ assert(normalize('#️⃣*️⃣0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣') === '#️⃣*️⃣0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣');
76
+ });
77
+
70
78
  });
71
79
 
72
80
  describe('escape', () => {
73
81
  it('', () => {
74
82
  assert(escape('\u200B') === '&ZeroWidthSpace;');
75
- assert(escape('\u200D') === '&zwj;');
83
+ assert(escape('\u200F') === '&rlm;');
76
84
  });
77
85
 
78
86
  });
@@ -9,17 +9,16 @@ export function normalize(source: string): string {
9
9
  }
10
10
 
11
11
  function format(source: string): string {
12
- return source
13
- .replace(/\r\n?/g, '\n');
12
+ return source.replace(/\r\n?/g, '\n');
14
13
  }
15
14
 
15
+ const invalid = new RegExp([
16
+ /(?![\t\r\n])[\x00-\x1F\x7F]/g.source,
17
+ /(?!\u200D)[\u2006\u200B-\u200F\u202A-\u202F\u2060\uFEFF]|(?<![\u1820\u1821])\u180E/g.source,
18
+ /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g.source,
19
+ ].join('|'), 'g');
16
20
  function sanitize(source: string): string {
17
- return source
18
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]|[\u2006\u200B-\u200F\u202A-\u202F\u2060\uFEFF]|(?<![\u1820\u1821])\u180E/g, UNICODE_REPLACEMENT_CHARACTER)
19
- .replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]?|[\uDC00-\uDFFF]/g, char =>
20
- char.length === 1
21
- ? UNICODE_REPLACEMENT_CHARACTER
22
- : char);
21
+ return source.replace(invalid, UNICODE_REPLACEMENT_CHARACTER);
23
22
  }
24
23
 
25
24
  // https://dev.w3.org/html5/html-author/charref
@@ -58,13 +57,20 @@ export const invisibleHTMLEntityNames = [
58
57
  'InvisibleComma',
59
58
  'ic',
60
59
  ] as const;
61
- const unreadableHTMLEntityNames: readonly string[] = invisibleHTMLEntityNames.slice(2);
62
- const unreadableEscapableCharacters = unreadableHTMLEntityNames
60
+ const unreadableEscapeHTMLEntityNames = invisibleHTMLEntityNames.filter(name => ![
61
+ 'Tab',
62
+ 'NewLine',
63
+ 'NonBreakingSpace',
64
+ 'nbsp',
65
+ 'zwj',
66
+ 'zwnj',
67
+ ].includes(name));
68
+ const unreadableEscapeCharacters = unreadableEscapeHTMLEntityNames
63
69
  .map(name => unsafehtmlentity(input(`&${name};`, {}))!.head!.value);
64
- assert(unreadableEscapableCharacters.length === unreadableHTMLEntityNames.length);
65
- assert(unreadableEscapableCharacters.every(c => c.length === 1));
66
- const unreadableEscapableCharacter = new RegExp(`[${unreadableEscapableCharacters.join('')}]`, 'g');
67
- assert(!unreadableEscapableCharacter.source.includes('&'));
70
+ assert(unreadableEscapeCharacters.length === unreadableEscapeHTMLEntityNames.length);
71
+ assert(unreadableEscapeCharacters.every(c => c.length === 1));
72
+ const unreadableEscapeCharacter = new RegExp(`[${unreadableEscapeCharacters.join('')}]`, 'g');
73
+ assert(!unreadableEscapeCharacter.source.includes('&'));
68
74
 
69
75
  // https://www.pandanoir.info/entry/2018/03/11/193000
70
76
  // http://anti.rosx.net/etc/memo/002_space.html
@@ -77,7 +83,7 @@ const unreadableSpecialCharacters = [
77
83
  // ZERO WIDTH NON-JOINER
78
84
  '\u200C',
79
85
  // ZERO WIDTH JOINER
80
- '\u200D',
86
+ //'\u200D',
81
87
  // LEFT-TO-RIGHT MARK
82
88
  '\u200E',
83
89
  // RIGHT-TO-LEFT MARK
@@ -103,7 +109,6 @@ assert(unreadableSpecialCharacters.every(c => sanitize(c) === UNICODE_REPLACEMEN
103
109
 
104
110
  // 特殊不可視文字はエディタおよびソースビューアでは等幅および強調表示により可視化する
105
111
  export function escape(source: string): string {
106
- return source
107
- .replace(unreadableEscapableCharacter, char =>
108
- `&${unreadableHTMLEntityNames[unreadableEscapableCharacters.indexOf(char)]};`);
112
+ return source.replace(unreadableEscapeCharacter, char =>
113
+ `&${unreadableEscapeHTMLEntityNames[unreadableEscapeCharacters.indexOf(char)]};`);
109
114
  }
@@ -361,9 +361,9 @@ describe('Unit: parser/api/parse', () => {
361
361
 
362
362
  it('backtrack', function () {
363
363
  this.timeout(5000);
364
- // 最悪計算量での実行速度はCommonMarkの公式JS実装の32nより速い。
364
+ // 最悪計算量での実行速度はCommonMarkの公式JS実装の32nに対して3倍遅い程度。
365
365
  // 5n = annotation/reference + link + url/math + ruby + text
366
- const source = `((([[[[#$[${'.'.repeat(19997)}`;
366
+ const source = `((([[[[#$[${'.'.repeat(19998)}`;
367
367
  assert.deepStrictEqual(
368
368
  [...parse(source, {}, { resources: { clock: 100000, recursions: [100] } }).children]
369
369
  .map(el => el.tagName),
@@ -372,7 +372,7 @@ describe('Unit: parser/api/parse', () => {
372
372
 
373
373
  it('backtrack error', function () {
374
374
  this.timeout(5000);
375
- const source = `((([[[[#$[${'.'.repeat(19997 + 1)}`;
375
+ const source = `((([[[[#$[${'.'.repeat(19998 + 1)}`;
376
376
  assert.deepStrictEqual(
377
377
  [...parse(source, {}, { resources: { clock: 100000, recursions: [100] } }).children]
378
378
  .map(el => el.tagName),
@@ -3,7 +3,6 @@ import { State, Backtrack } from '../../context';
3
3
  import { List, Data } from '../../../combinator/data/parser';
4
4
  import { union, state, constraint, rewrite, surround, convert, fmap, lazy } from '../../../combinator';
5
5
  import { unsafelink } from '../link';
6
- import { str } from '../../source';
7
6
  import { define } from 'typed-dom/dom';
8
7
 
9
8
  // https://example/@user must be a user page or a redirect page going there.
@@ -11,8 +10,8 @@ import { define } from 'typed-dom/dom';
11
10
  export const account: AutolinkParser.AccountParser = lazy(() => rewrite(
12
11
  surround(
13
12
  /(?<![0-9a-z])@/yi,
14
- str(/[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//yi),
15
- str(/[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*(?![-.]?[0-9a-z@#]|>>|:\S)/yi),
13
+ /[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//yi,
14
+ /[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*(?![-.]?[0-9a-z@#]|>>|:\S)/yi,
16
15
  true, undefined, undefined,
17
16
  [3 | Backtrack.autolink]),
18
17
  constraint(State.autolink, state(State.autolink, fmap(convert(
@@ -3,7 +3,6 @@ import { State, Backtrack } from '../../context';
3
3
  import { List, Data } from '../../../combinator/data/parser';
4
4
  import { union, state, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
5
5
  import { unsafelink } from '../link';
6
- import { str } from '../../source';
7
6
  import { define } from 'typed-dom/dom';
8
7
 
9
8
  // Timeline(pseudonym): user/tid
@@ -19,7 +18,7 @@ import { define } from 'typed-dom/dom';
19
18
  export const anchor: AutolinkParser.AnchorParser = lazy(() => rewrite(
20
19
  open(
21
20
  /(?<![0-9a-z])>>/yi,
22
- str(/(?:[a-z][0-9a-z]*(?:-[0-9a-z]+)*\/)?[0-9a-z]+(?:-[0-9a-z]+)*(?!-?[0-9a-z@#]|>>|:\S)/yi),
21
+ /(?:[a-z][0-9a-z]*(?:-[0-9a-z]+)*\/)?[0-9a-z]+(?:-[0-9a-z]+)*(?!-?[0-9a-z@#]|>>|:\S)/yi,
23
22
  false,
24
23
  [3 | Backtrack.autolink]),
25
24
  constraint(State.autolink, state(State.autolink, fmap(convert(
@@ -13,18 +13,18 @@ export const channel: AutolinkParser.ChannelParser = lazy(() => rewrite(
13
13
  sequence([
14
14
  surround(
15
15
  /(?<![0-9a-z])@/yi,
16
- str(/[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//yi),
17
- str(/[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*(?![-.]?[0-9a-z@]|>>|:\S)/yi),
16
+ /[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//yi,
17
+ /[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*(?![-.]?[0-9a-z@]|>>|:\S)/yi,
18
18
  true, undefined, undefined,
19
19
  [3 | Backtrack.autolink]),
20
20
  some(verify(surround(
21
21
  '#',
22
22
  str(new RegExp([
23
23
  /(?!['_])(?:[^\p{C}\p{S}\p{P}\s]|emoji|'(?=[0-9A-Za-z])|_(?=[^\p{C}\p{S}\p{P}\s]|emoji))+/yu.source,
24
- ].join('').replace(/emoji/g, emoji), 'yu')),
25
- str(new RegExp([
24
+ ].join('|').replace(/emoji/g, emoji.source), 'yu')),
25
+ new RegExp([
26
26
  /(?![0-9a-z@]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source,
27
- ].join('').replace(/emoji/g, emoji), 'yu')),
27
+ ].join('|').replace(/emoji/g, emoji.source), 'yu'),
28
28
  false, undefined, undefined,
29
29
  [3 | Backtrack.autolink]),
30
30
  ([{ value }]) => !/^[0-9]{1,4}$|^[0-9]{5}/.test(value as string))),
@@ -4,17 +4,16 @@ import { List, Data } from '../../../combinator/data/parser';
4
4
  import { union, state, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
5
5
  import { unsafelink } from '../link';
6
6
  import { emoji } from './hashtag';
7
- import { str } from '../../source';
8
7
  import { define } from 'typed-dom/dom';
9
8
 
10
9
  export const hashnum: AutolinkParser.HashnumParser = lazy(() => rewrite(
11
10
  open(
12
11
  new RegExp([
13
- /(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yiu.source,
14
- ].join('').replace(/emoji/g, emoji), 'yu'),
15
- str(new RegExp([
12
+ /(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yu.source,
13
+ ].join('|').replace(/emoji/g, emoji.source), 'yu'),
14
+ new RegExp([
16
15
  /[0-9]{1,9}(?![0-9a-z@#]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source,
17
- ].join('').replace(/emoji/g, emoji), 'yu')),
16
+ ].join('|').replace(/emoji/g, emoji.source), 'yu'),
18
17
  false,
19
18
  [1 | Backtrack.autolink]),
20
19
  constraint(State.autolink, state(State.autolink, fmap(convert(
@@ -54,10 +54,14 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
54
54
  assert.deepStrictEqual(inspect(parser('#a_b'), ctx), [['<a class="hashtag" href="/hashtags/a_b">#a_b</a>'], '']);
55
55
  assert.deepStrictEqual(inspect(parser('#a__b'), ctx), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '__b']);
56
56
  assert.deepStrictEqual(inspect(parser('#あ'), ctx), [['<a class="hashtag" href="/hashtags/あ">#あ</a>'], '']);
57
- assert.deepStrictEqual(inspect(parser('#👩'), ctx), [['<a class="hashtag" href="/hashtags/👩">#👩</a>'], '']);
57
+ assert.deepStrictEqual(inspect(parser('#😀'), ctx), [['<a class="hashtag" href="/hashtags/😀">#😀</a>'], '']);
58
+ assert.deepStrictEqual(inspect(parser('#🤚🏽'), ctx), [['<a class="hashtag" href="/hashtags/🤚🏽">#🤚🏽</a>'], '']);
59
+ assert.deepStrictEqual(inspect(parser('#👨‍👩‍👧'), ctx), [['<a class="hashtag" href="/hashtags/👨‍👩‍👧">#👨‍👩‍👧</a>'], '']);
60
+ assert.deepStrictEqual(inspect(parser('#🇺🇳'), ctx), [['<a class="hashtag" href="/hashtags/🇺🇳">#🇺🇳</a>'], '']);
61
+ assert.deepStrictEqual(inspect(parser('##️⃣*️⃣0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣'), ctx), [['<a class="hashtag" href="/hashtags/#️⃣*️⃣0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣">##️⃣*️⃣0️⃣1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣</a>'], '']);
58
62
  assert.deepStrictEqual(inspect(parser('#1a'), ctx), [['<a class="hashtag" href="/hashtags/1a">#1a</a>'], '']);
59
63
  assert.deepStrictEqual(inspect(parser('#1あ'), ctx), [['<a class="hashtag" href="/hashtags/1あ">#1あ</a>'], '']);
60
- assert.deepStrictEqual(inspect(parser('#1👩'), ctx), [['<a class="hashtag" href="/hashtags/1👩">#1👩</a>'], '']);
64
+ assert.deepStrictEqual(inspect(parser('#1😀'), ctx), [['<a class="hashtag" href="/hashtags/1😀">#1😀</a>'], '']);
61
65
  assert.deepStrictEqual(inspect(parser(`#a'`), ctx), [[`<a class="hashtag" href="/hashtags/a">#a</a>`], `'`]);
62
66
  assert.deepStrictEqual(inspect(parser(`#a''`), ctx), [[`<a class="hashtag" href="/hashtags/a">#a</a>`], `''`]);
63
67
  assert.deepStrictEqual(inspect(parser(`#a'b`), ctx), [[`<a class="hashtag" href="/hashtags/a'b">#a'b</a>`], '']);
@@ -9,19 +9,19 @@ import { define } from 'typed-dom/dom';
9
9
  // https://example/hashtags/a must be a hashtag page or a redirect page going there.
10
10
 
11
11
  // https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji
12
- export const emoji = String.raw`\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F`;
12
+ export const emoji = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F|\u200D/u;
13
13
 
14
14
  export const hashtag: AutolinkParser.HashtagParser = lazy(() => rewrite(
15
15
  verify(surround(
16
16
  new RegExp([
17
- /(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yiu.source,
18
- ].join('').replace(/emoji/g, emoji), 'yu'),
17
+ /(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yu.source,
18
+ ].join('|').replace(/emoji/g, emoji.source), 'yu'),
19
19
  str(new RegExp([
20
20
  /(?!['_])(?:[^\p{C}\p{S}\p{P}\s]|emoji|'(?=[0-9A-Za-z])|_(?=[^\p{C}\p{S}\p{P}\s]|emoji))+/yu.source,
21
- ].join('').replace(/emoji/g, emoji), 'yu')),
22
- str(new RegExp([
21
+ ].join('|').replace(/emoji/g, emoji.source), 'yu')),
22
+ new RegExp([
23
23
  /(?![0-9a-z@#]|>>|:\S|[^\p{C}\p{S}\p{P}\s]|emoji)/yu.source,
24
- ].join('').replace(/emoji/g, emoji), 'yu')),
24
+ ].join('|').replace(/emoji/g, emoji.source), 'yu'),
25
25
  false, undefined, undefined,
26
26
  [3 | Backtrack.autolink]),
27
27
  ([{ value }]) => !/^[0-9]{1,4}$|^[0-9]{5}/.test(value)),
@@ -1,35 +1,47 @@
1
1
  import { AutolinkParser } from '../inline';
2
2
  import { State } from '../context';
3
- import { union, state, validate, lazy } from '../../combinator';
3
+ import { state, lazy } from '../../combinator';
4
4
  import { url, lineurl } from './autolink/url';
5
5
  import { email } from './autolink/email';
6
6
  import { channel } from './autolink/channel';
7
7
  import { account } from './autolink/account';
8
- import { hashtag, emoji } from './autolink/hashtag';
8
+ import { hashtag } from './autolink/hashtag';
9
9
  import { hashnum } from './autolink/hashnum';
10
10
  import { anchor } from './autolink/anchor';
11
+ import { isAlphanumeric } from '../source/text';
11
12
 
12
13
  export const autolink: AutolinkParser = lazy(() =>
13
- validate(new RegExp([
14
- /(?<![0-9a-z])@/yi.source,
15
- /(?<![^\p{C}\p{S}\p{P}\s]|emoji)#/yiu.source,
16
- /(?<![0-9a-z])>>/yi.source,
17
- /(?<![0-9a-z][.+-]?|[@#])!?[0-9a-z]/yi.source,
18
- ].join('|').replace(/emoji/g, emoji), 'yiu'),
19
14
  state(~State.autolink,
20
- union([
21
- lineurl,
22
- url,
23
- email,
24
- // Escape unmatched email-like strings.
25
- //str(/[0-9a-z]+(?:[_.+-][0-9a-z]+[:@]?|:|@(?=@))*/yi),
26
- channel,
27
- account,
28
- // Escape unmatched account-like strings.
29
- //str(/@+(?:[0-9a-z]+(?:[_.+-][0-9a-z]+)*)?/yi),
30
- hashtag,
31
- hashnum,
32
- // Escape unmatched hashtag-like strings.
33
- //str(new RegExp(/#+(?:(?:[^\p{C}\p{S}\p{P}\s]|emoji)+(?:['_.+-](?:[^\p{C}\p{S}\p{P}\s]|emoji)+)*)?/yu.source.replace(/emoji/g, emoji), 'yu')),
34
- anchor,
35
- ]))));
15
+ input => {
16
+ const { context: { source, position } } = input;
17
+ if (position === source.length) return;
18
+ const fst = source[position];
19
+ switch (fst) {
20
+ case '@':
21
+ return channel(input) || account(input);
22
+ case '#':
23
+ return hashtag(input) || hashnum(input);
24
+ case '>':
25
+ return anchor(input);
26
+ case '!':
27
+ if (!source.startsWith('http', position + 1)) break;
28
+ if (position === 0) return lineurl(input);
29
+ switch (source[position - 1]) {
30
+ case '\r':
31
+ case '\n':
32
+ return lineurl(input);
33
+ }
34
+ break;
35
+ case 'h':
36
+ if (!source.startsWith('http', position)) return;
37
+ if (position === 0) return lineurl(input) || url(input) || email(input);
38
+ switch (source[position - 1]) {
39
+ case '\r':
40
+ case '\n':
41
+ return lineurl(input) || url(input) || email(input);
42
+ }
43
+ return url(input) || email(input);
44
+ default:
45
+ if (isAlphanumeric(fst)) return email(input);
46
+ }
47
+ }));
@@ -23,7 +23,7 @@ export const html: HTMLParser = lazy(() => validate(/<[a-z]+(?=[ >])/yi,
23
23
  union([
24
24
  surround(
25
25
  // https://html.spec.whatwg.org/multipage/syntax.html#void-elements
26
- str(/<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[ >])/yi),
26
+ str(/<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[ >])/y),
27
27
  some(union([attribute])),
28
28
  open(str(/ ?/y), str('>'), true),
29
29
  true,
@@ -1,19 +1,19 @@
1
1
  import { HTMLEntityParser, UnsafeHTMLEntityParser } from '../inline';
2
+ import { Backtrack } from '../context';
2
3
  import { List, Data } from '../../combinator/data/parser';
3
- import { union, focus, fmap } from '../../combinator';
4
+ import { union, surround, fmap } from '../../combinator';
5
+ import { str } from '../source';
4
6
  import { invalid } from '../util';
5
7
  import { html } from 'typed-dom/dom';
6
8
 
7
- export const unsafehtmlentity: UnsafeHTMLEntityParser = focus(
8
- /&(?:[0-9A-Za-z]+;?)?/y,
9
- //({ source }) => [[parser(source) ?? `${Command.Error}${source}`], '']));
10
- ({ context }) => {
11
- const { source } = context;
12
- context.position += source.length;
13
- return source.length > 1 && source.at(-1) === ';'
14
- ? new List([new Data(parser(source) ?? source)])
15
- : new List([new Data(source)]);
16
- });
9
+ export const unsafehtmlentity: UnsafeHTMLEntityParser = surround(
10
+ str('&'), str(/[0-9A-Za-z]+/y), str(';'),
11
+ false,
12
+ ([as, bs, cs]) =>
13
+ new List([new Data(parser(as.head!.value + bs.head!.value + cs.head!.value))]),
14
+ ([as, bs]) =>
15
+ new List([new Data(as.head!.value + (bs?.head?.value ?? ''))]),
16
+ [3 | Backtrack.bracket]);
17
17
 
18
18
  export const htmlentity: HTMLEntityParser = fmap(
19
19
  union([unsafehtmlentity]),
@@ -23,14 +23,11 @@ export const htmlentity: HTMLEntityParser = fmap(
23
23
  : new Data(html('span', {
24
24
  class: 'invalid',
25
25
  ...invalid('htmlentity', 'syntax', 'Invalid HTML entity'),
26
- }, value))
26
+ }, value))
27
27
  ]));
28
28
 
29
- const parser = (el => (entity: string): string | undefined => {
29
+ const parser = (el => (entity: string): string => {
30
30
  if (entity === '&NewLine;') return ' ';
31
31
  el.innerHTML = entity;
32
- const text = el.textContent!;
33
- return entity === text
34
- ? undefined
35
- : text;
32
+ return el.textContent!;
36
33
  })(html('span'));
@@ -63,6 +63,8 @@ export const ruby: RubyParser = lazy(() => bind(
63
63
  }
64
64
  }));
65
65
 
66
+ const delimiter = /[$"`\[\](){}<>()[]{}]|\\?\n/y;
67
+
66
68
  const text: RubyParser.TextParser = input => {
67
69
  const { context } = input;
68
70
  const { source } = context;
@@ -70,11 +72,14 @@ const text: RubyParser.TextParser = input => {
70
72
  let state = false;
71
73
  context.sequential = true;
72
74
  for (let { position } = context; position < source.length; position = context.position) {
73
- if (/[$"`\[\](){}<>()[]{}]|\\?\n/yi.test(source.slice(position, position + 2))) break;
75
+ delimiter.lastIndex = position;
76
+ if (delimiter.test(source)) break;
74
77
  assert(source[position] !== '\n');
75
78
  switch (source[position]) {
76
79
  case '&': {
77
- const result = unsafehtmlentity(input) ?? txt(input)!;
80
+ const result = source[position + 1] !== ' '
81
+ ? unsafehtmlentity(input) ?? txt(input)!
82
+ : txt(input)!;
78
83
  assert(result);
79
84
  acc.last!.value += result.head!.value;
80
85
  continue;
@@ -5,6 +5,7 @@ import { reference } from './inline/reference';
5
5
  import { template } from './inline/template';
6
6
  import { remark } from './inline/remark';
7
7
  import { extension } from './inline/extension';
8
+ import { label } from './inline/extension/label';
8
9
  import { textlink } from './inline/link';
9
10
  import { ruby } from './inline/ruby';
10
11
  import { html } from './inline/html';
@@ -94,7 +95,7 @@ export const inline: InlineParser = lazy(() => union([
94
95
  return html(input);
95
96
  case '$':
96
97
  if (source[position + 1] === '{') return math(input);
97
- return extension(input)
98
+ return label(input)
98
99
  || math(input);
99
100
  case '+':
100
101
  if (source[position + 1] === '+') return insertion(input);
@@ -185,12 +185,12 @@ export function backToEmailHead(source: string, position: number, index: number)
185
185
  ? delim
186
186
  : index;
187
187
  }
188
- function isAlphanumeric(char: string): boolean {
188
+ export function isAlphanumeric(char: string): boolean {
189
189
  assert(char.length === 1);
190
190
  if (char < '0' || '\x7F' < char) return false;
191
191
  return '0' <= char && char <= '9'
192
- || 'a' <= char && char <= 'z'
193
- || 'A' <= char && char <= 'Z';
192
+ || 'A' <= char && char <= 'Z'
193
+ || 'a' <= char && char <= 'z';
194
194
  }
195
195
 
196
196
  //const dict = new class {
@@ -238,7 +238,6 @@ function seek(source: string, position: number): number {
238
238
  case '@':
239
239
  case '#':
240
240
  case '$':
241
- case '&':
242
241
  case '"':
243
242
  case '`':
244
243
  case '[':
@@ -274,6 +273,9 @@ function seek(source: string, position: number): number {
274
273
  case ':':
275
274
  if (source[i + 1] === '/' && source[i + 2] === '/') return i;
276
275
  continue;
276
+ case '&':
277
+ if (source[i + 1] !== ' ') return i;
278
+ continue;
277
279
  case ' ':
278
280
  case '\t':
279
281
  case ' ':