` elements (and contents).
116 |
117 | `remove` can be called multiple times, with the newly added remove filters taking precedence over older ones. Remove filters will be overridden by the keep filters, standard CommonMark rules, and any added rules. To remove elements that are normally handled by those rules, add a rule with the desired behaviour.
118 |
119 | `remove` returns the `TurndownService` instance for chaining.
120 |
121 | ### `use(plugin|array)`
122 |
123 | Use a plugin, or an array of plugins. Example:
124 |
125 | ```js
126 | // Import plugins from turndown-plugin-gfm
127 | var turndownPluginGfm = require('turndown-plugin-gfm')
128 | var gfm = turndownPluginGfm.gfm
129 | var tables = turndownPluginGfm.tables
130 | var strikethrough = turndownPluginGfm.strikethrough
131 |
132 | // Use the gfm plugin
133 | turndownService.use(gfm)
134 |
135 | // Use the table and strikethrough plugins only
136 | turndownService.use([tables, strikethrough])
137 | ```
138 |
139 | `use` returns the `TurndownService` instance for chaining.
140 |
141 | See **Plugins** below.
142 |
143 | ## Extending with Rules
144 |
145 | Turndown can be extended by adding **rules**. A rule is a plain JavaScript object with `filter` and `replacement` properties. For example, the rule for converting `` elements is as follows:
146 |
147 | ```js
148 | {
149 | filter: 'p',
150 | replacement: function (content) {
151 | return '\n\n' + content + '\n\n'
152 | }
153 | }
154 | ```
155 |
156 | The filter selects `
` elements, and the replacement function returns the `
` contents separated by two new lines.
157 |
158 | ### `filter` String|Array|Function
159 |
160 | The filter property determines whether or not an element should be replaced with the rule's `replacement`. DOM nodes can be selected simply using a tag name or an array of tag names:
161 |
162 | * `filter: 'p'` will select `
` elements
163 | * `filter: ['em', 'i']` will select `` or `` elements
164 |
165 | The tag names in the `filter` property are expected in lowercase, regardless of their form in the document.
166 |
167 | Alternatively, the filter can be a function that returns a boolean depending on whether a given node should be replaced. The function is passed a DOM node as well as the `TurndownService` options. For example, the following rule selects `` elements (with an `href`) when the `linkStyle` option is `inlined`:
168 |
169 | ```js
170 | filter: function (node, options) {
171 | return (
172 | options.linkStyle === 'inlined' &&
173 | node.nodeName === 'A' &&
174 | node.getAttribute('href')
175 | )
176 | }
177 | ```
178 |
179 | ### `replacement` Function
180 |
181 | The replacement function determines how an element should be converted. It should return the Markdown string for a given node. The function is passed the node's content, the node itself, and the `TurndownService` options.
182 |
183 | The following rule shows how `` elements are converted:
184 |
185 | ```js
186 | rules.emphasis = {
187 | filter: ['em', 'i'],
188 |
189 | replacement: function (content, node, options) {
190 | return options.emDelimiter + content + options.emDelimiter
191 | }
192 | }
193 | ```
194 |
195 | ### Special Rules
196 |
197 | **Blank rule** determines how to handle blank elements. It overrides every rule (even those added via `addRule`). A node is blank if it only contains whitespace, and it's not an ``, `| `,` | ` or a void element. Its behaviour can be customised using the `blankReplacement` option.
198 |
199 | **Keep rules** determine how to handle the elements that should not be converted, i.e. rendered as HTML in the Markdown output. By default, no elements are kept. Block-level elements will be separated from surrounding content by blank lines. Its behaviour can be customised using the `keepReplacement` option.
200 |
201 | **Remove rules** determine which elements to remove altogether. By default, no elements are removed.
202 |
203 | **Default rule** handles nodes which are not recognised by any other rule. By default, it outputs the node's text content (separated by blank lines if it is a block-level element). Its behaviour can be customised with the `defaultReplacement` option.
204 |
205 | ### Rule Precedence
206 |
207 | Turndown iterates over the set of rules, and picks the first one that matches the `filter`. The following list describes the order of precedence:
208 |
209 | 1. Blank rule
210 | 2. Added rules (optional)
211 | 3. Commonmark rules
212 | 4. Keep rules
213 | 5. Remove rules
214 | 6. Default rule
215 |
216 | ## Plugins
217 |
218 | The plugin API provides a convenient way for developers to apply multiple extensions. A plugin is just a function that is called with the `TurndownService` instance.
219 |
220 | ## Escaping Markdown Characters
221 |
222 | Turndown uses backslashes (`\`) to escape Markdown characters in the HTML input. This ensures that these characters are not interpreted as Markdown when the output is compiled back to HTML. For example, the contents of `1. Hello world` needs to be escaped to `1\. Hello world`, otherwise it will be interpreted as a list item rather than a heading.
223 |
224 | To avoid the complexity and the performance implications of parsing the content of every HTML element as Markdown, Turndown uses a group of regular expressions to escape potential Markdown syntax. As a result, the escaping rules can be quite aggressive.
225 |
226 | ### Overriding `TurndownService.prototype.escape`
227 |
228 | If you are confident in doing so, you may want to customise the escaping behaviour to suit your needs. This can be done by overriding `TurndownService.prototype.escape`. `escape` takes the text of each HTML element and should return a version with the Markdown characters escaped.
229 |
230 | Note: text in code elements is never passed to`escape`.
231 |
232 | ## License
233 |
234 | turndown is copyright © 2017+ Dom Christie and released under the MIT license.
235 |
--------------------------------------------------------------------------------
/assets/clipboard2markdown.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | const turndownService = new TurndownService({
5 | headingStyle: 'atx',
6 | hr: '- - -',
7 | bulletListMarker: '-',
8 | codeBlockStyle: 'fenced',
9 | fence: '```',
10 | emDelimiter: '_',
11 | strongDelimiter: '**',
12 | linkStyle: 'inlined',
13 | linkReferenceStyle: 'full',
14 | br: ' ',
15 | preformattedCode: false,
16 | });
17 |
18 | // Use the tables plugin from turndown-plugin-gfm for table support
19 | turndownService.use(turndownPluginGfm.tables);
20 |
21 | // Custom rule: preserve as inside table cells
22 | turndownService.addRule('brInTableCell', {
23 | filter: function (node) {
24 | if (node.nodeName !== 'BR') return false;
25 | // Check if br is inside a table cell (td or th)
26 | var parent = node.parentNode;
27 | while (parent) {
28 | if (parent.nodeName === 'TD' || parent.nodeName === 'TH') {
29 | return true;
30 | }
31 | if (parent.nodeName === 'TABLE' || parent.nodeName === 'BODY') {
32 | break;
33 | }
34 | parent = parent.parentNode;
35 | }
36 | return false;
37 | },
38 | replacement: function () {
39 | return ' ';
40 | }
41 | });
42 |
43 | turndownService.remove('style');
44 |
45 | // http://pandoc.org/README.html#pandocs-markdown
46 | var pandoc = [
47 | {
48 | filter: 'h1',
49 | replacement: function (content, node) {
50 | return '# ' + content + '\n\n';
51 | }
52 | },
53 |
54 | {
55 | filter: 'h2',
56 | replacement: function (content, node) {
57 | return '## ' + content + '\n\n';
58 | }
59 | },
60 |
61 | {
62 | filter: 'sup',
63 | replacement: function (content) {
64 | return '^' + content + '^';
65 | }
66 | },
67 |
68 | {
69 | filter: 'sub',
70 | replacement: function (content) {
71 | return '~' + content + '~';
72 | }
73 | },
74 |
75 | {
76 | filter: 'br',
77 | replacement: function () {
78 | return '\\\n';
79 | }
80 | },
81 |
82 | {
83 | filter: 'hr',
84 | replacement: function () {
85 | return '\n\n* * * * *\n\n';
86 | }
87 | },
88 |
89 | {
90 | filter: ['em', 'i', 'cite', 'var'],
91 | replacement: function (content) {
92 | return '*' + content + '*';
93 | }
94 | },
95 |
96 | {
97 | filter: function (node) {
98 | var hasSiblings = node.previousSibling || node.nextSibling;
99 | var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings;
100 | var isCodeElem = node.nodeName === 'CODE' ||
101 | node.nodeName === 'KBD' ||
102 | node.nodeName === 'SAMP' ||
103 | node.nodeName === 'TT';
104 |
105 | return isCodeElem && !isCodeBlock;
106 | },
107 | replacement: function (content) {
108 | return '`' + content + '`';
109 | }
110 | },
111 |
112 | {
113 | filter: function (node) {
114 | return node.nodeName === 'A' && node.getAttribute('href');
115 | },
116 | replacement: function (content, node) {
117 | var url = node.getAttribute('href');
118 | var titlePart = node.title ? ' "' + node.title + '"' : '';
119 | if (content === '') {
120 | return '';
121 | } else if (content === url) {
122 | return '<' + url + '>';
123 | } else if (url === ('mailto:' + content)) {
124 | return '<' + content + '>';
125 | } else {
126 | return '[' + content + '](' + url + titlePart + ')';
127 | }
128 | }
129 | },
130 |
131 | {
132 | filter: 'li',
133 | replacement: function (content, node) {
134 | content = content.replace(/^\s+/, '').replace(/\n/gm, '\n ');
135 | var prefix = '- ';
136 | var parent = node.parentNode;
137 |
138 | if (/ol/i.test(parent.nodeName)) {
139 | var index = Array.prototype.indexOf.call(parent.children, node) + 1;
140 | prefix = index + '. ';
141 | }
142 |
143 | return prefix + content;
144 | }
145 | }
146 | ];
147 |
148 | // http://pandoc.org/README.html#smart-punctuation
149 | var escape = function (str) {
150 | return str.replace(/[\u2018\u2019\u00b4]/g, "'")
151 | .replace(/[\u201c\u201d\u2033]/g, '"')
152 | .replace(/[\u2212\u2022\u00b7\u25aa]/g, '-')
153 | .replace(/[\u2013\u2015]/g, '--')
154 | .replace(/\u2014/g, '---')
155 | .replace(/\u2026/g, '...')
156 | .replace(/[ ]+\n/g, '\n')
157 | .replace(/\s*\\\n/g, '\\\n')
158 | .replace(/\s*\\\n\s*\\\n/g, '\n\n')
159 | .replace(/\s*\\\n\n/g, '\n\n')
160 | .replace(/\n-\n/g, '\n')
161 | .replace(/\n\n\s*\\\n/g, '\n\n')
162 | .replace(/\n\n\n*/g, '\n\n')
163 | .replace(/[ ]+$/gm, '')
164 | .replace(/^\s+|[\s\\]+$/g, '')
165 | .replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, ' ')
166 | // ZERO WIDTH SPACE: https://jkorpela.fi/chars/spaces.html
167 | .replace(/[\u200B\uFEFF]/g, '');
168 | };
169 |
170 | var convert = function (str) {
171 | return escape(toMarkdown(str, { converters: pandoc, gfm: true }));
172 | }
173 |
174 | // Plain text processing rules
175 | var plainTextRules = {
176 | // Copilot CLI format: first line starts with ' ● ', remaining lines start with ' ' (3 spaces)
177 | copilotCli: function (text) {
178 | var lines = text.split('\n');
179 | if (lines.length === 0 || (!lines[0].startsWith(' ● ') && !lines[0].startsWith(' > '))) {
180 | return null; // Pattern not matched
181 | }
182 |
183 | // Check if all non-empty remaining lines start with ' ' (3 spaces)
184 | var isMatched = true;
185 | for (var i = 1; i < lines.length; i++) {
186 | if (lines[i].trim() !== '' && !lines[i].startsWith(' ')) {
187 | isMatched = false;
188 | break;
189 | }
190 | }
191 |
192 | if (!isMatched) {
193 | return null; // Pattern not matched
194 | }
195 |
196 | console.log('Matched: Copilot CLI format');
197 | // Remove ' ● ' from first line
198 | lines[0] = lines[0].substring(3);
199 | // Remove ' ' (3 spaces) from remaining lines
200 | for (var i = 1; i < lines.length; i++) {
201 | if (lines[i].startsWith(' ')) {
202 | lines[i] = lines[i].substring(3);
203 | }
204 | }
205 | return lines.join('\n');
206 | },
207 |
208 | // Generic plain text: remove common leading spaces from all lines
209 | genericPlainText: function (text) {
210 | var lines = text.split('\n');
211 | if (lines.length === 0) {
212 | return text;
213 | }
214 |
215 | // Find the minimum number of leading spaces (excluding empty lines)
216 | var minSpaces = Infinity;
217 | for (var i = 0; i < lines.length; i++) {
218 | if (lines[i].trim() !== '') {
219 | var spaces = lines[i].match(/^\s*/)[0].length;
220 | minSpaces = Math.min(minSpaces, spaces);
221 | }
222 | }
223 |
224 | // If no leading spaces found or infinite spaces, return original text
225 | if (minSpaces === Infinity || minSpaces === 0) {
226 | return text;
227 | }
228 |
229 | console.log('Matched: Generic plain text with ' + minSpaces + ' leading spaces');
230 | // Remove common leading spaces from all lines
231 | return lines.map(function (line) {
232 | return line.slice(minSpaces);
233 | }).join('\n');
234 | }
235 | };
236 |
237 | // Apply plain text rules in order
238 | var applyPlainTextRules = function (text) {
239 | // Try each rule in order
240 | for (var ruleName in plainTextRules) {
241 | var result = plainTextRules[ruleName](text);
242 | if (result !== null) {
243 | return result;
244 | }
245 | }
246 | // If no rule matched, return original text
247 | return text;
248 | }
249 |
250 | var insert = function (myField, myValue) {
251 | if (document.selection) {
252 | myField.focus();
253 | sel = document.selection.createRange();
254 | sel.text = myValue;
255 | sel.select()
256 | } else {
257 | if (myField.selectionStart || myField.selectionStart == "0") {
258 | var startPos = myField.selectionStart;
259 | var endPos = myField.selectionEnd;
260 | var beforeValue = myField.value.substring(0, startPos);
261 | var afterValue = myField.value.substring(endPos, myField.value.length);
262 | myField.value = beforeValue + myValue + afterValue;
263 | myField.selectionStart = startPos + myValue.length;
264 | myField.selectionEnd = startPos + myValue.length;
265 | myField.focus()
266 | } else {
267 | myField.value += myValue;
268 | myField.focus()
269 | }
270 | }
271 | };
272 |
273 | // http://stackoverflow.com/questions/2176861/javascript-get-clipboard-data-on-paste-event-cross-browser
274 | document.addEventListener('DOMContentLoaded', function () {
275 | var info = document.querySelector('#info');
276 | var pastebin = document.querySelector('#pastebin');
277 | var output = document.querySelector('#output');
278 | var wrapper = document.querySelector('#wrapper');
279 | var preview = document.querySelector('#preview');
280 |
281 | // Tab switching functionality
282 | var tabButtons = document.querySelectorAll('.tab-button');
283 | var tabContents = document.querySelectorAll('.tab-content');
284 |
285 | tabButtons.forEach(function(button) {
286 | button.addEventListener('click', function() {
287 | var targetTab = this.getAttribute('data-tab');
288 |
289 | // Remove active class from all buttons and contents
290 | tabButtons.forEach(function(btn) {
291 | btn.classList.remove('active');
292 | });
293 | tabContents.forEach(function(content) {
294 | content.classList.remove('active');
295 | });
296 |
297 | // Add active class to clicked button and corresponding content
298 | this.classList.add('active');
299 | document.getElementById(targetTab + '-tab').classList.add('active');
300 |
301 | // Update preview when switching to preview tab
302 | if (targetTab === 'preview') {
303 | updatePreview();
304 | }
305 | });
306 | });
307 |
308 | // Function to update preview with rendered markdown
309 | function updatePreview() {
310 | var markdown = output.value;
311 | if (markdown) {
312 | // Configure marked for security
313 | marked.setOptions({
314 | breaks: true,
315 | gfm: true,
316 | headerIds: false,
317 | mangle: false,
318 | sanitize: false // We sanitize the output manually
319 | });
320 |
321 | // Use marked to convert markdown to HTML
322 | var html = marked.parse(markdown);
323 |
324 | // Sanitize the output to prevent XSS
325 | html = sanitizeHtml(html);
326 |
327 | preview.innerHTML = html;
328 | } else {
329 | preview.innerHTML = '沒有內容可預覽 ';
330 | }
331 | }
332 |
333 | // Monitor output changes and update preview if preview tab is active
334 | output.addEventListener('input', function() {
335 | var previewTab = document.getElementById('preview-tab');
336 | if (previewTab.classList.contains('active')) {
337 | updatePreview();
338 | }
339 | });
340 |
341 | // Sanitize HTML and add Bootstrap classes
342 | function sanitizeHtml(html) {
343 | // Use DOMParser for safer HTML parsing (doesn't execute scripts)
344 | var parser = new DOMParser();
345 | var doc = parser.parseFromString(html, 'text/html');
346 |
347 | // Remove any script tags (convert to array first to avoid mutation issues)
348 | var scripts = Array.from(doc.querySelectorAll('script'));
349 | for (var i = 0; i < scripts.length; i++) {
350 | scripts[i].parentNode.removeChild(scripts[i]);
351 | }
352 |
353 | // Add Bootstrap classes to tables
354 | var tables = Array.from(doc.querySelectorAll('table'));
355 | for (var i = 0; i < tables.length; i++) {
356 | tables[i].className = 'table table-striped table-bordered';
357 | }
358 |
359 | // Add Bootstrap classes to images
360 | var images = Array.from(doc.querySelectorAll('img'));
361 | for (var i = 0; i < images.length; i++) {
362 | var src = images[i].getAttribute('src');
363 | if (!isSafeUrl(src)) {
364 | images[i].parentNode.removeChild(images[i]);
365 | } else {
366 | images[i].className = 'img-responsive';
367 | }
368 | }
369 |
370 | // Add Bootstrap classes to blockquotes
371 | var blockquotes = Array.from(doc.querySelectorAll('blockquote'));
372 | for (var i = 0; i < blockquotes.length; i++) {
373 | blockquotes[i].className = 'blockquote';
374 | }
375 |
376 | // Add Bootstrap classes to code blocks
377 | var codeBlocks = Array.from(doc.querySelectorAll('pre'));
378 | for (var i = 0; i < codeBlocks.length; i++) {
379 | codeBlocks[i].className = 'pre-scrollable';
380 | }
381 |
382 | // Style links with Bootstrap
383 | var links = Array.from(doc.querySelectorAll('a'));
384 | for (var i = 0; i < links.length; i++) {
385 | var href = links[i].getAttribute('href');
386 | if (!isSafeUrl(href)) {
387 | links[i].removeAttribute('href');
388 | } else {
389 | links[i].setAttribute('target', '_blank');
390 | links[i].setAttribute('rel', 'noopener noreferrer');
391 | }
392 | }
393 |
394 | // Add Bootstrap badge class to inline code
395 | var inlineCodes = Array.from(doc.querySelectorAll('code'));
396 | for (var i = 0; i < inlineCodes.length; i++) {
397 | // Only add badge class to inline code, not code inside pre blocks
398 | if (inlineCodes[i].parentNode.nodeName !== 'PRE') {
399 | inlineCodes[i].className = 'badge';
400 | }
401 | }
402 |
403 | return doc.body.innerHTML;
404 | }
405 |
406 | // Validate URL to prevent XSS attacks
407 | function isSafeUrl(url) {
408 | if (!url) return false;
409 | var trimmedUrl = url.trim().toLowerCase();
410 | // Only allow http, https, and relative URLs
411 | // Block javascript:, data:, vbscript:, file:, etc.
412 | return trimmedUrl.startsWith('http://') ||
413 | trimmedUrl.startsWith('https://') ||
414 | trimmedUrl.startsWith('/') ||
415 | trimmedUrl.startsWith('./') ||
416 | trimmedUrl.startsWith('../') ||
417 | (!trimmedUrl.includes(':'));
418 | }
419 |
420 | document.addEventListener('keydown', function (event) {
421 | if (event.ctrlKey || event.metaKey) {
422 | if (String.fromCharCode(event.which).toLowerCase() === 'v') {
423 | pastebin.innerHTML = '';
424 | pastebin.focus();
425 | info.classList.add('hidden');
426 | wrapper.classList.add('hidden');
427 | }
428 | }
429 | });
430 |
431 | pastebin.addEventListener('paste', function (event) {
432 |
433 | // list all clipboardData types
434 | console.log('clipboardData types', event.clipboardData.types);
435 |
436 | // Check if 'vscode-editor-data' is available in the clipboard
437 | if (event.clipboardData.types.includes('vscode-editor-data') && event.clipboardData.types.includes('text/plain')) {
438 | var text = event.clipboardData.getData('text/plain');
439 | console.log('Both vscode-editor-data and text/plain:', text);
440 | // 找到每一行中最少的前綴空白字元,然後將每一行的這幾個空白字元刪除
441 | var lines = text.split('\n');
442 | var minSpaces = lines.reduce((min, line) => {
443 | if (line.trim() === '') return min;
444 | const spaces = line.match(/^\s*/)[0].length;
445 | return (spaces < min) ? spaces : min;
446 | }, Infinity);
447 | text = lines.map(line => line.slice(minSpaces)).join('\n');
448 |
449 | console.log('Plain Text: ', text);
450 |
451 | insert(output, text);
452 | wrapper.classList.remove('hidden');
453 | output.focus();
454 | output.select();
455 | updatePreview();
456 | event.preventDefault();
457 | return;
458 | }
459 |
460 | // Word HTML ''
461 | if (event.clipboardData.types.includes('text/rtf') && event.clipboardData.types.includes('text/html')) {
462 | var html = event.clipboardData.getData('text/html');
463 | console.log('Both text/rtf and text/html:', html);
464 | var markdown = turndownService.turndown(html).trim();
465 | markdown = markdown.replace(/ü/g, ' - ');
466 | markdown = markdown.replace(/\.[^\S\r\n]+/g, '. ');
467 | markdown = markdown.replace(/-[^\S\r\n]+/g, '- ');
468 | markdown = markdown.replace(/[^\S\r\n]/g, ' ');
469 |
470 | console.log('Markdown: ', markdown);
471 |
472 | insert(output, markdown);
473 | wrapper.classList.remove('hidden');
474 | output.focus();
475 | output.select();
476 | updatePreview();
477 | event.preventDefault();
478 | return;
479 | }
480 |
481 | // Check if only text/plain is available
482 | if (event.clipboardData.types.includes('text/plain') && !event.clipboardData.types.includes('text/html')) {
483 | var plainText = event.clipboardData.getData('text/plain');
484 | console.log('Plain text only:', plainText);
485 |
486 | // Apply plain text processing rules
487 | plainText = applyPlainTextRules(plainText);
488 | console.log('After processing:', plainText);
489 |
490 | insert(output, plainText);
491 | wrapper.classList.remove('hidden');
492 | output.focus();
493 | output.select();
494 | updatePreview();
495 | event.preventDefault();
496 | return;
497 | }
498 |
499 | // Normal HTML
500 | var html = event.clipboardData.getData('text/html');
501 |
502 | // delete p tag inside li tag, including any attributes defined in p tag and li tag
503 | html = html.replace(/]*)>\s*]*)>(.*?)<\/p>\s*<\/li>/g, ' $3');
504 |
505 | // Normalize br tags from Excel (may have whitespace or newlines)
506 | // This ensures tags are properly formatted for HTML parsing
507 | html = html.replace(/ /gi, ' ');
508 |
509 | console.log('HTML:', html);
510 |
511 | var parser = new DOMParser()
512 | var doc = parser.parseFromString(html, 'text/html')
513 |
514 | var body = doc.querySelector('body').innerHTML;
515 |
516 | var markdown = convert(body);
517 |
518 | insert(output, markdown);
519 | wrapper.classList.remove('hidden');
520 | output.focus();
521 | output.select();
522 | updatePreview();
523 |
524 | event.preventDefault();
525 | });
526 | });
527 |
528 | document.addEventListener('keydown', function (event) {
529 | if (event.key === 'Escape') {
530 | document.getElementById('output').value = '';
531 | wrapper.classList.add('hidden');
532 | info.classList.remove('hidden');
533 | }
534 |
535 | // Alt+1: Switch to Edit mode
536 | if (event.altKey && event.key === '1') {
537 | event.preventDefault();
538 | var editButton = document.querySelector('.tab-button[data-tab="edit"]');
539 | if (editButton && !editButton.classList.contains('active')) {
540 | editButton.click();
541 | }
542 | }
543 |
544 | // Alt+2: Switch to Preview mode
545 | if (event.altKey && event.key === '2') {
546 | event.preventDefault();
547 | var previewButton = document.querySelector('.tab-button[data-tab="preview"]');
548 | if (previewButton && !previewButton.classList.contains('active')) {
549 | previewButton.click();
550 | }
551 | }
552 | });
553 |
554 | })();
555 |
--------------------------------------------------------------------------------
/assets/to-markdown.js:
--------------------------------------------------------------------------------
1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.toMarkdown = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) {
62 | elem = inqueue.shift()
63 | outqueue.push(elem)
64 | children = elem.childNodes
65 | for (i = 0; i < children.length; i++) {
66 | if (children[i].nodeType === 1) inqueue.push(children[i])
67 | }
68 | }
69 | outqueue.shift()
70 | return outqueue
71 | }
72 |
73 | /*
74 | * Contructs a Markdown string of replacement text for a given node
75 | */
76 |
77 | function getContent (node) {
78 | var text = ''
79 | for (var i = 0; i < node.childNodes.length; i++) {
80 | if (node.childNodes[i].nodeType === 1) {
81 | text += node.childNodes[i]._replacement
82 | } else if (node.childNodes[i].nodeType === 3) {
83 | text += node.childNodes[i].data
84 | } else continue
85 | }
86 | return text
87 | }
88 |
89 | /*
90 | * Returns the HTML string of an element with its contents converted
91 | */
92 |
93 | function outer (node, content) {
94 | return node.cloneNode(false).outerHTML.replace('><', '>' + content + '<')
95 | }
96 |
97 | function canConvert (node, filter) {
98 | if (typeof filter === 'string') {
99 | return filter === node.nodeName.toLowerCase()
100 | }
101 | if (Array.isArray(filter)) {
102 | return filter.indexOf(node.nodeName.toLowerCase()) !== -1
103 | } else if (typeof filter === 'function') {
104 | return filter.call(toMarkdown, node)
105 | } else {
106 | throw new TypeError('`filter` needs to be a string, array, or function')
107 | }
108 | }
109 |
110 | function isFlankedByWhitespace (side, node) {
111 | var sibling
112 | var regExp
113 | var isFlanked
114 |
115 | if (side === 'left') {
116 | sibling = node.previousSibling
117 | regExp = / $/
118 | } else {
119 | sibling = node.nextSibling
120 | regExp = /^ /
121 | }
122 |
123 | if (sibling) {
124 | if (sibling.nodeType === 3) {
125 | isFlanked = regExp.test(sibling.nodeValue)
126 | } else if (sibling.nodeType === 1 && !isBlock(sibling)) {
127 | isFlanked = regExp.test(sibling.textContent)
128 | }
129 | }
130 | return isFlanked
131 | }
132 |
133 | function flankingWhitespace (node) {
134 | var leading = ''
135 | var trailing = ''
136 |
137 | if (!isBlock(node)) {
138 | var hasLeading = /^[ \r\n\t]/.test(node.innerHTML)
139 | var hasTrailing = /[ \r\n\t]$/.test(node.innerHTML)
140 |
141 | if (hasLeading && !isFlankedByWhitespace('left', node)) {
142 | leading = ' '
143 | }
144 | if (hasTrailing && !isFlankedByWhitespace('right', node)) {
145 | trailing = ' '
146 | }
147 | }
148 |
149 | return { leading: leading, trailing: trailing }
150 | }
151 |
152 | /*
153 | * Finds a Markdown converter, gets the replacement, and sets it on
154 | * `_replacement`
155 | */
156 |
157 | function process (node) {
158 | var replacement
159 | var content = getContent(node)
160 |
161 | // Remove blank nodes
162 | if (!isVoid(node) && !/A|TH|TD/.test(node.nodeName) && /^\s*$/i.test(content)) {
163 | node._replacement = ''
164 | return
165 | }
166 |
167 | for (var i = 0; i < converters.length; i++) {
168 | var converter = converters[i]
169 |
170 | if (canConvert(node, converter.filter)) {
171 | if (typeof converter.replacement !== 'function') {
172 | throw new TypeError(
173 | '`replacement` needs to be a function that returns a string'
174 | )
175 | }
176 |
177 | var whitespace = flankingWhitespace(node)
178 |
179 | if (whitespace.leading || whitespace.trailing) {
180 | content = content.trim()
181 | }
182 | replacement = whitespace.leading +
183 | converter.replacement.call(toMarkdown, content, node) +
184 | whitespace.trailing
185 | break
186 | }
187 | }
188 |
189 | node._replacement = replacement
190 | }
191 |
192 | toMarkdown = function (input, options) {
193 | options = options || {}
194 |
195 | if (typeof input !== 'string') {
196 | throw new TypeError(input + ' is not a string')
197 | }
198 |
199 | // Escape potential ol triggers
200 | input = input.replace(/(>[\r\n\s]*)(\d+)\.( | )/g, '$1$2\\.$3')
201 |
202 | var clone = htmlToDom(input).body
203 | var nodes = bfsOrder(clone)
204 | var output
205 |
206 | converters = mdConverters.slice(0)
207 | if (options.gfm) {
208 | converters = gfmConverters.concat(converters)
209 | }
210 |
211 | if (options.converters) {
212 | converters = options.converters.concat(converters)
213 | }
214 |
215 | // Process through nodes in reverse (so deepest child elements are first).
216 | for (var i = nodes.length - 1; i >= 0; i--) {
217 | process(nodes[i])
218 | }
219 | output = getContent(clone)
220 |
221 | return output.replace(/^[\t\r\n]+|[\t\r\n\s]+$/g, '')
222 | .replace(/\n\s+\n/g, '\n\n')
223 | .replace(/\n{3,}/g, '\n\n')
224 | .replace(/\[\s*([^\[\]\n]*?)\s*\n\s*((?:(?:-|\*|\d+\.)\s+[^\n]*\n?)+)\s*\]\((https?:\/\/[^\)]+)\)/g, function(match, preList, listContent, url) {
225 | var result = preList.trim() ? '[' + preList.trim() + '](' + url + ')\n\n' : ''
226 | var lines = listContent.split('\n')
227 |
228 | for (var i = 0; i < lines.length; i++) {
229 | var line = lines[i]
230 | var listMatch = line.match(/^(\s*)([-*]|\d+\.)\s+(.*)$/)
231 | if (listMatch) {
232 | var indent = listMatch[1]
233 | var marker = listMatch[2]
234 | var itemText = listMatch[3].trim()
235 | if (itemText) {
236 | result += indent + marker + ' [' + itemText + '](' + url + ')\n'
237 | }
238 | }
239 | }
240 |
241 | return result.replace(/\n+$/, '')
242 | })
243 | }
244 |
245 | toMarkdown.isBlock = isBlock
246 | toMarkdown.isVoid = isVoid
247 | toMarkdown.outer = outer
248 |
249 | module.exports = toMarkdown
250 |
251 | },{"./lib/gfm-converters":2,"./lib/html-parser":3,"./lib/md-converters":4,"collapse-whitespace":7}],2:[function(require,module,exports){
252 | 'use strict'
253 |
254 | function cell (content, node) {
255 | var index = Array.prototype.indexOf.call(node.parentNode.childNodes, node)
256 | var prefix = ' '
257 | if (index === 0) prefix = '| '
258 | // Trim leading/trailing whitespace
259 | content = content.trim()
260 | // Replace any remaining newlines with
261 | content = content.replace(/\s*\n\s*/g, ' ')
262 | // Clean up whitespace around tags
263 | content = content.replace(/\s* \s*/g, ' ')
264 | return prefix + content + ' |'
265 | }
266 |
267 | var highlightRegEx = /highlight highlight-(\S+)/
268 |
269 | module.exports = [
270 | {
271 | filter: 'br',
272 | replacement: function (content, node) {
273 | // Check if br is inside a table cell (td or th)
274 | var parent = node.parentNode
275 | while (parent) {
276 | if (parent.nodeName === 'TD' || parent.nodeName === 'TH') {
277 | return ' '
278 | }
279 | if (parent.nodeName === 'TABLE') {
280 | break
281 | }
282 | parent = parent.parentNode
283 | }
284 | return '\n'
285 | }
286 | },
287 | {
288 | filter: ['del', 's', 'strike'],
289 | replacement: function (content) {
290 | return '~~' + content + '~~'
291 | }
292 | },
293 |
294 | {
295 | filter: function (node) {
296 | return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
297 | },
298 | replacement: function (content, node) {
299 | return (node.checked ? '[x]' : '[ ]') + ' '
300 | }
301 | },
302 |
303 | {
304 | filter: ['th', 'td'],
305 | replacement: function (content, node) {
306 | return cell(content, node)
307 | }
308 | },
309 |
310 | {
311 | filter: 'tr',
312 | replacement: function (content, node) {
313 | var borderCells = ''
314 | var alignMap = { left: ':--', right: '--:', center: ':-:' }
315 |
316 | // Check if this row should have a header separator
317 | var isHeaderRow = false
318 |
319 | if (node.parentNode.nodeName === 'THEAD') {
320 | // Traditional case: row is inside
321 | isHeaderRow = true
322 | } else {
323 | // For tables without , treat the first row as the header
324 | var parent = node.parentNode
325 | if (parent.nodeName === 'TBODY' || parent.nodeName === 'TABLE') {
326 | // Check if this is the first TR child of the parent
327 | var isFirstRowInParent = true
328 | var sibling = node.previousSibling
329 | while (sibling) {
330 | if (sibling.nodeName === 'TR') {
331 | isFirstRowInParent = false
332 | break
333 | }
334 | sibling = sibling.previousSibling
335 | }
336 |
337 | if (isFirstRowInParent) {
338 | // Find the table element
339 | var table = parent.nodeName === 'TABLE' ? parent : parent.parentNode
340 | // Check if table has a thead
341 | var thead = table.getElementsByTagName('thead')
342 | if (thead.length === 0) {
343 | // For TBODY, also check if there are previous sibling tbody/rows
344 | if (parent.nodeName === 'TBODY') {
345 | var prevTbody = parent.previousSibling
346 | while (prevTbody) {
347 | if (prevTbody.nodeName === 'TBODY' || prevTbody.nodeName === 'TR') {
348 | isFirstRowInParent = false
349 | break
350 | }
351 | prevTbody = prevTbody.previousSibling
352 | }
353 | }
354 | if (isFirstRowInParent) {
355 | isHeaderRow = true
356 | }
357 | }
358 | }
359 | }
360 | }
361 |
362 | if (isHeaderRow) {
363 | for (var i = 0; i < node.childNodes.length; i++) {
364 | var align = node.childNodes[i].attributes.align
365 | var border = '---'
366 |
367 | if (align) border = alignMap[align.value] || border
368 |
369 | borderCells += cell(border, node.childNodes[i])
370 | }
371 | }
372 | return '\n' + content + (borderCells ? '\n' + borderCells : '')
373 | }
374 | },
375 |
376 | {
377 | filter: 'table',
378 | replacement: function (content) {
379 | return '\n\n' + content + '\n\n'
380 | }
381 | },
382 |
383 | {
384 | filter: ['thead', 'tbody', 'tfoot'],
385 | replacement: function (content) {
386 | return content
387 | }
388 | },
389 |
390 | // Fenced code blocks
391 | {
392 | filter: function (node) {
393 | return node.nodeName === 'PRE' &&
394 | node.firstChild &&
395 | node.firstChild.nodeName === 'CODE'
396 | },
397 | replacement: function (content, node) {
398 | return '\n\n```\n' + node.firstChild.textContent.trim() + '\n```\n\n'
399 | }
400 | },
401 |
402 | // Syntax-highlighted code blocks
403 | {
404 | filter: function (node) {
405 | return node.nodeName === 'PRE' &&
406 | node.parentNode.nodeName === 'DIV' &&
407 | highlightRegEx.test(node.parentNode.className)
408 | },
409 | replacement: function (content, node) {
410 | var language = node.parentNode.className.match(highlightRegEx)[1]
411 | return '\n\n```' + language + '\n' + node.textContent + '\n```\n\n'
412 | }
413 | },
414 |
415 | {
416 | filter: function (node) {
417 | return node.nodeName === 'DIV' &&
418 | highlightRegEx.test(node.className)
419 | },
420 | replacement: function (content) {
421 | return '\n\n' + content + '\n\n'
422 | }
423 | }
424 | ]
425 |
426 | },{}],3:[function(require,module,exports){
427 | /*
428 | * Set up window for Node.js
429 | */
430 |
431 | var _window = (typeof window !== 'undefined' ? window : this)
432 |
433 | /*
434 | * Parsing HTML strings
435 | */
436 |
437 | function canParseHtmlNatively () {
438 | var Parser = _window.DOMParser
439 | var canParse = false
440 |
441 | // Adapted from https://gist.github.com/1129031
442 | // Firefox/Opera/IE throw errors on unsupported types
443 | try {
444 | // WebKit returns null on unsupported types
445 | if (new Parser().parseFromString('', 'text/html')) {
446 | canParse = true
447 | }
448 | } catch (e) {}
449 |
450 | return canParse
451 | }
452 |
453 | function createHtmlParser () {
454 | var Parser = function () {}
455 |
456 | // For Node.js environments
457 | if (typeof document === 'undefined') {
458 | var jsdom = require('jsdom')
459 | Parser.prototype.parseFromString = function (string) {
460 | return jsdom.jsdom(string, {
461 | features: {
462 | FetchExternalResources: [],
463 | ProcessExternalResources: false
464 | }
465 | })
466 | }
467 | } else {
468 | if (!shouldUseActiveX()) {
469 | Parser.prototype.parseFromString = function (string) {
470 | var doc = document.implementation.createHTMLDocument('')
471 | doc.open()
472 | doc.write(string)
473 | doc.close()
474 | return doc
475 | }
476 | } else {
477 | Parser.prototype.parseFromString = function (string) {
478 | var doc = new window.ActiveXObject('htmlfile')
479 | doc.designMode = 'on' // disable on-page scripts
480 | doc.open()
481 | doc.write(string)
482 | doc.close()
483 | return doc
484 | }
485 | }
486 | }
487 | return Parser
488 | }
489 |
490 | function shouldUseActiveX () {
491 | var useActiveX = false
492 |
493 | try {
494 | document.implementation.createHTMLDocument('').open()
495 | } catch (e) {
496 | if (window.ActiveXObject) useActiveX = true
497 | }
498 |
499 | return useActiveX
500 | }
501 |
502 | module.exports = canParseHtmlNatively() ? _window.DOMParser : createHtmlParser()
503 |
504 | },{"jsdom":6}],4:[function(require,module,exports){
505 | 'use strict'
506 |
507 | function trimInlineContent(content) {
508 | return typeof content === 'string' ? content.trim() : content
509 | }
510 |
511 | function normalizeLinkContent(content) {
512 | if (typeof content !== 'string') return content
513 |
514 | var trimmed = content.trim()
515 | if (!trimmed) return ''
516 |
517 | return trimmed
518 | .split(/\r?\n+/)
519 | .map(function (part) {
520 | return part.trim()
521 | })
522 | .join(' ')
523 | }
524 |
525 | function trimListContent(content) {
526 | if (typeof content !== 'string') return content
527 |
528 | var hasLeadingLineBreak = /^\s*\n/.test(content)
529 | var hasTrailingLineBreak = /\n\s*$/.test(content)
530 | var trimmed = content.trim()
531 |
532 | if (!trimmed) {
533 | return ''
534 | }
535 |
536 | if (hasLeadingLineBreak && trimmed.charAt(0) !== '\n') {
537 | trimmed = '\n' + trimmed
538 | }
539 |
540 | if (hasTrailingLineBreak && trimmed.charAt(trimmed.length - 1) !== '\n') {
541 | trimmed += '\n'
542 | }
543 |
544 | return trimmed
545 | }
546 |
547 | module.exports = [
548 | {
549 | filter: 'p',
550 | replacement: function (content) {
551 | return '\n\n' + content + '\n\n'
552 | }
553 | },
554 |
555 | {
556 | filter: 'br',
557 | replacement: function () {
558 | return ' \n'
559 | }
560 | },
561 |
562 | {
563 | filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
564 | replacement: function (content, node) {
565 | var hLevel = node.nodeName.charAt(1)
566 | var hPrefix = ''
567 | for (var i = 0; i < hLevel; i++) {
568 | hPrefix += '#'
569 | }
570 | return '\n\n' + hPrefix + ' ' + content + '\n\n'
571 | }
572 | },
573 |
574 | {
575 | filter: 'hr',
576 | replacement: function () {
577 | return '\n\n* * *\n\n'
578 | }
579 | },
580 |
581 | {
582 | filter: ['em', 'i'],
583 | replacement: function (content) {
584 | return '_' + content + '_'
585 | }
586 | },
587 |
588 | {
589 | filter: ['strong', 'b'],
590 | replacement: function (content) {
591 | var trimmed = trimInlineContent(content)
592 | return trimmed ? ' **' + trimmed + '** ' : ''
593 | }
594 | },
595 |
596 | // Inline code
597 | {
598 | filter: function (node) {
599 | var hasSiblings = node.previousSibling || node.nextSibling
600 | var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings
601 |
602 | return node.nodeName === 'CODE' && !isCodeBlock
603 | },
604 | replacement: function (content) {
605 | return '`' + content + '`'
606 | }
607 | },
608 |
609 | {
610 | filter: function (node) {
611 | return node.nodeName === 'A' && node.getAttribute('href')
612 | },
613 | replacement: function (content, node) {
614 | content = normalizeLinkContent(content)
615 | var titlePart = node.title ? ' "' + node.title + '"' : ''
616 | return '[' + content + '](' + node.getAttribute('href') + titlePart + ')'
617 | }
618 | },
619 |
620 | {
621 | filter: 'img',
622 | replacement: function (content, node) {
623 | var alt = node.alt || 'image'
624 | var src = node.getAttribute('src') || ''
625 | var title = node.title || ''
626 | var titlePart = title ? ' "' + title + '"' : ''
627 | return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : ''
628 | }
629 | },
630 |
631 | // Code blocks
632 | {
633 | filter: function (node) {
634 | return node.nodeName === 'PRE' && node.firstChild.nodeName === 'CODE'
635 | },
636 | replacement: function (content, node) {
637 | return '\n\n ' + node.firstChild.textContent.replace(/\n/g, '\n ') + '\n\n'
638 | }
639 | },
640 |
641 | {
642 | filter: 'blockquote',
643 | replacement: function (content) {
644 | content = content.trim()
645 | content = content.replace(/\n{3,}/g, '\n\n')
646 | content = content.replace(/^/gm, '> ')
647 | return '\n\n' + content + '\n\n'
648 | }
649 | },
650 |
651 | {
652 | filter: 'li',
653 | replacement: function (content, node) {
654 | content = trimListContent(content).replace(/\n/gm, '\n ')
655 | var prefix = '* '
656 | var parent = node.parentNode
657 | var index = Array.prototype.indexOf.call(parent.children, node) + 1
658 |
659 | prefix = /ol/i.test(parent.nodeName) ? index + '. ' : '* '
660 | return prefix + content
661 | }
662 | },
663 |
664 | {
665 | filter: ['ul', 'ol'],
666 | replacement: function (content, node) {
667 | var strings = []
668 | for (var i = 0; i < node.childNodes.length; i++) {
669 | strings.push(node.childNodes[i]._replacement)
670 | }
671 |
672 | if (/li/i.test(node.parentNode.nodeName)) {
673 | return '\n' + strings.join('\n')
674 | }
675 | return '\n\n' + strings.join('\n') + '\n\n'
676 | }
677 | },
678 |
679 | {
680 | filter: function (node) {
681 | return this.isBlock(node)
682 | },
683 | replacement: function (content, node) {
684 | // return '\n\n' + this.outer(node, content) + '\n\n'
685 | return '\n\n' + content + '\n\n'
686 | }
687 | },
688 |
689 | // Anything else!
690 | {
691 | filter: function () {
692 | return true
693 | },
694 | replacement: function (content, node) {
695 | // return this.outer(node, content)
696 | return content
697 | }
698 | }
699 | ]
700 |
701 | },{}],5:[function(require,module,exports){
702 | /**
703 | * This file automatically generated from `build.js`.
704 | * Do not manually edit.
705 | */
706 |
707 | module.exports = [
708 | "address",
709 | "article",
710 | "aside",
711 | "audio",
712 | "blockquote",
713 | "canvas",
714 | "dd",
715 | "div",
716 | "dl",
717 | "fieldset",
718 | "figcaption",
719 | "figure",
720 | "footer",
721 | "form",
722 | "h1",
723 | "h2",
724 | "h3",
725 | "h4",
726 | "h5",
727 | "h6",
728 | "header",
729 | "hgroup",
730 | "hr",
731 | "main",
732 | "nav",
733 | "noscript",
734 | "ol",
735 | "output",
736 | "p",
737 | "pre",
738 | "section",
739 | "table",
740 | "tfoot",
741 | "ul",
742 | "video"
743 | ];
744 |
745 | },{}],6:[function(require,module,exports){
746 |
747 | },{}],7:[function(require,module,exports){
748 | 'use strict';
749 |
750 | var voidElements = require('void-elements');
751 | Object.keys(voidElements).forEach(function (name) {
752 | voidElements[name.toUpperCase()] = 1;
753 | });
754 |
755 | var blockElements = {};
756 | require('block-elements').forEach(function (name) {
757 | blockElements[name.toUpperCase()] = 1;
758 | });
759 |
760 | /**
761 | * isBlockElem(node) determines if the given node is a block element.
762 | *
763 | * @param {Node} node
764 | * @return {Boolean}
765 | */
766 | function isBlockElem(node) {
767 | return !!(node && blockElements[node.nodeName]);
768 | }
769 |
770 | /**
771 | * isVoid(node) determines if the given node is a void element.
772 | *
773 | * @param {Node} node
774 | * @return {Boolean}
775 | */
776 | function isVoid(node) {
777 | return !!(node && voidElements[node.nodeName]);
778 | }
779 |
780 | /**
781 | * whitespace(elem [, isBlock]) removes extraneous whitespace from an
782 | * the given element. The function isBlock may optionally be passed in
783 | * to determine whether or not an element is a block element; if none
784 | * is provided, defaults to using the list of block elements provided
785 | * by the `block-elements` module.
786 | *
787 | * @param {Node} elem
788 | * @param {Function} blockTest
789 | */
790 | function collapseWhitespace(elem, isBlock) {
791 | if (!elem.firstChild || elem.nodeName === 'PRE') return;
792 |
793 | if (typeof isBlock !== 'function') {
794 | isBlock = isBlockElem;
795 | }
796 |
797 | var prevText = null;
798 | var prevVoid = false;
799 |
800 | var prev = null;
801 | var node = next(prev, elem);
802 |
803 | while (node !== elem) {
804 | if (node.nodeType === 3) {
805 | // Node.TEXT_NODE
806 | var text = node.data.replace(/[ \r\n\t]+/g, ' ');
807 |
808 | if ((!prevText || / $/.test(prevText.data)) && !prevVoid && text[0] === ' ') {
809 | text = text.substr(1);
810 | }
811 |
812 | // `text` might be empty at this point.
813 | if (!text) {
814 | node = remove(node);
815 | continue;
816 | }
817 |
818 | node.data = text;
819 | prevText = node;
820 | } else if (node.nodeType === 1) {
821 | // Node.ELEMENT_NODE
822 | if (isBlock(node) || node.nodeName === 'BR') {
823 | if (prevText) {
824 | prevText.data = prevText.data.replace(/ $/, '');
825 | }
826 |
827 | prevText = null;
828 | prevVoid = false;
829 | } else if (isVoid(node)) {
830 | // Avoid trimming space around non-block, non-BR void elements.
831 | prevText = null;
832 | prevVoid = true;
833 | }
834 | } else {
835 | node = remove(node);
836 | continue;
837 | }
838 |
839 | var nextNode = next(prev, node);
840 | prev = node;
841 | node = nextNode;
842 | }
843 |
844 | if (prevText) {
845 | prevText.data = prevText.data.replace(/ $/, '');
846 | if (!prevText.data) {
847 | remove(prevText);
848 | }
849 | }
850 | }
851 |
852 | /**
853 | * remove(node) removes the given node from the DOM and returns the
854 | * next node in the sequence.
855 | *
856 | * @param {Node} node
857 | * @return {Node} node
858 | */
859 | function remove(node) {
860 | var next = node.nextSibling || node.parentNode;
861 |
862 | node.parentNode.removeChild(node);
863 |
864 | return next;
865 | }
866 |
867 | /**
868 | * next(prev, current) returns the next node in the sequence, given the
869 | * current and previous nodes.
870 | *
871 | * @param {Node} prev
872 | * @param {Node} current
873 | * @return {Node}
874 | */
875 | function next(prev, current) {
876 | if (prev && prev.parentNode === current || current.nodeName === 'PRE') {
877 | return current.nextSibling || current.parentNode;
878 | }
879 |
880 | return current.firstChild || current.nextSibling || current.parentNode;
881 | }
882 |
883 | module.exports = collapseWhitespace;
884 |
885 | },{"block-elements":5,"void-elements":8}],8:[function(require,module,exports){
886 | /**
887 | * This file automatically generated from `pre-publish.js`.
888 | * Do not manually edit.
889 | */
890 |
891 | module.exports = {
892 | "area": true,
893 | "base": true,
894 | "br": true,
895 | "col": true,
896 | "embed": true,
897 | "hr": true,
898 | "img": true,
899 | "input": true,
900 | "keygen": true,
901 | "link": true,
902 | "menuitem": true,
903 | "meta": true,
904 | "param": true,
905 | "source": true,
906 | "track": true,
907 | "wbr": true
908 | };
909 |
910 | },{}]},{},[1])(1)
911 | });
912 |
--------------------------------------------------------------------------------
/vendor/turndown/lib/turndown.es.js:
--------------------------------------------------------------------------------
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
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 | var domino = require('domino');
583 | Parser.prototype.parseFromString = function (string) {
584 | return domino.createDocument(string)
585 | };
586 | }
587 | return Parser
588 | }
589 |
590 | var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
591 |
592 | function RootNode (input, options) {
593 | var root;
594 | if (typeof input === 'string') {
595 | var doc = htmlParser().parseFromString(
596 | // DOM parsers arrange elements in the and .
597 | // Wrapping in a custom element ensures elements are reliably arranged in
598 | // a single element.
599 | '' + input + '',
600 | 'text/html'
601 | );
602 | root = doc.getElementById('turndown-root');
603 | } else {
604 | root = input.cloneNode(true);
605 | }
606 | collapseWhitespace({
607 | element: root,
608 | isBlock: isBlock,
609 | isVoid: isVoid,
610 | isPre: options.preformattedCode ? isPreOrCode : null
611 | });
612 |
613 | return root
614 | }
615 |
616 | var _htmlParser;
617 | function htmlParser () {
618 | _htmlParser = _htmlParser || new HTMLParser();
619 | return _htmlParser
620 | }
621 |
622 | function isPreOrCode (node) {
623 | return node.nodeName === 'PRE' || node.nodeName === 'CODE'
624 | }
625 |
626 | function Node (node, options) {
627 | node.isBlock = isBlock(node);
628 | node.isCode = node.nodeName === 'CODE' || node.parentNode.isCode;
629 | node.isBlank = isBlank(node);
630 | node.flankingWhitespace = flankingWhitespace(node, options);
631 | return node
632 | }
633 |
634 | function isBlank (node) {
635 | return (
636 | !isVoid(node) &&
637 | !isMeaningfulWhenBlank(node) &&
638 | /^\s*$/i.test(node.textContent) &&
639 | !hasVoid(node) &&
640 | !hasMeaningfulWhenBlank(node)
641 | )
642 | }
643 |
644 | function flankingWhitespace (node, options) {
645 | if (node.isBlock || (options.preformattedCode && node.isCode)) {
646 | return { leading: '', trailing: '' }
647 | }
648 |
649 | var edges = edgeWhitespace(node.textContent);
650 |
651 | // abandon leading ASCII WS if left-flanked by ASCII WS
652 | if (edges.leadingAscii && isFlankedByWhitespace('left', node, options)) {
653 | edges.leading = edges.leadingNonAscii;
654 | }
655 |
656 | // abandon trailing ASCII WS if right-flanked by ASCII WS
657 | if (edges.trailingAscii && isFlankedByWhitespace('right', node, options)) {
658 | edges.trailing = edges.trailingNonAscii;
659 | }
660 |
661 | return { leading: edges.leading, trailing: edges.trailing }
662 | }
663 |
664 | function edgeWhitespace (string) {
665 | var m = string.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/);
666 | return {
667 | leading: m[1], // whole string for whitespace-only strings
668 | leadingAscii: m[2],
669 | leadingNonAscii: m[3],
670 | trailing: m[4], // empty for whitespace-only strings
671 | trailingNonAscii: m[5],
672 | trailingAscii: m[6]
673 | }
674 | }
675 |
676 | function isFlankedByWhitespace (side, node, options) {
677 | var sibling;
678 | var regExp;
679 | var isFlanked;
680 |
681 | if (side === 'left') {
682 | sibling = node.previousSibling;
683 | regExp = / $/;
684 | } else {
685 | sibling = node.nextSibling;
686 | regExp = /^ /;
687 | }
688 |
689 | if (sibling) {
690 | if (sibling.nodeType === 3) {
691 | isFlanked = regExp.test(sibling.nodeValue);
692 | } else if (options.preformattedCode && sibling.nodeName === 'CODE') {
693 | isFlanked = false;
694 | } else if (sibling.nodeType === 1 && !isBlock(sibling)) {
695 | isFlanked = regExp.test(sibling.textContent);
696 | }
697 | }
698 | return isFlanked
699 | }
700 |
701 | var reduce = Array.prototype.reduce;
702 | var escapes = [
703 | [/\\/g, '\\\\'],
704 | [/\*/g, '\\*'],
705 | [/^-/g, '\\-'],
706 | [/^\+ /g, '\\+ '],
707 | [/^(=+)/g, '\\$1'],
708 | [/^(#{1,6}) /g, '\\$1 '],
709 | [/`/g, '\\`'],
710 | [/^~~~/g, '\\~~~'],
711 | [/\[/g, '\\['],
712 | [/\]/g, '\\]'],
713 | [/^>/g, '\\>'],
714 | [/_/g, '\\_'],
715 | [/^(\d+)\. /g, '$1\\. ']
716 | ];
717 |
718 | function TurndownService (options) {
719 | if (!(this instanceof TurndownService)) return new TurndownService(options)
720 |
721 | var defaults = {
722 | rules: rules,
723 | headingStyle: 'setext',
724 | hr: '* * *',
725 | bulletListMarker: '*',
726 | codeBlockStyle: 'indented',
727 | fence: '```',
728 | emDelimiter: '_',
729 | strongDelimiter: '**',
730 | linkStyle: 'inlined',
731 | linkReferenceStyle: 'full',
732 | br: ' ',
733 | preformattedCode: false,
734 | blankReplacement: function (content, node) {
735 | return node.isBlock ? '\n\n' : ''
736 | },
737 | keepReplacement: function (content, node) {
738 | return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML
739 | },
740 | defaultReplacement: function (content, node) {
741 | return node.isBlock ? '\n\n' + content + '\n\n' : content
742 | }
743 | };
744 | this.options = extend({}, defaults, options);
745 | this.rules = new Rules(this.options);
746 | }
747 |
748 | TurndownService.prototype = {
749 | /**
750 | * The entry point for converting a string or DOM node to Markdown
751 | * @public
752 | * @param {String|HTMLElement} input The string or DOM node to convert
753 | * @returns A Markdown representation of the input
754 | * @type String
755 | */
756 |
757 | turndown: function (input) {
758 | if (!canConvert(input)) {
759 | throw new TypeError(
760 | input + ' is not a string, or an element/document/fragment node.'
761 | )
762 | }
763 |
764 | if (input === '') return ''
765 |
766 | var output = process.call(this, new RootNode(input, this.options));
767 | return postProcess.call(this, output)
768 | },
769 |
770 | /**
771 | * Add one or more plugins
772 | * @public
773 | * @param {Function|Array} plugin The plugin or array of plugins to add
774 | * @returns The Turndown instance for chaining
775 | * @type Object
776 | */
777 |
778 | use: function (plugin) {
779 | if (Array.isArray(plugin)) {
780 | for (var i = 0; i < plugin.length; i++) this.use(plugin[i]);
781 | } else if (typeof plugin === 'function') {
782 | plugin(this);
783 | } else {
784 | throw new TypeError('plugin must be a Function or an Array of Functions')
785 | }
786 | return this
787 | },
788 |
789 | /**
790 | * Adds a rule
791 | * @public
792 | * @param {String} key The unique key of the rule
793 | * @param {Object} rule The rule
794 | * @returns The Turndown instance for chaining
795 | * @type Object
796 | */
797 |
798 | addRule: function (key, rule) {
799 | this.rules.add(key, rule);
800 | return this
801 | },
802 |
803 | /**
804 | * Keep a node (as HTML) that matches the filter
805 | * @public
806 | * @param {String|Array|Function} filter The unique key of the rule
807 | * @returns The Turndown instance for chaining
808 | * @type Object
809 | */
810 |
811 | keep: function (filter) {
812 | this.rules.keep(filter);
813 | return this
814 | },
815 |
816 | /**
817 | * Remove a node that matches the filter
818 | * @public
819 | * @param {String|Array|Function} filter The unique key of the rule
820 | * @returns The Turndown instance for chaining
821 | * @type Object
822 | */
823 |
824 | remove: function (filter) {
825 | this.rules.remove(filter);
826 | return this
827 | },
828 |
829 | /**
830 | * Escapes Markdown syntax
831 | * @public
832 | * @param {String} string The string to escape
833 | * @returns A string with Markdown syntax escaped
834 | * @type String
835 | */
836 |
837 | escape: function (string) {
838 | return escapes.reduce(function (accumulator, escape) {
839 | return accumulator.replace(escape[0], escape[1])
840 | }, string)
841 | }
842 | };
843 |
844 | /**
845 | * Reduces a DOM node down to its Markdown string equivalent
846 | * @private
847 | * @param {HTMLElement} parentNode The node to convert
848 | * @returns A Markdown representation of the node
849 | * @type String
850 | */
851 |
852 | function process (parentNode) {
853 | var self = this;
854 | return reduce.call(parentNode.childNodes, function (output, node) {
855 | node = new Node(node, self.options);
856 |
857 | var replacement = '';
858 | if (node.nodeType === 3) {
859 | replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue);
860 | } else if (node.nodeType === 1) {
861 | replacement = replacementForNode.call(self, node);
862 | }
863 |
864 | return join(output, replacement)
865 | }, '')
866 | }
867 |
868 | /**
869 | * Appends strings as each rule requires and trims the output
870 | * @private
871 | * @param {String} output The conversion output
872 | * @returns A trimmed version of the ouput
873 | * @type String
874 | */
875 |
876 | function postProcess (output) {
877 | var self = this;
878 | this.rules.forEach(function (rule) {
879 | if (typeof rule.append === 'function') {
880 | output = join(output, rule.append(self.options));
881 | }
882 | });
883 |
884 | return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '')
885 | }
886 |
887 | /**
888 | * Converts an element node to its Markdown equivalent
889 | * @private
890 | * @param {HTMLElement} node The node to convert
891 | * @returns A Markdown representation of the node
892 | * @type String
893 | */
894 |
895 | function replacementForNode (node) {
896 | var rule = this.rules.forNode(node);
897 | var content = process.call(this, node);
898 | var whitespace = node.flankingWhitespace;
899 | if (whitespace.leading || whitespace.trailing) content = content.trim();
900 | return (
901 | whitespace.leading +
902 | rule.replacement(content, node, this.options) +
903 | whitespace.trailing
904 | )
905 | }
906 |
907 | /**
908 | * Joins replacement to the current output with appropriate number of new lines
909 | * @private
910 | * @param {String} output The current conversion output
911 | * @param {String} replacement The string to append to the output
912 | * @returns Joined output
913 | * @type String
914 | */
915 |
916 | function join (output, replacement) {
917 | var s1 = trimTrailingNewlines(output);
918 | var s2 = trimLeadingNewlines(replacement);
919 | var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
920 | var separator = '\n\n'.substring(0, nls);
921 |
922 | return s1 + separator + s2
923 | }
924 |
925 | /**
926 | * Determines whether an input can be converted
927 | * @private
928 | * @param {String|HTMLElement} input Describe this parameter
929 | * @returns Describe what it returns
930 | * @type String|Object|Array|Boolean|Number
931 | */
932 |
933 | function canConvert (input) {
934 | return (
935 | input != null && (
936 | typeof input === 'string' ||
937 | (input.nodeType && (
938 | input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11
939 | ))
940 | )
941 | )
942 | }
943 |
944 | export default TurndownService;
945 |
--------------------------------------------------------------------------------
/vendor/turndown/lib/turndown.cjs.js:
--------------------------------------------------------------------------------
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
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 | var domino = require('domino');
585 | Parser.prototype.parseFromString = function (string) {
586 | return domino.createDocument(string)
587 | };
588 | }
589 | return Parser
590 | }
591 |
592 | var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
593 |
594 | function RootNode (input, options) {
595 | var root;
596 | if (typeof input === 'string') {
597 | var doc = htmlParser().parseFromString(
598 | // DOM parsers arrange elements in the and .
599 | // Wrapping in a custom element ensures elements are reliably arranged in
600 | // a single element.
601 | '' + input + '',
602 | 'text/html'
603 | );
604 | root = doc.getElementById('turndown-root');
605 | } else {
606 | root = input.cloneNode(true);
607 | }
608 | collapseWhitespace({
609 | element: root,
610 | isBlock: isBlock,
611 | isVoid: isVoid,
612 | isPre: options.preformattedCode ? isPreOrCode : null
613 | });
614 |
615 | return root
616 | }
617 |
618 | var _htmlParser;
619 | function htmlParser () {
620 | _htmlParser = _htmlParser || new HTMLParser();
621 | return _htmlParser
622 | }
623 |
624 | function isPreOrCode (node) {
625 | return node.nodeName === 'PRE' || node.nodeName === 'CODE'
626 | }
627 |
628 | function Node (node, options) {
629 | node.isBlock = isBlock(node);
630 | node.isCode = node.nodeName === 'CODE' || node.parentNode.isCode;
631 | node.isBlank = isBlank(node);
632 | node.flankingWhitespace = flankingWhitespace(node, options);
633 | return node
634 | }
635 |
636 | function isBlank (node) {
637 | return (
638 | !isVoid(node) &&
639 | !isMeaningfulWhenBlank(node) &&
640 | /^\s*$/i.test(node.textContent) &&
641 | !hasVoid(node) &&
642 | !hasMeaningfulWhenBlank(node)
643 | )
644 | }
645 |
646 | function flankingWhitespace (node, options) {
647 | if (node.isBlock || (options.preformattedCode && node.isCode)) {
648 | return { leading: '', trailing: '' }
649 | }
650 |
651 | var edges = edgeWhitespace(node.textContent);
652 |
653 | // abandon leading ASCII WS if left-flanked by ASCII WS
654 | if (edges.leadingAscii && isFlankedByWhitespace('left', node, options)) {
655 | edges.leading = edges.leadingNonAscii;
656 | }
657 |
658 | // abandon trailing ASCII WS if right-flanked by ASCII WS
659 | if (edges.trailingAscii && isFlankedByWhitespace('right', node, options)) {
660 | edges.trailing = edges.trailingNonAscii;
661 | }
662 |
663 | return { leading: edges.leading, trailing: edges.trailing }
664 | }
665 |
666 | function edgeWhitespace (string) {
667 | var m = string.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/);
668 | return {
669 | leading: m[1], // whole string for whitespace-only strings
670 | leadingAscii: m[2],
671 | leadingNonAscii: m[3],
672 | trailing: m[4], // empty for whitespace-only strings
673 | trailingNonAscii: m[5],
674 | trailingAscii: m[6]
675 | }
676 | }
677 |
678 | function isFlankedByWhitespace (side, node, options) {
679 | var sibling;
680 | var regExp;
681 | var isFlanked;
682 |
683 | if (side === 'left') {
684 | sibling = node.previousSibling;
685 | regExp = / $/;
686 | } else {
687 | sibling = node.nextSibling;
688 | regExp = /^ /;
689 | }
690 |
691 | if (sibling) {
692 | if (sibling.nodeType === 3) {
693 | isFlanked = regExp.test(sibling.nodeValue);
694 | } else if (options.preformattedCode && sibling.nodeName === 'CODE') {
695 | isFlanked = false;
696 | } else if (sibling.nodeType === 1 && !isBlock(sibling)) {
697 | isFlanked = regExp.test(sibling.textContent);
698 | }
699 | }
700 | return isFlanked
701 | }
702 |
703 | var reduce = Array.prototype.reduce;
704 | var escapes = [
705 | [/\\/g, '\\\\'],
706 | [/\*/g, '\\*'],
707 | [/^-/g, '\\-'],
708 | [/^\+ /g, '\\+ '],
709 | [/^(=+)/g, '\\$1'],
710 | [/^(#{1,6}) /g, '\\$1 '],
711 | [/`/g, '\\`'],
712 | [/^~~~/g, '\\~~~'],
713 | [/\[/g, '\\['],
714 | [/\]/g, '\\]'],
715 | [/^>/g, '\\>'],
716 | [/_/g, '\\_'],
717 | [/^(\d+)\. /g, '$1\\. ']
718 | ];
719 |
720 | function TurndownService (options) {
721 | if (!(this instanceof TurndownService)) return new TurndownService(options)
722 |
723 | var defaults = {
724 | rules: rules,
725 | headingStyle: 'setext',
726 | hr: '* * *',
727 | bulletListMarker: '*',
728 | codeBlockStyle: 'indented',
729 | fence: '```',
730 | emDelimiter: '_',
731 | strongDelimiter: '**',
732 | linkStyle: 'inlined',
733 | linkReferenceStyle: 'full',
734 | br: ' ',
735 | preformattedCode: false,
736 | blankReplacement: function (content, node) {
737 | return node.isBlock ? '\n\n' : ''
738 | },
739 | keepReplacement: function (content, node) {
740 | return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML
741 | },
742 | defaultReplacement: function (content, node) {
743 | return node.isBlock ? '\n\n' + content + '\n\n' : content
744 | }
745 | };
746 | this.options = extend({}, defaults, options);
747 | this.rules = new Rules(this.options);
748 | }
749 |
750 | TurndownService.prototype = {
751 | /**
752 | * The entry point for converting a string or DOM node to Markdown
753 | * @public
754 | * @param {String|HTMLElement} input The string or DOM node to convert
755 | * @returns A Markdown representation of the input
756 | * @type String
757 | */
758 |
759 | turndown: function (input) {
760 | if (!canConvert(input)) {
761 | throw new TypeError(
762 | input + ' is not a string, or an element/document/fragment node.'
763 | )
764 | }
765 |
766 | if (input === '') return ''
767 |
768 | var output = process.call(this, new RootNode(input, this.options));
769 | return postProcess.call(this, output)
770 | },
771 |
772 | /**
773 | * Add one or more plugins
774 | * @public
775 | * @param {Function|Array} plugin The plugin or array of plugins to add
776 | * @returns The Turndown instance for chaining
777 | * @type Object
778 | */
779 |
780 | use: function (plugin) {
781 | if (Array.isArray(plugin)) {
782 | for (var i = 0; i < plugin.length; i++) this.use(plugin[i]);
783 | } else if (typeof plugin === 'function') {
784 | plugin(this);
785 | } else {
786 | throw new TypeError('plugin must be a Function or an Array of Functions')
787 | }
788 | return this
789 | },
790 |
791 | /**
792 | * Adds a rule
793 | * @public
794 | * @param {String} key The unique key of the rule
795 | * @param {Object} rule The rule
796 | * @returns The Turndown instance for chaining
797 | * @type Object
798 | */
799 |
800 | addRule: function (key, rule) {
801 | this.rules.add(key, rule);
802 | return this
803 | },
804 |
805 | /**
806 | * Keep a node (as HTML) that matches the filter
807 | * @public
808 | * @param {String|Array|Function} filter The unique key of the rule
809 | * @returns The Turndown instance for chaining
810 | * @type Object
811 | */
812 |
813 | keep: function (filter) {
814 | this.rules.keep(filter);
815 | return this
816 | },
817 |
818 | /**
819 | * Remove a node that matches the filter
820 | * @public
821 | * @param {String|Array|Function} filter The unique key of the rule
822 | * @returns The Turndown instance for chaining
823 | * @type Object
824 | */
825 |
826 | remove: function (filter) {
827 | this.rules.remove(filter);
828 | return this
829 | },
830 |
831 | /**
832 | * Escapes Markdown syntax
833 | * @public
834 | * @param {String} string The string to escape
835 | * @returns A string with Markdown syntax escaped
836 | * @type String
837 | */
838 |
839 | escape: function (string) {
840 | return escapes.reduce(function (accumulator, escape) {
841 | return accumulator.replace(escape[0], escape[1])
842 | }, string)
843 | }
844 | };
845 |
846 | /**
847 | * Reduces a DOM node down to its Markdown string equivalent
848 | * @private
849 | * @param {HTMLElement} parentNode The node to convert
850 | * @returns A Markdown representation of the node
851 | * @type String
852 | */
853 |
854 | function process (parentNode) {
855 | var self = this;
856 | return reduce.call(parentNode.childNodes, function (output, node) {
857 | node = new Node(node, self.options);
858 |
859 | var replacement = '';
860 | if (node.nodeType === 3) {
861 | replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue);
862 | } else if (node.nodeType === 1) {
863 | replacement = replacementForNode.call(self, node);
864 | }
865 |
866 | return join(output, replacement)
867 | }, '')
868 | }
869 |
870 | /**
871 | * Appends strings as each rule requires and trims the output
872 | * @private
873 | * @param {String} output The conversion output
874 | * @returns A trimmed version of the ouput
875 | * @type String
876 | */
877 |
878 | function postProcess (output) {
879 | var self = this;
880 | this.rules.forEach(function (rule) {
881 | if (typeof rule.append === 'function') {
882 | output = join(output, rule.append(self.options));
883 | }
884 | });
885 |
886 | return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '')
887 | }
888 |
889 | /**
890 | * Converts an element node to its Markdown equivalent
891 | * @private
892 | * @param {HTMLElement} node The node to convert
893 | * @returns A Markdown representation of the node
894 | * @type String
895 | */
896 |
897 | function replacementForNode (node) {
898 | var rule = this.rules.forNode(node);
899 | var content = process.call(this, node);
900 | var whitespace = node.flankingWhitespace;
901 | if (whitespace.leading || whitespace.trailing) content = content.trim();
902 | return (
903 | whitespace.leading +
904 | rule.replacement(content, node, this.options) +
905 | whitespace.trailing
906 | )
907 | }
908 |
909 | /**
910 | * Joins replacement to the current output with appropriate number of new lines
911 | * @private
912 | * @param {String} output The current conversion output
913 | * @param {String} replacement The string to append to the output
914 | * @returns Joined output
915 | * @type String
916 | */
917 |
918 | function join (output, replacement) {
919 | var s1 = trimTrailingNewlines(output);
920 | var s2 = trimLeadingNewlines(replacement);
921 | var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
922 | var separator = '\n\n'.substring(0, nls);
923 |
924 | return s1 + separator + s2
925 | }
926 |
927 | /**
928 | * Determines whether an input can be converted
929 | * @private
930 | * @param {String|HTMLElement} input Describe this parameter
931 | * @returns Describe what it returns
932 | * @type String|Object|Array|Boolean|Number
933 | */
934 |
935 | function canConvert (input) {
936 | return (
937 | input != null && (
938 | typeof input === 'string' ||
939 | (input.nodeType && (
940 | input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11
941 | ))
942 | )
943 | )
944 | }
945 |
946 | module.exports = TurndownService;
947 |
--------------------------------------------------------------------------------
/vendor/turndown/lib/turndown.browser.es.js:
--------------------------------------------------------------------------------
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
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 (window.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 and .
621 | // Wrapping in a custom element ensures elements are reliably arranged in
622 | // a single element.
623 | '' + input + '',
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.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;
969 |
--------------------------------------------------------------------------------
/vendor/turndown/lib/turndown.browser.cjs.js:
--------------------------------------------------------------------------------
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
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 (window.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 and .
623 | // Wrapping in a custom element ensures elements are reliably arranged in
624 | // a single element.
625 | '' + input + '',
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.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;
971 |
--------------------------------------------------------------------------------
/vendor/turndown/lib/turndown.umd.js:
--------------------------------------------------------------------------------
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
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 | var domino = require('domino');
589 | Parser.prototype.parseFromString = function (string) {
590 | return domino.createDocument(string)
591 | };
592 | }
593 | return Parser
594 | }
595 |
596 | var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
597 |
598 | function RootNode (input, options) {
599 | var root;
600 | if (typeof input === 'string') {
601 | var doc = htmlParser().parseFromString(
602 | // DOM parsers arrange elements in the and .
603 | // Wrapping in a custom element ensures elements are reliably arranged in
604 | // a single element.
605 | '' + input + '',
606 | 'text/html'
607 | );
608 | root = doc.getElementById('turndown-root');
609 | } else {
610 | root = input.cloneNode(true);
611 | }
612 | collapseWhitespace({
613 | element: root,
614 | isBlock: isBlock,
615 | isVoid: isVoid,
616 | isPre: options.preformattedCode ? isPreOrCode : null
617 | });
618 |
619 | return root
620 | }
621 |
622 | var _htmlParser;
623 | function htmlParser () {
624 | _htmlParser = _htmlParser || new HTMLParser();
625 | return _htmlParser
626 | }
627 |
628 | function isPreOrCode (node) {
629 | return node.nodeName === 'PRE' || node.nodeName === 'CODE'
630 | }
631 |
632 | function Node (node, options) {
633 | node.isBlock = isBlock(node);
634 | node.isCode = node.nodeName === 'CODE' || node.parentNode.isCode;
635 | node.isBlank = isBlank(node);
636 | node.flankingWhitespace = flankingWhitespace(node, options);
637 | return node
638 | }
639 |
640 | function isBlank (node) {
641 | return (
642 | !isVoid(node) &&
643 | !isMeaningfulWhenBlank(node) &&
644 | /^\s*$/i.test(node.textContent) &&
645 | !hasVoid(node) &&
646 | !hasMeaningfulWhenBlank(node)
647 | )
648 | }
649 |
650 | function flankingWhitespace (node, options) {
651 | if (node.isBlock || (options.preformattedCode && node.isCode)) {
652 | return { leading: '', trailing: '' }
653 | }
654 |
655 | var edges = edgeWhitespace(node.textContent);
656 |
657 | // abandon leading ASCII WS if left-flanked by ASCII WS
658 | if (edges.leadingAscii && isFlankedByWhitespace('left', node, options)) {
659 | edges.leading = edges.leadingNonAscii;
660 | }
661 |
662 | // abandon trailing ASCII WS if right-flanked by ASCII WS
663 | if (edges.trailingAscii && isFlankedByWhitespace('right', node, options)) {
664 | edges.trailing = edges.trailingNonAscii;
665 | }
666 |
667 | return { leading: edges.leading, trailing: edges.trailing }
668 | }
669 |
670 | function edgeWhitespace (string) {
671 | var m = string.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/);
672 | return {
673 | leading: m[1], // whole string for whitespace-only strings
674 | leadingAscii: m[2],
675 | leadingNonAscii: m[3],
676 | trailing: m[4], // empty for whitespace-only strings
677 | trailingNonAscii: m[5],
678 | trailingAscii: m[6]
679 | }
680 | }
681 |
682 | function isFlankedByWhitespace (side, node, options) {
683 | var sibling;
684 | var regExp;
685 | var isFlanked;
686 |
687 | if (side === 'left') {
688 | sibling = node.previousSibling;
689 | regExp = / $/;
690 | } else {
691 | sibling = node.nextSibling;
692 | regExp = /^ /;
693 | }
694 |
695 | if (sibling) {
696 | if (sibling.nodeType === 3) {
697 | isFlanked = regExp.test(sibling.nodeValue);
698 | } else if (options.preformattedCode && sibling.nodeName === 'CODE') {
699 | isFlanked = false;
700 | } else if (sibling.nodeType === 1 && !isBlock(sibling)) {
701 | isFlanked = regExp.test(sibling.textContent);
702 | }
703 | }
704 | return isFlanked
705 | }
706 |
707 | var reduce = Array.prototype.reduce;
708 | var escapes = [
709 | [/\\/g, '\\\\'],
710 | [/\*/g, '\\*'],
711 | [/^-/g, '\\-'],
712 | [/^\+ /g, '\\+ '],
713 | [/^(=+)/g, '\\$1'],
714 | [/^(#{1,6}) /g, '\\$1 '],
715 | [/`/g, '\\`'],
716 | [/^~~~/g, '\\~~~'],
717 | [/\[/g, '\\['],
718 | [/\]/g, '\\]'],
719 | [/^>/g, '\\>'],
720 | [/_/g, '\\_'],
721 | [/^(\d+)\. /g, '$1\\. ']
722 | ];
723 |
724 | function TurndownService (options) {
725 | if (!(this instanceof TurndownService)) return new TurndownService(options)
726 |
727 | var defaults = {
728 | rules: rules,
729 | headingStyle: 'setext',
730 | hr: '* * *',
731 | bulletListMarker: '*',
732 | codeBlockStyle: 'indented',
733 | fence: '```',
734 | emDelimiter: '_',
735 | strongDelimiter: '**',
736 | linkStyle: 'inlined',
737 | linkReferenceStyle: 'full',
738 | br: ' ',
739 | preformattedCode: false,
740 | blankReplacement: function (content, node) {
741 | return node.isBlock ? '\n\n' : ''
742 | },
743 | keepReplacement: function (content, node) {
744 | return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML
745 | },
746 | defaultReplacement: function (content, node) {
747 | return node.isBlock ? '\n\n' + content + '\n\n' : content
748 | }
749 | };
750 | this.options = extend({}, defaults, options);
751 | this.rules = new Rules(this.options);
752 | }
753 |
754 | TurndownService.prototype = {
755 | /**
756 | * The entry point for converting a string or DOM node to Markdown
757 | * @public
758 | * @param {String|HTMLElement} input The string or DOM node to convert
759 | * @returns A Markdown representation of the input
760 | * @type String
761 | */
762 |
763 | turndown: function (input) {
764 | if (!canConvert(input)) {
765 | throw new TypeError(
766 | input + ' is not a string, or an element/document/fragment node.'
767 | )
768 | }
769 |
770 | if (input === '') return ''
771 |
772 | var output = process.call(this, new RootNode(input, this.options));
773 | return postProcess.call(this, output)
774 | },
775 |
776 | /**
777 | * Add one or more plugins
778 | * @public
779 | * @param {Function|Array} plugin The plugin or array of plugins to add
780 | * @returns The Turndown instance for chaining
781 | * @type Object
782 | */
783 |
784 | use: function (plugin) {
785 | if (Array.isArray(plugin)) {
786 | for (var i = 0; i < plugin.length; i++) this.use(plugin[i]);
787 | } else if (typeof plugin === 'function') {
788 | plugin(this);
789 | } else {
790 | throw new TypeError('plugin must be a Function or an Array of Functions')
791 | }
792 | return this
793 | },
794 |
795 | /**
796 | * Adds a rule
797 | * @public
798 | * @param {String} key The unique key of the rule
799 | * @param {Object} rule The rule
800 | * @returns The Turndown instance for chaining
801 | * @type Object
802 | */
803 |
804 | addRule: function (key, rule) {
805 | this.rules.add(key, rule);
806 | return this
807 | },
808 |
809 | /**
810 | * Keep a node (as HTML) that matches the filter
811 | * @public
812 | * @param {String|Array|Function} filter The unique key of the rule
813 | * @returns The Turndown instance for chaining
814 | * @type Object
815 | */
816 |
817 | keep: function (filter) {
818 | this.rules.keep(filter);
819 | return this
820 | },
821 |
822 | /**
823 | * Remove a node that matches the filter
824 | * @public
825 | * @param {String|Array|Function} filter The unique key of the rule
826 | * @returns The Turndown instance for chaining
827 | * @type Object
828 | */
829 |
830 | remove: function (filter) {
831 | this.rules.remove(filter);
832 | return this
833 | },
834 |
835 | /**
836 | * Escapes Markdown syntax
837 | * @public
838 | * @param {String} string The string to escape
839 | * @returns A string with Markdown syntax escaped
840 | * @type String
841 | */
842 |
843 | escape: function (string) {
844 | return escapes.reduce(function (accumulator, escape) {
845 | return accumulator.replace(escape[0], escape[1])
846 | }, string)
847 | }
848 | };
849 |
850 | /**
851 | * Reduces a DOM node down to its Markdown string equivalent
852 | * @private
853 | * @param {HTMLElement} parentNode The node to convert
854 | * @returns A Markdown representation of the node
855 | * @type String
856 | */
857 |
858 | function process (parentNode) {
859 | var self = this;
860 | return reduce.call(parentNode.childNodes, function (output, node) {
861 | node = new Node(node, self.options);
862 |
863 | var replacement = '';
864 | if (node.nodeType === 3) {
865 | replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue);
866 | } else if (node.nodeType === 1) {
867 | replacement = replacementForNode.call(self, node);
868 | }
869 |
870 | return join(output, replacement)
871 | }, '')
872 | }
873 |
874 | /**
875 | * Appends strings as each rule requires and trims the output
876 | * @private
877 | * @param {String} output The conversion output
878 | * @returns A trimmed version of the ouput
879 | * @type String
880 | */
881 |
882 | function postProcess (output) {
883 | var self = this;
884 | this.rules.forEach(function (rule) {
885 | if (typeof rule.append === 'function') {
886 | output = join(output, rule.append(self.options));
887 | }
888 | });
889 |
890 | return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '')
891 | }
892 |
893 | /**
894 | * Converts an element node to its Markdown equivalent
895 | * @private
896 | * @param {HTMLElement} node The node to convert
897 | * @returns A Markdown representation of the node
898 | * @type String
899 | */
900 |
901 | function replacementForNode (node) {
902 | var rule = this.rules.forNode(node);
903 | var content = process.call(this, node);
904 | var whitespace = node.flankingWhitespace;
905 | if (whitespace.leading || whitespace.trailing) content = content.trim();
906 | return (
907 | whitespace.leading +
908 | rule.replacement(content, node, this.options) +
909 | whitespace.trailing
910 | )
911 | }
912 |
913 | /**
914 | * Joins replacement to the current output with appropriate number of new lines
915 | * @private
916 | * @param {String} output The current conversion output
917 | * @param {String} replacement The string to append to the output
918 | * @returns Joined output
919 | * @type String
920 | */
921 |
922 | function join (output, replacement) {
923 | var s1 = trimTrailingNewlines(output);
924 | var s2 = trimLeadingNewlines(replacement);
925 | var nls = Math.max(output.length - s1.length, replacement.length - s2.length);
926 | var separator = '\n\n'.substring(0, nls);
927 |
928 | return s1 + separator + s2
929 | }
930 |
931 | /**
932 | * Determines whether an input can be converted
933 | * @private
934 | * @param {String|HTMLElement} input Describe this parameter
935 | * @returns Describe what it returns
936 | * @type String|Object|Array|Boolean|Number
937 | */
938 |
939 | function canConvert (input) {
940 | return (
941 | input != null && (
942 | typeof input === 'string' ||
943 | (input.nodeType && (
944 | input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11
945 | ))
946 | )
947 | )
948 | }
949 |
950 | return TurndownService;
951 |
952 | })));
953 |
--------------------------------------------------------------------------------
|