@cloudrac3r/turndown 7.1.4

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