思源笔记折腾记录 - 运行你的笔记

本贴最后更新于 607 天前,其中的信息可能已经时过境迁

原理:

其实就是把笔记导出成 js 再重写一下导入啦。

使用方法:

安装

把本文最后那个代码块的所有内容全部都复制到一个代码片段里面去然后启用它。

image

就像这样,我知道它超级长啦,但是我懒得压缩。

然后直接看一看你的笔记编辑器窗口有没有多出点啥就行了:

image

使用

启用文档代码片段

点击运行到代码片段,文档会被转化成 js 代码片段并保存到 <工作空间>/snippets/jsInNote/<文档id>.js​中并直接启用.

点击保存文档到 js 但不运行,文档会被存储到同一个位置,但是并不会直接运行.

更新文档代码片段

已经保存成代码片段的文档,菜单中的 运行到代码片段​项目会被 重新保存代码片段​代替,点击这个项目对应的代码片段会自行更新.

停用文档代码片段

已经保存成代码片段的文档,菜单中会多出一项 停用代码片段​,点击之后对应的代码片段将会被停用.

限制

生成的代码片段是通过类似 import('< 文件名 >')的动态导入形式引入的,因此全部为异步引入,同时代码内容也需要遵循 es module 的一些限制.

生成的代码片段无法以 import 的形式引入 node 内部模块(这个可能之后也不会支持),你可以将一些需要使用这些模块的代码直接保存到 snippets 文件夹中.

本地化

如果你需要在无法访问 esm.sh 的 cdn 的网络环境下使用这个代码片段,直接通过 npm install 命令或者其他形式获取 siyuan-noob 的原始代码然后将上面这个代码片段中相关路径替换正确即可.

bug 和反馈

在论坛发帖或者到这里提出 issue(siyuan-noob 是 noob-core 的前端模块,只是可以独立使用而已)

leolee9086/noob-core (github.com)

生成 js 的逻辑:

笔记会如何导出

语言为 js 的会被当成 js 导出。

语言为 css 的,如果代码块最开始一行的内容是:

/*cssInJs*/

的话,会被注入到笔记开头。

例如:分享 CSS 题头图透明 - 链滴 (ld246.com)这里这个 css 片段,可以写成

/*cssInJS*/
/*题头图透明遮罩*/ 
.protyle-background__img{
-webkit-mask: linear-gradient( #000000 ,#000000,transparent);
}

然后运行它所在的文档(如果没有别的错误的话),效果就会是这样啦

image

引用会如何解析

裸导入会被解析到 esm.sh​,这是一个基于 esm 的代码 cdn,反正用 deno​的老哥应该挺熟悉 esmsh 的。

例如:

import {defineApp} from 'vue'

会被解析成

import {defineApp} from 'https://esm.sh/vue'

所以直接使用

const 自定义菜单 = (
    await import("https://esm.sh/siyuan-noob/customMenu/index.js")
  )["default"];

或者

import 自定义菜单 from '/customMenu/index.js'

都是可以的(这玩意是 noob 的菜单模块)

从其他笔记导入内容

在代码块之外的时候使用:@js:import ​​笛卡尔1​这样的形式

@js:import {name} from '笛卡尔1'

也就是 @js:import 打头,需要导入的笔记可以使用块引用,这个东西会自动把你的块引用处理成 id 形式的。

就像这样:

image

会被处理成类似:

image

实例

思源笔记折腾记录 - 可视化块宽度调节 - 链滴 (ld246.com),你可以试试把这个文档的内容全部复制到你的笔记里面,然后就可以运行它了。

更新方式

第一次使用这个之后,以后我每次更新这个的时候,直接复制笔记就可以了,记得删掉重复的代码片段

代码片段内容:

老长老长了

(async () => {
  const 自定义菜单 = (
    await import("https://esm.sh/siyuan-noob/customMenu/index.js")
  )["default"];
  const 核心api = (
    await import("https://esm.sh/siyuan-noob/utilKernel/kernelApi.js")
  )["default"];
  const lexer = await import("https://esm.sh/es-module-lexer");
  const MagicString = (await import("https://esm.sh/magic-string"))["default"];
  await lexer.init;
  自定义菜单.编辑器菜单.registMenuItem({
    id: "运行笔记",
    文字: "运行到代码片段(js)",
    图标: "#iconCode",
    判定函数: () => {
      return !document.querySelector(
        `script[id="snippetJS${自定义菜单.编辑器菜单.菜单状态.当前块id}js"]`
      );
    },
    点击回调函数: () => {
      运行当前文档为js(true);
    },
  });
  自定义菜单.编辑器菜单.registMenuItem({
    id: "保存笔记为js",
    文字: "保存文档到js但不运行",
    图标: "#iconCode",
    判定函数: () => {
      return !document.querySelector(
        `script[id="snippetJS${自定义菜单.编辑器菜单.菜单状态.当前块id}js"]`
      );
    },
    点击回调函数: () => {
      运行当前文档为js();
    },
  });
  自定义菜单.编辑器菜单.registMenuItem({
    id: "重新运行笔记",
    文字: "重新保存代码片段(js)",
    图标: "#iconCode",
    判定函数: () => {
      return document.querySelector(
        `script[id="snippetJS${自定义菜单.编辑器菜单.菜单状态.当前块id}js"]`
      );
    },
    点击回调函数: () => {
      运行当前文档为js(true);
    },
  });
  自定义菜单.编辑器菜单.registMenuItem({
    id: "关闭笔记",
    文字: "关闭代码片段(js)",
    图标: "#iconCode",
    判定函数: () => {
      return document.querySelector(
        `script[id="snippetJS${自定义菜单.编辑器菜单.菜单状态.当前块id}js"]`
      );
    },
    点击回调函数: () => {
      关闭文档js代码片段();
    },
  });
  async function 关闭文档js代码片段() {
    let 文档id = 自定义菜单.编辑器菜单.菜单状态.当前块id;
    let 代码片段id = 文档id + "js";
    let 现有代码片段 = await 核心api.getSnippet({ type: "all", enabled: 2 });
    let 序号 = 现有代码片段.snippets.findIndex((item) => {
      return item.id === 代码片段id;
    });
    现有代码片段.snippets[序号].enabled = false;
    await 核心api.setSnippet(现有代码片段);
    window.location.reload();
  }
  async function 运行当前文档为js(直接运行) {
    let 文档id = 自定义菜单.编辑器菜单.菜单状态.当前块id;
    let 文档内容 = await 核心api.getDoc(
      { id: 文档id, mode: 0, size: 102400, k: "" },
      ""
    );
    let 文档属性 = await 核心api.getDocInfo(
      { id: 文档id, mode: 0, size: 102400, k: "" },
      ""
    );
    let div = document.createElement("div");
    div.innerHTML = 文档内容.content;
    let codeBlocks = div.querySelectorAll(
      "div[data-node-id]:not(div[data-node-id] div[data-node-id])"
    );
    div.querySelectorAll('[data-type="block-ref"]').forEach((ref) => {
      ref.innerText = ref.getAttribute("data-id");
    });

    let code = "";
    codeBlocks.forEach((el) => {
      if (
        el.querySelector(".protyle-action__language") &&
        ["js", "javascript"].indexOf(
          el.querySelector(".protyle-action__language").innerHTML
        ) >= 0
      ) {
        code += el.querySelector(".hljs").innerText;
      } else if (
        el.querySelector(".protyle-action__language") &&
        ["css"].indexOf(
          el.querySelector(".protyle-action__language").innerHTML
        ) >= 0
      ) {
        let cssCode = el.querySelector(".hljs").innerText;
        if (cssCode.startsWith("/*cssInJS*/")) {
          let htmlcode = `<style>${cssCode}</style>`;
          code += `\ndocument.head.insertAdjacentHTML('beforeend',\`${htmlcode}\`);\n`;
        }
      } else {
        let textels = el.querySelectorAll(`div[contenteditable="true"]`);
        textels.forEach((child) => {
          let text = child.innerText;
          let textArray = text.split(/\r\n|\n|\r/);
          textArray.forEach((line) => {
            if (!line.startsWith("@js:import")) {
              code += "//" + line + "\n";
            } else {
              code += line.replace("@js:", "") + "\n";
            }
          });
        });
      }
    });
    code = 解析导入(code);
    code = `//siyuan://blocks/${文档属性.id}\n` + code;
    if (window.location.href.indexOf("?") >= 0) {
      code =
        `//${window.location.href.replace("app", "desktop")}&&blockID=${
          文档属性.id
        }\n` + code;
    } else {
      code =
        `//${window.location.href.replace("app", "desktop")}?blockID=${
          文档属性.id
        }\n` + code;
    }
    code = `//${文档属性.name}\n` + code;

    let blob = new Blob([code], {
      type: "application/javascript",
    });
    let path = "/data/snippets/jsInNote/" + 文档属性.id + ".js";

    let file = new File([blob], 文档属性.id + ".js", {
      lastModified: Date.now(),
    });

    let data = new FormData();
    data.append("path", path);
    data.append("file", file);
    data.append("isDir", false);
    data.append("modTime", Date.now());
    let res = await fetch("/api/file/putFile", {
      method: "POST",
      body: data,
    });
    let resdata = await res.json();
    if (resdata.code === 0) {
      await 核心api.pushMsg({
        msg: `${文档属性.id}已经导出到${
          "/data/snippets/jsInNote/" + 文档属性.id + ".js"
        }`,
      });
      if (直接运行) {
        await 添加笔记内js代码片段(文档属性.id, 文档属性.name, "js");
      }
    } else {
      await 核心api.pushErrMsg({
        msg: `${文档属性.id}导出为js出错:${resdata.msg}`,
      });
      console.error(`${文档属性.id}导出为js出错:${resdata.msg}`);
    }
  }
  async function 添加笔记内js代码片段(id, 名称, 类型) {
    let 代码片段内容 = `import('/snippets/jsInNote/${id + ".js"}')`;
    let 现有代码片段 = await 核心api.getSnippet({ type: "all", enabled: 2 });
    let 存在元素索引 = 现有代码片段.snippets.findIndex(
      (item) => item.id === id + "js"
    );
    if (存在元素索引 >= 0) {
      // 如果元素已存在,则替换元素value
      现有代码片段.snippets[存在元素索引].content = 代码片段内容;
      现有代码片段.snippets[存在元素索引].name = 名称;
      现有代码片段.snippets[存在元素索引].type = 类型;

      if (现有代码片段.snippets[存在元素索引].enabled) {
        await 核心api.setSnippet(现有代码片段);
        window.location.reload();
      } else {
        现有代码片段.snippets[存在元素索引].enabled = true;
        await 核心api.setSnippet(现有代码片段);
        document.head.appendChild(
          生成元素(
            "script",
            {
              id: `snippetJS${id}`,
              type: "text/javascript",
            },
            代码片段内容
          )
        );
      }
    } else {
      // 否则添加新元素
      现有代码片段.snippets.push({
        id: id + "js",
        content: 代码片段内容,
        name: 名称,
        type: 类型,
        enabled: true,
      });
      await 核心api.setSnippet(现有代码片段);
      document.head.appendChild(
        生成元素(
          "script",
          {
            id: `snippetJS${id}`,
            type: "text/javascript",
          },
          代码片段内容
        )
      );
    }
  }
  function 生成元素(标签, 属性对象, 内容) {
    let 元素 = document.createElement(标签);
    Object.getOwnPropertyNames(属性对象).forEach((属性名) =>
      元素.setAttribute(属性名, 属性对象[属性名])
    );
    元素.innerHTML = 内容;
    return 元素;
  }
  function 解析导入(code) {
    let [imports, exports] = lexer.parse(code);
    let codeMagicString = new MagicString(code);
    imports.forEach((导入声明) => {
      导入声明.n?codeMagicString.overwrite(导入声明.s, 导入声明.e, 重写导入(导入声明)):null;
    });
    return codeMagicString.toString();
  }
  function 重写导入(导入声明) {
    let name = 导入声明.n;
    name = name.replace(/\\/g, "/");
    name = name.replace("//", "/");
    name.startsWith("http:/")?name = name.replace("http:/", "http://"):null;
    name.startsWith("https:/")?name = name.replace("https:/", "https://"):null;
    let reg= /^\d{14}\-[0-9a-z]{7}$/
    if (reg.test(name)) {
      name = `./${name}.js`;
      return name
    } else if (
      name.startsWith("./") ||
      name.startsWith("../") ||
      name.startsWith("./") ||
      name.startsWith("/")||
      name.startsWith("http://")||
      name.startsWith("https://")
    ) {
      return name;
    } else {
      return "https://esm.sh/" + name;
    }
  }
})();

感谢:

esm.sh:一个基于 esm 格式分发 npm 代码包的 cdn

esm-dev/esm.sh: A fast, global CDN for NPM packages with ESM format. (github.com)

es-module-lexer:用于解析 esm 格式的依赖路径

guybedford/es-module-lexer: Low-overhead lexer dedicated to ES module parsing for fast analysis (github.com)

magicstring:更便利地 js 字符串操作包,用于对代码中浏览器无法识别的裸导入进行替换

Rich-Harris/magic-string: Manipulate strings like a wizard (github.com)


如果这玩意对你有用可以去爱发电给我买杯咖啡

leolee9086 正在创作一些简单的技术教程和小工具,以及设计方面内容 | 爱发电 (afdian.net)


  1. 笛卡尔

  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    23019 引用 • 92593 回帖
5 操作
leolee 在 2023-04-25 23:44:23 更新了该帖
leolee 在 2023-04-08 04:12:25 更新了该帖
leolee 在 2023-04-08 03:30:22 更新了该帖
leolee 在 2023-04-05 04:39:46 更新了该帖 leolee 在 2023-04-05 04:37:34 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...