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