obgnail / typora_plugin

Typora plugin. Feature enhancement tool | Typora 插件,功能增强工具
MIT License
1.57k stars 78 forks source link

代码块折叠、复制功能 #2

Closed jinghu-moon closed 1 year ago

jinghu-moon commented 1 year ago

如题,对于程序员来说,这两个功能挺有用的。老哥可以实现吗?

QHQIII commented 1 year ago

可以暂时尝试在控制台输入以下代码段临时实现这个功能,它只会更改显示,不会影响文本内容

document.getElementsByClassName("md-fences md-end-block md-fences-with-lineno ty-contain-cm modeLoaded").forEach((el)=>{el.outerHTML="<details open>"+el.outerHTML+"</details>"})
lion-no-back commented 1 year ago

感谢作者开源了这个项目,让我开了眼界,学习到了不少

目标:从我使用Typora以来便发现代码块没有一键复制功能,上网搜索发现2021年有给Typora开发者提过这方面issue,但之后杳无音信

问题:看到了作者的成品,我便想尝试一番,看看能不能实现,如果单纯在网页实现,问题不大,但在typora内我发现学习作者注入js代码不运行,控制台可运行

以下三张图辅助说明

image

image

image

请问有法子解决嘛

obgnail commented 1 year ago

感谢作者开源了这个项目,让我开了眼界,学习到了不少

目标:从我使用 Typora 以来便发现 代码块没有一键复制功能 ,上网搜索发现 2021 年有给 Typora 开发者提过这方面 issue,但之后杳无音信

问题:看到了作者的成品,我便想尝试一番,看看能不能实现,如果单纯在网页实现,问题不大,但在 typora 内我发现学习作者注入 js 代码不运行,控制台可运行

以下三张图辅助说明

image

image

image

请问有法子解决嘛

你掉坑里了。

Typora 是延迟加载页面的,文件内容都是通过延迟执行的 js 写入 document 的。

md-fences 在 frame.js 中是很晚生成的,并且有清空 innerHTML 操作。所以你必须等到 frame.js 执行完毕后才能执行脚本。

这里给出清空 md-fences innerHTML 的函数链条,打个断点就知道了: restoreEditStateFromData -> refresh -> refreshUnder -> refreshEditor -> addCodeBlock -> b.addClass("ty-contain-cm").html("");

我花了点时间,整了一个我的版本的 copy_code.js。代码文件已经上传,里面有更加详细的注释。你可以看看。有什么问题可以继续在这说。 代码本身很简单,主要还是花在了逆向上。

obgnail commented 1 year ago

@jinghu-moon @lion-no-back 折叠和复制功能已经做了,你们可以用用看。

不过折叠功能做到了标题上,我感觉标题折叠可能更有用一些 : )

jinghu-moon commented 1 year ago

下午闲着没事做把代码完善了下。效果如下:

修改后的 js 代码:

(() => {
  const config = {
    // 启用脚本,若为false,以下配置全部失效
    ENABLE: true,
    LOOP_DETECT_INTERVAL: 20,
    CLICK_CHECK_INTERVAL: 300,
    DEBUG: false
  }

  if (!config.ENABLE) {
    return;
  }

  const addCopyElement = (target) => {
    let a = target.querySelector(".typora-copy-code");
    if (!a) {
      a = document.createElement("a");
      a.setAttribute("class", "typora-copy-code");
      a.innerText = "Copy";
      target.appendChild(a);
    }
  };

  const decorator = (original, after) => {
    return function () {
      const result = original.apply(this, arguments);
      after.call(this, result, ...arguments);
      return result;
    };
  };

  const after = (result, ...args) => {
    const cid = args[0];
    if (cid) {
      const ele = document.querySelector(`#write .md-fences[cid=${cid}]`);
      addCopyElement(ele);
    }
  };

  const _timer = setInterval(() => {
    if (File?.editor?.fences?.addCodeBlock) {
      clearInterval(_timer);
      File.editor.fences.addCodeBlock = decorator(
        File.editor.fences.addCodeBlock,
        after
      );
    }
  }, config.LOOP_DETECT_INTERVAL);

  const badChars = [
    "%E2%80%8B", // ZERO WIDTH SPACE \u200b
    "%C2%A0", // NO-BREAK SPACE \u00A0
    "%0A" // NO-BREAK SPACE \u0A
  ];
  const replaceChars = ["", "%20", ""];

  let lastClickTime = 0;
  document.getElementById("write").addEventListener("click", (ev) => {
    const button = ev.target.closest(".typora-copy-code");
    if (!button) {
      return;
    }

    ev.preventDefault();
    ev.stopPropagation();

    if (ev.timeStamp - lastClickTime < config.CLICK_CHECK_INTERVAL) {
      return;
    }

    lastClickTime = ev.timeStamp;
    const lines = button
      .closest(".md-fences")
      .querySelectorAll(".CodeMirror-code .CodeMirror-line");
    if (!lines) {
      return;
    }
    const contentList = [];
    lines.forEach((line) => {
      let encodeText = encodeURI(line.textContent);
      for (let i = 0; i < badChars.length; i++) {
        if (encodeText.indexOf(badChars[i]) !== -1) {
          encodeText = encodeText.replace(
            new RegExp(badChars[i], "g"),
            replaceChars[i]
          );
        }
      }
      encodeText = decodeURI(encodeText);
      contentList.push(encodeText);
    });

    const result = contentList.join("\n");
    navigator.clipboard.writeText(result);

    button.classList.toggle("copied");
    setTimeout(() => {
      button.classList.toggle("copied");
    }, 2000);
  });

  if (config.DEBUG) {
    JSBridge.invoke("window.toggleDevTools");
  }
  console.log("copy_code.js has been injected");
})();

使用的 css 代码:

@font-face {
  font-family : "iconfont";
  src         : url("./icon/iconfont.woff2");
  font-display: swap;
}

#write .md-fences .typora-copy-code {
  position: absolute;
  top     : .1em;
  right   : .5em;
  z-index : 10;
  opacity : 0.6;
  cursor  : pointer;
  color   : transparent;
}

#write .md-fences .typora-copy-code::before,
#write .md-fences .typora-copy-code.copied::before {
  font-family: "iconfont";
  color      : #999;
  position   : absolute;
  font-size  : 13px;
  right      : 25px;
  top        : -1px;
}

#write .md-fences .typora-copy-code::before {
  content: "\e62b";
}

#write .md-fences .typora-copy-code.copied::before {
  content: "\e666";
  font-weight: bold;
}

css 也是在 window.html 里引入,或者直接添加到用户当前使用的主题代码里。

打算直接把 css 写入 js 里面,但是 css 代码里的转义字符,js 识别不出来,就只能分开了。老哥看看可以合在一起不。

jinghu-moon commented 1 year ago

此外,我想把这个功能加到我写的 Typora 主题,可以吗?会注明出处。

obgnail commented 1 year ago

此外,我想把这个功能加到我写的 Typora 主题,可以吗?会注明出处。

没问题的,如果能贴出 github 链接就更好了。

lion-no-back commented 1 year ago

@jinghu-moon @lion-no-back 折叠和复制功能已经做了,你们可以用用看。

不过折叠功能做到了标题上,我感觉标题折叠可能更有用一些 : )

真厉害啊,感觉标题折叠和代码块复制这两个功能可解决很多燃眉之急

我也想提供代码块复制交互效果,奈何俺太菜了,在typora上尝试了一天,哪怕对照你写的源码和注释(俺菜了,大部分看不懂),依旧无计可施,于是在热心网友@jinghu-moon提供的代码基础上做了点改进,自己用了感觉可以接受,所以先到此吧

Copy

(() => {
    const config = {
        // 启用脚本,若为false,以下配置全部失效
        ENABLE: true,
        LOOP_DETECT_INTERVAL: 20,
        CLICK_CHECK_INTERVAL: 300,

        DEBUG: false
    }

    if (!config.ENABLE) {
        return
    }

    (() => {
        const css = `
            #write .md-fences .typora-copy-code {
              position: absolute;
              top     : .1em;
              right   : .5em;
              z-index : 10;
              opacity : 0.6;
              cursor  : pointer;
              color   : transparent;
            }

            #write .md-fences .typora-copy-code::before,
            #write .md-fences .typora-copy-code.copied::before {
              font-family: "OPPOSans";
              color      : #999;
              position   : absolute;
              font-size  : 15px;
              right      : 3px;
              top        : -1px;
            }

            #write .md-fences .typora-copy-code::before {
              content: "Copy";
            }

            #write .md-fences .typora-copy-code.copied::before {
              content: "Copied";
              color: #32cd32;
              font-weight: bold;
            }
            `
        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = css;
        document.getElementsByTagName("head")[0].appendChild(style);
    })()

    const addCopyElement = (target) => {
        let a = target.querySelector(".typora-copy-code");
        if (!a) {
            a = document.createElement("a");
            a.setAttribute("class", "typora-copy-code");
            target.appendChild(a);
        }
    }

    const decorator = (original, after) => {
        return function () {
            const result = original.apply(this, arguments);
            after.call(this, result, ...arguments);
            return result;
        };
    }

    const after = (result, ...args) => {
        const cid = args[0];
        if (cid) {
            const ele = document.querySelector(`#write .md-fences[cid=${cid}]`);
            addCopyElement(ele);
        }
    }

    const _timer = setInterval(() => {
        if (File?.editor?.fences?.addCodeBlock) {
            clearInterval(_timer);
            File.editor.fences.addCodeBlock = decorator(File.editor.fences.addCodeBlock, after);
        }
    }, config.LOOP_DETECT_INTERVAL);

    const badChars = [
        "%E2%80%8B", // ZERO WIDTH SPACE \u200b
        "%C2%A0", // NO-BREAK SPACE \u00A0
        "%0A" // NO-BREAK SPACE \u0A
    ];
    const replaceChars = ["", "%20", ""];

    let lastClickTime = 0;
    document.getElementById("write").addEventListener("click", ev => {
        const button = ev.target.closest(".typora-copy-code");
        if (!button) {
            return
        }

        ev.preventDefault();
        ev.stopPropagation();

        if (ev.timeStamp - lastClickTime < config.CLICK_CHECK_INTERVAL) {
            return
        }
        lastClickTime = ev.timeStamp;

        const lines = button.closest(".md-fences").querySelectorAll(".CodeMirror-code .CodeMirror-line")
        if (!lines) {
            return
        }

        document.activeElement.blur();

        const contentList = [];
        lines.forEach(line => {
            let encodeText = encodeURI(line.textContent);
            for (let i = 0; i < badChars.length; i++) {
                if (encodeText.indexOf(badChars[i]) !== -1) {
                    encodeText = encodeText.replace(new RegExp(badChars[i], "g"), replaceChars[i]);
                }
            }
            const decodeText = decodeURI(encodeText);
            contentList.push(decodeText)
        })

        const result = contentList.join("\n");
        navigator.clipboard.writeText(result);

        button.classList.toggle("copied");
        setTimeout(() => {
            button.classList.toggle("copied");
        }, 1000);
    })

    if (config.DEBUG) {
        JSBridge.invoke("window.toggleDevTools");
    }
    console.log("copy_code.js had been injected");
})()

谢谢分享

jinghu-moon commented 1 year ago

又做了些改动。可实现代码块复制与显示代码块编程语言的功能。使用效果与前面发的图一致。前面那张图显示代码块编程语言是通过 CSS 实现的。

js 代码如下:

(() => {
  const config = {
    // 启用脚本,若为 false,以下配置全部失效
    ENABLE: true,
    LOOP_DETECT_INTERVAL: 20,
    CLICK_CHECK_INTERVAL: 300,
    DEBUG: false
  };

  // 添加常用编程语言字母和对应的全名
  const codeLang = {
    ABAP: "ABAP",
    APL: "APL",
    ASCIIARMOR: "ASCII Armor",
    ASN1: "ASN.1",
    ASP: "ASP",
    ASSEMBLY: "Assembly",
    BASH: "Bash",
    BASIC: "BASIC",
    C: "C",
    CSHARP: "C#",
    CPP: "C++",
    CASSANDRA: "Cassandra",
    CEYLON: "Ceylon",
    CLIKE: "C-like",
    CLOJURE: "Clojure",
    CMAKE: "CMake",
    COBOL: "COBOL",
    COFFEESCRIPT: "CoffeeScript",
    COMMONLISP: "Common Lisp",
    CQL: "CQL",
    CRYSTAL: "Crystal",
    CSS: "CSS",
    CYPHER: "Cypher",
    CYTHON: "Cython",
    D: "D",
    DART: "Dart",
    DIFF: "Diff",
    DJANGO: "Django",
    DOCKERFILE: "Dockerfile",
    DTD: "DTD",
    DYLAN: "Dylan",
    EJS: "EJS",
    ELIXIR: "Elixir",
    ELM: "Elm",
    EMBEDDEDJS: "EmbeddedJS",
    ERB: "ERB",
    ERLANG: "Erlang",
    FSHARP: "F#",
    FLOW: "Flow",
    FORTH: "Forth",
    FORTRAN: "Fortran",
    GAS: "Gas",
    GFM: "GFM",
    GHERKIN: "Gherkin",
    GLSL: "GLSL",
    GO: "Go",
    GROOVY: "Groovy",
    HANDLEBARS: "Handlebars",
    HASKELL: "Haskell",
    HAXE: "Haxe",
    HIVE: "Hive",
    HTACCESS: "htaccess",
    HTML: "HTML",
    HTTP: "HTTP",
    HXML: "HXML",
    IDL: "IDL",
    INI: "INI",
    JADE: "Jade",
    JAVA: "Java",
    JAVASCRIPT: "JavaScript",
    JINJA2: "Jinja2",
    JS: "JavaScript",
    JSON: "JSON",
    JSP: "JSP",
    JSX: "JSX",
    JULIA: "Julia",
    KOTLIN: "Kotlin",
    LATEX: "LaTeX",
    LESS: "Less",
    LISP: "Lisp",
    LIVESCRIPT: "LiveScript",
    LUA: "Lua",
    MAKEFILE: "Makefile",
    MARIADB: "MariaDB",
    MARKDOWN: "Markdown",
    MATHEMATICA: "Mathematica",
    MATLAB: "MATLAB",
    MBOX: "Mbox",
    MERMAID: "Mermaid",
    MSSQL: "MSSQL",
    MYSQL: "MySQL",
    NGINX: "Nginx",
    NIM: "Nim",
    NSIS: "NSIS",
    OBJC: "Objective-C",
    OCAML: "OCaml",
    OCTAVE: "Octave",
    OZ: "Oz",
    PASCAL: "Pascal",
    PEGJS: "PEG.js",
    PERL: "Perl",
    PERL6: "Perl6",
    PGP: "PGP",
    PHP: "PHP",
    PHPHTML: "PHP+HTML",
    PLSQL: "PL/SQL",
    POSTGRESQL: "PostgreSQL",
    POWERSHELL: "PowerShell",
    PROPERTIES: "Properties",
    PROTOBUF: "Protocol Buffers",
    PSEUDOCODE: "Pseudocode",
    PUG: "Pug",
    PYTHON: "Python",
    Q: "Q",
    R: "R",
    REACT: "React",
    RESTRUCTUREDTEXT: "reStructuredText",
    RST: "RST",
    RUBY: "Ruby",
    RUST: "Rust",
    SAS: "SAS",
    SCALA: "Scala",
    SCHEME: "Scheme",
    SCSS: "SCSS",
    SEQUENCE: "Sequence",
    SH: "Shell",
    SHELL: "Shell",
    SMALLTALK: "Smalltalk",
    SOLIDITY: "Solidity",
    SPARQL: "SPARQL",
    SPREADSHEET: "Spreadsheet",
    SQL: "SQL",
    SQLITE: "SQLite",
    SQUIRREL: "Squirrel",
    STATA: "Stata",
    STYLUS: "Stylus",
    SVELTE: "Svelte",
    SWIFT: "Swift",
    SYSTEMVERILOG: "SystemVerilog",
    TCL: "Tcl",
    TEX: "TeX",
    TIDDLYWIKI: "TiddlyWiki",
    TIKIWIKI: "Tiki Wiki",
    TOML: "TOML",
    TS: "TypeScript",
    TSX: "TSX",
    TURTLE: "Turtle",
    TWIG: "Twig",
    TYPESCRIPT: "TypeScript",
    V: "V",
    VB: "VB",
    VBSCRIPT: "VBScript",
    VELOCITY: "Velocity",
    VERILOG: "Verilog",
    VHDL: "VHDL",
    VISUALBASIC: "Visual Basic",
    VUE: "Vue",
    WEBIDL: "Web IDL",
    WIKI: "Wiki",
    XAML: "XAML",
    XML: "XML",
    XMLDTD: "XML DTD",
    XQUERY: "XQuery",
    YACAS: "Yacas",
    YAML: "YAML",
    YARA: "YARA",
  };

  if (!config.ENABLE) {
    return;
  }

  const addCopyElement = (target, language) => {
    const codeEnhanced = document.createElement("div");
    codeEnhanced.classList.add("code-enhanced");

    // 复制按钮
    const copyButton = document.createElement("div");
    copyButton.classList.add("typora-copy-code");
    copyButton.setAttribute("ty-hint", "复制代码"); // 添加 ty-hint 属性
    copyButton.innerHTML = "Copy";
    codeEnhanced.appendChild(copyButton);

    // 代码块编程语言
    const codeLangTag = document.createElement("div");
    codeLangTag.classList.add("code-lang-tag");
    codeLangTag.innerHTML = getCodeLangFullName(language);
    codeEnhanced.appendChild(codeLangTag);

    target.appendChild(codeEnhanced);
  };

  const getCodeLangFullName = (language) => {
    const lang = language.toUpperCase();
    return codeLang[lang] || language;
  };

  const decorator = (original, after) => {
    return function () {
      const result = original.apply(this, arguments);
      after.call(this, result, ...arguments);
      return result;
    };
  };

  const after = (result, ...args) => {
    const cid = args[0];
    if (cid) {
      const ele = document.querySelector(`pre.md-fences[cid=${cid}]`);
      const language = ele.getAttribute("lang");
      const codeEnhanced = ele.querySelector(".code-enhanced");
      if (!codeEnhanced) {
        addCopyElement(ele, language);
      }
    }
  };

  const _timer = setInterval(() => {
    if (File?.editor?.fences?.addCodeBlock) {
      clearInterval(_timer);
      File.editor.fences.addCodeBlock = decorator(
        File.editor.fences.addCodeBlock,
        after
      );
    }
  }, config.LOOP_DETECT_INTERVAL);

  const badChars = [
    "%E2%80%8B", // ZERO WIDTH SPACE \u200b
    "%C2%A0", // NO-BREAK SPACE \u00A0
    "%0A" // NO-BREAK SPACE \u0A
  ];
  const replaceChars = ["", "%20", ""];

  let lastClickTime = 0;
  document.getElementById("write").addEventListener("click", (ev) => {
    const button = ev.target.closest(".typora-copy-code");
    if (!button) {
      return;
    }

    ev.preventDefault();
    ev.stopPropagation();

    if (ev.timeStamp - lastClickTime < config.CLICK_CHECK_INTERVAL) {
      return;
    }

    lastClickTime = ev.timeStamp;
    const lines = button
      .closest("pre.md-fences")
      .querySelectorAll(".CodeMirror-code .CodeMirror-line");
    if (!lines) {
      return;
    }
    const contentList = [];
    lines.forEach((line) => {
      let encodeText = encodeURI(line.textContent);
      for (let i = 0; i < badChars.length; i++) {
        if (encodeText.indexOf(badChars[i]) !== -1) {
          encodeText = encodeText.replace(
            new RegExp(badChars[i], "g"),
            replaceChars[i]
          );
        }
      }
      encodeText = decodeURI(encodeText);
      contentList.push(encodeText);
    });

    const result = contentList.join("\n");
    navigator.clipboard.writeText(result);

    button.classList.toggle("copied");
    setTimeout(() => {
      button.classList.toggle("copied");
    }, 2000);
  });

  if (config.DEBUG) {
    JSBridge.invoke("window.toggleDevTools");
  }
  console.log("copy_code.js has been injected");
})();

直接覆盖原有的 copy_code.js。

CSS 代码如下:

/* 代码块增强 ———— 复制、编程语言 */
#write .md-fences .code-enhanced {
  position: absolute;
  top     : .1em;
  right   : .5em;
  z-index : 10 !important;
  cursor  : pointer;
  color   : transparent;
  display : flex;
}

#write .md-fences .code-enhanced .typora-copy-code {
  position: relative;
}

#write .md-fences .code-enhanced .typora-copy-code::after,
#write .md-fences .code-enhanced .typora-copy-code.copied::after {
  font-family: "iconfont";
  color      : var(--code-block-right-program-lang-text-color);
  position   : absolute;
  font-size  : 13px;
  right      : 0;
  top        : 0;
  opacity    : 0.6;
}

#write .md-fences .code-enhanced .typora-copy-code::after {
  content: "\e62b";
}

#write .md-fences .code-enhanced .typora-copy-code.copied::after {
  content    : "\e666";
  font-weight: bold;
}

.code-lang-tag {
  color      : #aaa;
  margin-left: 5px;
  font-size  : 15px;
}

CSS 代码放在当前使用主题的 CSS 文件里。

注意:CSS 代码使用了 iconfont 的图标字,。需要自行调整。直接复制无法显示复制按钮。

目前存在一个问题:在代码块语言输入框更改编程语言,右上角无法及时显示当前代码块编程语言,必须关闭窗口,再次打开,才行。