├── .nojekyll ├── CNAME ├── vendor ├── turndown │ ├── LICENSE │ ├── package.json │ ├── README.md │ └── lib │ │ ├── turndown.es.js │ │ ├── turndown.cjs.js │ │ ├── turndown.browser.es.js │ │ ├── turndown.browser.cjs.js │ │ └── turndown.umd.js └── turndown-plugin-gfm │ └── dist │ └── turndown-plugin-gfm.js ├── assets ├── background.svg ├── background-dark.svg ├── clipboard2markdown.js └── to-markdown.js └── index.html /.nojekyll: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | p2m.gh.miniasp.com -------------------------------------------------------------------------------- /vendor/turndown/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dom Christie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/background.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.13, written by Peter Selinger 2001-2015 9 | 10 | 12 | 17 | 20 | 22 | 24 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /assets/background-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.13, written by Peter Selinger 2001-2015 9 | 10 | 12 | 17 | 20 | 22 | 24 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /vendor/turndown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turndown", 3 | "description": "A library that converts HTML to Markdown", 4 | "version": "7.1.2", 5 | "author": "Dom Christie", 6 | "main": "lib/turndown.cjs.js", 7 | "module": "lib/turndown.es.js", 8 | "jsnext:main": "lib/turndown.es.js", 9 | "browser": { 10 | "domino": false, 11 | "./lib/turndown.cjs.js": "./lib/turndown.browser.cjs.js", 12 | "./lib/turndown.es.js": "./lib/turndown.browser.es.js", 13 | "./lib/turndown.umd.js": "./lib/turndown.browser.umd.js" 14 | }, 15 | "dependencies": { 16 | "domino": "^2.1.6" 17 | }, 18 | "devDependencies": { 19 | "@rollup/plugin-commonjs": "^19.0.0", 20 | "@rollup/plugin-node-resolve": "13.0.0", 21 | "@rollup/plugin-replace": "2.4.2", 22 | "browserify": "17.0.0", 23 | "rewire": "^6.0.0", 24 | "rollup": "2.52.3", 25 | "standard": "^10.0.3", 26 | "turndown-attendant": "0.0.3" 27 | }, 28 | "files": [ 29 | "lib", 30 | "dist" 31 | ], 32 | "keywords": [ 33 | "converter", 34 | "html", 35 | "markdown" 36 | ], 37 | "license": "MIT", 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/mixmark-io/turndown.git" 41 | }, 42 | "scripts": { 43 | "build": "npm run build-cjs && npm run build-es && npm run build-umd && npm run build-iife", 44 | "build-cjs": "rollup -c config/rollup.config.cjs.js && rollup -c config/rollup.config.browser.cjs.js", 45 | "build-es": "rollup -c config/rollup.config.es.js && rollup -c config/rollup.config.browser.es.js", 46 | "build-umd": "rollup -c config/rollup.config.umd.js && rollup -c config/rollup.config.browser.umd.js", 47 | "build-iife": "rollup -c config/rollup.config.iife.js", 48 | "build-test": "browserify test/turndown-test.js --outfile test/turndown-test.browser.js", 49 | "prepare": "npm run build", 50 | "test": "npm run build && npm run build-test && standard ./src/**/*.js && node test/internals-test.js && node test/turndown-test.js" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /vendor/turndown-plugin-gfm/dist/turndown-plugin-gfm.js: -------------------------------------------------------------------------------- 1 | var turndownPluginGfm = (function (exports) { 2 | 'use strict'; 3 | 4 | var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/; 5 | 6 | function highlightedCodeBlock (turndownService) { 7 | turndownService.addRule('highlightedCodeBlock', { 8 | filter: function (node) { 9 | var firstChild = node.firstChild; 10 | return ( 11 | node.nodeName === 'DIV' && 12 | highlightRegExp.test(node.className) && 13 | firstChild && 14 | firstChild.nodeName === 'PRE' 15 | ) 16 | }, 17 | replacement: function (content, node, options) { 18 | var className = node.className || ''; 19 | var language = (className.match(highlightRegExp) || [null, ''])[1]; 20 | 21 | return ( 22 | '\n\n' + options.fence + language + '\n' + 23 | node.firstChild.textContent + 24 | '\n' + options.fence + '\n\n' 25 | ) 26 | } 27 | }); 28 | } 29 | 30 | function strikethrough (turndownService) { 31 | turndownService.addRule('strikethrough', { 32 | filter: ['del', 's', 'strike'], 33 | replacement: function (content) { 34 | return '~' + content + '~' 35 | } 36 | }); 37 | } 38 | 39 | var indexOf = Array.prototype.indexOf; 40 | var every = Array.prototype.every; 41 | var rules = {}; 42 | 43 | rules.tableCell = { 44 | filter: ['th', 'td'], 45 | replacement: function (content, node) { 46 | return cell(content, node) 47 | } 48 | }; 49 | 50 | rules.tableRow = { 51 | filter: 'tr', 52 | replacement: function (content, node) { 53 | var borderCells = ''; 54 | var alignMap = { left: ':--', right: '--:', center: ':-:' }; 55 | 56 | if (isHeadingRow(node)) { 57 | for (var i = 0; i < node.childNodes.length; i++) { 58 | var border = '---'; 59 | var align = ( 60 | node.childNodes[i].getAttribute('align') || '' 61 | ).toLowerCase(); 62 | 63 | if (align) border = alignMap[align] || border; 64 | 65 | borderCells += cell(border, node.childNodes[i]); 66 | } 67 | } 68 | return '\n' + content + (borderCells ? '\n' + borderCells : '') 69 | } 70 | }; 71 | 72 | rules.table = { 73 | // Only convert tables with a heading row. 74 | // Tables with no heading row are kept using `keep` (see below). 75 | filter: function (node) { 76 | return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0]) 77 | }, 78 | 79 | replacement: function (content) { 80 | // Ensure there are no blank lines 81 | content = content.replace(/\n\n/g, '\n'); 82 | return '\n\n' + content + '\n\n' 83 | } 84 | }; 85 | 86 | rules.tableSection = { 87 | filter: ['thead', 'tbody', 'tfoot'], 88 | replacement: function (content) { 89 | return content 90 | } 91 | }; 92 | 93 | // A tr is a heading row if: 94 | // - the parent is a THEAD 95 | // - or if its the first child of the TABLE or the first TBODY (possibly 96 | // following a blank THEAD) 97 | // - Excel compatibility: Always treat first row as header for Excel tables 98 | // which use TD instead of TH for headers 99 | function isHeadingRow (tr) { 100 | var NODE_ELEMENT_NODE = 1; 101 | var parentNode = tr.parentNode; 102 | 103 | // Find first element child (skip text nodes) 104 | var firstElementChild = parentNode.firstChild; 105 | while (firstElementChild && firstElementChild.nodeType !== NODE_ELEMENT_NODE) { 106 | firstElementChild = firstElementChild.nextSibling; 107 | } 108 | 109 | var isFirstRowInTableOrFirstTbody = firstElementChild === tr && 110 | (parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)); 111 | 112 | return ( 113 | parentNode.nodeName === 'THEAD' || 114 | // Treat first row as header for standard tables (with TH) and Excel tables (with TD) 115 | isFirstRowInTableOrFirstTbody 116 | ) 117 | } 118 | 119 | function isFirstTbody (element) { 120 | var NODE_TEXT_NODE = 3; 121 | 122 | if (element.nodeName !== 'TBODY') return false; 123 | 124 | var previousSibling = element.previousSibling; 125 | 126 | // Skip text nodes and COLGROUP elements 127 | while (previousSibling && (previousSibling.nodeType === NODE_TEXT_NODE || previousSibling.nodeName === 'COLGROUP' || previousSibling.nodeName === 'COL')) { 128 | previousSibling = previousSibling.previousSibling; 129 | } 130 | 131 | return ( 132 | !previousSibling || 133 | ( 134 | previousSibling.nodeName === 'THEAD' && 135 | /^\s*$/i.test(previousSibling.textContent) 136 | ) 137 | ) 138 | } 139 | 140 | function cell (content, node) { 141 | var index = indexOf.call(node.parentNode.childNodes, node); 142 | var prefix = ' '; 143 | if (index === 0) prefix = '| '; 144 | // Trim and remove newlines from cell content 145 | content = content.trim().replace(/\s*\n\s*/g, ' '); 146 | return prefix + content + ' |' 147 | } 148 | 149 | function tables (turndownService) { 150 | turndownService.keep(function (node) { 151 | return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0]) 152 | }); 153 | for (var key in rules) turndownService.addRule(key, rules[key]); 154 | } 155 | 156 | function taskListItems (turndownService) { 157 | turndownService.addRule('taskListItems', { 158 | filter: function (node) { 159 | return node.type === 'checkbox' && node.parentNode.nodeName === 'LI' 160 | }, 161 | replacement: function (content, node) { 162 | return (node.checked ? '[x]' : '[ ]') + ' ' 163 | } 164 | }); 165 | } 166 | 167 | function gfm (turndownService) { 168 | turndownService.use([ 169 | highlightedCodeBlock, 170 | strikethrough, 171 | tables, 172 | taskListItems 173 | ]); 174 | } 175 | 176 | exports.gfm = gfm; 177 | exports.highlightedCodeBlock = highlightedCodeBlock; 178 | exports.strikethrough = strikethrough; 179 | exports.tables = tables; 180 | exports.taskListItems = taskListItems; 181 | 182 | return exports; 183 | 184 | }({})); 185 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Paste to Markdown 7 | 8 | 9 | 10 | 11 | 173 | 174 | 175 | 176 |
177 |
178 |

Paste to Markdown

179 |

使用方式

180 |
    181 |
  1. 在任意網頁透過 Ctrl+C⌘+C 複製內容下來
  2. 182 |
  3. 在這個網頁按下 Ctrl+V⌘+V 貼上剪貼簿內容
  4. 183 |
  5. 接著你就會得到一份完整的 Markdown 文件,直接按下 Ctrl+C⌘+C 就能複製回去用!
  6. 184 |
  7. 按下 Escape 可以重設狀態
  8. 185 |
  9. 按下 Alt+1 切換到編輯模式,Alt+2 切換到預覽模式
  10. 186 |
187 |

本轉換器基於 to-markdown 進行轉換,所有資料都不會傳送到後端,請安心使用。

188 |

網頁樣式取自 Paste to Markdown 網頁,並修正些許錯誤。

189 |

本網頁原始碼位於 https://github.com/doggy8088/Paste-to-Markdown,歡迎到 Issues 討論或提供建議!

190 |
191 |
192 | 204 |
205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /vendor/turndown/README.md: -------------------------------------------------------------------------------- 1 | # Turndown 2 | 3 | [![Build Status](https://travis-ci.org/domchristie/turndown.svg?branch=master)](https://travis-ci.org/domchristie/turndown) 4 | 5 | Convert HTML into Markdown with JavaScript. 6 | 7 | ## Project Updates 8 | * `to-markdown` has been renamed to Turndown. See the [migration guide](https://github.com/domchristie/to-markdown/wiki/Migrating-from-to-markdown-to-Turndown) for details. 9 | * Turndown repository has changed its URL to https://github.com/mixmark-io/turndown. 10 | 11 | ## Installation 12 | 13 | npm: 14 | 15 | ``` 16 | npm install turndown 17 | ``` 18 | 19 | Browser: 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | For usage with RequireJS, UMD versions are located in `lib/turndown.umd.js` (for Node.js) and `lib/turndown.browser.umd.js` for browser usage. These files are generated when the npm package is published. To generate them manually, clone this repo and run `npm run build`. 26 | 27 | ## Usage 28 | 29 | ```js 30 | // For Node.js 31 | var TurndownService = require('turndown') 32 | 33 | var turndownService = new TurndownService() 34 | var markdown = turndownService.turndown('

Hello world!

') 35 | ``` 36 | 37 | Turndown also accepts DOM nodes as input (either element nodes, document nodes, or document fragment nodes): 38 | 39 | ```js 40 | var markdown = turndownService.turndown(document.getElementById('content')) 41 | ``` 42 | 43 | ## Options 44 | 45 | Options can be passed in to the constructor on instantiation. For example: 46 | 47 | ```js 48 | var turndownService = new TurndownService({ option: 'value' }) 49 | ``` 50 | 51 | | Option | Valid values | Default | 52 | | :-------------------- | :------------ | :------ | 53 | | `headingStyle` | `setext` or `atx` | `setext` | 54 | | `hr` | Any [Thematic break](http://spec.commonmark.org/0.27/#thematic-breaks) | `* * *` | 55 | | `bulletListMarker` | `-`, `+`, or `*` | `*` | 56 | | `codeBlockStyle` | `indented` or `fenced` | `indented` | 57 | | `fence` | ` ``` ` or `~~~` | ` ``` ` | 58 | | `emDelimiter` | `_` or `*` | `_` | 59 | | `strongDelimiter` | `**` or `__` | `**` | 60 | | `linkStyle` | `inlined` or `referenced` | `inlined` | 61 | | `linkReferenceStyle` | `full`, `collapsed`, or `shortcut` | `full` | 62 | | `preformattedCode` | `false` or [`true`](https://github.com/lucthev/collapse-whitespace/issues/16) | `false` | 63 | 64 | ### Advanced Options 65 | 66 | | Option | Valid values | Default | 67 | | :-------------------- | :------------ | :------ | 68 | | `blankReplacement` | rule replacement function | See **Special Rules** below | 69 | | `keepReplacement` | rule replacement function | See **Special Rules** below | 70 | | `defaultReplacement` | rule replacement function | See **Special Rules** below | 71 | 72 | ## Methods 73 | 74 | ### `addRule(key, rule)` 75 | 76 | The `key` parameter is a unique name for the rule for easy reference. Example: 77 | 78 | ```js 79 | turndownService.addRule('strikethrough', { 80 | filter: ['del', 's', 'strike'], 81 | replacement: function (content) { 82 | return '~' + content + '~' 83 | } 84 | }) 85 | ``` 86 | 87 | `addRule` returns the `TurndownService` instance for chaining. 88 | 89 | See **Extending with Rules** below. 90 | 91 | ### `keep(filter)` 92 | 93 | Determines which elements are to be kept and rendered as HTML. By default, Turndown does not keep any elements. The filter parameter works like a rule filter (see section on filters belows). Example: 94 | 95 | ```js 96 | turndownService.keep(['del', 'ins']) 97 | turndownService.turndown('

Hello worldWorld

') // 'Hello worldWorld' 98 | ``` 99 | 100 | This will render `` and `` elements as HTML when converted. 101 | 102 | `keep` can be called multiple times, with the newly added keep filters taking precedence over older ones. Keep filters will be overridden by the standard CommonMark rules and any added rules. To keep elements that are normally handled by those rules, add a rule with the desired behaviour. 103 | 104 | `keep` returns the `TurndownService` instance for chaining. 105 | 106 | ### `remove(filter)` 107 | 108 | Determines which elements are to be removed altogether i.e. converted to an empty string. By default, Turndown does not remove any elements. The filter parameter works like a rule filter (see section on filters belows). Example: 109 | 110 | ```js 111 | turndownService.remove('del') 112 | turndownService.turndown('

Hello worldWorld

') // 'Hello World' 113 | ``` 114 | 115 | This will remove `` 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 | --------------------------------------------------------------------------------