UNPKG

7.79 kBJavaScriptView Raw
1const Renderer = require('./Renderer.js');
2const { defaults } = require('./defaults.js');
3const { inline } = require('./rules.js');
4const {
5 findClosingBracket,
6 escape
7} = require('./helpers.js');
8
9/**
10 * Inline Lexer & Compiler
11 */
12module.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};