// ==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();
}
});
})();