2025年3月29日 星期六 甲辰(龙)年 月廿八 设为首页 加入收藏
rss
您当前的位置:首页 > 计算机 > 编程开发 > JavaScript

一键将HTML网页选中区块复制转换为Markdown格式(支持table转换)

时间:09-19来源:作者:点击数:35

工具

浏览器插件:tampermonkey 油猴

JS脚本:nameldk 的《复制为Markdown格式》 0.3.1版本

基础上添加代码。

需求

程序员做笔记最简洁的就是markdown了,html转存为markdown格式就好了,样式统一,编辑也方便。

上面的工具和脚本,转换代码块、图片、超链接文字都很好,支持标签区块识别(像是印象笔记或有道云笔记的网页剪辑那种),随选随转,十分方便。

唯一的缺点就是不支持table标签。

实现

在原有脚本基础上修改,使其支持table标签转换。测试了几个想保存的页面,没什么问题。至于其他网页碰到再说,暂不确定兼容情况。

在转换规则中,添加tr、td&th、table规则

删掉顶部的// @require那句,将其引用的turndown.js内容完整复制,粘贴到在JS脚本头部,然后在其中添加table转换的部分。

  • // // ==============================================================================
  • // // 在turndown中添加的转换table部分(author:watfe)
  • // // ==============================================================================
  • rules.td = {
  • filter: ['td'],
  • replacement: function (content, node, options) {
  • content = content.trim();
  • content = content.replace(/\|/g,'/');
  • content = content.replace(/\n+/g,' <br> ');
  • content = '|'+content
  • // console.log('td:'+content);
  • return content
  • }
  • };
  • rules.th = {
  • filter: ['th'],
  • replacement: function (content, node, options) {
  • content = content.trim();
  • content = content.replace(/\|/g,'/');
  • content = content.replace(/\n+/g,' <br> ');
  • content = '|#$&%th$#%&'+content
  • if (node.getAttribute('colspan')!=null){ //如果表头存在合并单元格,对其进行处理
  • content = content+repeatStringNumTimes('|', node.getAttribute('colspan'));
  • }
  • // console.log('th:'+content);
  • return content
  • }
  • };
  • rules.tr = {
  • filter: ['tr'],
  • replacement: function (content, node, options) {
  • content = '|\n'+content.trim();
  • // console.log('tr:'+content);
  • return content
  • }
  • };
  • function repeatStringNumTimes(str, num) { //字符串重复N次
  • let repeatStr = '';
  • for(let i = 0; i < num; i++){
  • repeatStr += str;
  • }
  • return repeatStr;
  • }
  • rules.table = {
  • filter: ['table'],
  • replacement: function (content, node, options) {
  • // 删首尾空,删除最前面多余的|,并在最后补全|
  • //console.log('table1:'+content);
  • content = content.trim();
  • content = content.replace(/\n+/g, '\n');
  • content = content.replace(/\n\|\n/g, '|\n');
  • if (content.indexOf('|\n')===0){
  • content = content.substring(1, content.length)+'|';
  • }
  • content = content.trim();
  • // 如果表最前端包含<caption>表名标签的,通过下面代码优化让<caption>变成独立一行
  • //console.log('table2:'+content);
  • var captionLine = '';
  • if (content.slice(0,1)!='|' && (content.slice(-1)!='|')){
  • captionLine = content.slice(0,content.indexOf('|\n'));//captionn那行
  • content = content.slice(content.indexOf('|\n')+2);
  • content = content+'|'
  • }
  • // 检查表格包含几个|,数字减1就是列数,模拟出markdown表格中间的---:
  • var verticalLineCount = 0;
  • var strs = new Array();
  • var thExist = false;
  • strs = content.split('\n');
  • for (let i=0; i<strs.length; i++ ){ // 计算最多列数
  • if(strs[i].indexOf('|')>=0){
  • let tempnum = strs[i].match(/\|/ig).length;
  • if (tempnum>verticalLineCount){
  • verticalLineCount = tempnum;
  • }
  • }
  • }
  • var buildTh = repeatStringNumTimes('| ',verticalLineCount).trim(); // 构造没有表头时候,markdown需要的表头 比如:| | | |
  • var tableMDLine = '|'+repeatStringNumTimes('---|',verticalLineCount-1); // 构造markdown表中间的横线 比如:|---|---|---|
  • // 检查是否包含表头
  • //console.log('table3:'+content);
  • if (content.indexOf('|#$&%th$#%&')>=0){
  • content = content.replace(/\|#\$&%th\$#%&/g, '|');
  • content = content.replace('\n','\n'+tableMDLine+'\n');
  • }
  • else{
  • content = buildTh+'\n'+tableMDLine+'\n'+ content;
  • }
  • content = '\n'+captionLine+'\n\n'+content+'\n\n';
  • //console.log('table4:'+content);
  • return content
  • }
  • };
  • // // ==============================================================================
  • // // 添加完毕
  • // // ==============================================================================
调整markdown输出格式

比如:h1用#而不是下划线,代码块用```等等。

在JS脚本建立TurndownService实例前,加入这些配置。

  • // ==============================================================================
  • // 根据turndown文档,调整一些参数,让转化出来的markdown格式,满足自己的需要
  • // https://github.com/domchristie/turndown
  • // ==============================================================================
  • let options = {
  • headingStyle: 'atx',
  • bulletListMarker: '+',
  • codeBlockStyle: 'fenced',
  • emDelimiter: '*'
  • };
  • // ==============================================================================
  • // 添加完毕
  • // ==============================================================================
  • let turndownService = new TurndownService(options);
其他bug修正

.replace(/(<a.+?href=")(.*?")(.*?<\/a>)/gi, parseHref);修改为.replace(/(<a.+?href=")(.*?)(".*?<\/a>)/gi, parseHref);

原本的代码,因为这个错误会导致herf链接包含#号时出错

测试

转换成功

在这里插入图片描述

最终完整js代码

使用方法:

tampermonkey中选"添加新脚本", 清空默认内容, 复制代码粘贴到文本框中, 然后点"文件", 点"保存"即可

  • // ==UserScript==
  • // @name Markdown
  • // @name 网页转换为Markdown格式
  • // @version 0.0.1
  • // @description 复制网页内容为Markdown格式。点击右上角copy按钮开始选择内容,点击鼠标或按Enter进行复制。按Esc取消选择,上箭头选择父级,下箭头选择子级,左箭头选择前面的相邻元素,右箭头选择后面的相邻元素。按钮可以拖动。Ctrl+c+c激活。
  • // @author nameldk
  • // @author watfe基于nameldk的"复制为Markdown格式.js"和"https://unpkg.com/turndown/dist/turndown.js"的基础上修改
  • // @match https://*/*
  • // @match http://*/*
  • // @match file:///*
  • // @grant none
  • // ==/UserScript==
  • // ==============================================================
  • // 直接导入turndown@6.0.0
  • // https://unpkg.com/turndown/dist/turndown.js
  • var 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'
  • }
  • }
  • };
  • // // ==============================================================================
  • // // 转换table(自己写的,可能不太完善)
  • // // ==============================================================================
  • rules.td = {
  • filter: ['td'],
  • replacement: function (content, node, options) {
  • content = content.trim();
  • content = content.replace(/\|/g,'/');
  • content = content.replace(/\n+/g,' <br> ');
  • content = '|'+content
  • // console.log('td:'+content);
  • return content
  • }
  • };
  • rules.th = {
  • filter: ['th'],
  • replacement: function (content, node, options) {
  • content = content.trim();
  • content = content.replace(/\|/g,'/');
  • content = content.replace(/\n+/g,' <br> ');
  • content = '|#$&%th$#%&'+content
  • if (node.getAttribute('colspan')!=null){ //如果表头存在合并单元格,对其进行处理
  • content = content+repeatStringNumTimes('|', node.getAttribute('colspan'));
  • }
  • // console.log('th:'+content);
  • return content
  • }
  • };
  • rules.tr = {
  • filter: ['tr'],
  • replacement: function (content, node, options) {
  • content = '|\n'+content.trim();
  • // console.log('tr:'+content);
  • return content
  • }
  • };
  • function repeatStringNumTimes(str, num) { //字符串重复N次
  • let repeatStr = '';
  • for(let i = 0; i < num; i++){
  • repeatStr += str;
  • }
  • return repeatStr;
  • }
  • rules.table = {
  • filter: ['table'],
  • replacement: function (content, node, options) {
  • // 删首尾空,删除最前面多余的|,并在最后补全|
  • //console.log('table1:'+content);
  • content = content.trim();
  • content = content.replace(/\n+/g, '\n');
  • content = content.replace(/\n\|\n/g, '|\n');
  • if (content.indexOf('|\n')===0){
  • content = content.substring(1, content.length)+'|';
  • }
  • content = content.trim();
  • // 如果表最前端包含<caption>表名标签的,通过下面代码优化让<caption>变成独立一行
  • //console.log('table2:'+content);
  • var captionLine = '';
  • if (content.slice(0,1)!='|' && (content.slice(-1)!='|')){
  • captionLine = content.slice(0,content.indexOf('|\n'));//captionn那行
  • content = content.slice(content.indexOf('|\n')+2);
  • content = content+'|'
  • }
  • // 检查表格包含几个|,数字减1就是列数,模拟出markdown表格中间的---:
  • var verticalLineCount = 0;
  • var strs = new Array();
  • var thExist = false;
  • strs = content.split('\n');
  • for (let i=0; i<strs.length; i++ ){ // 计算最多列数
  • if(strs[i].indexOf('|')>=0){
  • let tempnum = strs[i].match(/\|/ig).length;
  • if (tempnum>verticalLineCount){
  • verticalLineCount = tempnum;
  • }
  • }
  • }
  • var buildTh = repeatStringNumTimes('| ',verticalLineCount).trim(); // 构造没有表头时候,markdown需要的表头 比如:| | | |
  • var tableMDLine = '|'+repeatStringNumTimes('---|',verticalLineCount-1); // 构造markdown表中间的横线 比如:|---|---|---|
  • // 检查是否包含表头
  • //console.log('table3:'+content);
  • if (content.indexOf('|#$&%th$#%&')>=0){
  • content = content.replace(/\|#\$&%th\$#%&/g, '|');
  • content = content.replace('\n','\n'+tableMDLine+'\n');
  • }
  • else{
  • content = buildTh+'\n'+tableMDLine+'\n'+ content;
  • }
  • content = '\n'+captionLine+'\n\n'+content+'\n\n';
  • //console.log('table4:'+content);
  • return content
  • }
  • };
  • // // ==============================================================================
  • 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];
  • var code = node.firstChild.textContent;
  • var fenceChar = options.fence.charAt(0);
  • var fenceSize = 3;
  • var fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm');
  • var match;
  • while ((match = fenceInCodeRegex.exec(code))) {
  • if (match[0].length >= fenceSize) {
  • fenceSize = match[0].length + 1;
  • }
  • }
  • var fence = repeat(fenceChar, fenceSize);
  • return (
  • '\n\n' + fence + language + '\n' +
  • code.replace(/\n$/, '') +
  • '\n' + 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) { //console.log('content:'+content); console.log('node:'+node);
  • var href = node.getAttribute('href'); //console.log('href:'+href);
  • var title = node.title ? ' "' + node.title + '"' : ''; //console.log('title:'+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('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 <lucthevenard@gmail.com>
  • *
  • * 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'); //console.log('createHTMLParser1:'+string);
  • 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(''); //console.log('createHTMLParser2:'+string);
  • 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 <head> and <body>.
  • // Wrapping in a custom element ensures elements are reliably arranged in
  • // a single element.
  • '<x-turndown id="turndown-root">' + input + '</x-turndown>',
  • '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 = /^\s/.test(node.textContent);
  • var hasTrailing = /\s$/.test(node.textContent);
  • var blankWithSpaces = node.isBlank && hasLeading && hasTrailing;
  • if (hasLeading && !isFlankedByWhitespace('left', node)) {
  • leading = ' ';
  • }
  • if (!blankWithSpaces && 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) {
  • //console.log(input);
  • if (!canConvert(input)) { //copy点击后传入
  • 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 () {
  • 'use strict';
  • const CLASS_HINT = "_myhint_";
  • let $curElement = null;
  • let $btn = document.createElement("div");
  • // ==============================================================================
  • // 根据turndown文档,调整一些参数,让转化出来的markdown格式,满足自己的需要
  • // https://github.com/domchristie/turndown
  • // ==============================================================================
  • let options = {
  • headingStyle: 'atx',
  • bulletListMarker: '+',
  • codeBlockStyle: 'fenced',
  • emDelimiter: '*'
  • };
  • // ==============================================================================
  • let turndownService = new TurndownService(options);
  • let isHold = 0,
  • isDrag = 0;
  • function addStyle(css) {
  • let s = document.createElement("style");
  • s.type = "text/css";
  • s.textContent = css;
  • document.body.prepend(s);
  • }
  • function showHint($this) {
  • if ($this) {
  • $this.classList.add(CLASS_HINT);
  • }
  • }
  • function hideHint($this) {
  • if ($this) {
  • $this.classList.remove(CLASS_HINT);
  • }
  • }
  • function handleMouseover(e) {
  • hideHint($curElement);
  • let $target = e.target;
  • $curElement = $target;
  • showHint($target);
  • }
  • function handleMouseout(e) {
  • let $target = e.target;
  • hideHint($target);
  • }
  • function handleKeyup(e) {
  • function isValidNode(node) {
  • return node && node.textContent && node.textContent.trim() !== "";
  • }
  • function findNextNode(node) {
  • if (node) {
  • var findNode = node.nextElementSibling;
  • while(findNode) {
  • if (isValidNode(findNode)) {
  • return findNode;
  • } else {
  • if (findNode.nextElementSibling) {
  • findNode = findNode.nextElementSibling;
  • }
  • }
  • }
  • }
  • return null;
  • }
  • if (e.keyCode === 13) { // enter
  • process(e);
  • return false;
  • } else if (e.keyCode === 27) { // esc
  • disable();
  • } else if (e.keyCode === 38) { // arrow up
  • if ($curElement && isValidNode($curElement.parentElement)) {
  • hideHint($curElement);
  • $curElement = $curElement.parentElement;
  • showHint($curElement);
  • }
  • } else if (e.keyCode === 37) { // arrow left
  • if ($curElement && isValidNode($curElement.previousElementSibling)) {
  • hideHint($curElement);
  • $curElement = $curElement.previousElementSibling;
  • showHint($curElement);
  • }
  • } else if (e.keyCode === 40) { // arrow down
  • let nextNode = null;
  • if ($curElement && (nextNode = findNextNode($curElement.firstElementChild))) {
  • hideHint($curElement);
  • $curElement = nextNode;
  • showHint($curElement);
  • }
  • } else if (e.keyCode === 39) { // arrow right
  • if ($curElement && isValidNode($curElement.nextElementSibling)) {
  • hideHint($curElement);
  • $curElement = $curElement.nextElementSibling;
  • showHint($curElement);
  • }
  • }
  • }
  • function disableScroll(e) {
  • if ([38, 40, 37, 39].indexOf(e.keyCode) > -1) {
  • e.preventDefault();
  • }
  • }
  • function handleClick(e) {
  • process(e);
  • return false;
  • }
  • function process(e) {
  • if ($curElement) {
  • e.preventDefault();
  • copyIt($curElement);
  • disable();
  • showTips();
  • }
  • }
  • function showTips() {
  • let t = document.createElement("div");
  • t.style.position = "fixed";
  • t.style.width = "80px";
  • t.style.height = "24px";
  • t.style.lineHeight = "24px";
  • t.style.top = "10px";
  • t.style.right = "50%";
  • t.style.background = "#68af02";
  • t.style.fontSize = "14px";
  • t.style.color = "#fff";
  • t.style.textAlign = "center";
  • t.style.borderRadius = "5px";
  • t.style.marginLeft = "300px";
  • t.style.zIndex = 10000;
  • t.innerHTML = "复制成功";
  • document.body.prepend(t);
  • setTimeout(function () {
  • document.body.removeChild(t);
  • }, 1000);
  • }
  • function copyIt($curElement) {//复制内容,并进行处理得到结果,写到剪切板
  • if ($curElement) {
  • let html = $curElement.innerHTML;
  • html = html.replace(/(<img.+\s?src=")(\/\/.+?")/gi, "$1" + document.location.protocol + "$2");
  • html = html.replace(/(<img.+\s?src=")(\/.+?")/gi, "$1" + document.location.origin + "$2");
  • html = html.replace(/(<img.+\s?src=")(?!http)(.+?")/gi, "$1" + document.location.origin +
  • (document.location.pathname.substring(0, document.location.pathname.lastIndexOf('/'))) + "/$2");
  • html = html.replace(/(<a.+?href=")(.*?)(".*?<\/a>)/gi, parseHref); //补全url链接,加上域名之类的
  • let markdown = turndownService.turndown(html);
  • markdown = markdown.replace(/<img.+?>/g, "");
  • copyToClipboard(markdown);
  • }
  • }
  • function parseHref(match, head, link, tail){ //对copyIt()中replace正则match到的匹配项head,link,tail,处理后返回,用于替换
  • let linkFormat = '';
  • var path = document.location.pathname.split('/');path.pop(); //domain
  • if (link.substr(0, 4) === 'http') {
  • //linkFormat = head + link.replace(/#.*/,"") + tail; //原本的这个替换不知道,为什么非要删除url#后的部分,这里改成不删
  • linkFormat = head + link + tail;
  • } else if (link[0] === '#' || link.substr(0, 10) === 'javascript' || link === '"') { // "#" "javascript:" ""
  • linkFormat = head + '#"' + tail;
  • } else if (link[0] === '.' && link[1] === '/'){ // "./xxx"
  • linkFormat = head + document.location.origin + path.join('/') + link.substring(1) + tail;
  • } else if (link[0] === '.' && link[1] === '.' && link[2] === '/') { // ../xxx
  • var p2Arr = link.split('../'),
  • tmpRes = [p2Arr.pop()];
  • path.pop();
  • while(p2Arr.length){
  • var t = p2Arr.pop();
  • if (t === ''){
  • tmpRes.unshift(path.pop());
  • }
  • }
  • linkFormat = head + document.location.origin + tmpRes.join('/') + tail;
  • } else if (link.match(/^\/\/.*/)) { // //xxx.com
  • linkFormat = head + document.location.protocol + link + tail;
  • } else if (link.match(/^\/.*/)) { // /abc
  • linkFormat = head + document.location.origin + link + tail;
  • } else { // "abc/xxx"
  • linkFormat = head + document.location.origin + path.join("/") + '/' + link + tail;
  • }
  • return linkFormat;
  • }
  • function copyToClipboard(text) {
  • const input = document.createElement('textarea');
  • input.style.position = 'fixed';
  • input.style.opacity = 0;
  • input.value = text;
  • document.body.appendChild(input);
  • input.select();
  • let res = document.execCommand('Copy');
  • document.body.removeChild(input);
  • return res;
  • }
  • function enable() {
  • document.addEventListener("mouseover", handleMouseover);
  • document.addEventListener("mouseout", handleMouseout);
  • document.addEventListener("click", handleClick);
  • document.addEventListener("keyup", handleKeyup);
  • window.addEventListener("keydown", disableScroll, false);
  • }
  • function disable() {
  • if ($curElement) {
  • hideHint($curElement);
  • $curElement = null;
  • }
  • document.removeEventListener("mouseover", handleMouseover);
  • document.removeEventListener("mouseout", handleMouseout);
  • document.removeEventListener("click", handleClick);
  • document.removeEventListener("keyup", handleKeyup);
  • window.removeEventListener("keydown", disableScroll, false);
  • $btn.style.display = "block";
  • }
  • function genDoublePressHandler(keyCode, callback, params) {
  • var intval = 500;
  • var lastKeypressTime = 0;
  • var useCtrl = params && params.useCtrl;
  • var pressCtrl = 0;
  • if (useCtrl) {
  • document.addEventListener('keydown', function(e) {
  • if (useCtrl && e.keyCode === 17) {
  • pressCtrl = 1;
  • }
  • }, false);
  • document.addEventListener('keyup', function(e) {
  • if (useCtrl && e.keyCode === 17) {
  • pressCtrl = 0;
  • }
  • }, false);
  • }
  • return function(e) {
  • var now = +new Date;
  • if (e.keyCode === keyCode) {
  • if ((now - lastKeypressTime <= intval) && (!useCtrl || useCtrl && pressCtrl)) {
  • callback && callback(e);
  • lastKeypressTime = 0;
  • }
  • }
  • lastKeypressTime = now;
  • };
  • }
  • function initBtn() {
  • let topDiff = 0,
  • leftDiff = 0;
  • $btn.style.position = "fixed";
  • $btn.style.width = "44px";
  • $btn.style.height = "22px";
  • $btn.style.lineHeight = "22px";
  • $btn.style.top = "14%";
  • $btn.style.right = "1%";
  • $btn.style.background = "#0084ff";
  • $btn.style.fontSize = "14px";
  • $btn.style.color = "#fff";
  • $btn.style.textAlign = "center";
  • $btn.style.borderRadius = "6px";
  • $btn.style.zIndex = 10000;
  • $btn.style.cursor = "pointer";
  • $btn.style.opacity = 0.1;
  • $btn.innerHTML = "copy";
  • $btn.addEventListener("click", function () {
  • if (isDrag) {
  • return false;
  • }
  • enable();
  • this.style.display = "none";
  • });
  • $btn.addEventListener("mouseover", function (e) {
  • this.style.opacity = 1;
  • });
  • $btn.addEventListener("mouseout", function () {
  • this.style.opacity = 0.1;
  • });
  • $btn.addEventListener("mousedown", function (e) {
  • isHold = 1;
  • leftDiff = e.pageX - this.offsetLeft;
  • topDiff = e.pageY - this.offsetTop;
  • $btn.onmousemove = function (e) {
  • if (isHold) {
  • isDrag = 1;
  • }
  • if (isDrag) {
  • this.style.top = (e.pageY - topDiff) + "px";
  • this.style.left = (e.pageX - leftDiff) + "px";
  • this.style.right = "auto";
  • }
  • };
  • });
  • document.addEventListener("mouseup", function () {
  • setTimeout(function () {
  • isHold = 0;
  • isDrag = 0;
  • $btn.onmousemove = null;
  • }, 0);
  • });
  • // Ctrl+c+c
  • document.addEventListener('keyup', genDoublePressHandler(67, function (e) {
  • $btn.click();
  • },{"useCtrl":1}), false);
  • }
  • function init() {
  • if (!document.body) {
  • console.warn("no body");
  • return;
  • }
  • addStyle("." + CLASS_HINT + "{background-color: #fafafa; outline: 2px dashed #1976d2; opacity: .8; cursor: pointer; -webkit-transition: opacity .5s ease; transition: opacity .5s ease;}");
  • document.body.prepend($btn);
  • initBtn();
  • }
  • init();
  • })();
  • // @namespace http://tampermonkey.net/
  • // @version 0.1
  • // @description try to take over the world!
  • // @author You
  • // @match https://www.cnblogs.com/zhangxinqi/p/8418545.html
  • // @grant none
  • // ==/UserScript==
  • (function() {
  • 'use strict';
  • // Your code here...
  • })();
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门