Convert selected HTML to Markdown
// ==UserScript== // @name Easy Web Page to Markdown // @name:zh 网页转Markdown工具 // @namespace http://tampermonkey.net/ // @version 0.3.6 // @description Convert selected HTML to Markdown // @description:zh 将选定的HTML转换为Markdown // @author shiquda // @match *://*/* // @namespace https://github.com/shiquda/shiquda_UserScript // @supportURL https://github.com/shiquda/shiquda_UserScript/issues // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_setValue // @grant GM_getValue // @require https://code.jquery.com/jquery-3.6.0.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js // @require https://unpkg.com/turndown/dist/turndown.js // @require https://unpkg.com/@guyplusplus/turndown-plugin-gfm/dist/turndown-plugin-gfm.js // @require https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.0/marked.min.js // @license AGPL-3.0 // ==/UserScript== (function () { 'use strict'; // User Config // Short cut const shortCutUserConfig = { /* Example: "Shift": false, "Ctrl": true, "Alt": false, "Key": "m" */ } // Obsidian const obsidianUserConfig = { /* Example: "my note": [ "Inbox/Web/", "Collection/Web/Reading/" ] */ } const guide = ` - 使用**方向键**选择元素 - 上:选择父元素 - 下:选择第一个子元素 - 左:选择上一个兄弟元素 - 右:选择下一个兄弟元素 - 使用**滚轮**放大缩小 - 上:选择父元素 - 下:选择第一个子元素 - 点击元素选择 - 按下 \`Esc\` 键取消选择 ` // 全局变量 var isSelecting = false; var selectedElement = null; let shortCutConfig, obsidianConfig; // 读取配置 // 初始化快捷键配置 let storedShortCutConfig = GM_getValue('shortCutConfig'); if (Object.keys(shortCutUserConfig).length !== 0) { GM_setValue('shortCutConfig', JSON.stringify(shortCutUserConfig)); shortCutConfig = shortCutUserConfig; } else if (storedShortCutConfig) { shortCutConfig = JSON.parse(storedShortCutConfig); } // 初始化Obsidian配置 let storedObsidianConfig = GM_getValue('obsidianConfig'); if (Object.keys(obsidianUserConfig).length !== 0) { GM_setValue('obsidianConfig', JSON.stringify(obsidianUserConfig)); obsidianConfig = obsidianUserConfig; } else if (storedObsidianConfig) { obsidianConfig = JSON.parse(storedObsidianConfig); } // HTML2Markdown function convertToMarkdown(element) { var html = element.outerHTML; let turndownMd = turndownService.turndown(html); turndownMd = turndownMd.replaceAll('[\n\n]', '[]'); // 防止 <a> 元素嵌套的暂时方法,并不完善 return turndownMd; } // 预览 function showMarkdownModal(markdown) { var $modal = $(` <div class="h2m-modal-overlay"> <div class="h2m-modal"> <textarea>${markdown}</textarea> <div class="h2m-preview">${marked.parse(markdown)}</div> <div class="h2m-buttons"> <button class="h2m-copy">Copy to clipboard</button> <button class="h2m-download">Download as MD</button> <select class="h2m-obsidian-select">Send to Obsidian</select> </div> <button class="h2m-close">X</button> </div> </div> `); $modal.find('.h2m-obsidian-select').append($('<option>').val('').text('Send to Obsidian')); for (const vault in obsidianConfig) { for (const path of obsidianConfig[vault]) { // 插入元素 const $option = $('<option>') .val(`obsidian://advanced-uri?vault=${vault}&filepath=${path}`) .text(`${vault}: ${path}`); $modal.find('.h2m-obsidian-select').append($option); } } $modal.find('textarea').on('input', function () { // console.log("Input event triggered"); var markdown = $(this).val(); var html = marked.parse(markdown); // console.log("Markdown:", markdown); // console.log("HTML:", html); $modal.find('.h2m-preview').html(html); }); $modal.on('keydown', function (e) { if (e.key === 'Escape') { $modal.remove(); } }); $modal.find('.h2m-copy').on('click', function () { // 复制到剪贴板 GM_setClipboard($modal.find('textarea').val()); $modal.find('.h2m-copy').text('Copied!'); setTimeout(() => { $modal.find('.h2m-copy').text('Copy to clipboard'); }, 1000); }); $modal.find('.h2m-download').on('click', function () { // 下载 var markdown = $modal.find('textarea').val(); var blob = new Blob([markdown], { type: 'text/markdown' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; // 当前页面标题 + 时间 a.download = `${document.title}-${new Date().toISOString().replace(/:/g, '-')}.md`; a.click(); }); $modal.find('.h2m-obsidian-select').on('change', function () { // 发送到 Obsidian const val = $(this).val(); if (!val) return; const markdown = $modal.find('textarea').val(); GM_setClipboard(markdown); const title = document.title.replaceAll(/[\\/:*?"<>|]/g, '_'); // File name cannot contain any of the following characters: * " \ / < > : | ? const url = `${val}${title}.md&clipboard=true`; window.open(url); }); $modal.find('.h2m-close').on('click', function () { // 关闭按钮 X $modal.remove(); }); // 同步滚动 // 获取两个元素 var $textarea = $modal.find('textarea'); var $preview = $modal.find('.h2m-preview'); var isScrolling = false; // 当 textarea 滚动时,设置 preview 的滚动位置 $textarea.on('scroll', function () { if (isScrolling) { isScrolling = false; return; } var scrollPercentage = this.scrollTop / (this.scrollHeight - this.offsetHeight); $preview[0].scrollTop = scrollPercentage * ($preview[0].scrollHeight - $preview[0].offsetHeight); isScrolling = true; }); // 当 preview 滚动时,设置 textarea 的滚动位置 $preview.on('scroll', function () { if (isScrolling) { isScrolling = false; return; } var scrollPercentage = this.scrollTop / (this.scrollHeight - this.offsetHeight); $textarea[0].scrollTop = scrollPercentage * ($textarea[0].scrollHeight - $textarea[0].offsetHeight); isScrolling = true; }); $(document).on('keydown', function (e) { if (e.key === 'Escape' && $('.h2m-modal-overlay').length > 0) { $('.h2m-modal-overlay').remove(); } }); $('body').append($modal); } // 开始选择 function startSelecting() { $('body').addClass('h2m-no-scroll'); // 防止页面滚动 isSelecting = true; // 操作指南 tip(marked.parse(guide)); } // 结束选择 function endSelecting() { isSelecting = false; $('.h2m-selection-box').removeClass('h2m-selection-box'); $('body').removeClass('h2m-no-scroll'); $('.h2m-tip').remove(); } function tip(message, timeout = null) { var $tipElement = $('<div>') .addClass('h2m-tip') .html(message) .appendTo('body') .hide() .fadeIn(200); if (timeout === null) { return; } setTimeout(function () { $tipElement.fadeOut(200, function () { $tipElement.remove(); }); }, timeout); } // Turndown 配置 var turndownPluginGfm = TurndownPluginGfmService; var turndownService = new TurndownService({ codeBlockStyle: 'fenced' }); turndownPluginGfm.gfm(turndownService); // 引入全部插件 // turndownService.addRule('strikethrough', { // filter: ['del', 's', 'strike'], // replacement: function (content) { // return '~' + content + '~' // } // }); // turndownService.addRule('latex', { // filter: ['mjx-container'], // replacement: function (content, node) { // const text = node.querySelector('img')?.title; // const isInline = !node.getAttribute('display'); // if (text) { // if (isInline) { // return '$' + text + '$' // } // else { // return '$$' + text + '$$' // } // } // return ''; // } // }); // 添加CSS样式 GM_addStyle(` .h2m-selection-box { border: 2px dashed #f00; background-color: rgba(255, 0, 0, 0.2); } .h2m-no-scroll { overflow: hidden; z-index: 9997; } .h2m-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80%; height: 80%; background: white; border-radius: 10px; display: flex; flex-direction: row; z-index: 9999; } .h2m-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 9998; } .h2m-modal textarea, .h2m-modal .h2m-preview { width: 50%; height: 100%; padding: 20px; box-sizing: border-box; overflow-y: auto; } .h2m-modal .h2m-buttons { position: absolute; bottom: 10px; right: 10px; } .h2m-modal .h2m-buttons button, .h2m-modal .h2m-obsidian-select { margin-left: 10px; background-color: #4CAF50; /* Green */ border: none; color: white; padding: 13px 16px; border-radius: 10px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; transition-duration: 0.4s; cursor: pointer; } .h2m-modal .h2m-buttons button:hover, .h2m-modal .h2m-obsidian-select:hover { background-color: #45a049; } .h2m-modal .h2m-close { position: absolute; top: 10px; right: 10px; cursor: pointer; width: 25px; height: 25px; background-color: #f44336; color: white; font-size: 16px; border-radius: 50%; display: flex; justify-content: center; align-items: center; } .h2m-tip { position: fixed; top: 22%; left: 82%; transform: translate(-50%, -50%); background-color: white; border: 1px solid black; padding: 8px; z-index: 9999; border-radius: 10px; box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5); background-color: rgba(255, 255, 255, 0.7); } `); // 注册触发器 shortCutConfig = shortCutConfig ? shortCutConfig : { "Shift": false, "Ctrl": true, "Alt": false, "Key": "m" }; $(document).on('keydown', function (e) { if (e.ctrlKey === shortCutConfig['Ctrl'] && e.altKey === shortCutConfig['Alt'] && e.shiftKey === shortCutConfig['Shift'] && e.key.toUpperCase() === shortCutConfig['Key'].toUpperCase()) { e.preventDefault(); startSelecting(); } // else { // console.log(e.ctrlKey, e.altKey, e.shiftKey, e.key.toUpperCase()); // } }); // $(document).on('keydown', function (e) { // if (e.ctrlKey && e.key === 'm') { // e.preventDefault(); // startSelecting() // } // }); GM_registerMenuCommand('Convert to Markdown', function () { startSelecting() }); $(document).on('mouseover', function (e) { // 开始选择 if (isSelecting) { $(selectedElement).removeClass('h2m-selection-box'); selectedElement = e.target; $(selectedElement).addClass('h2m-selection-box'); } }).on('wheel', function (e) { // 滚轮事件 if (isSelecting) { e.preventDefault(); if (e.originalEvent.deltaY < 0) { selectedElement = selectedElement.parentElement ? selectedElement.parentElement : selectedElement; // 扩大 if (selectedElement.tagName === 'HTML' || selectedElement.tagName === 'BODY') { selectedElement = selectedElement.firstElementChild; } } else { selectedElement = selectedElement.firstElementChild ? selectedElement.firstElementChild : selectedElement; // 缩小 } $('.h2m-selection-box').removeClass('h2m-selection-box'); $(selectedElement).addClass('h2m-selection-box'); } }).on('keydown', function (e) { // 键盘事件 if (isSelecting) { e.preventDefault(); if (e.key === 'Escape') { endSelecting(); return; } switch (e.key) { // 方向键:上下左右 case 'ArrowUp': selectedElement = selectedElement.parentElement ? selectedElement.parentElement : selectedElement; // 扩大 if (selectedElement.tagName === 'HTML' || selectedElement.tagName === 'BODY') { // 排除HTML 和 BODY selectedElement = selectedElement.firstElementChild; } break; case 'ArrowDown': selectedElement = selectedElement.firstElementChild ? selectedElement.firstElementChild : selectedElement; // 缩小 break; case 'ArrowLeft': // 寻找上一个元素,若是最后一个子元素则选择父元素的下一个兄弟元素,直到找到一个元素 var prev = selectedElement.previousElementSibling; while (prev === null && selectedElement.parentElement !== null) { selectedElement = selectedElement.parentElement; prev = selectedElement.previousElementSibling ? selectedElement.previousElementSibling.lastChild : null; } if (prev !== null) { if (selectedElement.tagName === 'HTML' || selectedElement.tagName === 'BODY') { selectedElement = selectedElement.firstElementChild; } selectedElement = prev; } break; case 'ArrowRight': var next = selectedElement.nextElementSibling; while (next === null && selectedElement.parentElement !== null) { selectedElement = selectedElement.parentElement; next = selectedElement.nextElementSibling ? selectedElement.nextElementSibling.firstElementChild : null; } if (next !== null) { if (selectedElement.tagName === 'HTML' || selectedElement.tagName === 'BODY') { selectedElement = selectedElement.firstElementChild; } selectedElement = next; } break; } $('.h2m-selection-box').removeClass('h2m-selection-box'); $(selectedElement).addClass('h2m-selection-box'); // 更新选中元素的样式 } } ).on('mousedown', function (e) { // 鼠标事件,选择 mousedown 是因为防止点击元素后触发其他事件 if (isSelecting) { e.preventDefault(); var markdown = convertToMarkdown(selectedElement); showMarkdownModal(markdown); endSelecting(); } }); })();