TeXでソースコードを"Zennっぽく"貼り付ける.
はじめに
みなさん.
ソースコードを載せる際によく使われるlstlisting
ですが,そのままだと殺風景ですし,色も見にくかったり今の時代にそぐわない形になってます.また,シンタックスハイライトが意図したとおりについてくれない場合もあります.
そこでソースコードを"Zenn"っぽく貼り付けて見ようと思います.
こちらを元にして書かせていただきました.非常にお世話になりました.ありがとうございます.
学べること
この記事では以下のことに触れていきます.
- シンタックスハイライトの設定方法全般
- ハイライト対象の設定
- ハイライト書式の設定
- 区切り文字の書式設定
-
tcolorbox
よる枠の装飾 -
lua
言語で を定義する\TeX
環境
Overleaf
でluaLaTeX
を使用しています.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()}
でそれを実行しています.出力された文字はあたかも
当然,for
の範囲が広くなるとタイプセットが遅くなるという欠点はありますが,それでもいちいち値を入力するのよりはマシです.ものすごく力技ですが全てはmorekeywords
でコマンドが展開しないことが原因です.
一部のエディタではシンタックスハイライトがイかれる可能性があります.
シンタックスハイライトを整備してたらこっちのハイライトがイかれちゃう.
ということで本日のオチでした.
最終的に以下のように表示されます.
まとめ
ということで,
参考リンク
ほとんどコードはパクらせて頂いて,修正や最適化を行いました.ありがとうございました.
元ネタ
listing
について
閉じ括弧のバグ修正
tcolorbox
について
lua
について
Discussion