これまで ipynb ファイルのレンダリングに Python が必要だったのを Node.js で完結するように変更しました。ほとんどブログを更新しないのに何の意味があるのかという気もしますが、めったに触らないからこそ開発環境は単純であるべきだろうと思います。
実装としては ipynb-loader が内部で行っていたことを Python を使わずに Node.js だけでやる、というのが基本的な内容です。以前の実装については Display Jupyter Notebook on Next.js を参照してください。
Node.js で ipynb ファイルを扱う
探したところ notebookjs が良さそうだったのでこれを使うことにしました。Jupyter はコードのシンタックスハイライトなどもやってくれますが、このパッケージはコールバックを提供するだけで自分ではそういう余計なことはせず、利用者が実装する必要があります。今回は MDX で既に使っている unified を使うことにしました。
そんなこんなで以下のような Webpack Loader を書きました:
const nb = require("notebookjs")
const unified = require("unified")
const rehypeKatex = require("rehype-katex")
const rehypeParse = require("rehype-parse")
const rehypePrism = require("@mapbox/rehype-prism")
const rehypeStringify = require("rehype-stringify")
const remarkGithub = require("remark-github")
const remarkMath = require("remark-math")
const remarkParse = require("remark-parse")
const remarkRehype = require("remark-rehype")
nb.markdown = function (markdown) {
// Jupyter の Markdown cell を MDX と同じルールで変換する
return unified()
.data("settings", {
footnotes: true,
})
.use(remarkParse)
.use(remarkMath)
.use(remarkGithub, {
repository: "dummy/repo",
mentionStrong: false,
})
.use(remarkRehype)
.use(rehypeKatex)
.use(rehypeStringify)
.processSync(markdown)
.toString()
}
nb.highlighter = function (text, pre, code, lang) {
if (code != null) {
// Prism のために class="language-python" 属性を付与する
code.className = `language-${lang || "text"}`
}
return text
}
module.exports = function (content) {
const html = nb.parse(JSON.parse(content)).render().outerHTML
// notebookjs で生成した HTML に Prism を適用する
return unified().use(rehypeParse).use(rehypePrism).use(rehypeStringify).processSync(html).toString()
}
このブログでしか使えないので NPM パッケージにしたりはしていません。
CSS
notebookjs が生成する HTML は Jupyter のものとは結構構造が異なっています。移行作業は若干面倒でしたが、正直いって Jupyter の HTML 構造はぐちゃぐちゃなので、書き始めたらこれはこれで良かったという感じがしてきました。
Jupyter といえば Input / Output の対応付があります。本家の HTML では In [1]:
という文字列が直接出力されますが、 notebookjs では代わりに data-prompt-number
属性を持った HTML が出力されます:
<div class="nb-cell nb-code-cell">
<div class="nb-input" data-prompt-number="1">
<pre>...</pre>
</div>
<div class="nb-output" data-prompt-number="1">
<pre class="nb-text-output">...</pre>
</div>
</div>
これに対応して In [1]:
を出力するために SCSS の for ループで data-prompt-number
に対応するルールを現実的な範囲で全部出力するようにしました。無駄な CSS ルールは多くなりますが、許容範囲だと思います。
@for $i from 1 through 100 {
.nb-input[data-prompt-number="#{$i}"] {
&::before {
content: "In [#{$i}]:";
}
}
.nb-output[data-prompt-number="#{$i}"] {
&::before {
content: "Out [#{$i}]:";
}
}
}
おわりに
ブログ開発しても肝心の記事を書かないのでは無意味なので、もっと書いていきたいですね。
参考
- yuku/yuku.github.io#1: Remove Python from dependencies and use Node.js 12