turndown 7.2.2 → 7.2.3

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.
@@ -1,146 +1,107 @@
1
1
  'use strict';
2
2
 
3
- function extend (destination) {
3
+ function extend(destination) {
4
4
  for (var i = 1; i < arguments.length; i++) {
5
5
  var source = arguments[i];
6
6
  for (var key in source) {
7
- if (source.hasOwnProperty(key)) destination[key] = source[key];
7
+ if (Object.prototype.hasOwnProperty.call(source, key)) destination[key] = source[key];
8
8
  }
9
9
  }
10
- return destination
10
+ return destination;
11
11
  }
12
-
13
- function repeat (character, count) {
14
- return Array(count + 1).join(character)
12
+ function repeat(character, count) {
13
+ return Array(count + 1).join(character);
15
14
  }
16
-
17
- function trimLeadingNewlines (string) {
18
- return string.replace(/^\n*/, '')
15
+ function trimLeadingNewlines(string) {
16
+ return string.replace(/^\n*/, '');
19
17
  }
20
-
21
- function trimTrailingNewlines (string) {
18
+ function trimTrailingNewlines(string) {
22
19
  // avoid match-at-end regexp bottleneck, see #370
23
20
  var indexEnd = string.length;
24
21
  while (indexEnd > 0 && string[indexEnd - 1] === '\n') indexEnd--;
25
- return string.substring(0, indexEnd)
22
+ return string.substring(0, indexEnd);
26
23
  }
27
-
28
- function trimNewlines (string) {
29
- return trimTrailingNewlines(trimLeadingNewlines(string))
24
+ function trimNewlines(string) {
25
+ return trimTrailingNewlines(trimLeadingNewlines(string));
30
26
  }
31
-
32
- var blockElements = [
33
- 'ADDRESS', 'ARTICLE', 'ASIDE', 'AUDIO', 'BLOCKQUOTE', 'BODY', 'CANVAS',
34
- 'CENTER', 'DD', 'DIR', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE',
35
- 'FOOTER', 'FORM', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER',
36
- 'HGROUP', 'HR', 'HTML', 'ISINDEX', 'LI', 'MAIN', 'MENU', 'NAV', 'NOFRAMES',
37
- 'NOSCRIPT', 'OL', 'OUTPUT', 'P', 'PRE', 'SECTION', 'TABLE', 'TBODY', 'TD',
38
- 'TFOOT', 'TH', 'THEAD', 'TR', 'UL'
39
- ];
40
-
41
- function isBlock (node) {
42
- return is(node, blockElements)
27
+ var blockElements = ['ADDRESS', 'ARTICLE', 'ASIDE', 'AUDIO', 'BLOCKQUOTE', 'BODY', 'CANVAS', 'CENTER', 'DD', 'DIR', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', 'FOOTER', 'FORM', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'HGROUP', 'HR', 'HTML', 'ISINDEX', 'LI', 'MAIN', 'MENU', 'NAV', 'NOFRAMES', 'NOSCRIPT', 'OL', 'OUTPUT', 'P', 'PRE', 'SECTION', 'TABLE', 'TBODY', 'TD', 'TFOOT', 'TH', 'THEAD', 'TR', 'UL'];
28
+ function isBlock(node) {
29
+ return is(node, blockElements);
43
30
  }
44
-
45
- var voidElements = [
46
- 'AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT',
47
- 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR'
48
- ];
49
-
50
- function isVoid (node) {
51
- return is(node, voidElements)
31
+ var voidElements = ['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR'];
32
+ function isVoid(node) {
33
+ return is(node, voidElements);
52
34
  }
53
-
54
- function hasVoid (node) {
55
- return has(node, voidElements)
35
+ function hasVoid(node) {
36
+ return has(node, voidElements);
56
37
  }
57
-
58
- var meaningfulWhenBlankElements = [
59
- 'A', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TH', 'TD', 'IFRAME', 'SCRIPT',
60
- 'AUDIO', 'VIDEO'
61
- ];
62
-
63
- function isMeaningfulWhenBlank (node) {
64
- return is(node, meaningfulWhenBlankElements)
38
+ var meaningfulWhenBlankElements = ['A', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TH', 'TD', 'IFRAME', 'SCRIPT', 'AUDIO', 'VIDEO'];
39
+ function isMeaningfulWhenBlank(node) {
40
+ return is(node, meaningfulWhenBlankElements);
65
41
  }
66
-
67
- function hasMeaningfulWhenBlank (node) {
68
- return has(node, meaningfulWhenBlankElements)
42
+ function hasMeaningfulWhenBlank(node) {
43
+ return has(node, meaningfulWhenBlankElements);
69
44
  }
70
-
71
- function is (node, tagNames) {
72
- return tagNames.indexOf(node.nodeName) >= 0
45
+ function is(node, tagNames) {
46
+ return tagNames.indexOf(node.nodeName) >= 0;
73
47
  }
74
-
75
- function has (node, tagNames) {
76
- return (
77
- node.getElementsByTagName &&
78
- tagNames.some(function (tagName) {
79
- return node.getElementsByTagName(tagName).length
80
- })
81
- )
48
+ function has(node, tagNames) {
49
+ return node.getElementsByTagName && tagNames.some(function (tagName) {
50
+ return node.getElementsByTagName(tagName).length;
51
+ });
52
+ }
53
+ var markdownEscapes = [[/\\/g, '\\\\'], [/\*/g, '\\*'], [/^-/g, '\\-'], [/^\+ /g, '\\+ '], [/^(=+)/g, '\\$1'], [/^(#{1,6}) /g, '\\$1 '], [/`/g, '\\`'], [/^~~~/g, '\\~~~'], [/\[/g, '\\['], [/\]/g, '\\]'], [/^>/g, '\\>'], [/_/g, '\\_'], [/^(\d+)\. /g, '$1\\. ']];
54
+ function escapeMarkdown(string) {
55
+ return markdownEscapes.reduce(function (accumulator, escape) {
56
+ return accumulator.replace(escape[0], escape[1]);
57
+ }, string);
82
58
  }
83
59
 
84
60
  var rules = {};
85
-
86
61
  rules.paragraph = {
87
62
  filter: 'p',
88
-
89
63
  replacement: function (content) {
90
- return '\n\n' + content + '\n\n'
64
+ return '\n\n' + content + '\n\n';
91
65
  }
92
66
  };
93
-
94
67
  rules.lineBreak = {
95
68
  filter: 'br',
96
-
97
69
  replacement: function (content, node, options) {
98
- return options.br + '\n'
70
+ return options.br + '\n';
99
71
  }
100
72
  };
101
-
102
73
  rules.heading = {
103
74
  filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
104
-
105
75
  replacement: function (content, node, options) {
106
76
  var hLevel = Number(node.nodeName.charAt(1));
107
-
108
77
  if (options.headingStyle === 'setext' && hLevel < 3) {
109
- var underline = repeat((hLevel === 1 ? '=' : '-'), content.length);
110
- return (
111
- '\n\n' + content + '\n' + underline + '\n\n'
112
- )
78
+ var underline = repeat(hLevel === 1 ? '=' : '-', content.length);
79
+ return '\n\n' + content + '\n' + underline + '\n\n';
113
80
  } else {
114
- return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n'
81
+ return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n';
115
82
  }
116
83
  }
117
84
  };
118
-
119
85
  rules.blockquote = {
120
86
  filter: 'blockquote',
121
-
122
87
  replacement: function (content) {
123
88
  content = trimNewlines(content).replace(/^/gm, '> ');
124
- return '\n\n' + content + '\n\n'
89
+ return '\n\n' + content + '\n\n';
125
90
  }
126
91
  };
127
-
128
92
  rules.list = {
129
93
  filter: ['ul', 'ol'],
130
-
131
94
  replacement: function (content, node) {
132
95
  var parent = node.parentNode;
133
96
  if (parent.nodeName === 'LI' && parent.lastElementChild === node) {
134
- return '\n' + content
97
+ return '\n' + content;
135
98
  } else {
136
- return '\n\n' + content + '\n\n'
99
+ return '\n\n' + content + '\n\n';
137
100
  }
138
101
  }
139
102
  };
140
-
141
103
  rules.listItem = {
142
104
  filter: 'li',
143
-
144
105
  replacement: function (content, node, options) {
145
106
  var prefix = options.bulletListMarker + ' ';
146
107
  var parent = node.parentNode;
@@ -152,273 +113,208 @@ rules.listItem = {
152
113
  var isParagraph = /\n$/.test(content);
153
114
  content = trimNewlines(content) + (isParagraph ? '\n' : '');
154
115
  content = content.replace(/\n/gm, '\n' + ' '.repeat(prefix.length)); // indent
155
- return (
156
- prefix + content + (node.nextSibling ? '\n' : '')
157
- )
116
+ return prefix + content + (node.nextSibling ? '\n' : '');
158
117
  }
159
118
  };
160
-
161
119
  rules.indentedCodeBlock = {
162
120
  filter: function (node, options) {
163
- return (
164
- options.codeBlockStyle === 'indented' &&
165
- node.nodeName === 'PRE' &&
166
- node.firstChild &&
167
- node.firstChild.nodeName === 'CODE'
168
- )
121
+ return options.codeBlockStyle === 'indented' && node.nodeName === 'PRE' && node.firstChild && node.firstChild.nodeName === 'CODE';
169
122
  },
170
-
171
123
  replacement: function (content, node, options) {
172
- return (
173
- '\n\n ' +
174
- node.firstChild.textContent.replace(/\n/g, '\n ') +
175
- '\n\n'
176
- )
124
+ return '\n\n ' + node.firstChild.textContent.replace(/\n/g, '\n ') + '\n\n';
177
125
  }
178
126
  };
179
-
180
127
  rules.fencedCodeBlock = {
181
128
  filter: function (node, options) {
182
- return (
183
- options.codeBlockStyle === 'fenced' &&
184
- node.nodeName === 'PRE' &&
185
- node.firstChild &&
186
- node.firstChild.nodeName === 'CODE'
187
- )
129
+ return options.codeBlockStyle === 'fenced' && node.nodeName === 'PRE' && node.firstChild && node.firstChild.nodeName === 'CODE';
188
130
  },
189
-
190
131
  replacement: function (content, node, options) {
191
132
  var className = node.firstChild.getAttribute('class') || '';
192
133
  var language = (className.match(/language-(\S+)/) || [null, ''])[1];
193
134
  var code = node.firstChild.textContent;
194
-
195
135
  var fenceChar = options.fence.charAt(0);
196
136
  var fenceSize = 3;
197
137
  var fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm');
198
-
199
138
  var match;
200
- while ((match = fenceInCodeRegex.exec(code))) {
139
+ while (match = fenceInCodeRegex.exec(code)) {
201
140
  if (match[0].length >= fenceSize) {
202
141
  fenceSize = match[0].length + 1;
203
142
  }
204
143
  }
205
-
206
144
  var fence = repeat(fenceChar, fenceSize);
207
-
208
- return (
209
- '\n\n' + fence + language + '\n' +
210
- code.replace(/\n$/, '') +
211
- '\n' + fence + '\n\n'
212
- )
145
+ return '\n\n' + fence + language + '\n' + code.replace(/\n$/, '') + '\n' + fence + '\n\n';
213
146
  }
214
147
  };
215
-
216
148
  rules.horizontalRule = {
217
149
  filter: 'hr',
218
-
219
150
  replacement: function (content, node, options) {
220
- return '\n\n' + options.hr + '\n\n'
151
+ return '\n\n' + options.hr + '\n\n';
221
152
  }
222
153
  };
223
-
224
154
  rules.inlineLink = {
225
155
  filter: function (node, options) {
226
- return (
227
- options.linkStyle === 'inlined' &&
228
- node.nodeName === 'A' &&
229
- node.getAttribute('href')
230
- )
156
+ return options.linkStyle === 'inlined' && node.nodeName === 'A' && node.getAttribute('href');
231
157
  },
232
-
233
158
  replacement: function (content, node) {
234
- var href = node.getAttribute('href');
235
- if (href) href = href.replace(/([()])/g, '\\$1');
236
- var title = cleanAttribute(node.getAttribute('title'));
237
- if (title) title = ' "' + title.replace(/"/g, '\\"') + '"';
238
- return '[' + content + '](' + href + title + ')'
159
+ var href = escapeLinkDestination(node.getAttribute('href'));
160
+ var title = escapeLinkTitle(cleanAttribute(node.getAttribute('title')));
161
+ var titlePart = title ? ' "' + title + '"' : '';
162
+ return '[' + content + '](' + href + titlePart + ')';
239
163
  }
240
164
  };
241
-
242
165
  rules.referenceLink = {
243
166
  filter: function (node, options) {
244
- return (
245
- options.linkStyle === 'referenced' &&
246
- node.nodeName === 'A' &&
247
- node.getAttribute('href')
248
- )
167
+ return options.linkStyle === 'referenced' && node.nodeName === 'A' && node.getAttribute('href');
249
168
  },
250
-
251
169
  replacement: function (content, node, options) {
252
- var href = node.getAttribute('href');
170
+ var href = escapeLinkDestination(node.getAttribute('href'));
253
171
  var title = cleanAttribute(node.getAttribute('title'));
254
- if (title) title = ' "' + title + '"';
172
+ if (title) title = ' "' + escapeLinkTitle(title) + '"';
255
173
  var replacement;
256
174
  var reference;
257
-
258
175
  switch (options.linkReferenceStyle) {
259
176
  case 'collapsed':
260
177
  replacement = '[' + content + '][]';
261
178
  reference = '[' + content + ']: ' + href + title;
262
- break
179
+ break;
263
180
  case 'shortcut':
264
181
  replacement = '[' + content + ']';
265
182
  reference = '[' + content + ']: ' + href + title;
266
- break
183
+ break;
267
184
  default:
268
185
  var id = this.references.length + 1;
269
186
  replacement = '[' + content + '][' + id + ']';
270
187
  reference = '[' + id + ']: ' + href + title;
271
188
  }
272
-
273
189
  this.references.push(reference);
274
- return replacement
190
+ return replacement;
275
191
  },
276
-
277
192
  references: [],
278
-
279
193
  append: function (options) {
280
194
  var references = '';
281
195
  if (this.references.length) {
282
196
  references = '\n\n' + this.references.join('\n') + '\n\n';
283
197
  this.references = []; // Reset references
284
198
  }
285
- return references
199
+ return references;
286
200
  }
287
201
  };
288
-
289
202
  rules.emphasis = {
290
203
  filter: ['em', 'i'],
291
-
292
204
  replacement: function (content, node, options) {
293
- if (!content.trim()) return ''
294
- return options.emDelimiter + content + options.emDelimiter
205
+ if (!content.trim()) return '';
206
+ return options.emDelimiter + content + options.emDelimiter;
295
207
  }
296
208
  };
297
-
298
209
  rules.strong = {
299
210
  filter: ['strong', 'b'],
300
-
301
211
  replacement: function (content, node, options) {
302
- if (!content.trim()) return ''
303
- return options.strongDelimiter + content + options.strongDelimiter
212
+ if (!content.trim()) return '';
213
+ return options.strongDelimiter + content + options.strongDelimiter;
304
214
  }
305
215
  };
306
-
307
216
  rules.code = {
308
217
  filter: function (node) {
309
218
  var hasSiblings = node.previousSibling || node.nextSibling;
310
219
  var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings;
311
-
312
- return node.nodeName === 'CODE' && !isCodeBlock
220
+ return node.nodeName === 'CODE' && !isCodeBlock;
313
221
  },
314
-
315
222
  replacement: function (content) {
316
- if (!content) return ''
223
+ if (!content) return '';
317
224
  content = content.replace(/\r?\n|\r/g, ' ');
318
-
319
225
  var extraSpace = /^`|^ .*?[^ ].* $|`$/.test(content) ? ' ' : '';
320
226
  var delimiter = '`';
321
227
  var matches = content.match(/`+/gm) || [];
322
228
  while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`';
323
-
324
- return delimiter + extraSpace + content + extraSpace + delimiter
229
+ return delimiter + extraSpace + content + extraSpace + delimiter;
325
230
  }
326
231
  };
327
-
328
232
  rules.image = {
329
233
  filter: 'img',
330
-
331
234
  replacement: function (content, node) {
332
- var alt = cleanAttribute(node.getAttribute('alt'));
333
- var src = node.getAttribute('src') || '';
235
+ var alt = escapeMarkdown(cleanAttribute(node.getAttribute('alt')));
236
+ var src = escapeLinkDestination(node.getAttribute('src') || '');
334
237
  var title = cleanAttribute(node.getAttribute('title'));
335
- var titlePart = title ? ' "' + title + '"' : '';
336
- return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''
238
+ var titlePart = title ? ' "' + escapeLinkTitle(title) + '"' : '';
239
+ return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '';
337
240
  }
338
241
  };
339
-
340
- function cleanAttribute (attribute) {
341
- return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : ''
242
+ function cleanAttribute(attribute) {
243
+ return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '';
244
+ }
245
+ function escapeLinkDestination(destination) {
246
+ var escaped = destination.replace(/([<>()])/g, '\\$1');
247
+ return escaped.indexOf(' ') >= 0 ? '<' + escaped + '>' : escaped;
248
+ }
249
+ function escapeLinkTitle(title) {
250
+ return title.replace(/"/g, '\\"');
342
251
  }
343
252
 
344
253
  /**
345
254
  * Manages a collection of rules used to convert HTML to Markdown
346
255
  */
347
256
 
348
- function Rules (options) {
257
+ function Rules(options) {
349
258
  this.options = options;
350
259
  this._keep = [];
351
260
  this._remove = [];
352
-
353
261
  this.blankRule = {
354
262
  replacement: options.blankReplacement
355
263
  };
356
-
357
264
  this.keepReplacement = options.keepReplacement;
358
-
359
265
  this.defaultRule = {
360
266
  replacement: options.defaultReplacement
361
267
  };
362
-
363
268
  this.array = [];
364
269
  for (var key in options.rules) this.array.push(options.rules[key]);
365
270
  }
366
-
367
271
  Rules.prototype = {
368
272
  add: function (key, rule) {
369
273
  this.array.unshift(rule);
370
274
  },
371
-
372
275
  keep: function (filter) {
373
276
  this._keep.unshift({
374
277
  filter: filter,
375
278
  replacement: this.keepReplacement
376
279
  });
377
280
  },
378
-
379
281
  remove: function (filter) {
380
282
  this._remove.unshift({
381
283
  filter: filter,
382
284
  replacement: function () {
383
- return ''
285
+ return '';
384
286
  }
385
287
  });
386
288
  },
387
-
388
289
  forNode: function (node) {
389
- if (node.isBlank) return this.blankRule
290
+ if (node.isBlank) return this.blankRule;
390
291
  var rule;
391
-
392
- if ((rule = findRule(this.array, node, this.options))) return rule
393
- if ((rule = findRule(this._keep, node, this.options))) return rule
394
- if ((rule = findRule(this._remove, node, this.options))) return rule
395
-
396
- return this.defaultRule
292
+ if (rule = findRule(this.array, node, this.options)) return rule;
293
+ if (rule = findRule(this._keep, node, this.options)) return rule;
294
+ if (rule = findRule(this._remove, node, this.options)) return rule;
295
+ return this.defaultRule;
397
296
  },
398
-
399
297
  forEach: function (fn) {
400
298
  for (var i = 0; i < this.array.length; i++) fn(this.array[i], i);
401
299
  }
402
300
  };
403
-
404
- function findRule (rules, node, options) {
301
+ function findRule(rules, node, options) {
405
302
  for (var i = 0; i < rules.length; i++) {
406
303
  var rule = rules[i];
407
- if (filterValue(rule, node, options)) return rule
304
+ if (filterValue(rule, node, options)) return rule;
408
305
  }
409
- return void 0
306
+ return undefined;
410
307
  }
411
-
412
- function filterValue (rule, node, options) {
308
+ function filterValue(rule, node, options) {
413
309
  var filter = rule.filter;
414
310
  if (typeof filter === 'string') {
415
- if (filter === node.nodeName.toLowerCase()) return true
311
+ if (filter === node.nodeName.toLowerCase()) return true;
416
312
  } else if (Array.isArray(filter)) {
417
- if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true
313
+ if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true;
418
314
  } else if (typeof filter === 'function') {
419
- if (filter.call(rule, node, options)) return true
315
+ if (filter.call(rule, node, options)) return true;
420
316
  } else {
421
- throw new TypeError('`filter` needs to be a string, array, or function')
317
+ throw new TypeError('`filter` needs to be a string, array, or function');
422
318
  }
423
319
  }
424
320
 
@@ -454,46 +350,39 @@ function filterValue (rule, node, options) {
454
350
  *
455
351
  * @param {Object} options
456
352
  */
457
- function collapseWhitespace (options) {
353
+ function collapseWhitespace(options) {
458
354
  var element = options.element;
459
355
  var isBlock = options.isBlock;
460
356
  var isVoid = options.isVoid;
461
357
  var isPre = options.isPre || function (node) {
462
- return node.nodeName === 'PRE'
358
+ return node.nodeName === 'PRE';
463
359
  };
464
-
465
- if (!element.firstChild || isPre(element)) return
466
-
360
+ if (!element.firstChild || isPre(element)) return;
467
361
  var prevText = null;
468
362
  var keepLeadingWs = false;
469
-
470
363
  var prev = null;
471
364
  var node = next(prev, element, isPre);
472
-
473
365
  while (node !== element) {
474
- if (node.nodeType === 3 || node.nodeType === 4) { // Node.TEXT_NODE or Node.CDATA_SECTION_NODE
366
+ if (node.nodeType === 3 || node.nodeType === 4) {
367
+ // Node.TEXT_NODE or Node.CDATA_SECTION_NODE
475
368
  var text = node.data.replace(/[ \r\n\t]+/g, ' ');
476
-
477
- if ((!prevText || / $/.test(prevText.data)) &&
478
- !keepLeadingWs && text[0] === ' ') {
369
+ if ((!prevText || / $/.test(prevText.data)) && !keepLeadingWs && text[0] === ' ') {
479
370
  text = text.substr(1);
480
371
  }
481
372
 
482
373
  // `text` might be empty at this point.
483
374
  if (!text) {
484
375
  node = remove(node);
485
- continue
376
+ continue;
486
377
  }
487
-
488
378
  node.data = text;
489
-
490
379
  prevText = node;
491
- } else if (node.nodeType === 1) { // Node.ELEMENT_NODE
380
+ } else if (node.nodeType === 1) {
381
+ // Node.ELEMENT_NODE
492
382
  if (isBlock(node) || node.nodeName === 'BR') {
493
383
  if (prevText) {
494
384
  prevText.data = prevText.data.replace(/ $/, '');
495
385
  }
496
-
497
386
  prevText = null;
498
387
  keepLeadingWs = false;
499
388
  } else if (isVoid(node) || isPre(node)) {
@@ -506,14 +395,12 @@ function collapseWhitespace (options) {
506
395
  }
507
396
  } else {
508
397
  node = remove(node);
509
- continue
398
+ continue;
510
399
  }
511
-
512
400
  var nextNode = next(prev, node, isPre);
513
401
  prev = node;
514
402
  node = nextNode;
515
403
  }
516
-
517
404
  if (prevText) {
518
405
  prevText.data = prevText.data.replace(/ $/, '');
519
406
  if (!prevText.data) {
@@ -529,12 +416,10 @@ function collapseWhitespace (options) {
529
416
  * @param {Node} node
530
417
  * @return {Node} node
531
418
  */
532
- function remove (node) {
419
+ function remove(node) {
533
420
  var next = node.nextSibling || node.parentNode;
534
-
535
421
  node.parentNode.removeChild(node);
536
-
537
- return next
422
+ return next;
538
423
  }
539
424
 
540
425
  /**
@@ -546,25 +431,24 @@ function remove (node) {
546
431
  * @param {Function} isPre
547
432
  * @return {Node}
548
433
  */
549
- function next (prev, current, isPre) {
550
- if ((prev && prev.parentNode === current) || isPre(current)) {
551
- return current.nextSibling || current.parentNode
434
+ function next(prev, current, isPre) {
435
+ if (prev && prev.parentNode === current || isPre(current)) {
436
+ return current.nextSibling || current.parentNode;
552
437
  }
553
-
554
- return current.firstChild || current.nextSibling || current.parentNode
438
+ return current.firstChild || current.nextSibling || current.parentNode;
555
439
  }
556
440
 
557
441
  /*
558
442
  * Set up window for Node.js
559
443
  */
560
444
 
561
- var root = (typeof window !== 'undefined' ? window : {});
445
+ var root = typeof window !== 'undefined' ? window : {};
562
446
 
563
447
  /*
564
448
  * Parsing HTML strings
565
449
  */
566
450
 
567
- function canParseHTMLNatively () {
451
+ function canParseHTMLNatively() {
568
452
  var Parser = root.DOMParser;
569
453
  var canParse = false;
570
454
 
@@ -576,13 +460,10 @@ function canParseHTMLNatively () {
576
460
  canParse = true;
577
461
  }
578
462
  } catch (e) {}
579
-
580
- return canParse
463
+ return canParse;
581
464
  }
582
-
583
- function createHTMLParser () {
465
+ function createHTMLParser() {
584
466
  var Parser = function () {};
585
-
586
467
  {
587
468
  if (shouldUseActiveX()) {
588
469
  Parser.prototype.parseFromString = function (string) {
@@ -591,7 +472,7 @@ function createHTMLParser () {
591
472
  doc.open();
592
473
  doc.write(string);
593
474
  doc.close();
594
- return doc
475
+ return doc;
595
476
  };
596
477
  } else {
597
478
  Parser.prototype.parseFromString = function (string) {
@@ -599,82 +480,82 @@ function createHTMLParser () {
599
480
  doc.open();
600
481
  doc.write(string);
601
482
  doc.close();
602
- return doc
483
+ return doc;
603
484
  };
604
485
  }
605
486
  }
606
- return Parser
487
+ return Parser;
607
488
  }
608
-
609
- function shouldUseActiveX () {
489
+ function shouldUseActiveX() {
610
490
  var useActiveX = false;
611
491
  try {
612
492
  document.implementation.createHTMLDocument('').open();
613
493
  } catch (e) {
614
494
  if (root.ActiveXObject) useActiveX = true;
615
495
  }
616
- return useActiveX
496
+ return useActiveX;
617
497
  }
618
-
619
498
  var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
620
499
 
621
- function RootNode (input, options) {
500
+ function RootNode(input, options) {
622
501
  var root;
623
502
  if (typeof input === 'string') {
624
503
  var doc = htmlParser().parseFromString(
625
- // DOM parsers arrange elements in the <head> and <body>.
626
- // Wrapping in a custom element ensures elements are reliably arranged in
627
- // a single element.
628
- '<x-turndown id="turndown-root">' + input + '</x-turndown>',
629
- 'text/html'
630
- );
504
+ // DOM parsers arrange elements in the <head> and <body>.
505
+ // Wrapping in a custom element ensures elements are reliably arranged in
506
+ // a single element.
507
+ '<x-turndown id="turndown-root">' + input + '</x-turndown>', 'text/html');
631
508
  root = doc.getElementById('turndown-root');
632
509
  } else {
633
510
  root = input.cloneNode(true);
634
511
  }
512
+ normalizePre(root);
635
513
  collapseWhitespace({
636
514
  element: root,
637
515
  isBlock: isBlock,
638
516
  isVoid: isVoid,
639
517
  isPre: options.preformattedCode ? isPreOrCode : null
640
518
  });
641
-
642
- return root
519
+ return root;
643
520
  }
644
-
645
521
  var _htmlParser;
646
- function htmlParser () {
522
+ function htmlParser() {
647
523
  _htmlParser = _htmlParser || new HTMLParser();
648
- return _htmlParser
524
+ return _htmlParser;
649
525
  }
650
-
651
- function isPreOrCode (node) {
652
- return node.nodeName === 'PRE' || node.nodeName === 'CODE'
526
+ function isPreOrCode(node) {
527
+ return node.nodeName === 'PRE' || node.nodeName === 'CODE';
528
+ }
529
+ function normalizePre(root) {
530
+ if (!root.getElementsByTagName) {
531
+ return; // unsupported DOM method
532
+ }
533
+ var preNodes = root.getElementsByTagName('PRE');
534
+ for (var i = 0; i < preNodes.length; i++) {
535
+ var brNodes = preNodes[i].getElementsByTagName('BR');
536
+ while (brNodes.length > 0) {
537
+ brNodes[0].parentNode.replaceChild(brNodes[0].ownerDocument.createTextNode('\n'), brNodes[0]);
538
+ }
539
+ }
653
540
  }
654
541
 
655
- function Node (node, options) {
542
+ function Node(node, options) {
656
543
  node.isBlock = isBlock(node);
657
544
  node.isCode = node.nodeName === 'CODE' || node.parentNode.isCode;
658
545
  node.isBlank = isBlank(node);
659
546
  node.flankingWhitespace = flankingWhitespace(node, options);
660
- return node
547
+ return node;
661
548
  }
662
-
663
- function isBlank (node) {
664
- return (
665
- !isVoid(node) &&
666
- !isMeaningfulWhenBlank(node) &&
667
- /^\s*$/i.test(node.textContent) &&
668
- !hasVoid(node) &&
669
- !hasMeaningfulWhenBlank(node)
670
- )
549
+ function isBlank(node) {
550
+ return !isVoid(node) && !isMeaningfulWhenBlank(node) && /^\s*$/i.test(node.textContent) && !hasVoid(node) && !hasMeaningfulWhenBlank(node);
671
551
  }
672
-
673
- function flankingWhitespace (node, options) {
674
- if (node.isBlock || (options.preformattedCode && node.isCode)) {
675
- return { leading: '', trailing: '' }
552
+ function flankingWhitespace(node, options) {
553
+ if (node.isBlock || options.preformattedCode && node.isCode) {
554
+ return {
555
+ leading: '',
556
+ trailing: ''
557
+ };
676
558
  }
677
-
678
559
  var edges = edgeWhitespace(node.textContent);
679
560
 
680
561
  // abandon leading ASCII WS if left-flanked by ASCII WS
@@ -686,27 +567,28 @@ function flankingWhitespace (node, options) {
686
567
  if (edges.trailingAscii && isFlankedByWhitespace('right', node, options)) {
687
568
  edges.trailing = edges.trailingNonAscii;
688
569
  }
689
-
690
- return { leading: edges.leading, trailing: edges.trailing }
570
+ return {
571
+ leading: edges.leading,
572
+ trailing: edges.trailing
573
+ };
691
574
  }
692
-
693
- function edgeWhitespace (string) {
575
+ function edgeWhitespace(string) {
694
576
  var m = string.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/);
695
577
  return {
696
- leading: m[1], // whole string for whitespace-only strings
578
+ leading: m[1],
579
+ // whole string for whitespace-only strings
697
580
  leadingAscii: m[2],
698
581
  leadingNonAscii: m[3],
699
- trailing: m[4], // empty for whitespace-only strings
582
+ trailing: m[4],
583
+ // empty for whitespace-only strings
700
584
  trailingNonAscii: m[5],
701
585
  trailingAscii: m[6]
702
- }
586
+ };
703
587
  }
704
-
705
- function isFlankedByWhitespace (side, node, options) {
588
+ function isFlankedByWhitespace(side, node, options) {
706
589
  var sibling;
707
590
  var regExp;
708
591
  var isFlanked;
709
-
710
592
  if (side === 'left') {
711
593
  sibling = node.previousSibling;
712
594
  regExp = / $/;
@@ -714,7 +596,6 @@ function isFlankedByWhitespace (side, node, options) {
714
596
  sibling = node.nextSibling;
715
597
  regExp = /^ /;
716
598
  }
717
-
718
599
  if (sibling) {
719
600
  if (sibling.nodeType === 3) {
720
601
  isFlanked = regExp.test(sibling.nodeValue);
@@ -724,29 +605,12 @@ function isFlankedByWhitespace (side, node, options) {
724
605
  isFlanked = regExp.test(sibling.textContent);
725
606
  }
726
607
  }
727
- return isFlanked
608
+ return isFlanked;
728
609
  }
729
610
 
730
611
  var reduce = Array.prototype.reduce;
731
- var escapes = [
732
- [/\\/g, '\\\\'],
733
- [/\*/g, '\\*'],
734
- [/^-/g, '\\-'],
735
- [/^\+ /g, '\\+ '],
736
- [/^(=+)/g, '\\$1'],
737
- [/^(#{1,6}) /g, '\\$1 '],
738
- [/`/g, '\\`'],
739
- [/^~~~/g, '\\~~~'],
740
- [/\[/g, '\\['],
741
- [/\]/g, '\\]'],
742
- [/^>/g, '\\>'],
743
- [/_/g, '\\_'],
744
- [/^(\d+)\. /g, '$1\\. ']
745
- ];
746
-
747
- function TurndownService (options) {
748
- if (!(this instanceof TurndownService)) return new TurndownService(options)
749
-
612
+ function TurndownService(options) {
613
+ if (!(this instanceof TurndownService)) return new TurndownService(options);
750
614
  var defaults = {
751
615
  rules: rules,
752
616
  headingStyle: 'setext',
@@ -761,19 +625,18 @@ function TurndownService (options) {
761
625
  br: ' ',
762
626
  preformattedCode: false,
763
627
  blankReplacement: function (content, node) {
764
- return node.isBlock ? '\n\n' : ''
628
+ return node.isBlock ? '\n\n' : '';
765
629
  },
766
630
  keepReplacement: function (content, node) {
767
- return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML
631
+ return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML;
768
632
  },
769
633
  defaultReplacement: function (content, node) {
770
- return node.isBlock ? '\n\n' + content + '\n\n' : content
634
+ return node.isBlock ? '\n\n' + content + '\n\n' : content;
771
635
  }
772
636
  };
773
637
  this.options = extend({}, defaults, options);
774
638
  this.rules = new Rules(this.options);
775
639
  }
776
-
777
640
  TurndownService.prototype = {
778
641
  /**
779
642
  * The entry point for converting a string or DOM node to Markdown
@@ -785,17 +648,12 @@ TurndownService.prototype = {
785
648
 
786
649
  turndown: function (input) {
787
650
  if (!canConvert(input)) {
788
- throw new TypeError(
789
- input + ' is not a string, or an element/document/fragment node.'
790
- )
651
+ throw new TypeError(input + ' is not a string, or an element/document/fragment node.');
791
652
  }
792
-
793
- if (input === '') return ''
794
-
653
+ if (input === '') return '';
795
654
  var output = process.call(this, new RootNode(input, this.options));
796
- return postProcess.call(this, output)
655
+ return postProcess.call(this, output);
797
656
  },
798
-
799
657
  /**
800
658
  * Add one or more plugins
801
659
  * @public
@@ -810,11 +668,10 @@ TurndownService.prototype = {
810
668
  } else if (typeof plugin === 'function') {
811
669
  plugin(this);
812
670
  } else {
813
- throw new TypeError('plugin must be a Function or an Array of Functions')
671
+ throw new TypeError('plugin must be a Function or an Array of Functions');
814
672
  }
815
- return this
673
+ return this;
816
674
  },
817
-
818
675
  /**
819
676
  * Adds a rule
820
677
  * @public
@@ -826,9 +683,8 @@ TurndownService.prototype = {
826
683
 
827
684
  addRule: function (key, rule) {
828
685
  this.rules.add(key, rule);
829
- return this
686
+ return this;
830
687
  },
831
-
832
688
  /**
833
689
  * Keep a node (as HTML) that matches the filter
834
690
  * @public
@@ -839,9 +695,8 @@ TurndownService.prototype = {
839
695
 
840
696
  keep: function (filter) {
841
697
  this.rules.keep(filter);
842
- return this
698
+ return this;
843
699
  },
844
-
845
700
  /**
846
701
  * Remove a node that matches the filter
847
702
  * @public
@@ -852,9 +707,8 @@ TurndownService.prototype = {
852
707
 
853
708
  remove: function (filter) {
854
709
  this.rules.remove(filter);
855
- return this
710
+ return this;
856
711
  },
857
-
858
712
  /**
859
713
  * Escapes Markdown syntax
860
714
  * @public
@@ -864,9 +718,7 @@ TurndownService.prototype = {
864
718
  */
865
719
 
866
720
  escape: function (string) {
867
- return escapes.reduce(function (accumulator, escape) {
868
- return accumulator.replace(escape[0], escape[1])
869
- }, string)
721
+ return escapeMarkdown(string);
870
722
  }
871
723
  };
872
724
 
@@ -878,20 +730,18 @@ TurndownService.prototype = {
878
730
  * @type String
879
731
  */
880
732
 
881
- function process (parentNode) {
733
+ function process(parentNode) {
882
734
  var self = this;
883
735
  return reduce.call(parentNode.childNodes, function (output, node) {
884
736
  node = new Node(node, self.options);
885
-
886
737
  var replacement = '';
887
738
  if (node.nodeType === 3) {
888
739
  replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue);
889
740
  } else if (node.nodeType === 1) {
890
741
  replacement = replacementForNode.call(self, node);
891
742
  }
892
-
893
- return join(output, replacement)
894
- }, '')
743
+ return join(output, replacement);
744
+ }, '');
895
745
  }
896
746
 
897
747
  /**
@@ -902,15 +752,14 @@ function process (parentNode) {
902
752
  * @type String
903
753
  */
904
754
 
905
- function postProcess (output) {
755
+ function postProcess(output) {
906
756
  var self = this;
907
757
  this.rules.forEach(function (rule) {
908
758
  if (typeof rule.append === 'function') {
909
759
  output = join(output, rule.append(self.options));
910
760
  }
911
761
  });
912
-
913
- return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '')
762
+ return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '');
914
763
  }
915
764
 
916
765
  /**
@@ -921,16 +770,12 @@ function postProcess (output) {
921
770
  * @type String
922
771
  */
923
772
 
924
- function replacementForNode (node) {
773
+ function replacementForNode(node) {
925
774
  var rule = this.rules.forNode(node);
926
775
  var content = process.call(this, node);
927
776
  var whitespace = node.flankingWhitespace;
928
777
  if (whitespace.leading || whitespace.trailing) content = content.trim();
929
- return (
930
- whitespace.leading +
931
- rule.replacement(content, node, this.options) +
932
- whitespace.trailing
933
- )
778
+ return whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing;
934
779
  }
935
780
 
936
781
  /**
@@ -942,13 +787,12 @@ function replacementForNode (node) {
942
787
  * @type String
943
788
  */
944
789
 
945
- function join (output, replacement) {
790
+ function join(output, replacement) {
946
791
  var s1 = trimTrailingNewlines(output);
947
792
  var s2 = trimLeadingNewlines(replacement);
948
793
  var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
949
794
  var separator = '\n\n'.substring(0, nls);
950
-
951
- return s1 + separator + s2
795
+ return s1 + separator + s2;
952
796
  }
953
797
 
954
798
  /**
@@ -959,15 +803,8 @@ function join (output, replacement) {
959
803
  * @type String|Object|Array|Boolean|Number
960
804
  */
961
805
 
962
- function canConvert (input) {
963
- return (
964
- input != null && (
965
- typeof input === 'string' ||
966
- (input.nodeType && (
967
- input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11
968
- ))
969
- )
970
- )
806
+ function canConvert(input) {
807
+ return input != null && (typeof input === 'string' || input.nodeType && (input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11));
971
808
  }
972
809
 
973
810
  module.exports = TurndownService;