在网上冲浪时,无意中发现了 abcjs 这个将文字渲染成乐谱的软件,感觉很有趣,而且还有开发者为 Obsidian 维护了 obsidian-plugin-abcjs 插件,我想试试能不能移植到 quartz 上。

菜鸟警告

  • 前端菜鸟,毫无经验;
  • 如有错误,多多指教;

失败的引入

在阅读 ObsidianFlavoredMarkdown 的处理代码的时候,我发现对 mermaid 的处理符合我的要求:

quartz\plugins\transformers\ofm.ts
      if (opts.mermaid) {
        js.push({
          script: `
          let mermaidImport = undefined
          document.addEventListener('nav', async () => {
            if (document.querySelector("code.mermaid")) {
              mermaidImport ||= await import('https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.7.0/mermaid.esm.min.mjs')
              const mermaid = mermaidImport.default
              const darkMode = document.documentElement.getAttribute('saved-theme') === 'dark'
              mermaid.initialize({
                startOnLoad: false,
                securityLevel: 'loose',
                theme: darkMode ? 'dark' : 'default'
              })
 
              await mermaid.run({
                querySelector: '.mermaid'
              })
            }
          });
          `,
          loadTime: "afterDOMReady",
          moduleType: "module",
          contentType: "inline",
        })
      }

简单来说就是在运行时动态导入 mermaid,而且它的选项开启也很简单:

      if (opts.mermaid) {
        plugins.push(() => {
          return (tree: Root, _file) => {
            visit(tree, "code", (node: Code) => {
              if (node.lang === "mermaid") {
                node.data = {
                  hProperties: {
                    className: ["mermaid"],
                  },
                }
              }
            })
          }
        })
      }

在检查到 mermaid 代码的时候做替换。

那我们可不可以用相同的方法导入 abcjs 呢?很遗憾,不可以。简单总结导入的代码如下:

<html>
<!doctype html>
<script type="module">
  async function load() {
    let abcObj = await import('https://cdn.jsdelivr.net/npm/[email protected]/dist/abcjs-basic-min.js');
  }
  load();
</script>
<button>Click me</button>
</html>

报错

abcjs-basic-min.js:3 Uncaught (in promise) TypeError: Cannot set properties of undefined (setting 'ABCJS')
    at abcjs-basic-min.js:3:186
    at abcjs-basic-min.js:3:191

对比了一下 mermaid 提供的 mjs 和 abcjs 提供的 js,问题其实在于 abcjs 没有提供 export,我们只能导入整个文件。

成功的引入

在阅读了 latex.ts 引入的方式之后,我有了一点新的想法。我们可以将 abcjs 作为 externalResources 引入,仿照 ofs.ts 处理。简单来说流程是:

  1. 在解析代码时将带有 music-abc 的代码赋予 abcjs 类;
  2. 在渲染时将 abcjs 类的数据交给 ABCJS.renderAbc 渲染;
  3. 最终显示在屏幕上。

整体代码如下所示:

quartz/plugins/transformers/musicabc.ts
import { PluggableList } from "unified"
import { QuartzTransformerPlugin } from "../types"
import { Root, Html, BlockContent, DefinitionContent, Paragraph, Code } from "mdast"
import { SKIP, visit } from "unist-util-visit"
import { JSResource } from "../../util/resources"
 
// Configuration documented at https://remark42.com/docs/configuration/frontend/
interface Options {
}
 
export const MusicABC: QuartzTransformerPlugin<Partial<Options> | undefined> = () => {
 
  return {
    name: "MusicABC",
    markdownPlugins(_ctx) {
      const plugins: PluggableList = []
      plugins.push(() => {
        return (tree: Root, _file) => {
          visit(tree, "code", (node: Code) => {
            if (node.lang === "music-abc") {
              node.data = {
                hProperties: {
                  className: ["abcjs"],
                },
              }
            }
          })
        }
      })
      return plugins
    },
    externalResources() {
      const js: JSResource[] = []
      js.push({
        script: `
        document.addEventListener('nav', async () => {
          if (document.querySelector("code.abcjs")) {
            document.querySelectorAll("code.abcjs").forEach((element) => {
                const abcNotation = element.textContent;
                var abcElement = document.createElement('p');
                element.parentNode.replaceWith(abcElement);
                ABCJS.renderAbc(abcElement, abcNotation);
                abcElement.style.overflow = null;
            });
          }
        });
        `,
        loadTime: "afterDOMReady",
        moduleType: "module",
        contentType: "inline",
      })
      js.push({
        src: "https://cdn.jsdelivr.net/npm/[email protected]/dist/abcjs-basic-min.js",
        loadTime: "afterDOMReady",
        contentType: "external",
      })
      return {
        css: [
          "https://cdn.jsdelivr.net/npm/[email protected]/abcjs-audio.css",
        ],
        js: js,
      }
    },
  }
}

这里面做了一堆操作,在渲染的时候将父节点替换成 <p></p> 然后在渲染之后将 overflow 这个选项设置为 null。因为我发现如果不将 overflow 设置为 null 或在 code 这个节点上渲染的话,乐谱不会渲染完全,因此就在父节点上渲染了。

Usage

目前还没有合并进主线,我也没想好是否要合并(感觉可以趁机练习一下 Cherry Pick),等解决下面的问题之后再去提交 issue 吧。

现在的用法很简单,将上面的代码保存为 quartz/plugins/transformers/musicabc.ts,在 quartz/plugins/transformers/index.ts 上导出,然后在 quartz.config.ts 里面的 transformers 使用即可。

Problems

  • 在手机上或者小屏幕乐谱可能会超出边界

  • % 标记渲染错误

abc 记号法用两个 % 标记一些额外信息,比如 MIDI 声道之类的,但是由于两个 % 在 Obsidian 中是注释标记,位于行首的时候似乎会被 quartz 忽略导致渲染错误。

Info

嚯,看上去是上游的问题,我直接打两个 % 会被 quartz 渲染出错。找个时间确认一下吧。

我尝试将插件的编译位置提前,但是并没有解决。

之前添加的标记:

\%\%MIDI program 0
\%\%barnumbers 0
\%\%MIDI program 0
\%\%barnumbers 0

TODOs

  • 添加播放按钮

番外

尝试添加一段乐谱(在这个乐谱上面做了一些修改),拙劣的弹奏见【原神】清昼细雨

X:1
T: 清昼细雨
C: 陈致逸, HOYO-MiX
L: 1/4
Q: 1/4=155
M: 4/4
K: G
V:1
 z4       | z4 | [f'4-^c''4-] | [f'4c''4]  |
V:2
"_LEGATO" \
B d a f- | f4 | z4           | z4         |
V:1
z4       | z4 | [^c'4-a'4-] | [c'4a'4]  |
V:2
F A e ^c- | c4 | z4           | z4         |
V:1
 z4       | z4 | [f'4-^c''4-] | [f'4c''4]  |
V:2
B d a f- | f4 | z4           | z4         |
V:1
z4       | z4 | [^c'4-a'4-] | [c'4a'4]  |
V:2
F A e ^c- | c4 | z4           | z4         |
V:1
b z f a- | a4 | [ae'] z z2 | z4 |
V:2
E B z F- | F4 | F A e2- | e4 |
V:1
d'' z z2    | z4 | [e'^c''] z z2 | z4 |
V:2
B   d  a f- | f4 | ^c e a2-      | a4 |
V:1
b z f a- | a4 | [ae'] z z2 | z4 |
V:2
E B z F- | F4 | F A e2- | e4 |
V:1
B d a f- | f4 | [f'4-^c''4-] | [f'4c''4]  |]
V:2
B, z z2 | z4 | z4 | z4 |]