marked 0.8.2 → 1.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
@@ -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,323 @@ 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;
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();
92
- } 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
- });
101
- }
128
+ if (token = this.tokenizer.code(src, tokens)) {
129
+ src = src.substring(token.raw.length);
130
+ tokens.push(token);
102
131
  continue;
103
132
  }
104
133
 
105
134
  // 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
- });
135
+ if (token = this.tokenizer.fences(src)) {
136
+ src = src.substring(token.raw.length);
137
+ tokens.push(token);
113
138
  continue;
114
139
  }
115
140
 
116
141
  // 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
- });
142
+ if (token = this.tokenizer.heading(src)) {
143
+ src = src.substring(token.raw.length);
144
+ tokens.push(token);
124
145
  continue;
125
146
  }
126
147
 
127
148
  // 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
- }
149
+ if (token = this.tokenizer.nptable(src)) {
150
+ src = src.substring(token.raw.length);
151
+ tokens.push(token);
152
+ continue;
159
153
  }
160
154
 
161
155
  // hr
162
- if (cap = this.rules.hr.exec(src)) {
163
- src = src.substring(cap[0].length);
164
- this.tokens.push({
165
- type: 'hr'
166
- });
156
+ if (token = this.tokenizer.hr(src)) {
157
+ src = src.substring(token.raw.length);
158
+ tokens.push(token);
167
159
  continue;
168
160
  }
169
161
 
170
162
  // 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
- });
163
+ if (token = this.tokenizer.blockquote(src)) {
164
+ src = src.substring(token.raw.length);
165
+ token.tokens = this.blockTokens(token.text, [], top);
166
+ tokens.push(token);
167
+ continue;
168
+ }
177
169
 
178
- cap = cap[0].replace(/^ *> ?/gm, '');
170
+ // list
171
+ if (token = this.tokenizer.list(src)) {
172
+ src = src.substring(token.raw.length);
173
+ l = token.items.length;
174
+ for (i = 0; i < l; i++) {
175
+ token.items[i].tokens = this.blockTokens(token.items[i].text, [], false);
176
+ }
177
+ tokens.push(token);
178
+ continue;
179
+ }
179
180
 
180
- // Pass `top` to keep the current
181
- // "toplevel" state. This is exactly
182
- // how markdown.pl works.
183
- this.token(cap, top);
181
+ // html
182
+ if (token = this.tokenizer.html(src)) {
183
+ src = src.substring(token.raw.length);
184
+ tokens.push(token);
185
+ continue;
186
+ }
184
187
 
185
- this.tokens.push({
186
- type: 'blockquote_end'
187
- });
188
+ // def
189
+ if (top && (token = this.tokenizer.def(src))) {
190
+ src = src.substring(token.raw.length);
191
+ if (!this.tokens.links[token.tag]) {
192
+ this.tokens.links[token.tag] = {
193
+ href: token.href,
194
+ title: token.title
195
+ };
196
+ }
197
+ continue;
198
+ }
188
199
 
200
+ // table (gfm)
201
+ if (token = this.tokenizer.table(src)) {
202
+ src = src.substring(token.raw.length);
203
+ tokens.push(token);
189
204
  continue;
190
205
  }
191
206
 
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
- }
207
+ // lheading
208
+ if (token = this.tokenizer.lheading(src)) {
209
+ src = src.substring(token.raw.length);
210
+ tokens.push(token);
211
+ continue;
212
+ }
231
213
 
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
- }
214
+ // top-level paragraph
215
+ if (top && (token = this.tokenizer.paragraph(src))) {
216
+ src = src.substring(token.raw.length);
217
+ tokens.push(token);
218
+ continue;
219
+ }
242
220
 
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
- }
221
+ // text
222
+ if (token = this.tokenizer.text(src)) {
223
+ src = src.substring(token.raw.length);
224
+ tokens.push(token);
225
+ continue;
226
+ }
251
227
 
252
- if (loose) {
253
- listStart.loose = true;
254
- }
228
+ if (src) {
229
+ const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0);
230
+ if (this.options.silent) {
231
+ console.error(errMsg);
232
+ break;
233
+ } else {
234
+ throw new Error(errMsg);
235
+ }
236
+ }
237
+ }
255
238
 
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
- }
239
+ return tokens;
240
+ }
263
241
 
264
- t = {
265
- type: 'list_item_start',
266
- task: istask,
267
- checked: ischecked,
268
- loose: loose
242
+ inline(tokens) {
243
+ let i,
244
+ j,
245
+ k,
246
+ l2,
247
+ row,
248
+ token;
249
+
250
+ const l = tokens.length;
251
+ for (i = 0; i < l; i++) {
252
+ token = tokens[i];
253
+ switch (token.type) {
254
+ case 'paragraph':
255
+ case 'text':
256
+ case 'heading': {
257
+ token.tokens = [];
258
+ this.inlineTokens(token.text, token.tokens);
259
+ break;
260
+ }
261
+ case 'table': {
262
+ token.tokens = {
263
+ header: [],
264
+ cells: []
269
265
  };
270
266
 
271
- listItems.push(t);
272
- this.tokens.push(t);
267
+ // header
268
+ l2 = token.header.length;
269
+ for (j = 0; j < l2; j++) {
270
+ token.tokens.header[j] = [];
271
+ this.inlineTokens(token.header[j], token.tokens.header[j]);
272
+ }
273
273
 
274
- // Recurse.
275
- this.token(item, false);
274
+ // cells
275
+ l2 = token.cells.length;
276
+ for (j = 0; j < l2; j++) {
277
+ row = token.cells[j];
278
+ token.tokens.cells[j] = [];
279
+ for (k = 0; k < row.length; k++) {
280
+ token.tokens.cells[j][k] = [];
281
+ this.inlineTokens(row[k], token.tokens.cells[j][k]);
282
+ }
283
+ }
276
284
 
277
- this.tokens.push({
278
- type: 'list_item_end'
279
- });
285
+ break;
280
286
  }
281
-
282
- if (listStart.loose) {
283
- l = listItems.length;
284
- i = 0;
285
- for (; i < l; i++) {
286
- listItems[i].loose = true;
287
+ case 'blockquote': {
288
+ this.inline(token.tokens);
289
+ break;
290
+ }
291
+ case 'list': {
292
+ l2 = token.items.length;
293
+ for (j = 0; j < l2; j++) {
294
+ this.inline(token.items[j].tokens);
287
295
  }
296
+ break;
297
+ }
298
+ default: {
299
+ // do nothing
288
300
  }
301
+ }
302
+ }
289
303
 
290
- this.tokens.push({
291
- type: 'list_end'
292
- });
304
+ return tokens;
305
+ }
293
306
 
307
+ /**
308
+ * Lexing/Compiling
309
+ */
310
+ inlineTokens(src, tokens = [], inLink = false, inRawBlock = false) {
311
+ let token;
312
+
313
+ while (src) {
314
+ // escape
315
+ if (token = this.tokenizer.escape(src)) {
316
+ src = src.substring(token.raw.length);
317
+ tokens.push(token);
294
318
  continue;
295
319
  }
296
320
 
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
- });
321
+ // tag
322
+ if (token = this.tokenizer.tag(src, inLink, inRawBlock)) {
323
+ src = src.substring(token.raw.length);
324
+ inLink = token.inLink;
325
+ inRawBlock = token.inRawBlock;
326
+ tokens.push(token);
308
327
  continue;
309
328
  }
310
329
 
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
- };
330
+ // link
331
+ if (token = this.tokenizer.link(src)) {
332
+ src = src.substring(token.raw.length);
333
+ if (token.type === 'link') {
334
+ token.tokens = this.inlineTokens(token.text, [], true, inRawBlock);
321
335
  }
336
+ tokens.push(token);
322
337
  continue;
323
338
  }
324
339
 
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
- }
340
+ // reflink, nolink
341
+ if (token = this.tokenizer.reflink(src, this.tokens.links)) {
342
+ src = src.substring(token.raw.length);
343
+ if (token.type === 'link') {
344
+ token.tokens = this.inlineTokens(token.text, [], true, inRawBlock);
345
+ }
346
+ tokens.push(token);
347
+ continue;
348
+ }
348
349
 
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
- }
350
+ // strong
351
+ if (token = this.tokenizer.strong(src)) {
352
+ src = src.substring(token.raw.length);
353
+ token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock);
354
+ tokens.push(token);
355
+ continue;
356
+ }
354
357
 
355
- this.tokens.push(item);
358
+ // em
359
+ if (token = this.tokenizer.em(src)) {
360
+ src = src.substring(token.raw.length);
361
+ token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock);
362
+ tokens.push(token);
363
+ continue;
364
+ }
356
365
 
357
- continue;
358
- }
366
+ // code
367
+ if (token = this.tokenizer.codespan(src)) {
368
+ src = src.substring(token.raw.length);
369
+ tokens.push(token);
370
+ continue;
359
371
  }
360
372
 
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
- });
373
+ // br
374
+ if (token = this.tokenizer.br(src)) {
375
+ src = src.substring(token.raw.length);
376
+ tokens.push(token);
369
377
  continue;
370
378
  }
371
379
 
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
- });
380
+ // del (gfm)
381
+ if (token = this.tokenizer.del(src)) {
382
+ src = src.substring(token.raw.length);
383
+ token.tokens = this.inlineTokens(token.text, [], inLink, inRawBlock);
384
+ tokens.push(token);
385
+ continue;
386
+ }
387
+
388
+ // autolink
389
+ if (token = this.tokenizer.autolink(src, mangle)) {
390
+ src = src.substring(token.raw.length);
391
+ tokens.push(token);
392
+ continue;
393
+ }
394
+
395
+ // url (gfm)
396
+ if (!inLink && (token = this.tokenizer.url(src, mangle))) {
397
+ src = src.substring(token.raw.length);
398
+ tokens.push(token);
381
399
  continue;
382
400
  }
383
401
 
384
402
  // 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
- });
403
+ if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) {
404
+ src = src.substring(token.raw.length);
405
+ tokens.push(token);
392
406
  continue;
393
407
  }
394
408
 
395
409
  if (src) {
396
- throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
410
+ const errMsg = 'Infinite loop on byte: ' + src.charCodeAt(0);
411
+ if (this.options.silent) {
412
+ console.error(errMsg);
413
+ break;
414
+ } else {
415
+ throw new Error(errMsg);
416
+ }
397
417
  }
398
418
  }
399
419
 
400
- return this.tokens;
401
- };
420
+ return tokens;
421
+ }
402
422
  };