UNPKG

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