Closed murachi closed 1 year ago
コードハイライト、 js だと highlight.js か prismjs 辺りが有名みたいだけど、どちらもカスタマイズしたものをダウンロードしてきてリンクする方式。 highlight.js は node.js 上でハイライト済み HTML に変換するサーバーサイド用の API も用意されているようだけど、 prism の方はそういうのは考慮されていないっぽい。
js で行くんであれば commonmark ライブラリと併用できる。 node.js だけで必要なシステムを構築することは十分可能。
Python の場合は Pygments 一択。 commonmark モジュールというのが commonmark.js と同じように使えるものになっているようなので (この辺の実装とか見る限り、 NodeWalker
クラスというのが node の探索に使えそう)、これらを組み合わせる感じになるのかな (そうなると node.js の方が動作は軽そうな気もするが…)。
Perl でやるんであれば、 Syntax::Highlight::Universal というのが Colorer ライブラリというのを wrap しているようなので、これと CommonMark モジュールを組み合わせれば行けると思う。あるいはコードハイライトはもうちょっとマシなモジュールがあったかもしれない。
処理速度を重視するのであれば golang または Rust を検討対象に含めるのも手かもしれないと思い始めている。ライブラリがどの程度充実しているかと、言語仕様がどの程度馴染むか次第。
様々な環境でプロトタイピングしてみることにします。
python の commonmark モジュールは使い勝手としては以下の通り。
>>> parser = commonmark.Parser()
>>> with open("../sample/typescript-without-module-bundler.md", "r") as fin:
... md = fin.read()
... doc = parser.parse(md)
...
>>> for cur, entering in commonmark.node.NodeWalker(doc):
... if cur.t == "code_block":
... print("```{}\n{}\n```\n".format(cur.info, cur.literal))
...
```json
{
"compilerOptions": {
"target": "es5"
, "module": "commonjs"
, "lib": ["es5", "dom"]
, "outDir": "./src/static"
}
, "compileOnSave": true
, "filesGlob": [
"./ts/*.ts"
]
}
```
```ts
export namespace validator {
export function validateNameToken(val: string): boolean {
return /^[a-z]\w*$/i.test(val);
}
};
```
...
node.t == "code_block"
のノードがいわゆるコードブロックとなる。node.info
でコードブロックに指定した言語の種類を取得できる。>>> ast = parser.parse("""```
... brah brah brah...
... hegehegehegehege...
... ```
... """)
>>> ast.first_child.info
''
>>>
node.literal
で取れる内容は HTML escape されていない。
>>> ast = parser.parse("""```html
... <!doctype html>
... <html>
... <head><title>sample HTML</title></head>
... <body><h1>sample HTML</h1>
... <p>Brah brah brah ...</p>
... </body></html>
... ```
... """)
>>> ast.first_child.literal
'<!doctype html>\n<html>\n<head><title>sample HTML</title></head>\n<body><h1>sample HTML</h1>\n<p>Brah brah brah ...</p>\n</body></html>\n'
>>>
markdown 中に <pre>
タグでコードブロックを書いた場合、 commonmark モジュールは html_block
ブロックとして解釈してくれる模様。
## HTML で何か書くテスト
<b>太字</b>、<i>斜体</i>、<s>打ち消し線</s>。
<address><a href="mailto:email-addr@example.jp">email-addr@example.jp</a></address>
<pre><code class="py">import os
# スクリプトファイルが存在する場所
base_dir = os.path.join(os.path.dirname(__file__))
</code></pre>
>>> for cur, entering in commonmark.node.NodeWalker(doc):
... print("{} [{}] - {}".format(cur.t, cur.literal, entering))
...
document [None] - True
(中略)
heading [None] - True
text [HTML で何か書くテスト] - True
heading [None] - False
paragraph [None] - True
html_inline [<b>] - True
text [太字] - True
html_inline [</b>] - True
text [、] - True
html_inline [<i>] - True
text [斜体] - True
html_inline [</i>] - True
text [、] - True
html_inline [<s>] - True
text [打ち消し線] - True
html_inline [</s>] - True
text [。] - True
paragraph [None] - False
html_block [<address><a href="mailto:email-addr@example.jp">email-addr@example.jp</a></address>] - True
html_block [<pre><code class="py">import os
# スクリプトファイルが存在する場所
base_dir = os.path.join(os.path.dirname(__file__))
</code></pre>] - True
document [None] - False
>>>
そんなわけで、シンタックスハイライトの実現方法としては、 node.t == "code_block"
のコードリテラルをシンタックスハイライト済みの HTML に変換して、それを <pre>
タグの node.t == "html_block"
なノードに変換して元のノードを置き換えれば良さそう。できるのかどうかわからんが…
cur.insert_before(node)
してから cur.unlink()
すればいい… のか?
Python の commonmark モジュールはマニュアルが commonmark.js のを参照してねでオシマイなのでそこがネックではあるものの、 Pygments との組み合わせはとても使いやすいと思う。シンタックスハイライト用の css 例を出力する方法も用意されているので、それをベースにカスタマイズツールを作るのもやりやすそう。
そもそも node.js でのファイル I/O が分かっとらん>わし(´・_・`)
highlight.js の hljs.highlight()
は本当に <pre>
タグの中身だけを生成してくれる模様。<pre>
タグをさらにクラス付きの <div>
タグで包んだものを返す Pygments とは対照的。
commonmark.js は Python 版の commonmark と実質同じもので使い勝手も似たようなものなので (for ... in
構文が使える分 Python の方が使い勝手は良い)、比較対象は Pygments と highlihgt.js になりますね。
以下の HTML コードに対して、
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>HTML 出力例</title>
<link rel="stylesheet" href="/static/main.css">
<script><!--
var exports = {};
function require(mod_path) {
return exports;
}
--></script>
<script src="/static/validator.js"></script>
<script src="/static/edit-account.js"></script>
</head>
<body>
...
</body>
</html>
シンタックスハイライトを施した HTML コードはそれぞれ以下の通り。
Pygments
<div class="highlight"><pre><span></span><span class="cp"><!doctype html></span>
<span class="p"><</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">"ja"</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">></span>
<span class="p"><</span><span class="nt">title</span><span class="p">></span>HTML 出力例<span class="p"></</span><span class="nt">title</span><span class="p">></span>
<span class="p"><</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">"stylesheet"</span> <span class="na">href</span><span class="o">=</span><span class="s">"/static/main.css"</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span><span class="p">></span><span class="c"><!--</span>
<span class="kd">var</span> <span class="nx">exports</span> <span class="o">=</span> <span class="p">{};</span>
<span class="kd">function</span> <span class="nx">require</span><span class="p">(</span><span class="nx">mod_path</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">exports</span><span class="p">;</span>
<span class="p">}</span>
<span class="o">--></span><span class="p"></</span><span class="nt">script</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">"/static/validator.js"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">"/static/edit-account.js"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span><span class="p">></span>
...
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">></span>
</pre></div>
<div class="highlight"><pre> ... </pre></div>
で包んでくれる (<code> ... </code>
では包まない模様)。<script>
タグ内は HTML コメント化していても JavaScript コードとしてハイライトしてくれる。HtmlFormatter().get_style_defs('.highlight')
すれば生成してくれるようになっている。highlight.js
<span class="hljs-meta"><!doctype <span class="hljs-meta-keyword">html</span>></span>
<span class="hljs-tag"><<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ja"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">head</span>></span>
<span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">title</span>></span>HTML 出力例<span class="hljs-tag"></<span class="hljs-name">title</span>></span>
<span class="hljs-tag"><<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/static/main.css"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="handlebars"><span class="xml"><span class="hljs-comment"><!--
var exports = {};
function require(mod_path) {
return exports;
}
--></span></span></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/static/validator.js"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/static/edit-account.js"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="hljs-tag"></<span class="hljs-name">head</span>></span>
<span class="hljs-tag"><<span class="hljs-name">body</span>></span>
...
<span class="hljs-tag"></<span class="hljs-name">body</span>></span>
<span class="hljs-tag"></<span class="hljs-name">html</span>></span>
<pre> ... </pre>
の中身だけを生成する。 <pre>
タグ等は自前で付け足してあげる必要がある (その分自由度があるとも言える)。<script>
タグの中身が HTML コメント化されている場合、中の JavaScript コードも単にコメントとして扱われる模様 (HTML コメント化していない場合の挙動は未確認)。hljs-
で始まる、比較的意味のわかりやすい名前になっている。但し冗長なのでその分文字数も多くなる。Perl のシンタックスハイライトは Arch::FileHighlighter を試してみることにする。
Perl の CommonMark ライブラリは libcmark を使っているとのことで、パフォーマンスの高さが期待できたのでぜひ試してみたかったんですが、 Ubuntu 18.04 環境下では libcmark を apt install
できず、ソースからインストールはできたものの、 cpan -i CommonMark
するとテスト中にエラーが出てしまうという状況。セットアップが難しそうなので、これについては一旦保留することにします…。
libcmark そのものの使い勝手が悪くないようであれば C++ で実装するという手もなくはないのですがそれはさておき…(´・_・`)
libcmark を C++ から試してみているんですが、 cmark_iter_new(root_node)
から生成できる反復子の扱いが commonmark.js 等とはどうも使い勝手が違うっぽい(´・_・`)
もしかして再帰的な操作が必要なやつなのかも…(´・_・`)
libcmark を C++ から試してみているんですが、
cmark_iter_new(root_node)
から生成できる反復子の扱いが commonmark.js 等とはどうも使い勝手が違うっぽい(´・_・`)
どうも勘違いだった模様(´・_・`) 単に document root に対して cmark_node_get_literal()
が NULL
を吐いてただけだったっぽい (´・_・`)
GNU Source-highlight を試してみたんだが、出力結果が最悪だった…。
murachi@maha:~/github/static-cms/proto/cpp$ g++ -o srchl-sample srchl-sample.cpp -lsource-highlight
murachi@maha:~/github/static-cms/proto/cpp$ ./srchl-sample
<!-- Generator: GNU source-highlight 3.1.8
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt><i><font color="#9A1900">#!/usr/bin/perl</font></i>
<b><font color="#0000FF">use</font></b> strict<font color="#990000">;</font>
<b><font color="#0000FF">use</font></b> warnings<font color="#990000">;</font>
<i><font color="#9A1900"># 関数</font></i>
<b><font color="#0000FF">sub</font></b> hoge <font color="#FF0000">{</font>
<b><font color="#0000FF">my</font></b> <font color="#009900">$hoge</font> <font color="#990000">=</font> <b><font color="#0000FF">shift</font></b><font color="#990000">;</font>
<b><font color="#0000FF">my</font></b> <font color="#009900">$result</font> <font color="#990000">=</font> <font color="#FF0000">"[hoge] $hoge"</font><font color="#990000">;</font>
<b><font color="#0000FF">print</font></b> <font color="#FF0000">"$result\n"</font><font color="#990000">;</font>
<b><font color="#0000FF">return</font></b> <font color="#009900">$result</font><font color="#990000">;</font>
<font color="#FF0000">}</font>
<i><font color="#9A1900"># 関数呼び出し</font></i>
<b><font color="#000000">hoge</font></b><font color="#990000">(</font><font color="#FF0000">"piyo piyo"</font><font color="#990000">);</font>
__EOF__
<font color="#990000">=</font>pod
<i><font color="#9A1900">=head1 hoge</font></i>
<i><font color="#9A1900">=head2 これは</font></i>
<i><font color="#9A1900">なんだろうね…。</font></i>
<i><font color="#9A1900">=cut</font></i>
</tt></pre>
murachi@maha:~/github/static-cms/proto/cpp$
<tt>
タグ、 <i>
タグはまぁ許容するにしても <font>
タグ、お前は駄目だ…(´・_・`)
HTML5 形式もサポートしてるっぽいからそっちを試したほうがいいかな…。
いや、 HTML5 形式も試してみたけど、 <font>
を <span>
に置き換えてスタイル直指定にしただけという… これはひどい(´・_・`)
murachi@maha:~/github/static-cms/proto/cpp$ g++ -o srchl-sample srchl-sample.cpp -lsource-highlight
murachi@maha:~/github/static-cms/proto/cpp$ ./srchl-sample
<!-- Generator: GNU source-highlight 3.1.8
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><i><span style="color:#9A1900">#!/usr/bin/perl</span></i>
<b><span style="color:#0000FF">use</span></b> strict<span style="color:#990000">;</span>
<b><span style="color:#0000FF">use</span></b> warnings<span style="color:#990000">;</span>
<i><span style="color:#9A1900"># 関数</span></i>
<b><span style="color:#0000FF">sub</span></b> hoge <span style="color:#FF0000">{</span>
<b><span style="color:#0000FF">my</span></b> <span style="color:#009900">$hoge</span> <span style="color:#990000">=</span> <b><span style="color:#0000FF">shift</span></b><span style="color:#990000">;</span>
<b><span style="color:#0000FF">my</span></b> <span style="color:#009900">$result</span> <span style="color:#990000">=</span> <span style="color:#FF0000">"[hoge] $hoge"</span><span style="color:#990000">;</span>
<b><span style="color:#0000FF">print</span></b> <span style="color:#FF0000">"$result\n"</span><span style="color:#990000">;</span>
<b><span style="color:#0000FF">return</span></b> <span style="color:#009900">$result</span><span style="color:#990000">;</span>
<span style="color:#FF0000">}</span>
<i><span style="color:#9A1900"># 関数呼び出し</span></i>
<b><span style="color:#000000">hoge</span></b><span style="color:#990000">(</span><span style="color:#FF0000">"piyo piyo"</span><span style="color:#990000">);</span>
__EOF__
<span style="color:#990000">=</span>pod
<i><span style="color:#9A1900">=head1 hoge</span></i>
<i><span style="color:#9A1900">=head2 これは</span></i>
<i><span style="color:#9A1900">なんだろうね…。</span></i>
<i><span style="color:#9A1900">=cut</span></i>
</pre>
murachi@maha:~/github/static-cms/proto/cpp$
Colorer のドキュメント目を通してみたけど、 HTML への変換とかよりもっと低レベルの機能のサポートにとどまっているっぽい。設計が複雑で直感的でもなく、古臭さもあってあまり使いたいとは思えなかった。
GtkSourceView は GTK の使用が前提。 gchar
型とかのお世話になりたいとも思えないのであまり検討に含めたいとは思えないです(´・_・`)。
Scintilla もちゃんと見てないけど、シンタックスハイライトをやってくれるライブラリというよりは、テキストエディタの実装そのものっぽい。でっかくなりすぎる気がする(´・_・`)。
結局 GNU Source-highlight のフォーマッタをカスタマイズして使うのが正解のような気がしてきた(´・_・`)。
結局 GNU Source-highlight のフォーマッタをカスタマイズして使うのが正解のような気がしてきた(´・_・`)。
やってみた。なかなか良さげ。
murachi@maha:~/github/static-cms/proto/cpp$ g++ -o srchl-sample srchl-sample.cpp -lsource-highlight
murachi@maha:~/github/static-cms/proto/cpp$ ./srchl-sample
<span class="comment">#</span><span class="comment">!/usr/bin/perl</span>
<span class="keyword">use</span> strict<span class="symbol">;</span>
<span class="keyword">use</span> warnings<span class="symbol">;</span>
<span class="comment">#</span><span class="comment"> 関数</span>
<span class="keyword">sub</span> hoge {
<span class="keyword">my</span> $hoge <span class="symbol">=</span> <span class="keyword">shift</span><span class="symbol">;</span>
<span class="keyword">my</span> $result <span class="symbol">=</span> <span class="string">"[hoge] $hoge"</span><span class="symbol">;</span>
<span class="keyword">print</span> <span class="string">"$result\n"</span><span class="symbol">;</span>
<span class="keyword">return</span> $result<span class="symbol">;</span>
}
<span class="comment">#</span><span class="comment"> 関数呼び出し</span>
hoge<span class="symbol">(</span><span class="string">"piyo piyo"</span><span class="symbol">)</span><span class="symbol">;</span>
__EOF__
<span class="symbol">=</span>pod
<span class="comment">=head1</span><span class="comment"> hoge</span>
<span class="comment">=head2 これは</span>
<span class="comment">なんだろうね…。</span>
<span class="comment">=cut</span>
murachi@maha:~/github/static-cms/proto/cpp$
GNU Source-highlight の設定ファイル (/usr/share/source-highlight
配下) を眺めていたら、 default.css
というファイルに、よく網羅された要素名がそのままクラス名になった完璧なスタイル定義例があって、最初っからこれに対応するような HTML 吐いてくれればいいのに何でそうしてくれないの? ってなった件(´・_・`)
console.lang
とか pycon.lang
とかが用意されていないのつらい(´・_・`) 追々自分で用意するしか無いのかな…(´・_・`)
まあまあ苦労してなんとか実装例をでっち上げてみたが、結果にはいまいち満足できない。 ここで示した HTML のコード例に対する変換結果を見る限り、コメント部分については複数行コメントとして処理できていない。そもそも行を跨いだ状態の維持に対応できていないような気がする。
<pre><code class="lang html"><span class="preproc"><!doctype</span> <span class="type">html</span><span class="preproc">></span>
<span class="keyword"><html</span> <span class="type">lang</span><span class="symbol">=</span><span class="string">"</span><span class="string">ja</span><span class="string">"</span><span class="keyword">></span>
<span class="keyword"><head></span>
<span class="keyword"><meta</span> <span class="type">charset</span><span class="symbol">=</span><span class="string">"</span><span class="string">utf-8</span><span class="string">"</span><span class="keyword">></span>
<span class="keyword"><title></span>HTML 出力例<span class="keyword"></title></span>
<span class="keyword"><link</span> <span class="type">rel</span><span class="symbol">=</span><span class="string">"</span><span class="string">stylesheet</span><span class="string">"</span> <span class="type">href</span><span class="symbol">=</span><span class="string">"</span><span class="string">/static/main.css</span><span class="string">"</span><span class="keyword">></span>
<span class="keyword"><script</span><span class="keyword">></span><span class="symbol"><</span><span class="symbol">!</span><span class="symbol">-</span><span class="symbol">-</span>
<span class="keyword">var</span> exports <span class="symbol">=</span> <span class="cbracket">{</span><span class="cbracket">}</span><span class="symbol">;</span>
<span class="keyword">function</span> <span class="function">require</span><span class="symbol">(</span>mod_path<span class="symbol">)</span> <span class="cbracket">{</span>
<span class="keyword">return</span> exports<span class="symbol">;</span>
<span class="cbracket">}</span>
<span class="symbol">-</span><span class="symbol">-</span><span class="symbol">></span><span class="keyword"></script></span>
<span class="keyword"><script</span> <span class="type">src</span><span class="symbol">=</span><span class="string">"</span><span class="string">/static/validator.js</span><span class="string">"</span><span class="keyword">></span><span class="keyword"></script></span>
<span class="keyword"><script</span> <span class="type">src</span><span class="symbol">=</span><span class="string">"</span><span class="string">/static/edit-account.js</span><span class="string">"</span><span class="keyword">></span><span class="keyword"></script></span>
<span class="keyword"></head></span>
<span class="keyword"><body></span>
...
<span class="keyword"></body></span>
<span class="keyword"></html></span>
</code></pre>
それから今更になって TypeScript に対応していないことに気づいた。 JavaScript も js
ではなく javascript
と指定しないといけないっぽい (これについては追加の設定ファイル置き場の中にシンボリックリンクを置くなどすれば対応できると思うが)。
例えば C の複数行コメントがこれで正しく処理できないとかなんだとするとちょっと致命的なんだが… どうなんだろう…(´・_・`)
複数行コメントを含む C++ サンプルソースを試してみたところ、コメントブロックはちゃんとコメントとして扱われた。しかも Doxygen の特殊コマンドを別属性でハイライトしてくれる。
コマンドライツールの source-highlight
も試してみたが、 HTML の <script>
タグ中のコメントは開始・終端のみシンボルとして扱い、中身はちゃんと JavaScript として処理してくれている模様。一気に評価が上がった。
ていうか、ここまで頑張らなくても、出力形式のファイルに htmlcss.outlang
を選択するだけで良しなに変換してくれたっぽい。ギャフン orz
rust.lang はあるっぽいんだけど、 typescript.lang はついぞ見つからん…。 GtkSourceView のものはあるようなんだが (つーかどっちも拡張子 .lang なのすげー困るんだが…)
<!-- Generator: GNU source-highlight 3.1.8
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><tt><span class="preproc"><!doctype</span><span class="normal"> </span><span class="type">html</span><span class="preproc">></span>
<span class="keyword"><html</span><span class="normal"> </span><span class="type">lang</span><span class="symbol">=</span><span class="string">"ja"</span><span class="keyword">></span>
<span class="keyword"><head></span>
<span class="normal"> </span><span class="keyword"><meta</span><span class="normal"> </span><span class="type">charset</span><span class="symbol">=</span><span class="string">"utf-8"</span><span class="keyword">></span>
<span class="normal"> </span><span class="keyword"><title></span><span class="normal">HTML 出力例</span><span class="keyword"></title></span>
<span class="normal"> </span><span class="keyword"><link</span><span class="normal"> </span><span class="type">rel</span><span class="symbol">=</span><span class="string">"stylesheet"</span><span class="normal"> </span><span class="type">href</span><span class="symbol">=</span><span class="string">"/static/main.css"</span><span class="keyword">></span>
<span class="normal"> </span><span class="keyword"><script></span><span class="symbol"><!--</span>
<span class="normal"> </span><span class="keyword">var</span><span class="normal"> exports </span><span class="symbol">=</span><span class="normal"> </span><span class="cbracket">{}</span><span class="symbol">;</span>
<span class="normal"> </span><span class="keyword">function</span><span class="normal"> </span><span class="function">require</span><span class="symbol">(</span><span class="normal">mod_path</span><span class="symbol">)</span><span class="normal"> </span><span class="cbracket">{</span>
<span class="normal"> </span><span class="keyword">return</span><span class="normal"> exports</span><span class="symbol">;</span>
<span class="normal"> </span><span class="cbracket">}</span>
<span class="normal"> </span><span class="symbol">--></span><span class="keyword"></script></span>
<span class="normal"> </span><span class="keyword"><script</span><span class="normal"> </span><span class="type">src</span><span class="symbol">=</span><span class="string">"/static/validator.js"</span><span class="keyword">></script></span>
<span class="normal"> </span><span class="keyword"><script</span><span class="normal"> </span><span class="type">src</span><span class="symbol">=</span><span class="string">"/static/edit-account.js"</span><span class="keyword">></script></span>
<span class="keyword"></head></span>
<span class="keyword"><body></span>
<span class="normal">...</span>
<span class="keyword"></body></span>
<span class="keyword"></html></span>
</tt></pre>
例えば <!--
の部分が一文字ずつ <span>
マークアップに分割されず、 <span class="symbol"><!--</span>
のようにひとまとめにされるようになったのはとても良いと思う。その一方で、全体が勝手に <pre><tt> ... </tt></pre>
で括られるようになった他、その手前に何だか余計なコメントが挿入されるようになった。ただ、この辺は出力フォーマット定義ファイル中の以下の部分で定義されているようなので、見様見真似でこのファイルを改造したものを用意することで回避できそう。
nodoctemplate
"<!-- Generator: $additional -->
$header<pre><tt>"
"</tt></pre>$footer
"
end
Rust を使う場合、 CommonMark は pulldown-cmark、 syntax highlighting は syntect を使うのが良さげ。
pulldown-cmark パッケージはオプション指定で GitHub 風のテーブルやタスクリストなんかにも対応可能らしい。オプションで対応ってのがいいね。
で、その中に「smart punctuation」ってのがあって、なんじゃこりゃ、と思ったんだが、どうやらこんな感じで動くものらしい… (詳細な動作結果は若干異なるかもしれんが多分似たようなものだと思う)。これもまぁ、ユーザーが使いたいと思うならオプション指定で利用可能としてもいいかもしれんが、おいらは別に要らんなぁ…(´・_・`)
syntect は Sublime Text というテキストエディタのハイライティングをエミュレーションするものっぽい。 .sublime-syntax
形式の文法定義ファイルが使えるらしいが、どこかにまとめて配布されてたりしないかな…。デフォルトで使えるものも一覧してみたけど、思ってたほど豊富でもなさげでちょっとしょげている(´・_・`)
tree-sitter-highlight をちょっと検討していたんだけど、文法定義が C で書かれたプロジェクトになっているようで (ex: tree-sitter-c, tree-sitter-javascript, etc...)、 Available Parsers に無いものを見つけ出すのが最も骨が折れるパターンだしメンテナンスコストもきつそうなのでやめた(´・_・`)
つか、 Perl がない時点でおいら的に話にならないですね(´・_・`)
文法定義が C で書かれたプロジェクトになっているようで (ex: tree-sitter-c, tree-sitter-javascript, etc...)、
そうでもなかった。 tree-sitter の Creating Parsers ページによれば、文法定義は grammar.js
という JavaScript ファイルで記述し、それを tree-sitter generate
コマンドとやらで parser.c
に変換する、というやり方になるらしい。
C 言語用の文法定義ファイルはこんな感じ。しんどさとしては他の方式とそんなに変わらん気もする。
Atom が TextMate 方式から tree-sitter に順次切り替えを進めているらしいので、公式で作られていない文法定義も探せば色々と転がってるかもしれない。
一応 tree-sitter-perl 作ってる人も居るね。有り難し…。
流石に tree-sitter-console
とか tree-sitter-pycon
とかは出てこないか…(´・_・`)
とりあえず tree-sitter は一旦脇において、 syntect で作ってみることにしまふ(´・_・`)
sample.md
をロードする場合と typescript-without-module-bundler.md
をロードする場合とで、 CodeBlock
中の Text
イベントの扱いが異なるのは何故なんだぜ? (´・_・`)
1回の Text
イベントにコードブロック中の全行が含まれる場合もあれば、行ごとに Text
イベントが分解される場合もある模様… 両方を考慮した実装にする必要がある。めんどいなぁ(´・_・`)
あと syntect は C++ の変換結果がなんか気に食わないんだが… 何か設定を間違えてるのか? (´・_・`)
でけた。 Iterator.flatten()
便利。
syntect は ClassedHTMLGenerator
を使う場合、 SyntaxSet::load_defaults_nonewlines()
を使って SyntaxSet
を生成するのが正解だったっぽい。 C++ や JavaScript で //
からの 1行コメントがファイルの終わりまで全部コメントにしちゃっていたのは SyntaxSet::load_defaults_newlines()
を使おうとしていたのが原因だった。
しかしコメント開始文字 (/*
とか //
とか #
とか) がシンボルとしてハイライトされてるっぽいんだけど、これらもあくまでコメントとしてハイライトしてほしい。 JSON の色付けの仕方も気に食わない。ていうか全般的にやたらとリッチに配色切り替え過ぎな気がする。 HTML 中の JavaScript のハイライティングは悪くない。
この後やるべきこと。
とりあえずパフォーマンス比較を優先する。
/usr/bin/time -v
コマンドを介して実行し、 Maximum resident set size
を見て実行中の最大メモリー容量を確認する。試しに Rust のサンプルコードを Release 版で走らせてみたが、こうして見ると意外とメモリーを食っていることが分かる。
murachi@maha:~/github/static-cms/proto/rust/cm2html/target/release$ /usr/bin/time -v ./cm2html ~/github/static-cms/proto/sample/typescript-without-module-bundler.md ts-without-mb.html
Command being timed: "./cm2html /home/murachi/github/static-cms/proto/sample/typescript-without-module-bundler.md ts-without-mb.html"
User time (seconds): 0.04
System time (seconds): 0.00
Percent of CPU this job got: 100%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.04
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 10224
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 1792
Voluntary context switches: 1
Involuntary context switches: 0
Swaps: 0
File system inputs: 0
File system outputs: 160
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
murachi@maha:~/github/static-cms/proto/rust/cm2html/target/release$
テストデータ生成スクリプト、まだ完璧じゃないけどだいたい動くようになった。
covid19 のソースツリーを Markdown にしたファイル (11.8MB 程) を、すでに作ってあるプロトタイプにかけて HTML をファイルに出力し、計測してみる。計測には古いノート PC である ThinkPad E130 (Core i3-3227U / 8GB RAM / Ubuntu 20.04) を使用。
トップバッターは C++ + libcmark + libsource-highlight 。メモリーは 183MB 程度使用、実行時間は 7.63秒。さすが。
murachi@yuma:~/github/static-cms/proto/cpp$ /usr/bin/time -v ./cm2html ../sample/cov19.md cov19.html
Command being timed: "./cm2html ../sample/cov19.md cov19.html"
User time (seconds): 7.33
System time (seconds): 0.26
Percent of CPU this job got: 99%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:07.63
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 182980
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 75022
Voluntary context switches: 4
Involuntary context switches: 42
Swaps: 0
File system inputs: 23144
File system outputs: 114728
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
murachi@yuma:~/github/static-cms/proto/cpp$
メモリー使用量はアロケータを工夫すればもっと抑えられる可能性がある。その場合、実行時間も同時に抑えられるかもしれない。
同じファイルを Python + commonmark + Pygments で変換中なんですが、いつまで経っても終わらない…(´・_・`)
ここまででかいファイルが運用上想定し得ないものであるとはいえ、流石にちょっと時間かかり過ぎだな…(´・_・`)
Python やっと結果が出た。実行にかかった時間は 21分、メモリー使用量は最大で 337MB。
murachi@yuma:~/github/static-cms/proto/python$ pipenv run /usr/bin/time -v python cm2html.py ../sample/cov19.md cov19.html
Command being timed: "python cm2html.py ../sample/cov19.md cov19.html"
User time (seconds): 716.66
System time (seconds): 554.95
Percent of CPU this job got: 99%
Elapsed (wall clock) time (h:mm:ss or m:ss): 21:12.44
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 337176
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 290379675
Voluntary context switches: 1
Involuntary context switches: 15392
Swaps: 0
File system inputs: 0
File system outputs: 64776
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
murachi@yuma:~/github/static-cms/proto/python$
メモリー使用量は C++ の倍未満なので思っていたほど喰わなかったなという印象。時間はかかったがこれはファイルサイズが大きい場合特有の動きである可能性もあるので現時点では結論は保留。
JavaScript (Node.js) + commonmark.js + highlight.js 計測完了。これには驚いた。
murachi@yuma:~/github/static-cms/proto/js$ /usr/bin/time -v node cm2html.js ../sample/cov19.md cov19.html
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Could not find the language '', did you forget to load/include a language module?
Command being timed: "node cm2html.js ../sample/cov19.md cov19.html"
User time (seconds): 12.21
System time (seconds): 0.47
Percent of CPU this job got: 118%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:10.73
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 437072
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 117886
Voluntary context switches: 5700
Involuntary context switches: 229
Swaps: 0
File system inputs: 0
File system outputs: 51792
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
murachi@yuma:~/github/static-cms/proto/js$
highlight 絡みで警告が出ているのはご愛嬌。 HTML ファイルは概ねちゃんと出力できてるっぽい。かかる時間は 10.7秒。メモリーは 437MB で比較的多めではあるものの、このサイズのファイルを処理したことを思えば及第点。パフォーマンス的には申し分ない成績。
そうなるとやはり惜しむらくはコードハイライティングのイマイチさなんだよなぁ…(´・_・`) そこさえもうちょっとマシな出来なら TypeScript と組み合わせて非常に簡潔・明瞭に開発できる上に十分なパフォーマンスが得られるという一石二鳥が得られたのだけど…(´・_・`)
満を持して Rust + pulldown-cmark + syntect を試してみたところ、残念なことに途中エラーで止まってしまった(´・_・`)
エラー発生状況はすでに再現可能なスモールケースが特定できていて、どうやら JavaScript コードの逆チルダ文字列リテラル中に改行を含むようなケースで再現する模様。
デバッグしてみてこっちで修正できそうなら pull request 投げることも視野に含めて検討する。
tree-sitter 採用論再浮上…かなぁ(´・_・`)
tree-sitter-cli
のビルドが通りません(´・_・`)
とりあえず tree-sitter で今使える全ての文法定義を自動で取得する環境づくりを構築中。 GitHub からの最新ソース取得のために REST API を叩こうとして、 private access token を要求するようにしたりと何だか色々面倒なことになってきてる。
直近の問題点:
/src/
ディレクトリ配下に parser.c
等のファイルが存在する想定で書いていたところ、 tree-sitter-ocaml
のクローリングで /ocaml/src/
と /interface/src/
に存在するというイレギュラーパターンに遭遇。これをどう取り扱うべきか。
文法だけ取ってこなきゃいけないのに tree-sitter-cli
とかいう余計なものまで取ってきてしまっていたので、こういうのを除外する処理も必要だ罠(´・_・`)
調べてみたけど結局 tree-sitter-ocaml みたいな特殊事例は他にはなかった。これだけ特別扱いしておけば良さそう。
tree-sitter-razor だけ src/tree-sitter/parser.h
が無いでやんの(´・_・`)
razor コンパイラ通らん(´・_・`) やたらと大量に warning 出てる(´・_・`)
あと ocaml の interface の方の scanner.cc がまさかの include 操作でエラー出しとる(´・_・`)
cargo:warning=/home/murachi/github/static-cms/proto/rust/cm2html-ts/src/c/ocaml/interface/scanner.cc:1:10: fatal error: ../../ocaml/src/scanner.cc: そのようなファイルやディレクトリはありません
cargo:warning= #include "../../ocaml/src/scanner.cc"
cargo:warning= ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
cargo:warning=compilation terminated.
当然 github リポジトリ上でのディレクトリ構成が前提になっているので、構成変えてソースコピーしている限りにおいてはこれは通らん罠(´・_・`)
やっと tree-sitter を動かせるところまで来た。で、ちょっとした C++ コードを含む markdown を処理し、構文解析ツリーを眺めてみて感じたこと:
translation_unit
(翻訳単位?) になる模様 (これは無視して良さそう)。queries/highlights.scm
内で、 @keyword
として予約語が定義されている。その他、 @function.builtin
として組み込み関数名が定義されていたり、 operator
として演算子が定義されていたりする。highlights.scm
を解析するのもいいが、tree-sitter-highlight はこの辺を利用して tree-sitter でシンタックスハイライトを実現するものらしいので、利用するのも手だと思う。コードサンプルの highlight_names
は highlight-schema リポジトリの schema.js
に定義が網羅されている。c_sharp
という文字列で関連付けているが、実際には拡張子として使われる cs
や、そのまんまの c#
で関連付けできたほうが嬉しい。そういった関連付けキーワードを網羅したい場合、自分で用意する必要がある。色々書いたけど、総じて tree-sitter をシンタックスハイライトに用いるのはコストがかかりすぎるという印象を持ちました(´・_・`)
今後の方針:
書いてて pure C++ で書くよりパフォーマンス落ちそうな気がしてきた。というのも、 Rust 側で拾ってきたソースコードの文字列を C 関数に渡す際、 &str
で表される文字列の末尾にヌル文字 '\0'
を付加したものを用意する必要があって、普通のやり方 (std::ffi::CString
を使用する方法) だとその都度文字列のコピーが発生してしまう。これを回避するには与えられた文字列自体にヌル文字を追加してあげる必要があるが、それをすると Rust 側で文字列スライスがイミュータブルであることを保証できなくなる。
Markdown 解析、およびコードハイライトをプロトタイプし、技術選定を行う。