Greasy Fork is available in English.

Easy Web Page to Markdown

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

})();