📖

TeXでソースコードを"Zennっぽく"貼り付ける.

2023/06/19に公開

はじめに

みなさん.\TeXしてますか?
\TeXしてるよってかた,\TeXにソースコードは載せたことがありますでしょうか.
ソースコードを載せる際によく使われるlstlistingですが,そのままだと殺風景ですし,色も見にくかったり今の時代にそぐわない形になってます.また,シンタックスハイライトが意図したとおりについてくれない場合もあります.

そこでソースコードを"Zenn"っぽく貼り付けて見ようと思います.

こちらを元にして書かせていただきました.非常にお世話になりました.ありがとうございます.
https://zenn.dev/kyaon/articles/68867e2657e605

学べること

この記事では以下のことに触れていきます.

  • シンタックスハイライトの設定方法全般
  • ハイライト対象の設定
  • ハイライト書式の設定
  • 区切り文字の書式設定
  • tcolorboxよる枠の装飾
  • lua言語で\TeXを定義する

環境

OverleafluaLaTeXを使用しています.luaLaTeXが必要だと明記しない限り他ののでも構わないと思います.もし,動かないなどあればご連絡ください.

色を定義する

まずは色を定義してみましょう.

\usepackage{color}
\usepackage{xcolor}


% 背景色
\definecolor{bg}{HTML}{1a2638}
% タイトルの背景色
\definecolor{titlebg}{HTML}{323e52}
% 以下シンタックスハイライト設定
% リテラルと演算記号
\definecolor{literal}{HTML}{efba69}
% 関数/変数の識別子(青)
\definecolor{identifier}{HTML}{35b7eb}
% コメント
\definecolor{comment}{HTML}{818ea0}
% 予約語
\definecolor{reserved}{HTML}{f88ca0}
% 区切り文字
\definecolor{delimiter}{HTML}{8a92b6}

言語設定

続いて,Pythonの文法を設定に落とし込みます.mypyという新しい言語設定として実装していきます.

\usepackage{etoolbox}
\usepackage{listings}

% 閉じ括弧バグ修正
\makeatletter
\patchcmd{\lsthk@SelectCharTable}{\lst@ifbreaklines\lst@Def{`)}{\lst@breakProcessOther)}\fi}{}{}{}
\makeatother

\lstdefinelanguage{mypy}{
  % リテラルと演算記号
  morekeywords=[1]{+=,=,==,!=,!,>,<,>=,<=,++,-,+,*,\%,/},
  % 予約語
  morekeywords=[2]{False,None,True,and,as,assert,async,await,break,
    class,continue,def,del,elif,else,except,finally,for,from,global,
    if,import,in,is,lambda,nonlocal,not,or,pass,raise,return,try,while,
    with,yield,print,match,case
  },
  % 識別子
  morekeywords=[3]{defined_func},
  % 区切り文字を強制的に色付け
  literate=*{.}{{\color{delimiter}.}}1
            {,}{{\color{delimiter},}}1 {:}{{\color{delimiter}:}}1
            {)}{{\color{delimiter})}}1 {(}{{\color{delimiter}(}}1
            {[}{{\color{delimiter}[}}1 {]}{{\color{delimiter}]}}1
            {\{}{{\color{delimiter}\{}}1 {\}}{{\color{delimiter}\}}}1,
  % 大文字小文字を区別
  sensitive=true,
  % 行コメントの設定
  morecomment=[l]{\#},
  % Stringリテラルの設定
  morestring=[b]{\'},
  morestring=[b]{\"},
  % 単語として扱う文字
  alsoletter={\%<>=+-*\/1234567890!},
  % 枠
  frame=none,
  % 長くなったら途中で改行
  breaklines=true,
  % 自動改行時のインデント
  breakindent=12pt,
  % 文字間隔を一定に
  columns=fixed,
  % 文字の横のサイズを小さく
  basewidth=0.5em,
  % 行番号を左に
  numbers=left,
  % 行番号の書式
  numberstyle={\scriptsize\color{white}},
  % 行番号の増加数は1=連番に
  stepnumber=1,
  % フレームの左の余白
  framexleftmargin=18pt,
  % スペースを省略せず保持
  keepspaces=true,
  % インデントサイズ
  tabsize=4,
  backgroundcolor={},                                   % 背景色=透明
  basicstyle={\small\ttfamily\color{white}},            % 通常部分の書式
  identifierstyle={\small\color{white}},                % 識別子の書式
  commentstyle={\small\color{comment}},                 % コメントの書式
  keywordstyle=[1]{\small\bfseries\color{literal}},     % リテラルと演算記号の書式
  keywordstyle=[2]{\small\bfseries\color{reserved}},    % 予約語の書式
  keywordstyle=[3]{\small\bfseries\color{identifier}},  % 自分で定義した識別子の書式
  stringstyle={\small\ttfamily\color{literal}},         % 文字列の書式
}

なっが!えぇ,申し訳ありませんが長いです.それなりにコメントは付けたつもりですが色んな所から引っ張ってきていますので中には意味をなしてないパラメータもあるかもしれません.

みなさんがカスタマイズするときのことを考えて3点ほど補足したいと思います.

morekeywords

まず,listingsはスペースや区切り文字を目印に単語を認識しています.その単語がmorekeywordsに登録されていれば,keywordstyleに設定した書式が適応されます.[1][2]は単語の種別です.番号に意味はなくてただのグループ番号です.上のコードでは[1]がリテラルと演算記号,[2]が予約語,[3]が自分で定義した識別子です.

listingsは言語の文法を理解することはできません.ので,これが関数定義だ,これは関数の呼び出しだ,と判別することはできません.(私が思いつかないだけでなにか方法があるかもしれないですが.)下のコードを見てください.

def defined_func():
  print("ハクション!")

defined_func()

defined_func関数の定義時には青になっていますが,最後の呼び出しは白色のままです.これに対応できませんでした...そこで開き直って「呼び出しでも青くしちゃえばいいじゃん.そっちのほうがわかりやすくない?」ということで,morekeywords[3]にハイライトしたい関数名や変数名を入れられるようにしました.何をハイライトするそこはお好みでいいと思います.これがZennっぽいに留まってしまう理由の1つです.

1つということは当然2つ目もあります.それが数字がハイライトされないということです.morekeywordsは「単語単位」でハイライトを行いますのでalsoletterで数字を登録しても実際にすべての数字を単語としてmorekeywordsに列挙しなければなりません.後述しますが,literateで強制的に変えることもできます.が,変数名の数字やコメントの数字もこれの対象となってしまいます.luaLaTeX以外の方はお手上げですので,ソースコードで使った数字だけでもmorekeywords[1]に追加してください.ん?luaLaTeXだったら?それは後ほどのお楽しみです.

ともかく,登録した語句をmorekeywordsにそれぞれ適切なところに追加すればハイライトされるようになります.

literate

literateは強制的に文字を置き換える命令です.literate=*の後に

{置き換える文字}{置き換え先}文字数

を羅列して書くことで置き換えられます.主に(){},,といったいわゆる「区切り文字」を登録しています.;もこれに当たると思います.ただこれにバグがありまして,),]といった閉じ括弧がなぜか登録されません.それを修正しているのが% 閉じ括弧バグ修正の部分です.詳しくは参考リンクをご覧ください.

alsoletter

alsoletterは本来,単語として認識されない文字を認識させるパラメータです.{}内のすべての文字が単語を形成するようになり,morekeywordsに含めることができるようになります.

枠設定

先程,背景色を透明にし,枠も設定しておりません.なぜなら枠と背景色は別途tcolorboxで作ることで自由度が高くなり角を丸くしたりタイトルをつけることができるようになるからです.

\usepackage{tcolorbox}
\tcbuselibrary{breakable, skins}

\newtcolorbox{zennwt}[2][]{
    enhanced,                         %tikzを用いた記法の処理
    left=22pt,right=22pt,             %box内左右の余白
    fonttitle=\small,                 %タイトルの書式
    coltitle=white,                   %タイトルの文字の色
    colbacktitle=titlebg,             %タイトルの背景の色
    attach boxed title to top left={},%タイトルを左寄せに,少し微調整
    boxed title style={               %タイトルボックスの装飾
      skin=enhancedfirst jigsaw,
      arc=1.5mm,                      %タイトルボックスの角の弧
      bottom=0mm,
      boxrule=0mm
    },
    boxrule=0pt,                      %枠線の太さ
    colback=bg,                       %本文の背景色
    colframe=bg,                      %本文の枠の色
    sharp corners=northwest,          %左上の角の調整
    breakable,                        %ページ跨ぎOK
    title=\texttt{#2},                %タイトル
    arc=2.5mm,                        %角の弧の背景
    #1
}

\newtcolorbox{zenn}[1][]{
    left=22pt,right=22pt,
    boxrule=0pt,
    colback=bg,
    colframe=bg,
    breakable,
    arc=2.5mm,
    #1
}

運良く,"Zennっぽい"枠の作り方がありましたのでほとんどパクリです.(参考リンク参照)
タイトル付きとタイトル無しを作ってみました.タイトルというのは

これのことです.

使い方

ここまで来たら完成です!すごい量でしたね...早速使ってみましょう.

\begin{zennwt}{test.py}
\begin{lstlisting}[language= mypy]
# プリミティブ型の例
num1 = 10
num2 = num1

print("num1:", num1)  # 出力: num1: 10
print("num2:", num2)  # 出力: num2: 10

num1 = 20

print("num1:", num1)  # 出力: num1: 20
print("num2:", num2)  # 出力: num2: 10 (num2は変更されていない)

# 参照型の例
list1 = [1, 2, 3]
list2 = list1

print("list1:", list1)  # 出力: list1: [1, 2, 3]
print("list2:", list2)  # 出力: list2: [1, 2, 3]

list1.append(4)

print("list1:", list1)  # 出力: list1: [1, 2, 3, 4] (list1が変更された)
print("list2:", list2)  # 出力: list2: [1, 2, 3, 4] (list2も同じ変更が反映されている)
\end{lstlisting}
\end{zennwt}

\begin{zenn}
\begin{lstlisting}[language= mypy]
# 関数の定義
def my_func():
  print("Hello, World!")
\end{lstlisting}
\end{zenn}

やっぱり数字も含めたいよなぁ!

やっぱ数字もハイライトしたいなぁ...手でいちいち入力するのだるいなぁ...
わかりますわかります!luaLaTeXを使ってるそこのあなたのために頑張っちゃいました!下のように定義丸々をluaにやらせばいいんです!\lstdefinelanguageの定義全体を以下のように置き換えます.

\usepackage{luacode}

\begin{luacode*}
function init()
  local numlist = ""
  for i = -100, 1000 do
    numlist = numlist .. tostring(i)
    if i < 1000 then
      numlist = numlist .. ","
    end
  end
  output = [[
  \lstdefinelanguage{mypy}{
  morekeywords=[1]{+=,=,==,!=,!,>,<,>=,<=,++,-,+,*,\%,/,]]..numlist..[[},
  morekeywords=[2]{False,None,True,and,as,assert,async,await,break,class,continue,def,del,elif,else,except,finally,for,from,global,if,import,in,is,lambda,nonlocal,not,or,pass,raise,return,try,while,with,yield,print,match,case},
  morekeywords=[3]{defined_func},
  literate=*{.}{{\color{delimiter}.}}1
            {,}{{\color{delimiter},}}1 {:}{{\color{delimiter}:}}1
            {)}{{\color{delimiter})}}1 {(}{{\color{delimiter}(}}1
            {[}{{\color{delimiter}[}}1 {]}{{\color{delimiter}]}}1
            {\{}{{\color{delimiter}\{}}1 {\}}{{\color{delimiter}\}}}1,
  sensitive=true,
  morecomment=[l]{\#},
  morestring=[b]{\'},
  morestring=[b]{\"},
  alsoletter={\%<>=+-*\/1234567890!},
  frame=none,
  breaklines=true,
  breakindent=12pt,
  columns=fixed,
  basewidth=0.5em,
  numbers=left,
  numberstyle={\scriptsize\color{white}},
  stepnumber=1,
  framexleftmargin=18pt,
  keepspaces=true,
  lineskip=-0.1ex,
  tabsize=4,
  backgroundcolor={},
  basicstyle={\small\ttfamily\color{white}},
  identifierstyle={\small\color{white}},
  commentstyle={\small\color{comment}},
  keywordstyle=[1]{\small\bfseries\color{literal}},
  keywordstyle=[2]{\small\bfseries\color{reserved}},
  keywordstyle=[3]{\small\bfseries\color{identifier}},
  keywordstyle=[4]{\small\bfseries\color{delimiter}},
  keywordstyle=[5]{\small\bfseries\color{keyword4}},
  stringstyle={\small\ttfamily\color{literal}},}
  ]]
  output_ = string.gsub(output, "\n", "")
  tex.sprint(output_)
end
\end{luacode*}

% 実行
\directlua{init()}

まず,-100から1000まで数え上げてそれらすべてを1,2,3...という形にします.あとは[[]]の中に先程書いたmypyの定義を入れて,数え上げた文字列をぶちこみます.このままだと改行が多く,謎のエラーが起こってしまうのでstring.gsubで改行をすべて消し,tex.sprinr()で出力します.というような関数init()で定義し\directlua{init()}でそれを実行しています.出力された文字はあたかも\TeXのコードのように振る舞い,\TeXのタイプセットがそれを実際に出力内容を実行してくれます.

当然,forの範囲が広くなるとタイプセットが遅くなるという欠点はありますが,それでもいちいち値を入力するのよりはマシです.ものすごく力技ですが全てはmorekeywordsでコマンドが展開しないことが原因です.

一部のエディタではシンタックスハイライトがイかれる可能性があります.

シンタックスハイライトを整備してたらこっちのハイライトがイかれちゃう.

ということで本日のオチでした.

最終的に以下のように表示されます.

まとめ

ということで,TeXにソースコードを"Zennっぽく"表示してみました.まだまだ改善の余地がありそうですがある程度形になったと思います.これで美文書を制作したらめっちゃ気持ちよさそうですね.では良き\TeXライフを!

参考リンク

ほとんどコードはパクらせて頂いて,修正や最適化を行いました.ありがとうございました.

元ネタ

https://zenn.dev/kyaon/articles/68867e2657e605

listingについて

https://e8l.hatenablog.com/entry/2015/11/29/232800

http://xyoshiki.web.fc2.com/tex/listings.html

https://whitecat-student.hatenablog.com/entry/2016/09/05/180705

https://www.reddit.com/r/LaTeX/comments/u98usu/listings_using_literate_to_color_single_symbols/

閉じ括弧のバグ修正

https://tex.stackexchange.com/questions/69472/closing-parenthesis-as-delimiter-not-matched-when-breaklines-true

tcolorboxについて

https://texmedicine.hatenadiary.jp/entry/2015/12/17/000339

luaについて

https://qiita.com/zr_tex8r/items/8fad42e4b8bad284e984

GitHubで編集を提案

Discussion