marked 1.2.9 → 2.0.0

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/src/Lexer.js CHANGED
@@ -136,14 +136,15 @@ module.exports = class Lexer {
136
136
  }
137
137
 
138
138
  // code
139
- if (token = this.tokenizer.code(src, tokens)) {
139
+ if (token = this.tokenizer.code(src)) {
140
140
  src = src.substring(token.raw.length);
141
- if (token.type) {
142
- tokens.push(token);
143
- } else {
144
- lastToken = tokens[tokens.length - 1];
141
+ lastToken = tokens[tokens.length - 1];
142
+ // An indented code block cannot interrupt a paragraph.
143
+ if (lastToken && lastToken.type === 'paragraph') {
145
144
  lastToken.raw += '\n' + token.raw;
146
145
  lastToken.text += '\n' + token.text;
146
+ } else {
147
+ tokens.push(token);
147
148
  }
148
149
  continue;
149
150
  }
@@ -236,14 +237,14 @@ module.exports = class Lexer {
236
237
  }
237
238
 
238
239
  // text
239
- if (token = this.tokenizer.text(src, tokens)) {
240
+ if (token = this.tokenizer.text(src)) {
240
241
  src = src.substring(token.raw.length);
241
- if (token.type) {
242
- tokens.push(token);
243
- } else {
244
- lastToken = tokens[tokens.length - 1];
242
+ lastToken = tokens[tokens.length - 1];
243
+ if (lastToken && lastToken.type === 'text') {
245
244
  lastToken.raw += '\n' + token.raw;
246
245
  lastToken.text += '\n' + token.text;
246
+ } else {
247
+ tokens.push(token);
247
248
  }
248
249
  continue;
249
250
  }
@@ -331,7 +332,7 @@ module.exports = class Lexer {
331
332
  * Lexing/Compiling
332
333
  */
333
334
  inlineTokens(src, tokens = [], inLink = false, inRawBlock = false) {
334
- let token;
335
+ let token, lastToken;
335
336
 
336
337
  // String with links masked to avoid interference with em and strong
337
338
  let maskedSrc = src;
@@ -354,11 +355,17 @@ module.exports = class Lexer {
354
355
  maskedSrc = maskedSrc.slice(0, match.index) + '[' + repeatString('a', match[0].length - 2) + ']' + maskedSrc.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);
355
356
  }
356
357
 
358
+ // Mask out escaped em & strong delimiters
359
+ while ((match = this.tokenizer.rules.inline.escapedEmSt.exec(maskedSrc)) != null) {
360
+ maskedSrc = maskedSrc.slice(0, match.index) + '++' + maskedSrc.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex);
361
+ }
362
+
357
363
  while (src) {
358
364
  if (!keepPrevChar) {
359
365
  prevChar = '';
360
366
  }
361
367
  keepPrevChar = false;
368
+
362
369
  // escape
363
370
  if (token = this.tokenizer.escape(src)) {
364
371
  src = src.substring(token.raw.length);
@@ -371,7 +378,13 @@ module.exports = class Lexer {
371
378
  src = src.substring(token.raw.length);
372
379
  inLink = token.inLink;
373
380
  inRawBlock = token.inRawBlock;
374
- tokens.push(token);
381
+ const lastToken = tokens[tokens.length - 1];
382
+ if (lastToken && token.type === 'text' && lastToken.type === 'text') {
383
+ lastToken.raw += token.raw;
384
+ lastToken.text += token.text;
385
+ } else {
386
+ tokens.push(token);
387
+ }
375
388
  continue;
376
389
  }
377
390
 
@@ -388,23 +401,21 @@ module.exports = class Lexer {
388
401
  // reflink, nolink
389
402
  if (token = this.tokenizer.reflink(src, this.tokens.links)) {
390
403
  src = src.substring(token.raw.length);
404
+ const lastToken = tokens[tokens.length - 1];
391
405
  if (token.type === 'link') {
392
406
  token.tokens = this.inlineTokens(token.text, [], true, inRawBlock);
407
+ tokens.push(token);
408
+ } else if (lastToken && token.type === 'text' && lastToken.type === 'text') {
409
+ lastToken.raw += token.raw;
410
+ lastToken.text += token.text;
411
+ } else {
412
+ tokens.push(token);
393
413
  }
394
- tokens.push(token);
395
- continue;
396
- }
397
-
398
- // strong
399
- if (token = this.tokenizer.strong(src, maskedSrc, prevChar)) {
400
- src = src.substring(token.raw.length);
401
- token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock);
402
- tokens.push(token);
403
414
  continue;
404
415
  }
405
416
 
406
- // em
407
- if (token = this.tokenizer.em(src, maskedSrc, prevChar)) {
417
+ // em & strong
418
+ if (token = this.tokenizer.emStrong(src, maskedSrc, prevChar)) {
408
419
  src = src.substring(token.raw.length);
409
420
  token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock);
410
421
  tokens.push(token);
@@ -450,9 +461,17 @@ module.exports = class Lexer {
450
461
  // text
451
462
  if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) {
452
463
  src = src.substring(token.raw.length);
453
- prevChar = token.raw.slice(-1);
464
+ if (token.raw.slice(-1) !== '_') { // Track prevChar before string of ____ started
465
+ prevChar = token.raw.slice(-1);
466
+ }
454
467
  keepPrevChar = true;
455
- tokens.push(token);
468
+ lastToken = tokens[tokens.length - 1];
469
+ if (lastToken && lastToken.type === 'text') {
470
+ lastToken.raw += token.raw;
471
+ lastToken.text += token.text;
472
+ } else {
473
+ tokens.push(token);
474
+ }
456
475
  continue;
457
476
  }
458
477
 
package/src/Tokenizer.js CHANGED
@@ -79,18 +79,9 @@ module.exports = class Tokenizer {
79
79
  }
80
80
  }
81
81
 
82
- code(src, tokens) {
82
+ code(src) {
83
83
  const cap = this.rules.block.code.exec(src);
84
84
  if (cap) {
85
- const lastToken = tokens[tokens.length - 1];
86
- // An indented code block cannot interrupt a paragraph.
87
- if (lastToken && lastToken.type === 'paragraph') {
88
- return {
89
- raw: cap[0],
90
- text: cap[0].trimRight()
91
- };
92
- }
93
-
94
85
  const text = cap[0].replace(/^ {1,4}/gm, '');
95
86
  return {
96
87
  type: 'code',
@@ -410,17 +401,9 @@ module.exports = class Tokenizer {
410
401
  }
411
402
  }
412
403
 
413
- text(src, tokens) {
404
+ text(src) {
414
405
  const cap = this.rules.block.text.exec(src);
415
406
  if (cap) {
416
- const lastToken = tokens[tokens.length - 1];
417
- if (lastToken && lastToken.type === 'text') {
418
- return {
419
- raw: cap[0],
420
- text: cap[0]
421
- };
422
- }
423
-
424
407
  return {
425
408
  type: 'text',
426
409
  raw: cap[0],
@@ -544,46 +527,61 @@ module.exports = class Tokenizer {
544
527
  }
545
528
  }
546
529
 
547
- strong(src, maskedSrc, prevChar = '') {
548
- let match = this.rules.inline.strong.start.exec(src);
530
+ emStrong(src, maskedSrc, prevChar = '') {
531
+ let match = this.rules.inline.emStrong.lDelim.exec(src);
532
+ if (!match) return;
533
+
534
+ if (match[3] && prevChar.match(/[\p{L}\p{N}]/u)) return; // _ can't be between two alphanumerics. \p{L}\p{N} includes non-english alphabet/numbers as well
549
535
 
550
- if (match && (!match[1] || (match[1] && (prevChar === '' || this.rules.inline.punctuation.exec(prevChar))))) {
551
- maskedSrc = maskedSrc.slice(-1 * src.length);
552
- const endReg = match[0] === '**' ? this.rules.inline.strong.endAst : this.rules.inline.strong.endUnd;
536
+ const nextChar = match[1] || match[2] || '';
553
537
 
538
+ if (!nextChar || (nextChar && (prevChar === '' || this.rules.inline.punctuation.exec(prevChar)))) {
539
+ const lLength = match[0].length - 1;
540
+ let rDelim, rLength, delimTotal = lLength, midDelimTotal = 0;
541
+
542
+ const endReg = match[0][0] === '*' ? this.rules.inline.emStrong.rDelimAst : this.rules.inline.emStrong.rDelimUnd;
554
543
  endReg.lastIndex = 0;
555
544
 
556
- let cap;
545
+ maskedSrc = maskedSrc.slice(-1 * src.length + lLength); // Bump maskedSrc to same section of string as src (move to lexer?)
546
+
557
547
  while ((match = endReg.exec(maskedSrc)) != null) {
558
- cap = this.rules.inline.strong.middle.exec(maskedSrc.slice(0, match.index + 3));
559
- if (cap) {
560
- return {
561
- type: 'strong',
562
- raw: src.slice(0, cap[0].length),
563
- text: src.slice(2, cap[0].length - 2)
564
- };
548
+ rDelim = match[1] || match[2] || match[3] || match[4] || match[5] || match[6];
549
+
550
+ if (!rDelim) continue; // matched the first alternative in rules.js (skip the * in __abc*abc__)
551
+
552
+ rLength = rDelim.length;
553
+
554
+ if (match[3] || match[4]) { // found another Left Delim
555
+ delimTotal += rLength;
556
+ continue;
557
+ } else if (match[5] || match[6]) { // either Left or Right Delim
558
+ if (lLength % 3 && !((lLength + rLength) % 3)) {
559
+ midDelimTotal += rLength;
560
+ continue; // CommonMark Emphasis Rules 9-10
561
+ }
565
562
  }
566
- }
567
- }
568
- }
569
563
 
570
- em(src, maskedSrc, prevChar = '') {
571
- let match = this.rules.inline.em.start.exec(src);
564
+ delimTotal -= rLength;
572
565
 
573
- if (match && (!match[1] || (match[1] && (prevChar === '' || this.rules.inline.punctuation.exec(prevChar))))) {
574
- maskedSrc = maskedSrc.slice(-1 * src.length);
575
- const endReg = match[0] === '*' ? this.rules.inline.em.endAst : this.rules.inline.em.endUnd;
566
+ if (delimTotal > 0) continue; // Haven't found enough closing delimiters
576
567
 
577
- endReg.lastIndex = 0;
568
+ // If this is the last rDelimiter, remove extra characters. *a*** -> *a*
569
+ if (delimTotal + midDelimTotal - rLength <= 0 && !maskedSrc.slice(endReg.lastIndex).match(endReg)) {
570
+ rLength = Math.min(rLength, rLength + delimTotal + midDelimTotal);
571
+ }
578
572
 
579
- let cap;
580
- while ((match = endReg.exec(maskedSrc)) != null) {
581
- cap = this.rules.inline.em.middle.exec(maskedSrc.slice(0, match.index + 2));
582
- if (cap) {
573
+ if (Math.min(lLength, rLength) % 2) {
583
574
  return {
584
575
  type: 'em',
585
- raw: src.slice(0, cap[0].length),
586
- text: src.slice(1, cap[0].length - 1)
576
+ raw: src.slice(0, lLength + match.index + rLength + 1),
577
+ text: src.slice(1, lLength + match.index + rLength)
578
+ };
579
+ }
580
+ if (Math.min(lLength, rLength) % 2 === 0) {
581
+ return {
582
+ type: 'strong',
583
+ raw: src.slice(0, lLength + match.index + rLength + 1),
584
+ text: src.slice(2, lLength + match.index + rLength - 1)
587
585
  };
588
586
  }
589
587
  }
package/src/rules.js CHANGED
@@ -173,74 +173,41 @@ const inline = {
173
173
  reflink: /^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,
174
174
  nolink: /^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,
175
175
  reflinkSearch: 'reflink|nolink(?!\\()',
176
- strong: {
177
- start: /^(?:(\*\*(?=[*punctuation]))|\*\*)(?![\s])|__/, // (1) returns if starts w/ punctuation
178
- middle: /^\*\*(?:(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)|\*(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)*?\*)+?\*\*$|^__(?![\s])((?:(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)|_(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)*?_)+?)__$/,
179
- endAst: /[^punctuation\s]\*\*(?!\*)|[punctuation]\*\*(?!\*)(?:(?=[punctuation_\s]|$))/, // last char can't be punct, or final * must also be followed by punct (or endline)
180
- endUnd: /[^\s]__(?!_)(?:(?=[punctuation*\s])|$)/ // last char can't be a space, and final _ must preceed punct or \s (or endline)
181
- },
182
- em: {
183
- start: /^(?:(\*(?=[punctuation]))|\*)(?![*\s])|_/, // (1) returns if starts w/ punctuation
184
- middle: /^\*(?:(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)|\*(?:(?!overlapSkip)(?:[^*]|\\\*)|overlapSkip)*?\*)+?\*$|^_(?![_\s])(?:(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)|_(?:(?!overlapSkip)(?:[^_]|\\_)|overlapSkip)*?_)+?_$/,
185
- endAst: /[^punctuation\s]\*(?!\*)|[punctuation]\*(?!\*)(?:(?=[punctuation_\s]|$))/, // last char can't be punct, or final * must also be followed by punct (or endline)
186
- endUnd: /[^\s]_(?!_)(?:(?=[punctuation*\s])|$)/ // last char can't be a space, and final _ must preceed punct or \s (or endline)
176
+ emStrong: {
177
+ lDelim: /^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,
178
+ // (1) and (2) can only be a Right Delimiter. (3) and (4) can only be Left. (5) and (6) can be either Left or Right.
179
+ // () Skip other delimiter (1) #*** (2) a***#, a*** (3) #***a, ***a (4) ***# (5) #***# (6) a***a
180
+ rDelimAst: /\_\_[^_]*?\*[^_]*?\_\_|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,
181
+ rDelimUnd: /\*\*[^*]*?\_[^*]*?\*\*|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/ // ^- Not allowed for _
187
182
  },
188
183
  code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,
189
184
  br: /^( {2,}|\\)\n(?!\s*$)/,
190
185
  del: noopTest,
191
- text: /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*]|\b_|$)|[^ ](?= {2,}\n)))/,
192
- punctuation: /^([\s*punctuation])/
186
+ text: /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,
187
+ punctuation: /^([\spunctuation])/
193
188
  };
194
189
 
195
- // list of punctuation marks from common mark spec
196
- // without * and _ to workaround cases with double emphasis
190
+ // list of punctuation marks from CommonMark spec
191
+ // without * and _ to handle the different emphasis markers * and _
197
192
  inline._punctuation = '!"#$%&\'()+\\-.,/:;<=>?@\\[\\]`^{|}~';
198
193
  inline.punctuation = edit(inline.punctuation).replace(/punctuation/g, inline._punctuation).getRegex();
199
194
 
200
195
  // sequences em should skip over [title](link), `code`, <html>
201
- inline._blockSkip = '\\[[^\\]]*?\\]\\([^\\)]*?\\)|`[^`]*?`|<[^>]*?>';
202
- inline._overlapSkip = '__[^_]*?__|\\*\\*\\[^\\*\\]*?\\*\\*';
196
+ inline.blockSkip = /\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g;
197
+ inline.escapedEmSt = /\\\*|\\_/g;
203
198
 
204
199
  inline._comment = edit(block._comment).replace('(?:-->|$)', '-->').getRegex();
205
200
 
206
- inline.em.start = edit(inline.em.start)
207
- .replace(/punctuation/g, inline._punctuation)
208
- .getRegex();
209
-
210
- inline.em.middle = edit(inline.em.middle)
211
- .replace(/punctuation/g, inline._punctuation)
212
- .replace(/overlapSkip/g, inline._overlapSkip)
213
- .getRegex();
214
-
215
- inline.em.endAst = edit(inline.em.endAst, 'g')
216
- .replace(/punctuation/g, inline._punctuation)
217
- .getRegex();
218
-
219
- inline.em.endUnd = edit(inline.em.endUnd, 'g')
220
- .replace(/punctuation/g, inline._punctuation)
221
- .getRegex();
222
-
223
- inline.strong.start = edit(inline.strong.start)
224
- .replace(/punctuation/g, inline._punctuation)
225
- .getRegex();
226
-
227
- inline.strong.middle = edit(inline.strong.middle)
228
- .replace(/punctuation/g, inline._punctuation)
229
- .replace(/overlapSkip/g, inline._overlapSkip)
230
- .getRegex();
231
-
232
- inline.strong.endAst = edit(inline.strong.endAst, 'g')
233
- .replace(/punctuation/g, inline._punctuation)
234
- .getRegex();
235
-
236
- inline.strong.endUnd = edit(inline.strong.endUnd, 'g')
237
- .replace(/punctuation/g, inline._punctuation)
201
+ inline.emStrong.lDelim = edit(inline.emStrong.lDelim)
202
+ .replace(/punct/g, inline._punctuation)
238
203
  .getRegex();
239
204
 
240
- inline.blockSkip = edit(inline._blockSkip, 'g')
205
+ inline.emStrong.rDelimAst = edit(inline.emStrong.rDelimAst, 'g')
206
+ .replace(/punct/g, inline._punctuation)
241
207
  .getRegex();
242
208
 
243
- inline.overlapSkip = edit(inline._overlapSkip, 'g')
209
+ inline.emStrong.rDelimUnd = edit(inline.emStrong.rDelimUnd, 'g')
210
+ .replace(/punct/g, inline._punctuation)
244
211
  .getRegex();
245
212
 
246
213
  inline._escapes = /\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g;
@@ -319,7 +286,7 @@ inline.gfm = merge({}, inline.normal, {
319
286
  url: /^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,
320
287
  _backpedal: /(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,
321
288
  del: /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,
322
- text: /^([`~]+|[^`~])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*~]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))/
289
+ text: /^([`~]+|[^`~])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@))/
323
290
  });
324
291
 
325
292
  inline.gfm.url = edit(inline.gfm.url, 'i')