marked 0.6.1 → 0.8.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.
@@ -0,0 +1,293 @@
1
+ const Renderer = require('./Renderer.js');
2
+ const { defaults } = require('./defaults.js');
3
+ const { inline } = require('./rules.js');
4
+ const {
5
+ findClosingBracket,
6
+ escape
7
+ } = require('./helpers.js');
8
+
9
+ /**
10
+ * Inline Lexer & Compiler
11
+ */
12
+ module.exports = class InlineLexer {
13
+ constructor(links, options) {
14
+ this.options = options || defaults;
15
+ this.links = links;
16
+ this.rules = inline.normal;
17
+ this.options.renderer = this.options.renderer || new Renderer();
18
+ this.renderer = this.options.renderer;
19
+ this.renderer.options = this.options;
20
+
21
+ if (!this.links) {
22
+ throw new Error('Tokens array requires a `links` property.');
23
+ }
24
+
25
+ if (this.options.pedantic) {
26
+ this.rules = inline.pedantic;
27
+ } else if (this.options.gfm) {
28
+ if (this.options.breaks) {
29
+ this.rules = inline.breaks;
30
+ } else {
31
+ this.rules = inline.gfm;
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Expose Inline Rules
38
+ */
39
+ static get rules() {
40
+ return inline;
41
+ }
42
+
43
+ /**
44
+ * Static Lexing/Compiling Method
45
+ */
46
+ static output(src, links, options) {
47
+ const inline = new InlineLexer(links, options);
48
+ return inline.output(src);
49
+ }
50
+
51
+ /**
52
+ * Lexing/Compiling
53
+ */
54
+ output(src) {
55
+ let out = '',
56
+ link,
57
+ text,
58
+ href,
59
+ title,
60
+ cap,
61
+ prevCapZero;
62
+
63
+ while (src) {
64
+ // escape
65
+ if (cap = this.rules.escape.exec(src)) {
66
+ src = src.substring(cap[0].length);
67
+ out += escape(cap[1]);
68
+ continue;
69
+ }
70
+
71
+ // tag
72
+ if (cap = this.rules.tag.exec(src)) {
73
+ if (!this.inLink && /^<a /i.test(cap[0])) {
74
+ this.inLink = true;
75
+ } else if (this.inLink && /^<\/a>/i.test(cap[0])) {
76
+ this.inLink = false;
77
+ }
78
+ if (!this.inRawBlock && /^<(pre|code|kbd|script)(\s|>)/i.test(cap[0])) {
79
+ this.inRawBlock = true;
80
+ } else if (this.inRawBlock && /^<\/(pre|code|kbd|script)(\s|>)/i.test(cap[0])) {
81
+ this.inRawBlock = false;
82
+ }
83
+
84
+ src = src.substring(cap[0].length);
85
+ out += this.options.sanitize
86
+ ? this.options.sanitizer
87
+ ? this.options.sanitizer(cap[0])
88
+ : escape(cap[0])
89
+ : cap[0];
90
+ continue;
91
+ }
92
+
93
+ // link
94
+ if (cap = this.rules.link.exec(src)) {
95
+ const lastParenIndex = findClosingBracket(cap[2], '()');
96
+ if (lastParenIndex > -1) {
97
+ const start = cap[0].indexOf('!') === 0 ? 5 : 4;
98
+ const linkLen = start + cap[1].length + lastParenIndex;
99
+ cap[2] = cap[2].substring(0, lastParenIndex);
100
+ cap[0] = cap[0].substring(0, linkLen).trim();
101
+ cap[3] = '';
102
+ }
103
+ src = src.substring(cap[0].length);
104
+ this.inLink = true;
105
+ href = cap[2];
106
+ if (this.options.pedantic) {
107
+ link = /^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(href);
108
+
109
+ if (link) {
110
+ href = link[1];
111
+ title = link[3];
112
+ } else {
113
+ title = '';
114
+ }
115
+ } else {
116
+ title = cap[3] ? cap[3].slice(1, -1) : '';
117
+ }
118
+ href = href.trim().replace(/^<([\s\S]*)>$/, '$1');
119
+ out += this.outputLink(cap, {
120
+ href: InlineLexer.escapes(href),
121
+ title: InlineLexer.escapes(title)
122
+ });
123
+ this.inLink = false;
124
+ continue;
125
+ }
126
+
127
+ // reflink, nolink
128
+ if ((cap = this.rules.reflink.exec(src))
129
+ || (cap = this.rules.nolink.exec(src))) {
130
+ src = src.substring(cap[0].length);
131
+ link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
132
+ link = this.links[link.toLowerCase()];
133
+ if (!link || !link.href) {
134
+ out += cap[0].charAt(0);
135
+ src = cap[0].substring(1) + src;
136
+ continue;
137
+ }
138
+ this.inLink = true;
139
+ out += this.outputLink(cap, link);
140
+ this.inLink = false;
141
+ continue;
142
+ }
143
+
144
+ // strong
145
+ if (cap = this.rules.strong.exec(src)) {
146
+ src = src.substring(cap[0].length);
147
+ out += this.renderer.strong(this.output(cap[4] || cap[3] || cap[2] || cap[1]));
148
+ continue;
149
+ }
150
+
151
+ // em
152
+ if (cap = this.rules.em.exec(src)) {
153
+ src = src.substring(cap[0].length);
154
+ out += this.renderer.em(this.output(cap[6] || cap[5] || cap[4] || cap[3] || cap[2] || cap[1]));
155
+ continue;
156
+ }
157
+
158
+ // code
159
+ if (cap = this.rules.code.exec(src)) {
160
+ src = src.substring(cap[0].length);
161
+ out += this.renderer.codespan(escape(cap[2].trim(), true));
162
+ continue;
163
+ }
164
+
165
+ // br
166
+ if (cap = this.rules.br.exec(src)) {
167
+ src = src.substring(cap[0].length);
168
+ out += this.renderer.br();
169
+ continue;
170
+ }
171
+
172
+ // del (gfm)
173
+ if (cap = this.rules.del.exec(src)) {
174
+ src = src.substring(cap[0].length);
175
+ out += this.renderer.del(this.output(cap[1]));
176
+ continue;
177
+ }
178
+
179
+ // autolink
180
+ if (cap = this.rules.autolink.exec(src)) {
181
+ src = src.substring(cap[0].length);
182
+ if (cap[2] === '@') {
183
+ text = escape(this.mangle(cap[1]));
184
+ href = 'mailto:' + text;
185
+ } else {
186
+ text = escape(cap[1]);
187
+ href = text;
188
+ }
189
+ out += this.renderer.link(href, null, text);
190
+ continue;
191
+ }
192
+
193
+ // url (gfm)
194
+ if (!this.inLink && (cap = this.rules.url.exec(src))) {
195
+ if (cap[2] === '@') {
196
+ text = escape(cap[0]);
197
+ href = 'mailto:' + text;
198
+ } else {
199
+ // do extended autolink path validation
200
+ do {
201
+ prevCapZero = cap[0];
202
+ cap[0] = this.rules._backpedal.exec(cap[0])[0];
203
+ } while (prevCapZero !== cap[0]);
204
+ text = escape(cap[0]);
205
+ if (cap[1] === 'www.') {
206
+ href = 'http://' + text;
207
+ } else {
208
+ href = text;
209
+ }
210
+ }
211
+ src = src.substring(cap[0].length);
212
+ out += this.renderer.link(href, null, text);
213
+ continue;
214
+ }
215
+
216
+ // text
217
+ if (cap = this.rules.text.exec(src)) {
218
+ src = src.substring(cap[0].length);
219
+ if (this.inRawBlock) {
220
+ out += this.renderer.text(this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]);
221
+ } else {
222
+ out += this.renderer.text(escape(this.smartypants(cap[0])));
223
+ }
224
+ continue;
225
+ }
226
+
227
+ if (src) {
228
+ throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
229
+ }
230
+ }
231
+
232
+ return out;
233
+ }
234
+
235
+ static escapes(text) {
236
+ return text ? text.replace(InlineLexer.rules._escapes, '$1') : text;
237
+ }
238
+
239
+ /**
240
+ * Compile Link
241
+ */
242
+ outputLink(cap, link) {
243
+ const href = link.href,
244
+ title = link.title ? escape(link.title) : null;
245
+
246
+ return cap[0].charAt(0) !== '!'
247
+ ? this.renderer.link(href, title, this.output(cap[1]))
248
+ : this.renderer.image(href, title, escape(cap[1]));
249
+ }
250
+
251
+ /**
252
+ * Smartypants Transformations
253
+ */
254
+ smartypants(text) {
255
+ if (!this.options.smartypants) return text;
256
+ return text
257
+ // em-dashes
258
+ .replace(/---/g, '\u2014')
259
+ // en-dashes
260
+ .replace(/--/g, '\u2013')
261
+ // opening singles
262
+ .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
263
+ // closing singles & apostrophes
264
+ .replace(/'/g, '\u2019')
265
+ // opening doubles
266
+ .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
267
+ // closing doubles
268
+ .replace(/"/g, '\u201d')
269
+ // ellipses
270
+ .replace(/\.{3}/g, '\u2026');
271
+ }
272
+
273
+ /**
274
+ * Mangle Links
275
+ */
276
+ mangle(text) {
277
+ if (!this.options.mangle) return text;
278
+ const l = text.length;
279
+ let out = '',
280
+ i = 0,
281
+ ch;
282
+
283
+ for (; i < l; i++) {
284
+ ch = text.charCodeAt(i);
285
+ if (Math.random() > 0.5) {
286
+ ch = 'x' + ch.toString(16);
287
+ }
288
+ out += '&#' + ch + ';';
289
+ }
290
+
291
+ return out;
292
+ }
293
+ };
package/src/Lexer.js ADDED
@@ -0,0 +1,402 @@
1
+ const { defaults } = require('./defaults.js');
2
+ const { block } = require('./rules.js');
3
+ const {
4
+ rtrim,
5
+ splitCells,
6
+ escape
7
+ } = require('./helpers.js');
8
+
9
+ /**
10
+ * Block Lexer
11
+ */
12
+ module.exports = class Lexer {
13
+ constructor(options) {
14
+ this.tokens = [];
15
+ this.tokens.links = Object.create(null);
16
+ this.options = options || defaults;
17
+ this.rules = block.normal;
18
+
19
+ if (this.options.pedantic) {
20
+ this.rules = block.pedantic;
21
+ } else if (this.options.gfm) {
22
+ this.rules = block.gfm;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Expose Block Rules
28
+ */
29
+ static get rules() {
30
+ return block;
31
+ }
32
+
33
+ /**
34
+ * Static Lex Method
35
+ */
36
+ static lex(src, options) {
37
+ const lexer = new Lexer(options);
38
+ return lexer.lex(src);
39
+ };
40
+
41
+ /**
42
+ * Preprocessing
43
+ */
44
+ lex(src) {
45
+ src = src
46
+ .replace(/\r\n|\r/g, '\n')
47
+ .replace(/\t/g, ' ');
48
+
49
+ return this.token(src, true);
50
+ };
51
+
52
+ /**
53
+ * Lexing
54
+ */
55
+ token(src, top) {
56
+ 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;
73
+
74
+ while (src) {
75
+ // 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
+ });
82
+ }
83
+ }
84
+
85
+ // 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
+ }
102
+ continue;
103
+ }
104
+
105
+ // 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
+ });
113
+ continue;
114
+ }
115
+
116
+ // 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
+ });
124
+ continue;
125
+ }
126
+
127
+ // 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
+ }
159
+ }
160
+
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
+ });
167
+ continue;
168
+ }
169
+
170
+ // 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
+ });
177
+
178
+ cap = cap[0].replace(/^ *> ?/gm, '');
179
+
180
+ // Pass `top` to keep the current
181
+ // "toplevel" state. This is exactly
182
+ // how markdown.pl works.
183
+ this.token(cap, top);
184
+
185
+ this.tokens.push({
186
+ type: 'blockquote_end'
187
+ });
188
+
189
+ continue;
190
+ }
191
+
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
+ }
231
+
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
+ }
242
+
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
+ }
251
+
252
+ if (loose) {
253
+ listStart.loose = true;
254
+ }
255
+
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
+ }
263
+
264
+ t = {
265
+ type: 'list_item_start',
266
+ task: istask,
267
+ checked: ischecked,
268
+ loose: loose
269
+ };
270
+
271
+ listItems.push(t);
272
+ this.tokens.push(t);
273
+
274
+ // Recurse.
275
+ this.token(item, false);
276
+
277
+ this.tokens.push({
278
+ type: 'list_item_end'
279
+ });
280
+ }
281
+
282
+ if (listStart.loose) {
283
+ l = listItems.length;
284
+ i = 0;
285
+ for (; i < l; i++) {
286
+ listItems[i].loose = true;
287
+ }
288
+ }
289
+
290
+ this.tokens.push({
291
+ type: 'list_end'
292
+ });
293
+
294
+ continue;
295
+ }
296
+
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
+ });
308
+ continue;
309
+ }
310
+
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
+ };
321
+ }
322
+ continue;
323
+ }
324
+
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
+ }
348
+
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
+ }
354
+
355
+ this.tokens.push(item);
356
+
357
+ continue;
358
+ }
359
+ }
360
+
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
+ });
369
+ continue;
370
+ }
371
+
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
+ });
381
+ continue;
382
+ }
383
+
384
+ // 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
+ });
392
+ continue;
393
+ }
394
+
395
+ if (src) {
396
+ throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
397
+ }
398
+ }
399
+
400
+ return this.tokens;
401
+ };
402
+ };