marked 0.8.0 → 1.1.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
@@ -1,10 +1,47 @@
1
+ const Tokenizer = require('./Tokenizer.js');
1
2
  const { defaults } = require('./defaults.js');
2
- const { block } = require('./rules.js');
3
- const {
4
- rtrim,
5
- splitCells,
6
- escape
7
- } = require('./helpers.js');
3
+ const { block, inline } = require('./rules.js');
4
+
5
+ /**
6
+ * smartypants text replacement
7
+ */
8
+ function smartypants(text) {
9
+ return text
10
+ // em-dashes
11
+ .replace(/---/g, '\u2014')
12
+ // en-dashes
13
+ .replace(/--/g, '\u2013')
14
+ // opening singles
15
+ .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
16
+ // closing singles & apostrophes
17
+ .replace(/'/g, '\u2019')
18
+ // opening doubles
19
+ .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
20
+ // closing doubles
21
+ .replace(/"/g, '\u201d')
22
+ // ellipses
23
+ .replace(/\.{3}/g, '\u2026');
24
+ }
25
+
26
+ /**
27
+ * mangle email addresses
28
+ */
29
+ function mangle(text) {
30
+ let out = '',
31
+ i,
32
+ ch;
33
+
34
+ const l = text.length;
35
+ for (i = 0; i < l; i++) {
36
+ ch = text.charCodeAt(i);
37
+ if (Math.random() > 0.5) {
38
+ ch = 'x' + ch.toString(16);
39
+ }
40
+ out += '&#' + ch + ';';
41
+ }
42
+
43
+ return out;
44
+ }
8
45
 
9
46
  /**
10
47
  * Block Lexer
@@ -14,20 +51,37 @@ module.exports = class Lexer {
14
51
  this.tokens = [];
15
52
  this.tokens.links = Object.create(null);
16
53
  this.options = options || defaults;
17
- this.rules = block.normal;
54
+ this.options.tokenizer = this.options.tokenizer || new Tokenizer();
55
+ this.tokenizer = this.options.tokenizer;
56
+ this.tokenizer.options = this.options;
57
+
58
+ const rules = {
59
+ block: block.normal,
60
+ inline: inline.normal
61
+ };
18
62
 
19
63
  if (this.options.pedantic) {
20
- this.rules = block.pedantic;
64
+ rules.block = block.pedantic;
65
+ rules.inline = inline.pedantic;
21
66
  } else if (this.options.gfm) {
22
- this.rules = block.gfm;
67
+ rules.block = block.gfm;
68
+ if (this.options.breaks) {
69
+ rules.inline = inline.breaks;
70
+ } else {
71
+ rules.inline = inline.gfm;
72
+ }
23
73
  }
74
+ this.tokenizer.rules = rules;
24
75
  }
25
76
 
26
77
  /**
27
- * Expose Block Rules
78
+ * Expose Rules
28
79
  */
29
80
  static get rules() {
30
- return block;
81
+ return {
82
+ block,
83
+ inline
84
+ };
31
85
  }
32
86
 
33
87
  /**
@@ -36,7 +90,7 @@ module.exports = class Lexer {
36
90
  static lex(src, options) {
37
91
  const lexer = new Lexer(options);
38
92
  return lexer.lex(src);
39
- };
93
+ }
40
94
 
41
95
  /**
42
96
  * Preprocessing
@@ -46,357 +100,335 @@ module.exports = class Lexer {
46
100
  .replace(/\r\n|\r/g, '\n')
47
101
  .replace(/\t/g, ' ');
48
102
 
49
- return this.token(src, true);
50
- };
103
+ this.blockTokens(src, this.tokens, true);
104
+
105
+ this.inline(this.tokens);
106
+
107
+ return this.tokens;
108
+ }
51
109
 
52
110
  /**
53
111
  * Lexing
54
112
  */
55
- token(src, top) {
113
+ blockTokens(src, tokens = [], top = true) {
56
114
  src = src.replace(/^ +$/gm, '');
57
- let next,
58
- loose,
59
- cap,
60
- bull,
61
- b,
62
- item,
63
- listStart,
64
- listItems,
65
- t,
66
- space,
67
- i,
68
- tag,
69
- l,
70
- isordered,
71
- istask,
72
- ischecked;
115
+ let token, i, l, lastToken;
73
116
 
74
117
  while (src) {
75
118
  // newline
76
- if (cap = this.rules.newline.exec(src)) {
77
- src = src.substring(cap[0].length);
78
- if (cap[0].length > 1) {
79
- this.tokens.push({
80
- type: 'space'
81
- });
119
+ if (token = this.tokenizer.space(src)) {
120
+ src = src.substring(token.raw.length);
121
+ if (token.type) {
122
+ tokens.push(token);
82
123
  }
124
+ continue;
83
125
  }
84
126
 
85
127
  // code
86
- if (cap = this.rules.code.exec(src)) {
87
- const lastToken = this.tokens[this.tokens.length - 1];
88
- src = src.substring(cap[0].length);
89
- // An indented code block cannot interrupt a paragraph.
90
- if (lastToken && lastToken.type === 'paragraph') {
91
- lastToken.text += '\n' + cap[0].trimRight();
128
+ if (token = this.tokenizer.code(src, tokens)) {
129
+ src = src.substring(token.raw.length);
130
+ if (token.type) {
131
+ tokens.push(token);
92
132
  } else {
93
- cap = cap[0].replace(/^ {4}/gm, '');
94
- this.tokens.push({
95
- type: 'code',
96
- codeBlockStyle: 'indented',
97
- text: !this.options.pedantic
98
- ? rtrim(cap, '\n')
99
- : cap
100
- });
133
+ lastToken = tokens[tokens.length - 1];
134
+ lastToken.raw += '\n' + token.raw;
135
+ lastToken.text += '\n' + token.text;
101
136
  }
102
137
  continue;
103
138
  }
104
139
 
105
140
  // fences
106
- if (cap = this.rules.fences.exec(src)) {
107
- src = src.substring(cap[0].length);
108
- this.tokens.push({
109
- type: 'code',
110
- lang: cap[2] ? cap[2].trim() : cap[2],
111
- text: cap[3] || ''
112
- });
141
+ if (token = this.tokenizer.fences(src)) {
142
+ src = src.substring(token.raw.length);
143
+ tokens.push(token);
113
144
  continue;
114
145
  }
115
146
 
116
147
  // heading
117
- if (cap = this.rules.heading.exec(src)) {
118
- src = src.substring(cap[0].length);
119
- this.tokens.push({
120
- type: 'heading',
121
- depth: cap[1].length,
122
- text: cap[2]
123
- });
148
+ if (token = this.tokenizer.heading(src)) {
149
+ src = src.substring(token.raw.length);
150
+ tokens.push(token);
124
151
  continue;
125
152
  }
126
153
 
127
154
  // table no leading pipe (gfm)
128
- if (cap = this.rules.nptable.exec(src)) {
129
- item = {
130
- type: 'table',
131
- header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')),
132
- align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
133
- cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : []
134
- };
135
-
136
- if (item.header.length === item.align.length) {
137
- src = src.substring(cap[0].length);
138
-
139
- for (i = 0; i < item.align.length; i++) {
140
- if (/^ *-+: *$/.test(item.align[i])) {
141
- item.align[i] = 'right';
142
- } else if (/^ *:-+: *$/.test(item.align[i])) {
143
- item.align[i] = 'center';
144
- } else if (/^ *:-+ *$/.test(item.align[i])) {
145
- item.align[i] = 'left';
146
- } else {
147
- item.align[i] = null;
148
- }
149
- }
150
-
151
- for (i = 0; i < item.cells.length; i++) {
152
- item.cells[i] = splitCells(item.cells[i], item.header.length);
153
- }
154
-
155
- this.tokens.push(item);
156
-
157
- continue;
158
- }
155
+ if (token = this.tokenizer.nptable(src)) {
156
+ src = src.substring(token.raw.length);
157
+ tokens.push(token);
158
+ continue;
159
159
  }
160
160
 
161
161
  // hr
162
- if (cap = this.rules.hr.exec(src)) {
163
- src = src.substring(cap[0].length);
164
- this.tokens.push({
165
- type: 'hr'
166
- });
162
+ if (token = this.tokenizer.hr(src)) {
163
+ src = src.substring(token.raw.length);
164
+ tokens.push(token);
167
165
  continue;
168
166
  }
169
167
 
170
168
  // blockquote
171
- if (cap = this.rules.blockquote.exec(src)) {
172
- src = src.substring(cap[0].length);
173
-
174
- this.tokens.push({
175
- type: 'blockquote_start'
176
- });
169
+ if (token = this.tokenizer.blockquote(src)) {
170
+ src = src.substring(token.raw.length);
171
+ token.tokens = this.blockTokens(token.text, [], top);
172
+ tokens.push(token);
173
+ continue;
174
+ }
177
175
 
178
- cap = cap[0].replace(/^ *> ?/gm, '');
176
+ // list
177
+ if (token = this.tokenizer.list(src)) {
178
+ src = src.substring(token.raw.length);
179
+ l = token.items.length;
180
+ for (i = 0; i < l; i++) {
181
+ token.items[i].tokens = this.blockTokens(token.items[i].text, [], false);
182
+ }
183
+ tokens.push(token);
184
+ continue;
185
+ }
179
186
 
180
- // Pass `top` to keep the current
181
- // "toplevel" state. This is exactly
182
- // how markdown.pl works.
183
- this.token(cap, top);
187
+ // html
188
+ if (token = this.tokenizer.html(src)) {
189
+ src = src.substring(token.raw.length);
190
+ tokens.push(token);
191
+ continue;
192
+ }
184
193
 
185
- this.tokens.push({
186
- type: 'blockquote_end'
187
- });
194
+ // def
195
+ if (top && (token = this.tokenizer.def(src))) {
196
+ src = src.substring(token.raw.length);
197
+ if (!this.tokens.links[token.tag]) {
198
+ this.tokens.links[token.tag] = {
199
+ href: token.href,
200
+ title: token.title
201
+ };
202
+ }
203
+ continue;
204
+ }
188
205
 
206
+ // table (gfm)
207
+ if (token = this.tokenizer.table(src)) {
208
+ src = src.substring(token.raw.length);
209
+ tokens.push(token);
189
210
  continue;
190
211
  }
191
212
 
192
- // list
193
- if (cap = this.rules.list.exec(src)) {
194
- src = src.substring(cap[0].length);
195
- bull = cap[2];
196
- isordered = bull.length > 1;
197
-
198
- listStart = {
199
- type: 'list_start',
200
- ordered: isordered,
201
- start: isordered ? +bull : '',
202
- loose: false
203
- };
204
-
205
- this.tokens.push(listStart);
206
-
207
- // Get each top-level item.
208
- cap = cap[0].match(this.rules.item);
209
-
210
- listItems = [];
211
- next = false;
212
- l = cap.length;
213
- i = 0;
214
-
215
- for (; i < l; i++) {
216
- item = cap[i];
217
-
218
- // Remove the list item's bullet
219
- // so it is seen as the next token.
220
- space = item.length;
221
- item = item.replace(/^ *([*+-]|\d+\.) */, '');
222
-
223
- // Outdent whatever the
224
- // list item contains. Hacky.
225
- if (~item.indexOf('\n ')) {
226
- space -= item.length;
227
- item = !this.options.pedantic
228
- ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
229
- : item.replace(/^ {1,4}/gm, '');
230
- }
213
+ // lheading
214
+ if (token = this.tokenizer.lheading(src)) {
215
+ src = src.substring(token.raw.length);
216
+ tokens.push(token);
217
+ continue;
218
+ }
231
219
 
232
- // Determine whether the next list item belongs here.
233
- // Backpedal if it does not belong in this list.
234
- if (i !== l - 1) {
235
- b = block.bullet.exec(cap[i + 1])[0];
236
- if (bull.length > 1 ? b.length === 1
237
- : (b.length > 1 || (this.options.smartLists && b !== bull))) {
238
- src = cap.slice(i + 1).join('\n') + src;
239
- i = l - 1;
240
- }
241
- }
220
+ // top-level paragraph
221
+ if (top && (token = this.tokenizer.paragraph(src))) {
222
+ src = src.substring(token.raw.length);
223
+ tokens.push(token);
224
+ continue;
225
+ }
242
226
 
243
- // Determine whether item is loose or not.
244
- // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
245
- // for discount behavior.
246
- loose = next || /\n\n(?!\s*$)/.test(item);
247
- if (i !== l - 1) {
248
- next = item.charAt(item.length - 1) === '\n';
249
- if (!loose) loose = next;
250
- }
227
+ // text
228
+ if (token = this.tokenizer.text(src, tokens)) {
229
+ src = src.substring(token.raw.length);
230
+ if (token.type) {
231
+ tokens.push(token);
232
+ } else {
233
+ lastToken = tokens[tokens.length - 1];
234
+ lastToken.raw += '\n' + token.raw;
235
+ lastToken.text += '\n' + token.text;
236
+ }
237
+ continue;
238
+ }
251
239
 
252
- if (loose) {
253
- listStart.loose = true;
254
- }
240
+ if (src) {
241
+ const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0);
242
+ if (this.options.silent) {
243
+ console.error(errMsg);
244
+ break;
245
+ } else {
246
+ throw new Error(errMsg);
247
+ }
248
+ }
249
+ }
255
250
 
256
- // Check for task list items
257
- istask = /^\[[ xX]\] /.test(item);
258
- ischecked = undefined;
259
- if (istask) {
260
- ischecked = item[1] !== ' ';
261
- item = item.replace(/^\[[ xX]\] +/, '');
262
- }
251
+ return tokens;
252
+ }
263
253
 
264
- t = {
265
- type: 'list_item_start',
266
- task: istask,
267
- checked: ischecked,
268
- loose: loose
254
+ inline(tokens) {
255
+ let i,
256
+ j,
257
+ k,
258
+ l2,
259
+ row,
260
+ token;
261
+
262
+ const l = tokens.length;
263
+ for (i = 0; i < l; i++) {
264
+ token = tokens[i];
265
+ switch (token.type) {
266
+ case 'paragraph':
267
+ case 'text':
268
+ case 'heading': {
269
+ token.tokens = [];
270
+ this.inlineTokens(token.text, token.tokens);
271
+ break;
272
+ }
273
+ case 'table': {
274
+ token.tokens = {
275
+ header: [],
276
+ cells: []
269
277
  };
270
278
 
271
- listItems.push(t);
272
- this.tokens.push(t);
279
+ // header
280
+ l2 = token.header.length;
281
+ for (j = 0; j < l2; j++) {
282
+ token.tokens.header[j] = [];
283
+ this.inlineTokens(token.header[j], token.tokens.header[j]);
284
+ }
273
285
 
274
- // Recurse.
275
- this.token(item, false);
286
+ // cells
287
+ l2 = token.cells.length;
288
+ for (j = 0; j < l2; j++) {
289
+ row = token.cells[j];
290
+ token.tokens.cells[j] = [];
291
+ for (k = 0; k < row.length; k++) {
292
+ token.tokens.cells[j][k] = [];
293
+ this.inlineTokens(row[k], token.tokens.cells[j][k]);
294
+ }
295
+ }
276
296
 
277
- this.tokens.push({
278
- type: 'list_item_end'
279
- });
297
+ break;
280
298
  }
281
-
282
- if (listStart.loose) {
283
- l = listItems.length;
284
- i = 0;
285
- for (; i < l; i++) {
286
- listItems[i].loose = true;
299
+ case 'blockquote': {
300
+ this.inline(token.tokens);
301
+ break;
302
+ }
303
+ case 'list': {
304
+ l2 = token.items.length;
305
+ for (j = 0; j < l2; j++) {
306
+ this.inline(token.items[j].tokens);
287
307
  }
308
+ break;
309
+ }
310
+ default: {
311
+ // do nothing
288
312
  }
313
+ }
314
+ }
289
315
 
290
- this.tokens.push({
291
- type: 'list_end'
292
- });
316
+ return tokens;
317
+ }
293
318
 
319
+ /**
320
+ * Lexing/Compiling
321
+ */
322
+ inlineTokens(src, tokens = [], inLink = false, inRawBlock = false) {
323
+ let token;
324
+
325
+ while (src) {
326
+ // escape
327
+ if (token = this.tokenizer.escape(src)) {
328
+ src = src.substring(token.raw.length);
329
+ tokens.push(token);
294
330
  continue;
295
331
  }
296
332
 
297
- // html
298
- if (cap = this.rules.html.exec(src)) {
299
- src = src.substring(cap[0].length);
300
- this.tokens.push({
301
- type: this.options.sanitize
302
- ? 'paragraph'
303
- : 'html',
304
- pre: !this.options.sanitizer
305
- && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
306
- text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]
307
- });
333
+ // tag
334
+ if (token = this.tokenizer.tag(src, inLink, inRawBlock)) {
335
+ src = src.substring(token.raw.length);
336
+ inLink = token.inLink;
337
+ inRawBlock = token.inRawBlock;
338
+ tokens.push(token);
308
339
  continue;
309
340
  }
310
341
 
311
- // def
312
- if (top && (cap = this.rules.def.exec(src))) {
313
- src = src.substring(cap[0].length);
314
- if (cap[3]) cap[3] = cap[3].substring(1, cap[3].length - 1);
315
- tag = cap[1].toLowerCase().replace(/\s+/g, ' ');
316
- if (!this.tokens.links[tag]) {
317
- this.tokens.links[tag] = {
318
- href: cap[2],
319
- title: cap[3]
320
- };
342
+ // link
343
+ if (token = this.tokenizer.link(src)) {
344
+ src = src.substring(token.raw.length);
345
+ if (token.type === 'link') {
346
+ token.tokens = this.inlineTokens(token.text, [], true, inRawBlock);
321
347
  }
348
+ tokens.push(token);
322
349
  continue;
323
350
  }
324
351
 
325
- // table (gfm)
326
- if (cap = this.rules.table.exec(src)) {
327
- item = {
328
- type: 'table',
329
- header: splitCells(cap[1].replace(/^ *| *\| *$/g, '')),
330
- align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
331
- cells: cap[3] ? cap[3].replace(/\n$/, '').split('\n') : []
332
- };
333
-
334
- if (item.header.length === item.align.length) {
335
- src = src.substring(cap[0].length);
336
-
337
- for (i = 0; i < item.align.length; i++) {
338
- if (/^ *-+: *$/.test(item.align[i])) {
339
- item.align[i] = 'right';
340
- } else if (/^ *:-+: *$/.test(item.align[i])) {
341
- item.align[i] = 'center';
342
- } else if (/^ *:-+ *$/.test(item.align[i])) {
343
- item.align[i] = 'left';
344
- } else {
345
- item.align[i] = null;
346
- }
347
- }
352
+ // reflink, nolink
353
+ if (token = this.tokenizer.reflink(src, this.tokens.links)) {
354
+ src = src.substring(token.raw.length);
355
+ if (token.type === 'link') {
356
+ token.tokens = this.inlineTokens(token.text, [], true, inRawBlock);
357
+ }
358
+ tokens.push(token);
359
+ continue;
360
+ }
348
361
 
349
- for (i = 0; i < item.cells.length; i++) {
350
- item.cells[i] = splitCells(
351
- item.cells[i].replace(/^ *\| *| *\| *$/g, ''),
352
- item.header.length);
353
- }
362
+ // strong
363
+ if (token = this.tokenizer.strong(src)) {
364
+ src = src.substring(token.raw.length);
365
+ token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock);
366
+ tokens.push(token);
367
+ continue;
368
+ }
354
369
 
355
- this.tokens.push(item);
370
+ // em
371
+ if (token = this.tokenizer.em(src)) {
372
+ src = src.substring(token.raw.length);
373
+ token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock);
374
+ tokens.push(token);
375
+ continue;
376
+ }
356
377
 
357
- continue;
358
- }
378
+ // code
379
+ if (token = this.tokenizer.codespan(src)) {
380
+ src = src.substring(token.raw.length);
381
+ tokens.push(token);
382
+ continue;
359
383
  }
360
384
 
361
- // lheading
362
- if (cap = this.rules.lheading.exec(src)) {
363
- src = src.substring(cap[0].length);
364
- this.tokens.push({
365
- type: 'heading',
366
- depth: cap[2].charAt(0) === '=' ? 1 : 2,
367
- text: cap[1]
368
- });
385
+ // br
386
+ if (token = this.tokenizer.br(src)) {
387
+ src = src.substring(token.raw.length);
388
+ tokens.push(token);
369
389
  continue;
370
390
  }
371
391
 
372
- // top-level paragraph
373
- if (top && (cap = this.rules.paragraph.exec(src))) {
374
- src = src.substring(cap[0].length);
375
- this.tokens.push({
376
- type: 'paragraph',
377
- text: cap[1].charAt(cap[1].length - 1) === '\n'
378
- ? cap[1].slice(0, -1)
379
- : cap[1]
380
- });
392
+ // del (gfm)
393
+ if (token = this.tokenizer.del(src)) {
394
+ src = src.substring(token.raw.length);
395
+ token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock);
396
+ tokens.push(token);
397
+ continue;
398
+ }
399
+
400
+ // autolink
401
+ if (token = this.tokenizer.autolink(src, mangle)) {
402
+ src = src.substring(token.raw.length);
403
+ tokens.push(token);
404
+ continue;
405
+ }
406
+
407
+ // url (gfm)
408
+ if (!inLink && (token = this.tokenizer.url(src, mangle))) {
409
+ src = src.substring(token.raw.length);
410
+ tokens.push(token);
381
411
  continue;
382
412
  }
383
413
 
384
414
  // text
385
- if (cap = this.rules.text.exec(src)) {
386
- // Top-level should never reach here.
387
- src = src.substring(cap[0].length);
388
- this.tokens.push({
389
- type: 'text',
390
- text: cap[0]
391
- });
415
+ if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) {
416
+ src = src.substring(token.raw.length);
417
+ tokens.push(token);
392
418
  continue;
393
419
  }
394
420
 
395
421
  if (src) {
396
- throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
422
+ const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0);
423
+ if (this.options.silent) {
424
+ console.error(errMsg);
425
+ break;
426
+ } else {
427
+ throw new Error(errMsg);
428
+ }
397
429
  }
398
430
  }
399
431
 
400
- return this.tokens;
401
- };
432
+ return tokens;
433
+ }
402
434
  };