🔨

TeXの出力を文字列として得る術(テコテフ⹀TecoTeX)

2025/02/02に公開

TeXの出力を文字列として得る術(テコテフ⹀TecoTeX)

今日は立春の前日、節分ですね。
聞くところによると節分は、TeXへの理解を整理し節目とする日だそうです。
そんなわけで、この記事を仕上げることにしました。

TeX言語を組版以外にも利用したい

TeXの出力は、PDFへの変換元となるDVIという形式です。
通常の方法では、TeXが処理した完成形の文章を文字列として取り出して別の用途で使うことはできません。

しかし、TeXで整形した完成形の文章を文字列で得たいというのは、誰しもが思うところでしょう。
文字列として得られれば、HTMLやマークダウンなどの他の形にしたり、校正や推敲のために自動校正器に掛けるといったことができます。

TeX言語の良さも活かしたい

TeXの記述言語は、計算完備(チューリング完全)な命令型算譜言語であり、文章の記述に特化した独創的な記法の一つといえます。
TeXを組版のためだけでなく、文字列処理器として使えたら、さぞ楽しいことでしょう。

つまり、sed, perl, awk, m4などの殻書き(シェルスクリプト)命令と同じように、TeX言語を使えたら面白そうです。

./tecotex.sh "[TeX文]"
前処理 | ./tecotex.sh | 後処理 > 文字列.txt

LuaLaTeX で内部処理に割り込んで取り出す

文字列として得ることは、LuaLaTeXで内部処理に割り込んで印字を取り出し、ファイルに書き込むことでできました。

標準出力する例(テコテフ)

tecotex.sh
#!/bin/sh
# tecotex テコテフ
#%MIT許諾 - 津茶利休 2025

# tmp.tex の作成
cat > tmp.tex << EOF
%lualatex
\documentclass{article}
\pagestyle{empty}
\usepackage{fontspec}

\directlua{
local file = io.open("tmp.txt", "w")
file:write('')
file:close()

function 言化(n)
    if n.id == node.id("glyph") then
        if n.char == 0xF0000 then
            return ''
        end
        return utf8.char(n.char)
        
    elseif n.id == node.id("glue") then
        return ' '
    elseif n.id == node.id("local_par") then
        return '\string\n\string\n'
    else
        return ''
    end
end

function 追記(filename, char)
    local file = io.open(filename, "a")
    file:write(char)
    file:close()
end

function ProcessList(filename, head)
    local 前字 = nil
    for n in node.traverse(head) do
        local 字 = 言化(n)
        if not (字 == ' ' and 前字 == ' ') then
            追記(filename, 字)
            前字 = 字
        end
        if n.head ~= nil then
            ProcessList(filename, n.head)
        end
    end
end

function 出力(filename)
    local soBox = tex.getbox("ShipoutBox")
    ProcessList(filename, soBox)
end
}
\AddToHook{shipout/before}{\directlua{出力("tmp.txt")}}
\begin{document}
\fontspec{IPAexMincho}
EOF

# 引数がある場合は本文として追加、なければ標準入力を受け取る
if [ "$#" -gt 0 ]; then
    printf "%s " "$@" >> tmp.tex
    #echo -E "$@" >> tmp.tex
else
    cat >> tmp.tex
fi

# LaTeX の終了処理
cat >> tmp.tex << EOF
\end{document}
EOF

lualatex -interaction=nonstopmode tmp.tex > /dev/null 2>&1
# 先頭の2行を削除
sed '1,2d' tmp.txt > tmp2.txt
cat tmp2.txt

実行権限を付ける

chmod +x tecotex.sh

テコテフで文字列処理して標準出力する。

./tecotex.sh "\def\年月日#1/#2/#3;{#1年#2月#3日}\年月日2025/2/2;"
# 標準出力:2025年2月2日

組版しつつマークダウンなどを得る応用例

組版

本文を別ファイルに分離し、XeLaTeXで組版する。

組版.tex
\documentclass[a4paper,xelatex,ja=standard]{bxjsreport}
\usepackage{xeCJK}
\setCJKmainfont{IPAexMincho}
\setCJKsansfont{IPAexGothic}
\setCJKmonofont{IPAexGothic}
\XeTeXcharclass`☃=1
\font\絵文字="[NotoEmoji-VariableFont_wght.ttf]" at 12pt%https://fonts.google.com/noto/specimen/Noto+Emoji
\def\鬼{{\絵文字 👹}}
\def\TecoTeX{\leavevmode{\sf\rlap{\fontsize{0.5\baselineskip}{1.2\baselineskip}\selectfont \kern-.4em\lower.7ex \hbox{eco}}\kern-.2em\lower-.4ex\hbox{}\kern-.2em〒\rlap{\fontsize{0.5\baselineskip}{1.2\baselineskip}\selectfont \kern-.4em\lower.7ex \hbox{ex}}\kern-0.2em\lower-.4ex\hbox{}}}
\begin{document}
\input{本文}
\end{document}
本文.tex
\chapter{\TecoTeX : \TeX から文字列を標準出力へ}
\section{てこてふです}
\TecoTeX{}デス。 

再帰定義も出力できます。\par
\verb|\def\a{\b}\def\b{\def\a{\def\a{\def\a{\b}c}b}a}\a\a\a\a\a| → \def\a{\b}\def\b{\def\a{\def\a{\def\a{\b}c}b}a}\a\a\a\a\a \par
\hfill ---カヌース 『\TeX ブック』(\TeX Book 日本語版)p.271 参照

そのまま出力するには、\verb|\verb+\a+| → \verb+\a+ と書く。\verb+\def+内では使えない。

\subsection{オイラの公式だ}
☃おいらは雪だるま☃。\鬼 おいらは鬼だ\鬼\def\数{$({}^{\stackrel{\mathrm{i}}{\mathrm{\theta}}}\mathrm{e}^{\stackrel{\mathrm{i}}{\mathrm{\theta}}})$}
\数鬼 おいらは数鬼だ\数鬼 。

オイラ$$\mathrm{e}^{\mathrm{i}\theta} = \cos (\theta) + \mathrm{i}\,\sin (\theta)$$は、独立表示の数式だ。

鬼は外、福は内。

xelatexで組版する。

xelatex 組版.tex

マークダウンを得る

本文で使っている命令を上書きしマークダウン出力用のTeX環境にする様式を作る。
(用途にあわせて、このTeX定義を変えることで、他の形式にもできる。)

mdsty.tex
%MIT許諾 - 津茶利休 2025
\def\chapter#1{\par\# #1\par}%
\def\section#1{\par\#\# #1\par}%
\def\subsection#1{\par\#\#\# #1\par}%
\def\subsubsection#1{\par\#\#\#\# #1\par}%
%
\def\ドル式始#1${\scantokens{#1}\$ $} 
\def\ドルドル式始#1$${\scantokens{#1}\$\$$$} 
\everymath{\$\catcode`\~=12 \catcode`\^=12\catcode`\{=12 \catcode`\}=12\relax\ドル式始}
\everydisplay{\$\$\catcode`\~=12 \catcode`\^=12\catcode`\{=12 \catcode`\}=12\relax\ドルドル式始}
\def\theta{\mbox{\char92}theta}
\def\stackrel{\mbox{\char92}stackrel}
\def\mathrm{\mbox{\char92}mathrm}
\def\cos{\mbox{\char92}cos}
\def\sin{\mbox{\char92}sin}
\def\,{\mbox{\char92,}}

\def\verb#1{```\begingroup
  \catcode`\\=12 \catcode`\{=12 \catcode`\}=12 
  \catcode`\#=12 \catcode`\$=12 \catcode`\&=12 \catcode`\_=12
  \catcode`\~=12 \catcode`\^=12
  \def\tempa##1#1{\endgroup ##1```}% 区切り文字が現れるまで読む
  \tempa
}

\def\TeX{T<span style="margin-left: -0.2em;"><sub>E</sub></span><span style="margin-left: -0.1em;">X</span>}
\def\LuaTeX{L<span style="margin-left: -0.3em;"><sup>ua</sup></span>\TeX}

%分綴機能を切る
\hyphenpenalty=10000\relax
\exhyphenpenalty=10000\relax
\sloppy

\def\TecoTeX{テコテフ⹀TecoTeX }
\def\鬼{👹}

mdsty.tex と 本文.tex を結合し、テコテフでマークダウンに出力

cat mdsty.tex 本文.tex | ./tecotex.sh > 前書きなし.md

マークダウンをHTMLへ変換

HTMLに変換するためのマークダウンの前書き

mathjax.txt
<script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js">
</script>
<script type="text/x-mathjax-config">
 MathJax.Hub.Config({
 tex2jax: {
 inlineMath: [['$', '$'] ],
 displayMath: [ ['$$','$$'], ["\\[","\\]"] ]
 }
 });
</script>

前書きを付け、pandocで変換

cat mathjax.txt 前書きなし.md > テコ.md
pandoc -f markdown -t html テコ.md --mathjax > テコ.html

梃子となった✨️TeX✨️

ガンカーズのUNIX思想では、小さな処理器を繋げて使うことの良さが説かれています。
単純な道具でも大きな力を生み出すので、ガンカースはこのことを梃子(てこ)に喩(たと)えています。
小さな処理器を繋げて使うこととは、具体的には、殻書きにおける配管(パイプ)処理を指しています。

組版にしか使えなかったTeX言語がついに、他の応用器と管で繋いでやり取りできる算譜言語として使えるようになりました。
TeX言語は、複雑なことをさせず小さく使う分には、使いやすい文字列処理言語です。
扱いが難しいと嫌われてきたTeX言語が、✨️テコTeX✨️となり、ようやく輝きを放つ時が来たようです。

まとめ

LuaLaTeXを使うことで、命令展開後の完成された文章が文字列として得られ、TeXを文字列処理器として使えるようになりました。
使い道にあわせて、\sectionなどの命令を上書きすることで、マークダウンなどの形式をTeXから出力することもできました。
TeXの使い道が広がり、✨️TeX言語✨️の明るい未来が開かれました。

より詳しく

模倣器の例

命令展開結果を文字列として出力できるような、TeXの模倣器がすでに多く作られているようです。

これらはいずれも重要な試みですが、あくまでも模倣であり、TeXと同一の正確な動作は期待できないと思われます。
特に、字類番号catcodeの変更などの複雑な動作は模倣が難しいため対応していないものが多いようです。

正確な出力を得るためには、やはり本物のTeXからの出力を得たいところです。

TeXから取り出すことの難しさ

そこで、まず思いつくのは\write\messageの命令を使うことでしょう。
しかし、次のような無限の再帰を含む命令があるとつまづきます。

%Knuth『TeXブック』EXERCISE 20.2 より
\def\a{\b}\def\b{\def\a{\def\a{\def\a{\b}c}b}a}
\def\puzzle{\a\a\a\a\a}
\puzzle

この\puzzleは、組版処理ではabcabが印字され止まります。
しかし、\write{\puzzle}\message{\puzzle}は再帰的な命令定義を無限に繰り返してしまい失敗します。

処理記録から取り出す荒業

\tracingallで処理の追跡を入にして、記録ファイル(.log)から取り出すことも考えられますが、これもうまくいきません。
TeXもXeTeXも、印字の一文字一文字を追跡できる仕組みにはなっていないのです。

ただし、LuaTeXは、印字される文字の一文字一文字を追跡できる仕組みになっています。
処理記録を調べて取り出せば、出力を文字列として取り出すこともできます。
とはいえ、これは要らない記録を伴うためかなり遅く、悪いやり方です。

TeX4htの良さ

TeX4ht https://tug.org/tex4ht/ は、DVIを変換してHTMLを得る処理器です。
TeXから出力されたDVIを入力としているため、\puzzleのような命令が含まれていても問題なく使えます。

TeX4ht はかなり使える仕組みと言えるでしょう。
しかし、望みの文字列出力を得たい場合には、やはりTeXをうまく扱うことでTeXから直に文字列を得るのが理想的です。

LuaTeXの良さ

こんなときにLuaTeXの出番です。
TeXの内部処理に割り込むことは難しかったですが、LuaTeXで出来るようになりました。

LuaTeXは、Luaという別の言語系を載せており、処理が遅いという弱みもあります。
そのため、LaTeXや様々なTeX上言語と同じく、純粋な組版処理系としてのTeXを好む人々からは好まれません。

しかし、LuaTeXにはこのようにTeX言語の弱みを補うことができる特色があります。
これはLuaTeXの大いなる誉(ほま)れと言えるでしょう。

多言語TeXおのおのの良さ

ユニコードを礎とし多言語を扱えるTeX処理系には、XeTeXとLuaTeXがあります。

XeTeXは、処理が速く、TeXのもともとの思想に近い形の、LuaTeXと比べてより純粋な処理系です。
LuaTeXは、内部処理に割り込むことのできるより強力な処理系です。

XeTeXは、多言語の組版や通常の組版に向いています。
また、XeTeXは処理が速いので、単一図表の作成や、Manimの動画字幕で使われる短文の組版、といった応用的な使い方にも、より快適に利用できます。
他にも、SwiftLatexのように網上低級言語(ウェブアセンブリ)へ移植され、すでに網絡閲覧器上で組版できるようになるなど、多用途への利用が広がっています。

LuaTeXは、特殊な見た目の組版に向いています。
また、テコテフのように文字列を得ることや、PDFへの追加情報付け、といった特殊な用途にも応用的に利用できます。

2つの多言語TeXにはそれぞれの良さがあり、共に末永く使われることでしょう。

テコテフ

「テコテフ」の表示(〒ecoコ〒exフ)は、便り(書き物)が〒(郵便役務にみたてた配管)を通してコやフ(郵便受けにみたてた入力界面)に送られ、やり取りが繋がりゆく様を表しています。

おわりに

お読みくださりありがとうございました!
いいねやバッジを頂けますととても嬉しく思います。
ご感想、ご指摘などもお気軽にお寄せください。

Discussion