2017/12/24

TeXでつくるMarkdownパーサ

この記事はTeX & LaTeX Advent Calendar 2017の24日めのために書きました。

TeXの中でMarkdownを書ける

先月のことなんですが、TeXの、TeXによる、TeXのためのMarkdownパーサをつくました。

TeXで実装されてるので、当然、TeXで書く文書のなかで、シームレスにMarkdownを使えます。 具体的には、こんな感じに、\begin{markdown}から\end{markdown}のなかにMarkdown記法が書けます。

\documentclass{article}
\usepackage{md}

\begin{document}
\begin{markdown}

# markdown-tex

markdown processor in TeX. 
As inline styles, `tt`, *italic*, and **bold** are available. 

## subsection

Here is a enumerate list.

1. enumerate
1. in 
1. the markdown way

This is an itemize list. 

* this block
* will be
* itemized

And a quotation. 

> Quote Somethin
> Really **Awesome**!

\end{markdown}
\end{document}

これをふつうにTeXで処理すると、記法から期待されるようなPDFができるわけです。

とはいっても、先月の時点でサポートしていた記法はそんなに多くなくて、これくらい。

  • 行頭の「#」の個数に応じた見出し化
  • 行頭の「*」で箇条書き
  • 行頭の「<」でインデント
  • 行頭の数字で連番箇条書き
  • 行頭スペース4個でインデントするとコードブロック化
  • `..`で囲むとインライン等幅
  • *..*で囲むとインラインイタリック
  • **..**で囲むとインライン強調

これ以外の要素を書きたかったら、いったん\end{markdown}して、生のTeXを書いてください。TeXなので、組版に関することなら、なんでもできます。

Markdownの中でTeXを書ける

この記事を読んでいる人はみんなTeX Conf 2017に参加しているので既知だと思いますが、念のため補足すると、このMarkdownパーサはTeX Conf 2017で発表した「TeXは軽量マークアップ言語の夢を見るか」のために作ったものです。 この発表では、Markdownをはじめとするマークアップ記法と、TeX(LaTeX)によって得られる表現力との関係について考察しました。 「Markdownについて話すのにMarkdownパーサの実装経験がなければ刺されるかも」という強迫観念から取り組んだ、概念実証のための実装です。

で、TeXのなかでMarkdownを使えるからといってうれしいことはあまりないし、それで済ませるつもりだったですが、せっかくTeXなので、先日ちょっと手を入れて\TeXくらいは生で書けるようにしました。 その副作用で、コントロールシーケンスや数式も入れられるようになりました!

# markdown-tex

markdown processor in TeX. 
As inline styles, `tt`, *italic*, and **bold** are available. 
Inline equation in \TeX notation $\int_0^1\frac{1}{x}dx = \infty$.

\hfill December 24 2017

## subsection

Here is a enumerate list.

1. enumerate
1. in 
…

しかし、実はこれは誇大広告で、厳密にいうと2つの条件を満たすマクロやコマンドだけがMarkdown記法の中で使えます。

1つめの制約は、完全展開できる必要があるという点です。 たとえば、\sqrtは使えません。 これは、latex.ltxにおける\sqrtの定義では\@ifnextcharが利用されていて、そのなかでは展開されない\futureletが使われているからです。

なんで完全展開可能性が必要かというと、Markdown記法の構文解析を簡単にするために、対象となるトークンリストをいったん「完全展開された文字列」へと変換しているからです。 これにはexpl3の\tl_set_rescan:Nnxを使っています。 その際、TeXの入力を完全展開すると空白文字が集約されてしまってMarkdown記法として構文解析できなくなるので、これらは文字列とみなして完全展開しています。

\tl_set_rescan:Nnx \md_tokens
  {
    \char_set_catcode_letter:n {  32 }% SPACE
  } { \BODY }

これで\md_tokens変数に「完全展開された文字列」が入るので、それを構文解析し、それをTeXの胃袋に送ってPDFを作らせる、というのが先月までの実装でした。 そんなわけで、この状態でMarkdown記法の中に\TeXと書くと、このマクロが完全展開されたT\kern -..5ex\hbox E\kern -.\@mがそのままPDFに出てきてしまう状態でした。

では、Markdown記法の中に\TeXと書いて期待する出力結果を得るにはどうすればいいでしょうか? 完全展開をやめればよさそうですが、前述したようにスペースの扱いの都合があるので、文字列にしてからパースするという方針は変えがたい。 となると、完全展開された文字列をパースするときにTeXのプリミティブをパースしてコントロールシーケンスに変換し直し、それをTeXの胃袋に送ればいいのではないか?

というわけで、そんなようなコードを追加して、無事に\TeXのようなマクロがMarkdown記法のなかに埋め込めるようになりました。

\cs_new:Npn \inside_cs:Nnnn #1#2#3#4 {
  \tl_set:Nn \car_line { \tl_head:n { #4 } }
  \tl_set:Nn \cdr_line { \tl_tail:n { #4 } }
  
  \tl_if_head_eq_charcode:nNTF { #4 } { \sp_letter }
     {
       \tl_put_right:Nn #2 { 
         \cs:w #3 \cs_end:
       }
       \exp_args:NNnff \parse_to_end_line_with:Nnnn { #1 } { #2 } { } { \cdr_line }
     }
     {
       \tl_set:Nx \so_far { #3\car_line }
       \exp_args:NNnff \inside_cs:Nnnn { #1 } { #2 } { \so_far } { \cdr_line }
     }
}

ただし、実装をサボっているので、非標準的(?)な利用方法のコントロールシーケンスはパースできません。 たとえば、\hbot{foo}はいいけど、\hbox to 10pt{foo}とかはだめ。 これがもう1つの制約です。

今回のオチ

にしても、ここまでにやったことを振り返ると、事実上TeXでTeXをパースしているわけで、むだなことをした感が半端ない……。

  1. TeX記法の入力をTeXでパースし、トークンリストが作られた(TeXの仕事)
  2. トークンリストをMarkdown記法としてパースするために、完全展開して文字列化した(\tl_set_rescan:Nnx
  3. その文字列からコントロールシーケンスを取り出すのに、あらためてTeX記法のパーサを実装に追加した(\cs_new:Npn \inside_cs:Nnnn

0 件のコメント: