zhihu to telegraph 知乎回答和文章发送到Telegraph

一键将知乎文章或回答发送到Telegraph,点击回答或者文章下面的三个点即可看到"发送到telegraph"按钮。

// ==UserScript==
// @name         zhihu to telegraph 知乎回答和文章发送到Telegraph
// @namespace    http://tampermonkey.net
// @version      0.9.11
// @description  一键将知乎文章或回答发送到Telegraph,点击回答或者文章下面的三个点即可看到"发送到telegraph"按钮。
// @author       huaji (Modified by AI)
// @match        *://www.zhihu.com/*
// @match        *://zhuanlan.zhihu.com/*
// @license      MIT
// @icon         https://static.zhihu.com/heifetz/favicon.ico
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      api.telegra.ph
// ==/UserScript==

(() => {
  "use strict";

    // --- Start of Telegraph helper functions (无变动) ---

    function showShareOverlay(url) {
        var overlay = document.createElement("div");
        var $=q=>overlay.querySelector(q);
        overlay.style.cssText = `
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background-color: rgba(0, 0, 0, 0.5); display: flex;
            justify-content: center; align-items: center; z-index: 10000;
        `;
        overlay.innerHTML = `
            <div style="background-color: white; padding: 20px; border-radius: 5px; text-align: center; max-width: 90%;">
                <p>分享链接:</p>
                <p><a href="${url}" target="_blank" style="word-break: break-all;">${url}</a></p>
                <button class="copy-button" style="margin-right:10px;">复制链接</button>
                <button class="close-button">关闭</button>
                <button class="delete-button" style="margin-left:10px;color:maroon;">删除页面</button>
            </div>
        `;
        document.body.appendChild(overlay);
        $(".copy-button").onclick=()=>navigator.clipboard.writeText(url).then(()=>alert("链接已复制!"));
        $(".close-button").onclick=()=>overlay.remove();
        $(".delete-button").onclick= async () => {
            if (!confirm("确定要删除这个 Telegraph 页面吗? 这个操作无法撤销。")) return;
            try {
                await deleteTelegraph(url);
                alert("页面已删除!\n你可以关闭此窗口。");
                overlay.remove();
            } catch(e) {
                alert("删除失败: " + e.message);
            }
        };
    }

    async function getTgphToken(){
        let token = GM_getValue('tgphToken');
        if(token){ return token; }
        const r = await GM.xmlHttpRequest({method:'GET', url:'https://api.telegra.ph/createAccount?short_name=ZhihuSharer&author_name=ZhihuToTelegraph', responseType:'json'});
        if(r.status>299||r.status<200)throw new Error(`Telegraph API error: ${r.statusText}`);
        const data=r.response;
        if(!data.ok)throw new Error(`Telegraph API error: ${data.error}`);
        token = data.result.access_token;
        GM_setValue('tgphToken', token);
        return token;
    }

    async function uploadToTelegraph(title, content, authorName, authorUrl){
        var telegraphAccessToken=await getTgphToken();
        const payload = {
            access_token: telegraphAccessToken,
            title: title,
            content: content,
            return_content: true,
        };
        if (authorName) payload.author_name = authorName;
        if (authorUrl) payload.author_url = authorUrl;

        const r = await GM.xmlHttpRequest({
            method:'POST',
            url:'https://api.telegra.ph/createPage',
            headers:{'Content-Type':'application/json'},
            data:JSON.stringify(payload),
            responseType:'json'
        });
        if(r.status>299||r.status<200) throw new Error(`Telegraph API error: ${r.statusText}`);
        const data=r.response;
        if(!data.ok) throw new Error(`Telegraph API error: ${data.error}`);
        return `https://telegra.ph/${data.result.path}`;
    }

    async function deleteTelegraph(postUrl){
        var telegraphAccessToken=GM_getValue('tgphToken');
        if(!telegraphAccessToken)throw new Error("telegraphAccessToken NOT FOUND, cannot delete the post!");
        const path = postUrl.split('/').pop().split('?')[0];
        const r = await GM.xmlHttpRequest({
            method: 'POST',
            url: `https://api.telegra.ph/editPage/${path}`,
            headers: {'Content-Type': 'application/json'},
            data: JSON.stringify({
                access_token: telegraphAccessToken,
                title: "Removed by user",
                content: [{"tag":"p","children":["This page has been removed by the author."]}]
            }),
            responseType: 'json'
        });
        if(r.status > 299 || r.status < 200) throw new Error(`Telegraph API error: ${r.statusText}`);
        const j = r.response;
        if(!j.ok) throw new Error(`Telegraph API error: ${j.error}`);
        return j;
    }

    // --- End of Telegraph helper functions ---

  const n = (e) => {
      var t = new URL(e);
      return "link.zhihu.com" == t.hostname ?
        ((t = new URLSearchParams(t.search).get("target")),
          decodeURIComponent(t)) :
        e;
    },
    i = (e, t) =>
    null != e && (e.classList.contains(t) ? e : i(e.parentElement, t));

  var e,
    y = e = e || {};
  (y[(y.H1 = 0)] = "H1"), (y[(y.H2 = 1)] = "H2"), (y[(y.Text = 2)] = "Text"), (y[(y.Figure = 3)] = "Figure"),
  (y[(y.Gif = 4)] = "Gif"), (y[(y.InlineLink = 5)] = "InlineLink"), (y[(y.InlineCode = 6)] = "InlineCode"),
  (y[(y.Math = 7)] = "Math"), (y[(y.Italic = 8)] = "Italic"), (y[(y.Bold = 9)] = "Bold"),
  (y[(y.PlainText = 10)] = "PlainText"), (y[(y.UList = 11)] = "UList"), (y[(y.Olist = 12)] = "Olist"),
  (y[(y.BR = 13)] = "BR"), (y[(y.HR = 14)] = "HR"), (y[(y.Blockquote = 15)] = "Blockquote"),
  (y[(y.Code = 16)] = "Code"), (y[(y.Link = 17)] = "Link"), (y[(y.Table = 18)] = "Table"), (y[(y.Video = 19)] = "Video");

    // --- START: Zhihu DOM Parser functions (无变动) ---
    function getRealImageUrl(imgElement) {
        if (!imgElement) return null;
        let src = imgElement.getAttribute('data-actualsrc');
        if (src) return src;
        src = imgElement.getAttribute('data-original');
        if (src) return src;
        src = imgElement.getAttribute('src');
        if (src && !src.startsWith('data:image/svg+xml')) {
            return src;
        }
        // Fallback for some weird cases where only noscript has the real url
        const parentFigure = imgElement.closest('figure');
        if (parentFigure) {
            const noscriptTag = parentFigure.querySelector('noscript');
            if (noscriptTag) {
                const tempDiv = document.createElement('div');
                tempDiv.innerHTML = noscriptTag.innerHTML;
                const noscriptImg = tempDiv.querySelector('img');
                if (noscriptImg) {
                    return noscriptImg.getAttribute('data-original') || noscriptImg.getAttribute('src');
                }
            }
        }
        return null;
    }

    const u = (t) => {
      var r = [];
      for (let u = 0; u < t.length; u++) {
        var i = t[u];
        if (i.nodeType !== Node.ELEMENT_NODE) continue;
        switch (i.tagName.toLowerCase()) {
          case "h2": r.push({ type: e.H1, text: i.textContent, dom: i }); break;
          case "h3": r.push({ type: e.H2, text: i.textContent, dom: i }); break;
          case "div":
            if (i.classList.contains("highlight")) {
              r.push({ type: e.Code, content: i.textContent, language: i.querySelector("pre > code")?.classList.value.slice(9) || '', dom: i });
            } else if (i.classList.contains("RichText-LinkCardContainer")) {
              const s = i.firstChild;
              r.push({ type: e.Link, text: s.getAttribute("data-text"), href: n(s.href), dom: i });
            } else if (i.querySelector("video")) {
              r.push({ type: e.Video, src: i.querySelector("video").getAttribute("src"), local: !1, dom: i });
            }
            break;
          case "blockquote": r.push({ type: e.Blockquote, content: h(i), dom: i }); break;
          case "figure":
            var s = i.querySelector("img");
            if (!s) break;
            var a = getRealImageUrl(s);
            if (a) {
                if (s.classList.contains("ztext-gif") || a.endsWith('.gif')) {
                    r.push({ type: e.Gif, src: a.replace('_r.jpg', '_1440w.jpg'), local: !1, dom: i });
                } else {
                    r.push({ type: e.Figure, src: a.replace('_r.jpg', '_1440w.jpg'), local: !1, dom: i });
                }
            }
            break;
          case "ul": var o = Array.from(i.querySelectorAll("li")); r.push({ type: e.UList, content: o.map((e) => h(e)), dom: i }); break;
          case "ol": o = Array.from(i.querySelectorAll("li")); r.push({ type: e.Olist, content: o.map((e) => h(e)), dom: i }); break;
          case "p": r.push({ type: e.Text, content: h(i), dom: i }); break;
          case "hr": r.push({ type: e.HR, dom: i }); break;
          case "table":
            var tableData = ((e) => {
              var t, r = [];
              for (t of Array.from(e.rows)) {
                var n = Array.from(t.cells);
                r.push(n.map((e) => e.innerText.trim()));
              }
              return r;
            })(i);
            r.push({ type: e.Table, content: tableData, dom: i });
        }
      }
      return r;
    };
    const h = (t) => {
      if ("string" == typeof t) return [{ type: e.PlainText, text: t }];
      let r = Array.from(t.childNodes);
      var i, a = [];
      try {
        1 == r.length && "p" == r[0].tagName.toLowerCase() && (r = Array.from(r[0].childNodes));
      } catch {}
      for (i of r) {
        if (i.nodeType == i.TEXT_NODE) {
            a.push({ type: e.PlainText, text: i.textContent, dom: i });
        } else if (i.nodeType == Node.ELEMENT_NODE) {
          var s = i;
          switch (s.tagName.toLowerCase()) {
            case "b": case "strong": a.push({ type: e.Bold, content: h(s), dom: s }); break;
            case "i": case "em": a.push({ type: e.Italic, content: h(s), dom: s }); break;
            case "br": a.push({ type: e.BR, dom: s }); break;
            case "code": a.push({ type: e.InlineCode, content: s.innerText, dom: s }); break;
            case "span":
              try {
                if (s.classList.contains("ztext-math")) {
                    a.push({ type: e.Math, content: s.getAttribute("data-tex"), dom: s });
                } else if (s.children[0]?.classList.contains("RichContent-EntityWord")) {
                    a.push({ type: e.PlainText, text: s.innerText, dom: s });
                } else {
                    a.push({ type: e.PlainText, text: s.innerText, dom: s });
                }
              } catch {
                a.push({ type: e.PlainText, text: s.innerText, dom: s });
              }
              break;
            case "a": a.push({ type: e.InlineLink, text: s.textContent, href: n(s.href), dom: s }); break;
          }
        }
      }
      return a;
    };
    const _ = (t) => {
        const r = { type: e.Text, content: [] };
        t.innerText.split("\n").forEach((t, i, arr) => {
            r.content.push({ type: e.PlainText, text: t });
            if (i < arr.length - 1) r.content.push({ type: e.BR });
        });
        return [r];
    };
    // --- END: Zhihu DOM Parser functions ---


    // --- START: IR to Telegraph Node converters (无变动) ---
    function inlineIrToTelegraphNode(inline_ir_array) {
        const tgNodes = [];
        for (const node of inline_ir_array) {
            switch (node.type) {
                case e.PlainText: if (node.text) { tgNodes.push(node.text); } break;
                case e.Bold: tgNodes.push({ tag: 'strong', children: inlineIrToTelegraphNode(node.content) }); break;
                case e.Italic: tgNodes.push({ tag: 'em', children: inlineIrToTelegraphNode(node.content) }); break;
                case e.InlineLink: tgNodes.push({ tag: 'a', attrs: { href: node.href }, children: [node.text] }); break;
                case e.InlineCode: tgNodes.push({ tag: 'code', children: [node.content] }); break;
                case e.BR: tgNodes.push({ tag: 'br' }); break;
                case e.Math: tgNodes.push({ tag: 'code', children: [`$${node.content}$`] }); break;
            }
        }
        return tgNodes.filter(n => n);
    }

    function irToTelegraphNode(ir_array) {
        const tgNodes = [];
        for (const node of ir_array) {
            switch (node.type) {
                case e.H1: tgNodes.push({ tag: 'h3', children: [node.text] }); break;
                case e.H2: tgNodes.push({ tag: 'h4', children: [node.text] }); break;
                case e.Text:
                    if (node.content.length === 1 && node.content[0].type === e.Math) {
                        tgNodes.push({ tag: 'pre', children: [`$$\n${node.content[0].content}\n$$`] });
                    } else {
                        const pChildren = inlineIrToTelegraphNode(node.content);
                        if (pChildren.length > 0) tgNodes.push({ tag: 'p', children: pChildren });
                    }
                    break;
                case e.Figure:
                case e.Gif:
                    tgNodes.push({ tag: 'figure', children: [{ tag: 'img', attrs: { src: node.src } }] });
                    break;
                case e.UList:
                    tgNodes.push({ tag: 'ul', children: node.content.map(li => ({ tag: 'li', children: inlineIrToTelegraphNode(li) }))});
                    break;
                case e.Olist:
                    tgNodes.push({ tag: 'ol', children: node.content.map(li => ({ tag: 'li', children: inlineIrToTelegraphNode(li) }))});
                    break;
                case e.Blockquote:
                    const bqChildren = inlineIrToTelegraphNode(node.content);
                    if (bqChildren.length > 0) tgNodes.push({ tag: 'blockquote', children: [{ tag: 'p', children: bqChildren }] });
                    break;
                case e.Code:
                    tgNodes.push({ tag: 'pre', children: [node.content] });
                    break;
                case e.HR: tgNodes.push({ tag: 'hr' }); break;
                case e.Link: tgNodes.push({ tag: 'p', children: [{ tag: 'a', attrs: { href: node.href }, children: [node.text] }] }); break;
                case e.Video: tgNodes.push({ tag: 'video', attrs: { src: node.src, controls: true } }); break;
                case e.Table:
                    const tableContent = node.content;
                    if (tableContent.length === 0 || tableContent[0].length === 0) break;
                    const colWidths = new Array(tableContent[0].length).fill(0);
                    tableContent.forEach(row => row.forEach((cell, i) => colWidths[i] = Math.max(colWidths[i], cell.length)));
                    let preformattedText = tableContent[0].map((cell, i) => cell.padEnd(colWidths[i])).join(' | ') + '\n';
                    preformattedText += colWidths.map(w => '-'.repeat(w)).join('-|-') + '\n';
                    preformattedText += tableContent.slice(1).map(row => row.map((cell, i) => cell.padEnd(colWidths[i])).join(' | ')).join('\n');
                    tgNodes.push({ tag: 'pre', children: [preformattedText] });
                    break;
            }
        }
        return tgNodes;
    }
    // --- END: IR to Telegraph Node converters ---

    // --- START: Main execution logic (【已修复】) ---
    var k = async () => {
        const contentContainers = document.querySelectorAll(".ContentItem, .Post-content");

        for (const container of contentContainers) {
            try {
                const richText = container.querySelector(".RichText");
                let moreButton = container.querySelector('.ContentItem-actions .OptionsButton');
                if (!moreButton) {
                    moreButton = container.querySelector('.ContentItem-actions .Post-ActionMenuButton button');
                }

                if (!richText || !moreButton || moreButton.dataset.telegraphListenerAdded) {
                    continue;
                }
                moreButton.dataset.telegraphListenerAdded = 'true';

                moreButton.addEventListener('click', () => {
                    setTimeout(() => {
                        const menu = document.querySelector('.Popover-content .Menu');

                        if (menu && !menu.querySelector('.telegraph-menu-item')) {
                            const templateItem = menu.querySelector('button.Menu-item');

                            if (templateItem) {
                                const menuItem = templateItem.cloneNode(true);
                                menuItem.classList.add('telegraph-menu-item');

                                const textSpan = menuItem.querySelector('span');
                                if (textSpan) {
                                    textSpan.textContent = '发送到Telegraph';
                                } else {
                                    menuItem.textContent = '发送到Telegraph';
                                }

                                menuItem.addEventListener('click', async (event) => {
                                    event.stopPropagation();

                                    const popover = menu.closest('.Popover-content');
                                    if (popover) popover.style.display = 'none';

                                    const tempLoadingItem = menuItem.cloneNode(true);
                                    const tempTextSpan = tempLoadingItem.querySelector('span') || tempLoadingItem;
                                    tempTextSpan.textContent = "发送中...";
                                    tempLoadingItem.disabled = true;

                                    const originalDivider = menu.querySelector('.Menu-divider');
                                    if(originalDivider) {
                                        menu.insertBefore(tempLoadingItem, originalDivider);
                                    } else {
                                        menu.appendChild(tempLoadingItem);
                                    }
                                    menuItem.style.display = 'none';

                                    try {
                                        let f;
                                        if (i(richText, "PinItem")) {
                                            const pinLex = _(richText);
                                            Array.from(i(richText, "PinItem").querySelectorAll(".Image-PreviewVague > img")).forEach(img => {
                                                const imgSrc = getRealImageUrl(img);
                                                if (imgSrc) pinLex.push({ type: e.Figure, src: imgSrc });
                                            });
                                            f = { lex: pinLex };
                                        } else {
                                            f = { lex: u(richText.childNodes) };
                                        }

                                        const title = document.querySelector('.Post-Title')?.textContent.trim() || document.querySelector('.QuestionHeader-title')?.textContent.trim() || 'Zhihu Content';
                                        let authorName, authorUrl;

                                        if (container.classList.contains('Post-content')) {
                                             const authorElement = container.querySelector('.AuthorInfo-name .UserLink-link');
                                             authorName = authorElement?.textContent.trim() || '知乎专栏作者';
                                             authorUrl = window.location.href.split('?')[0];
                                        } else {
                                            const authorElement = container.querySelector('.AuthorInfo-name .UserLink-link');
                                            authorName = authorElement?.textContent.trim() || '知乎用户';
                                            const timeLink = container.querySelector('.ContentItem-time a');
                                            authorUrl = window.location.href.split('?')[0];
                                            if (timeLink?.href) {
                                                authorUrl = new URL(timeLink.href, window.location.origin).href;
                                            }
                                        }

                                        const telegraphNodes = irToTelegraphNode(f.lex);
                                        const telegraphUrl = await uploadToTelegraph(title, telegraphNodes, authorName, authorUrl);

                                        showShareOverlay(telegraphUrl);
                                        tempTextSpan.textContent = "发送成功✅";

                                    } catch (error) {
                                        console.error("Error sending to Telegraph:", error);
                                        tempTextSpan.textContent = "发送失败❌";
                                        alert("发送失败: " + error.message);
                                    } finally {
                                        setTimeout(() => {
                                            tempLoadingItem.remove();
                                            menuItem.style.display = '';
                                         }, 2000);
                                    }
                                });

                                const divider = menu.querySelector('.Menu-divider');
                                if (divider) {
                                    menu.insertBefore(menuItem, divider);
                                } else {
                                    menu.appendChild(menuItem);
                                }
                            }
                        }
                    }, 100);
                });
            } catch (err) {
                 console.error("Error processing content container:", err, container);
            }
        }
    };

    const observer = new MutationObserver(() => k());
    observer.observe(document.body, { childList: true, subtree: true });
    k();
    // --- END: Main execution logic ---
})();