// ==UserScript== // @name MD一键生成 // @namespace http://tampermonkey.net/ // @version 0.1.4 // @description try to take over the world! // @author You // @match https://juejin.im/post/* // @match https://segmentfault.com/* // @match https://www.yuque.com/* // @grant none // @downloadURL https://update.greasyfork.icu/scripts/383158/MD%E4%B8%80%E9%94%AE%E7%94%9F%E6%88%90.user.js // @updateURL https://update.greasyfork.icu/scripts/383158/MD%E4%B8%80%E9%94%AE%E7%94%9F%E6%88%90.meta.js // ==/UserScript== (function () { 'use strict' // Your code here... const origin = location.origin setTimeout(downloadFile, 1000) function downloadFile () { let fileName, content, parentNode, style if (origin.includes('juejin')) { // 掘金 const contentNode = document.querySelector('.article-content') || document.querySelector('.article') content = contentNode.innerHTML const fileNode = document.querySelector('.article-title') fileName = fileNode ? fileNode.innerText : `掘金${new Date().getMonth() + 1}-${new Date().getDate()}` parentNode = document.querySelector('.article-suspended-panel ') style = `display: block; position: relative; margin-bottom: .75rem; width: 3rem; height: 3rem; background-color: #fff; background-position: 50%; background-repeat: no-repeat; border-radius: 50%; box-shadow: 0 2px 4px 0 rgba(0,0,0,.04); cursor: pointer;` } else if (origin.includes('segmentfault')) { // SegmentFault content = document.querySelector('.article__content').innerHTML fileName = document.querySelector('#articleTitle a').innerText parentNode = document.querySelector('.side-widget') style = `display: block; width: 38px; height: 44px; margin-bottom: 15px; border: 1px solid transparent; border-radius: 4px; background: #26a2ff; text-align: center; color: #ccc; font-size: 18px;` } else if (origin.includes('yuque')) { // yuque content = document.querySelector('.lake-engine-view').innerHTML fileName = document.querySelector('#article-title').innerText parentNode = document.querySelector('.entry___odTWc') style = `display: block; position: relative; cursor: pointer; padding: 0px; z-index: 400; width:40px; height: 40px; background: #fff; border-radius: 40px; box-shadow: 0 2px 6px rgba(0,0,0,.15)` } html2md(content, fileName, parentNode, style) } const TurndownService = (function () { 'use strict' function extend (destination) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] for (var key in source) { if (source.hasOwnProperty(key)) destination[key] = source[key] } } return destination } function repeat (character, count) { return Array(count + 1).join(character) } var blockElements = [ 'address', 'article', 'aside', 'audio', 'blockquote', 'body', 'canvas', 'center', 'dd', 'dir', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'html', 'isindex', 'li', 'main', 'menu', 'nav', 'noframes', 'noscript', 'ol', 'output', 'p', 'pre', 'section', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'ul' ] function isBlock (node) { return blockElements.indexOf(node.nodeName.toLowerCase()) !== -1 } var voidElements = [ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ] function isVoid (node) { return voidElements.indexOf(node.nodeName.toLowerCase()) !== -1 } var voidSelector = voidElements.join() function hasVoid (node) { return node.querySelector && node.querySelector(voidSelector) } var rules = {} rules.paragraph = { filter: 'p', replacement: function (content) { return '\n\n' + content + '\n\n' } } rules.lineBreak = { filter: 'br', replacement: function (content, node, options) { return options.br + '\n' } } rules.heading = { filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], replacement: function (content, node, options) { var hLevel = Number(node.nodeName.charAt(1)) if (options.headingStyle === 'setext' && hLevel < 3) { var underline = repeat((hLevel === 1 ? '=' : '-'), content.length) return ( '\n\n' + content + '\n' + underline + '\n\n' ) } else { return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n' } } } rules.blockquote = { filter: 'blockquote', replacement: function (content) { content = content.replace(/^\n+|\n+$/g, '') content = content.replace(/^/gm, '> ') return '\n\n' + content + '\n\n' } } rules.list = { filter: ['ul', 'ol'], replacement: function (content, node) { var parent = node.parentNode if (parent.nodeName === 'LI' && parent.lastElementChild === node) { return '\n' + content } else { return '\n\n' + content + '\n\n' } } } rules.listItem = { filter: 'li', replacement: function (content, node, options) { content = content .replace(/^\n+/, '') // remove leading newlines .replace(/\n+$/, '\n') // replace trailing newlines with just a single one .replace(/\n/gm, '\n ') // indent var prefix = options.bulletListMarker + ' ' var parent = node.parentNode if (parent.nodeName === 'OL') { var start = parent.getAttribute('start') var index = Array.prototype.indexOf.call(parent.children, node) prefix = (start ? Number(start) + index : index + 1) + '. ' } return ( prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') ) } } rules.indentedCodeBlock = { filter: function (node, options) { return ( options.codeBlockStyle === 'indented' && node.nodeName === 'PRE' && node.firstChild && node.firstChild.nodeName === 'CODE' ) }, replacement: function (content, node, options) { return ( '\n\n ' + node.firstChild.textContent.replace(/\n/g, '\n ') + '\n\n' ) } } rules.fencedCodeBlock = { filter: function (node, options) { return ( options.codeBlockStyle === 'fenced' && node.nodeName === 'PRE' && node.firstChild && node.firstChild.nodeName === 'CODE' ) }, replacement: function (content, node, options) { var className = node.firstChild.className || '' var language = (className.match(/language-(\S+)/) || [null, ''])[1] return ( '\n\n' + options.fence + language + '\n' + node.firstChild.textContent + '\n' + options.fence + '\n\n' ) } } rules.horizontalRule = { filter: 'hr', replacement: function (content, node, options) { return '\n\n' + options.hr + '\n\n' } } rules.inlineLink = { filter: function (node, options) { return ( options.linkStyle === 'inlined' && node.nodeName === 'A' && node.getAttribute('href') ) }, replacement: function (content, node) { var href = node.getAttribute('href') var title = node.title ? ' "' + node.title + '"' : '' return '[' + content + '](' + href + title + ')' } } rules.referenceLink = { filter: function (node, options) { return ( options.linkStyle === 'referenced' && node.nodeName === 'A' && node.getAttribute('href') ) }, replacement: function (content, node, options) { var href = node.getAttribute('href') var title = node.title ? ' "' + node.title + '"' : '' var replacement var reference switch (options.linkReferenceStyle) { case 'collapsed': replacement = '[' + content + '][]' reference = '[' + content + ']: ' + href + title break case 'shortcut': replacement = '[' + content + ']' reference = '[' + content + ']: ' + href + title break default: var id = this.references.length + 1 replacement = '[' + content + '][' + id + ']' reference = '[' + id + ']: ' + href + title } this.references.push(reference) return replacement }, references: [], append: function (options) { var references = '' if (this.references.length) { references = '\n\n' + this.references.join('\n') + '\n\n' this.references = [] // Reset references } return references } } rules.emphasis = { filter: ['em', 'i'], replacement: function (content, node, options) { if (!content.trim()) return '' return options.emDelimiter + content + options.emDelimiter } } rules.strong = { filter: ['strong', 'b'], replacement: function (content, node, options) { if (!content.trim()) return '' return options.strongDelimiter + content + options.strongDelimiter } } rules.code = { filter: function (node) { var hasSiblings = node.previousSibling || node.nextSibling var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings return node.nodeName === 'CODE' && !isCodeBlock }, replacement: function (content) { if (!content.trim()) return '' var delimiter = '`' var leadingSpace = '' var trailingSpace = '' var matches = content.match(/`+/gm) if (matches) { if (/^`/.test(content)) leadingSpace = ' ' if (/`$/.test(content)) trailingSpace = ' ' while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`' } return delimiter + leadingSpace + content + trailingSpace + delimiter } } rules.image = { filter: 'img', replacement: function (content, node) { var alt = node.alt || '' var src = node.getAttribute('data-src') || node.getAttribute('src') src = src.includes('http') ? src : origin + src var title = node.title || '' var titlePart = title ? ' "' + title + '"' : '' return src ? `![${alt}](${src}${titlePart})` : '' } } /** * Manages a collection of rules used to convert HTML to Markdown */ function Rules (options) { this.options = options this._keep = [] this._remove = [] this.blankRule = { replacement: options.blankReplacement } this.keepReplacement = options.keepReplacement this.defaultRule = { replacement: options.defaultReplacement } this.array = [] for (var key in options.rules) this.array.push(options.rules[key]) } Rules.prototype = { add: function (key, rule) { this.array.unshift(rule) }, keep: function (filter) { this._keep.unshift({ filter: filter, replacement: this.keepReplacement }) }, remove: function (filter) { this._remove.unshift({ filter: filter, replacement: function () { return '' } }) }, forNode: function (node) { if (node.isBlank) return this.blankRule var rule if ((rule = findRule(this.array, node, this.options))) return rule if ((rule = findRule(this._keep, node, this.options))) return rule if ((rule = findRule(this._remove, node, this.options))) return rule return this.defaultRule }, forEach: function (fn) { for (var i = 0; i < this.array.length; i++) fn(this.array[i], i) } } function findRule (rules, node, options) { for (var i = 0; i < rules.length; i++) { var rule = rules[i] if (filterValue(rule, node, options)) return rule } return void 0 } function filterValue (rule, node, options) { var filter = rule.filter if (typeof filter === 'string') { if (filter === node.nodeName.toLowerCase()) return true } else if (Array.isArray(filter)) { if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true } else if (typeof filter === 'function') { if (filter.call(rule, node, options)) return true } else { throw new TypeError('`filter` needs to be a string, array, or function') } } /** * The collapseWhitespace function is adapted from collapse-whitespace * by Luc Thevenard. * * The MIT License (MIT) * * Copyright (c) 2014 Luc Thevenard * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * collapseWhitespace(options) removes extraneous whitespace from an the given element. * * @param {Object} options */ function collapseWhitespace (options) { var element = options.element var isBlock = options.isBlock var isVoid = options.isVoid var isPre = options.isPre || function (node) { return node.nodeName === 'PRE' } if (!element.firstChild || isPre(element)) return var prevText = null var prevVoid = false var prev = null var node = next(prev, element, isPre) while (node !== element) { if (node.nodeType === 3 || node.nodeType === 4) { // Node.TEXT_NODE or Node.CDATA_SECTION_NODE var text = node.data.replace(/[ \r\n\t]+/g, ' ') if ((!prevText || / $/.test(prevText.data)) && !prevVoid && text[0] === ' ') { text = text.substr(1) } // `text` might be empty at this point. if (!text) { node = remove(node) continue } node.data = text prevText = node } else if (node.nodeType === 1) { // Node.ELEMENT_NODE if (isBlock(node) || node.nodeName === 'BR') { if (prevText) { prevText.data = prevText.data.replace(/ $/, '') } prevText = null prevVoid = false } else if (isVoid(node)) { // Avoid trimming space around non-block, non-BR void elements. prevText = null prevVoid = true } } else { node = remove(node) continue } var nextNode = next(prev, node, isPre) prev = node node = nextNode } if (prevText) { prevText.data = prevText.data.replace(/ $/, '') if (!prevText.data) { remove(prevText) } } } /** * remove(node) removes the given node from the DOM and returns the * next node in the sequence. * * @param {Node} node * @return {Node} node */ function remove (node) { var next = node.nextSibling || node.parentNode node.parentNode.removeChild(node) return next } /** * next(prev, current, isPre) returns the next node in the sequence, given the * current and previous nodes. * * @param {Node} prev * @param {Node} current * @param {Function} isPre * @return {Node} */ function next (prev, current, isPre) { if ((prev && prev.parentNode === current) || isPre(current)) { return current.nextSibling || current.parentNode } return current.firstChild || current.nextSibling || current.parentNode } /* * Set up window for Node.js */ var root = (typeof window !== 'undefined' ? window : {}) /* * Parsing HTML strings */ function canParseHTMLNatively () { var Parser = root.DOMParser var canParse = false // Adapted from https://gist.github.com/1129031 // Firefox/Opera/IE throw errors on unsupported types try { // WebKit returns null on unsupported types if (new Parser().parseFromString('', 'text/html')) { canParse = true } } catch (e) {} return canParse } function createHTMLParser () { var Parser = function () {} if (shouldUseActiveX()) { Parser.prototype.parseFromString = function (string) { var doc = new window.ActiveXObject('htmlfile') doc.designMode = 'on' // disable on-page scripts doc.open() doc.write(string) doc.close() return doc } } else { Parser.prototype.parseFromString = function (string) { var doc = document.implementation.createHTMLDocument('') doc.open() doc.write(string) doc.close() return doc } } return Parser } function shouldUseActiveX () { var useActiveX = false try { document.implementation.createHTMLDocument('').open() } catch (e) { if (window.ActiveXObject) useActiveX = true } return useActiveX } var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser() function RootNode (input) { var root if (typeof input === 'string') { var doc = htmlParser().parseFromString( // DOM parsers arrange elements in the and . // Wrapping in a custom element ensures elements are reliably arranged in // a single element. '' + input + '', 'text/html' ) root = doc.getElementById('turndown-root') } else { root = input.cloneNode(true) } collapseWhitespace({ element: root, isBlock: isBlock, isVoid: isVoid }) return root } var _htmlParser function htmlParser () { _htmlParser = _htmlParser || new HTMLParser() return _htmlParser } function Node (node) { node.isBlock = isBlock(node) node.isCode = node.nodeName.toLowerCase() === 'code' || node.parentNode.isCode node.isBlank = isBlank(node) node.flankingWhitespace = flankingWhitespace(node) return node } function isBlank (node) { return ( ['A', 'TH', 'TD', 'IFRAME', 'SCRIPT', 'AUDIO', 'VIDEO'].indexOf(node.nodeName) === -1 && /^\s*$/i.test(node.textContent) && !isVoid(node) && !hasVoid(node) ) } function flankingWhitespace (node) { var leading = '' var trailing = '' if (!node.isBlock) { var hasLeading = /^[ \r\n\t]/.test(node.textContent) var hasTrailing = /[ \r\n\t]$/.test(node.textContent) if (hasLeading && !isFlankedByWhitespace('left', node)) { leading = ' ' } if (hasTrailing && !isFlankedByWhitespace('right', node)) { trailing = ' ' } } return { leading: leading, trailing: trailing } } function isFlankedByWhitespace (side, node) { var sibling var regExp var isFlanked if (side === 'left') { sibling = node.previousSibling regExp = / $/ } else { sibling = node.nextSibling regExp = /^ / } if (sibling) { if (sibling.nodeType === 3) { isFlanked = regExp.test(sibling.nodeValue) } else if (sibling.nodeType === 1 && !isBlock(sibling)) { isFlanked = regExp.test(sibling.textContent) } } return isFlanked } var reduce = Array.prototype.reduce var leadingNewLinesRegExp = /^\n*/ var trailingNewLinesRegExp = /\n*$/ var escapes = [ [/\\/g, '\\\\'], [/\*/g, '\\*'], [/^-/g, '\\-'], [/^\+ /g, '\\+ '], [/^(=+)/g, '\\$1'], [/^(#{1,6}) /g, '\\$1 '], [/`/g, '\\`'], [/^~~~/g, '\\~~~'], [/\[/g, '\\['], [/\]/g, '\\]'], [/^>/g, '\\>'], [/_/g, '\\_'], [/^(\d+)\. /g, '$1\\. '] ] function TurndownService (options) { if (!(this instanceof TurndownService)) return new TurndownService(options) var defaults = { rules: rules, headingStyle: 'setext', hr: '* * *', bulletListMarker: '*', codeBlockStyle: 'indented', fence: '```', emDelimiter: '_', strongDelimiter: '**', linkStyle: 'inlined', linkReferenceStyle: 'full', br: ' ', blankReplacement: function (content, node) { return node.isBlock ? '\n\n' : '' }, keepReplacement: function (content, node) { return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML }, defaultReplacement: function (content, node) { return node.isBlock ? '\n\n' + content + '\n\n' : content } } this.options = extend({}, defaults, options) this.rules = new Rules(this.options) } TurndownService.prototype = { /** * The entry point for converting a string or DOM node to Markdown * @public * @param {String|HTMLElement} input The string or DOM node to convert * @returns A Markdown representation of the input * @type String */ turndown: function (input) { if (!canConvert(input)) { throw new TypeError( input + ' is not a string, or an element/document/fragment node.' ) } if (input === '') return '' var output = process.call(this, new RootNode(input)) return postProcess.call(this, output) }, /** * Add one or more plugins * @public * @param {Function|Array} plugin The plugin or array of plugins to add * @returns The Turndown instance for chaining * @type Object */ use: function (plugin) { if (Array.isArray(plugin)) { for (var i = 0; i < plugin.length; i++) this.use(plugin[i]) } else if (typeof plugin === 'function') { plugin(this) } else { throw new TypeError('plugin must be a Function or an Array of Functions') } return this }, /** * Adds a rule * @public * @param {String} key The unique key of the rule * @param {Object} rule The rule * @returns The Turndown instance for chaining * @type Object */ addRule: function (key, rule) { this.rules.add(key, rule) return this }, /** * Keep a node (as HTML) that matches the filter * @public * @param {String|Array|Function} filter The unique key of the rule * @returns The Turndown instance for chaining * @type Object */ keep: function (filter) { this.rules.keep(filter) return this }, /** * Remove a node that matches the filter * @public * @param {String|Array|Function} filter The unique key of the rule * @returns The Turndown instance for chaining * @type Object */ remove: function (filter) { this.rules.remove(filter) return this }, /** * Escapes Markdown syntax * @public * @param {String} string The string to escape * @returns A string with Markdown syntax escaped * @type String */ escape: function (string) { return escapes.reduce(function (accumulator, escape) { return accumulator.replace(escape[0], escape[1]) }, string) } } /** * Reduces a DOM node down to its Markdown string equivalent * @private * @param {HTMLElement} parentNode The node to convert * @returns A Markdown representation of the node * @type String */ function process (parentNode) { var self = this return reduce.call(parentNode.childNodes, function (output, node) { node = new Node(node) var replacement = '' if (node.nodeType === 3) { replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue) } else if (node.nodeType === 1) { replacement = replacementForNode.call(self, node) } return join(output, replacement) }, '') } /** * Appends strings as each rule requires and trims the output * @private * @param {String} output The conversion output * @returns A trimmed version of the ouput * @type String */ function postProcess (output) { var self = this this.rules.forEach(function (rule) { if (typeof rule.append === 'function') { output = join(output, rule.append(self.options)) } }) return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '') } /** * Converts an element node to its Markdown equivalent * @private * @param {HTMLElement} node The node to convert * @returns A Markdown representation of the node * @type String */ function replacementForNode (node) { var rule = this.rules.forNode(node) var content = process.call(this, node) var whitespace = node.flankingWhitespace if (whitespace.leading || whitespace.trailing) content = content.trim() return ( whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing ) } /** * Determines the new lines between the current output and the replacement * @private * @param {String} output The current conversion output * @param {String} replacement The string to append to the output * @returns The whitespace to separate the current output and the replacement * @type String */ function separatingNewlines (output, replacement) { var newlines = [ output.match(trailingNewLinesRegExp)[0], replacement.match(leadingNewLinesRegExp)[0] ].sort() var maxNewlines = newlines[newlines.length - 1] return maxNewlines.length < 2 ? maxNewlines : '\n\n' } function join (string1, string2) { var separator = separatingNewlines(string1, string2) // Remove trailing/leading newlines and replace with separator string1 = string1.replace(trailingNewLinesRegExp, '') string2 = string2.replace(leadingNewLinesRegExp, '') return string1 + separator + string2 } /** * Determines whether an input can be converted * @private * @param {String|HTMLElement} input Describe this parameter * @returns Describe what it returns * @type String|Object|Array|Boolean|Number */ function canConvert (input) { return ( input != null && ( typeof input === 'string' || (input.nodeType && ( input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11 )) ) ) } return TurndownService }()) function html2md (content, fileName, parentNode, style) { const turndownService = new TurndownService() content = turndownService.turndown(content) const aLink = document.createElement('a') const blob = new Blob([content]) const evt = document.createEvent('HTMLEvents') evt.initEvent('click', false, false) aLink.download = `${fileName}.md` aLink.href = URL.createObjectURL(blob) aLink.dispatchEvent(evt) aLink.style = style parentNode.insertBefore(aLink, parentNode.childNodes[0]) } })()