在网上冲浪时,无意中发现了 abcjs 这个将文字渲染成乐谱的软件,感觉很有趣,而且还有开发者为 Obsidian 维护了 obsidian-plugin-abcjs 插件,我想试试能不能移植到 quartz 上。
菜鸟警告
- 前端菜鸟,毫无经验;
- 如有错误,多多指教;
失败的引入
在阅读 ObsidianFlavoredMarkdown 的处理代码的时候,我发现对 mermaid 的处理符合我的要求:
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
处理。简单来说流程是:
- 在解析代码时将带有
music-abc
的代码赋予abcjs
类; - 在渲染时将 abcjs 类的数据交给
ABCJS.renderAbc
渲染; - 最终显示在屏幕上。
整体代码如下所示:
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 |]