閉じる

UI言語[UI Language]

記事自体は翻訳されません! 記事によって英語版があったりなかったりします。翻訳がある記事は文頭に記載があるよ!
Each articles themselves will not be translated by this setting. Some of article has translation and some of them doesn't. You will notice if the article has its translation by its preamble!

テーマ[Theme]


アイキャッチ画像

JavaScriptで簡単なソースコードの構文強調表示機能を自作した話


ニーズに合う構文強調表示ライブラリが無い

Hugoに標準搭載の構文強調表示機能(Chroma)(このブログはHugoを使っています)、シンタックスハイライト用ライブラリとして有名なSyntaxHighliterPrism.jshighlight.jsも使ってはみたんですが、どれもしっくりきませんでした。
個人的には、下記のような機能が欲しく……

Syntax Highlighting Sourcecode
Syntax Highlighting Sourcecode
  1. 行番号は必須。そしてその幅が簡単に調整できる。

  2. 全体の幅も記事ページに簡単に合わせることができ、パディング調整も可能。

  3. 長い行は折り返され、それによって行番号がズレない。

  4. タブも表示できる。

  5. Hugoのショートコード(Go Template)構文に対応している。

これらを標準で満たしてくれる構文強調表示ライブラリが無かったのです。
後はまぁ、ライブラリは仰々しすぎるのもあって。百種類以上の言語に対応している必要もないし、任意の行をハイライトする機能も要らないし。万人向けにしっかりと動くよう堅牢に作られているので、痒い所に手が届かないと言うか、ちょっとだけ変えたいみたいな事がなかなか難しい。Prism.jsやhighlight.jsを使って自分のブログのデザインに合うようCSSをこねくり回してはみたんですが、結局満足のゆく見た目を実現することは出来ませんでした……

というわけで、ちょっと頑張って自分で作ることにしました。ちょーテキトーな仕組みなのでソースコードによってはうまく表示できないでしょうが、自分のブログ内の限られたコードが見栄えすれば良いだけなので困りません。アルゴリズムは超絶シンプルで、ソースコードはたった600行弱。何しろ自分で作ったものですから、カスタマイズも自由自在。
とりあえず、出来たものはこんな感じです。便宜上、この記事ではこれを 『なんちゃってシンタックスハイライト』と呼びます。

class LoremIpsum
{
	public static void main(String[] args)
	{
		System.out.println("Lorem ipsum, dolor sit amet consectetur adipisicing elit. Commodi exercitationem repellendus iure incidunt aperiam,");
	}
}

記事本文の左パディングと行番号エリアの幅が揃うようにしてあります。スマホでは行番号を非表示にしてありますが、やはり本文の左端とコードの左端が揃っているはず。
長い行はちゃんと折り返されていますし、行番号もズレていません。見やすいよう行の背景色を1行ずつ交互に変えているんですが、折り返した行は同じ色なのもお気に入りポイント。オマケでクリップボードへのコピーボタンなんかも付けてみました。
あとは当ブログ、ページ一番上の右上にあるボタンからテーマを切り替えられるんですが、ライト/ダークモード、読書モード、そしてハイコントラストモードに合わせてソースコードも綺麗に色分けされます。少なくとも見た目は悪くないでしょ?😚


言語ごとの表示サンプル

対応している言語はたったの5つ、自分が、さらに言えばこのブログで必要だったものだけです。でもHugoのショートコードに対応した構文強調表示ライブラリはそうそう無いはず。
あ、下記各コードはダミーなので気にしないで下さい。

Java(Processing)

void setup()
{
	size(1500, 1500);
	noSmooth();
	fontAtlasImage = loadImage("漢字タイポス12px.png");

	ControlP5 cp5 = new ControlP5(this);
	ControlFont font = new ControlFont(createFont("PICO-8.ttf", 16));
	final int buttonWidth = 300;
	final int buttonSpace = 40;
	int x = 20;
	cp5.addButton("ExportImages").setLabel("Export Images").setSize(buttonWidth, 80).setPosition(x, 20).setFont(font);
	x += buttonWidth + buttonSpace;
	cp5.addButton("SaveToFile").setLabel("Save Info").setSize(buttonWidth, 80).setPosition(x, 20).setFont(font);
	x += buttonWidth + buttonSpace;
	cp5.addButton("LoadFromFile").setLabel("Load Info").setSize(buttonWidth, 80).setPosition(x, 20).setFont(font);
	x += buttonWidth + buttonSpace;
}

Javaと言うか、正確にはProcessingです。自分はもうJavaを書くことはないので…… Android開発はまだJavaが現役でしょうか。自分も昔はAndroidとか、それこそiアプリとかアプレットとか書くのに使ってました。
おっと話が逸れてしまいました。欲を言えば折り返しをディープインデントにしたかったんですが、方法が見つからず。折り返し処理はHTMLに任せているので…… CSSの疑似要素::first-linepaddingとかが指定できれば行けそうなんだけどなぁ。


Hugo(Go Template)

<pre class="syntax-highlight-placeholder" data-syntax="{{$syntax}}"><code>
	{{- $inner := .Inner -}}
	{{- if $inner -}}
		{{- /* ショートコード直後の空白、タブ、改行の削除。 */ -}}
		{{- $code := trim .Inner "\r\n\t" -}}
		{{- $code | safeHTML -}}
	{{- else -}}
		{{- $codeFile := .Get "file" -}}
		{{- $codeFile = print "static/" site.Params.postImageDir "/" .Page.File.ContentBaseName "/" $codeFile -}}
		{{- readFile $codeFile -}}
	{{- end -}}
</code></pre>

地味に頑張ったやつです。Hugoのテンプレートをここまでしっかりと構文強調表示してくれるライブラリはそうそう無いのでは?(自画自賛) VSCodeもHugoのファイル、これくらい綺麗に色分けしてくれると嬉しいんだけどなー。


CSS(SASS)

td.codeline[lang=ja]
{
	word-break: break-all;
	padding-left: 0.5em;
	padding-right: var(--post-horizontal-margin);
	mask-image: url('angle-right.svg');

	// モバイルレイアウトだと行番号は非表示なので本文と同じパディング
	@include phone
	{
		padding-left: var(--post-horizontal-margin);
		padding-right: var(--post-horizontal-margin);
		background-color: #808080;
	}

	pre, code
	{
		text-align: left;
		white-space: pre-wrap;
		color: #ff0000;
	}
}

CSSは要素(タグ)セレクタとクラスセレクタを色分けしてくれて、あとはプロパティの名前と値を別の色で表示してくれれば十分かなって。せっかくなので値の中のvarも色分けするようにしてみました。一応SASSのコメントも色分けできるようにはしましたが、メディアクエリとか複雑な構文には対応してません。
background-colorcolorで実際の色を表示する機能は完全にオマケ。こういう賑やかし好きです。Prism.jsだと同等の機能がInline Colorというプラグインで実装できます。


JSON(JSONC)

{
	// コメント
	"Romly-RegexpSearchTemplate.templates":
	[
		{
			"label": "C#",
			"templates":
			[
				{
					"name": "C# class name",
					"label": "$(symbol-class) $1",
					"pattern" : "^\\s*(?:private|public)\\sclass\\s([\\w]+)\\s\\:",
					"label_index" : 1
				},
				{
					"name": "C# default method",
					"label": "    $(symbol-method) $1$3",
					"pattern" : "^\\s*(private\\s|public\\s|)([\\w]+)\\s([\\w]+)\\(",
					"label_index" : 3
				}
			],
			"extensions": [".cs"]
		}
	]
}

JSONフォーマットもコメントを色分けできるようにしたので、JSONCも表示できます。


Terminal

hugo server -D -F --bind 192.168.0.7 --baseURL=http://192.168.0.7/blog/

ターミナルのコマンドはWindowsのPowerShellみたくコマンド名とオプションが別の色で表示されるようにしてみました。一番最初の単語をコマンド名、他は-(ハイフン)で始まるものをオプションと判定しているだけなので、他の言語に比べたら圧倒的にシンプルな処理です。


仕組み

出力するHTMLについて

Webページ用の構文強調表示ライブラリは構文解析そのものの他に、HTML上でどうやって表示させるか(どういうHTMLを吐き出すのか)も、CSSで見た目をカスタマイズしようとした時に重要なポイントになります。 『なんちゃってシンタックスハイライト』を作るにあたって、既存のライブラリが出力するHTMLを調査してみました。

SyntaxHighliter

SyntaxHighliterは改行で区切られた全ての行番号を含むセル、ソースコード全体を含むセルのたった2つのセルを持つ<table>タグを使っていて、ソースコードの各行は<div>タグで別れているというやや特殊な構造でした。さらにそれぞれの一行分のソースコードの各トークンは<code>タグに囲まれていました。
この方法だとページの幅に合わせたソースコードの折り返しは無理ですよね…… 行番号が絶対ずれちゃう。

Prism.js

Prism.jsはソースコードをHTMLで表示する時の定石である<pre><code>内にソースコード全体が入っていました。コードの各トークンは<span>で囲まれ、トークンの種類に合わせたクラス名が指定されているようです。
Line Numbersプラグインにより行番号表示に対応しますが、その行番号は<code>の一番最後の子要素として全ての行番号が入った<span>タグがposition: absolute;スタイルで追加されるという特殊な形でした。
この方法もやはり、行番号とソースコードの各行が完全に独立してしまっているので、折り返し表示は難しいですね。

highlight.js

highlight.jsはPrism.jsと同様<pre><code>で始まりますが、<code>の中には<table>があり、ソースコードの1行ごとに<tr>が作られています。
この方法であれば行が折り返されてもその行を示す<tr>の高さが広がるだけなので、行番号がズレてしまうことはなさそうです。

『なんちゃってシンタックスハイライト』での実装方法

さて、 『なんちゃってシンタックスハイライト』は折り返しや行番号を考慮した結果、highlight.jsの様に<table>タグを使ってソースコードの行を<tr>に対応させ、行番号とソースコード一行分を隣り合うセルにしました。ただし全体を<pre><code>タグで囲むことはしません。<code>の中に<table>が入ってるのもなんだか変ですしね。ソースコードの各トークンは<span>で囲み、トークンの種類に合わせたCSSクラスを設定するようにします。
これにより行番号の幅がCSSで簡単に調節でき、なおかつその行のコードが長いために折り返されても行番号がズレない表示が実現できます。折り返したくない場合は、CSSで折返しを禁止して<table>overflow: scroll;あたりを設定すれば簡単に横にスクロール表示にも対応できるハズ。

class LoremIpsum
{
	public static void main(String[] args)
	{
		System.out.println("Lorem ipsum, dolor sit amet consectetur adipisicing elit. Commodi exercitationem repellendus iure incidunt aperiam,");
	}
}

上の構文強調表示であれば、下記のようなHTML(と適当なCSS)で実現されているというわけです。単語ごとに<span>で囲まれるような形になるので、HTMLソースはどうしてもしますが、俯瞰すればソースコード一行ごとに<tr>というシンプルな構造が見えてくると思います。

<div class="codeblock">
	<table class="codeblock-table">
		<colgroup>
			<col class="linenumber-column">
			<col class="code-column">
		</colgroup>
		<tbody>
			<tr class="odd-line">
				<td class="linenumber">1</td>
				<td class="codeline"><pre><code><span class="reserved-word">class</span><span class="text"> </span><span class="class-name">LoremIpsum</span></code></pre></td>
			</tr>
			<tr class="even-line">
				<td class="linenumber">2</td>
				<td class="codeline"><pre><code><span class="punctuation">{</span></code></pre></td>
			</tr>
			<tr class="odd-line">
				<td class="linenumber">3</td>
				<td class="codeline"><pre><code><span class="tab">	</span><span class="reserved-word">public static void </span><span class="function-name">main</span><span class="punctuation">(</span><span class="class-name">String</span><span class="text">[] args</span><span class="punctuation">)</span></code></pre></td>
			</tr>
			<tr class="even-line">
				<td class="linenumber">4</td>
				<td class="codeline"><pre><code><span class="tab">	</span><span class="punctuation">{</span></code></pre></td>
			</tr>
			<tr class="odd-line">
				<td class="linenumber">5</td>
				<td class="codeline"><pre><code><span class="tab">	</span><span class="tab">	</span><span class="text">System</span><span class="symbol">.</span><span class="text">out</span><span class="symbol">.</span><span class="text">println</span><span class="punctuation">(</span><span class="string">"Lorem ipsum, dolor sit amet consectetur adipisicing elit. Commodi exercitationem repellendus iure incidunt aperiam,"</span><span class="punctuation">)</span><span class="symbol">;</span></code></pre></td>
			</tr>
			<tr class="even-line">
				<td class="linenumber">6</td>
				<td class="codeline"><pre><code><span class="tab">	</span><span class="punctuation">}</span></code></pre></td>
			</tr>
			<tr class="odd-line">
				<td class="linenumber">7</td>
				<td class="codeline"><pre><code><span class="punctuation">}</span></code></pre></td>
			</tr>
		</tbody>
	</table>
	<!--コピーボタン-->
	<button type="button" class="sourcecode-copy-button" onclick="copyCodeToClipboard()"><div class="icon"></div><div>COPY</div></button>
</div>

各行を<pre><code>で囲んでいるのはセマンティックHTMLの呪縛と言うか(笑)。ここまで見た目重視で<table>タグ使っておいて今更セマンティックもへちまも無いだろと自分でも思うんですけどね(笑。あとで消そ……
って言うか、コピーボタン周りとかも無駄が多いHTMLですね。この辺もあとで見直そっと。


構文解析

ではどうやってソースコードを前段のHTMLに変換しているかですが、構文解析と言うほど大層な事はしていません。まず任意の正規表現その正規表現がマッチした時のCSSクラス名、この2つの値のセットを各要素とする配列を用意します。これが構文解析パターンになり、この配列は言語ごとに中身が異なります。便宜上、今後これを 『構文解析パターンリスト』と呼びます。

与えられたソースコード文字列に対して、先程の 『構文解析パターンリスト』から順番に正規表現をテストします。この時、取りこぼしが無いよう、必ずソースコードの先頭からテスト(パターンの頭に先頭マッチの^を必ず付与する)させます。マッチしたら<span>で囲って"class"正規表現に対応するCSSクラス名を付与し、結果格納用のHTML文字列に足します。
マッチした範囲はソースコードから削除し、再び 『構文解析パターンリスト』の最初の要素からテストを繰り返していきます。リスト内のいずれにもマッチしなかった場合は、単なる文字とみなして適当なCSSクラス名を付与した<span>で囲い、一文字削除します。
こうしてソースコード文字列を先頭から繰り返し処理していき、空になった時、結果格納用のHTML文字列には構文強調表示のためのHTMLが出来上がっているという寸法です。
しかしこれだけだと、複雑な構文への対応が難しいです。

構文解析パターンリストのスタック化

// Terminalの解析ルール
const SYNTAX_RULE_TERMINAL =
[
	[RegExp(/^-\S+/), CSS_CLASS_NAME_TERMINAL_FLAGS],
	[RegExp(/^\S+/), CSS_CLASS_NAME_RESERVED_WORD]
];

上記はTerminal構文ソースコード用の正規表現とクラス名のリストです。
ターミナルのコマンドのようなシンプルな構文の処理はこれで問題ありませんが、プログラミング言語のスコープのような構造を持つ構文に対応した 『構文解析パターンリスト』を用意するのが難しくなります。
例えば、Hugoのショートコードの中にはHTMLタグとGo Templateのコードが入り乱れるわけで、HTMLタグの< >内と、Go Templateの言語である{{ }}の中では構文が全く異なります。

そこで、先程の 『構文解析パターンリスト』を、< >の内部用{{ }}の内部用、そしてそのどちらでもない場合用(通常用)と、計3つ用意しておき、使用する 『構文解析パターンリスト』を後入れ先出しのスタック構造で保持するようにします。
構文解析開始時は通常用の構文解析パターンをスタックにプッシュしておきます。この構文解析パターンでは、{{が見つかったら{{ }}内部用の構文解析パターンをスタックにプッシュするようにします。するとそれ以後のソースコードはGo Templateの言語として解析できます。先程とは逆に、{{ }}内部用の構文解析パターンでは、}}が見つかったらスタックをポップするようにしておきます。こうすることで}}が現れたらGo Templateとしての解析を辞め、通常用の解析に戻れます。HTMLの< >内部用構文解析パターン<でプッシュ、>でポップという同様の処理を行います。

const ANALYZE_SHORTCODE_PATTERNS =
{
	'outside': [
		// 3番目の要素がこのパターンにマッチした時にスタックにプッシュする構文解析パターンリストの名前
		[RegExp('^{{-?'), CLASS_NAME_SHORTCODE_PUNC, 'inside_hugocode'],
		[RegExp('^</?'), CLASS_NAME_HTML_TAG_PUNK, 'inside_html_tag']
	],

	// Hugoの構文解析パターンリスト
	'inside_hugocode': [
		// }}が見つかったらスタックを一つ戻る
		[RegExp('^-?}}'), CLASS_NAME_SHORTCODE_PUNC, POP_STACK],
		//
		// ここにHugoのコードの解析パターンを追加していく
		//
	],

	// HTMLタグの構文解析パターンリスト
	'inside_html_tag': [
		// >が見つかったらスタックを一つ戻る
		[RegExp('^>'), 'tag-punctuation', POP_STACK],
		//
		// ここにHTMLタグの解析パターンを追加していく
		//
	],
};

スタック構造という大変シンプルな味付けですが効果は絶大で、Hugoのショートコードのようにソースコードの中で構文解析パターンを大きく切り替えなければならない場合の他にも、doSomething1(doSomething2(param));のように、メソッドの引数にメソッドを渡すような再帰的な構造の解析時も、 “メソッドの引数部分を解析するパターン”をスタックしていくことで簡単に対応できるようになります。
また、引用符で囲まれた文字列リテラルのような “正規表現で表せない事はないがちょっと面倒”みたいな部分の解析も、"を見つけたら文字列を解析するパターンをプッシュ → 文字列を解析するパターンでは"のエスケープの有無だけに注視し、エスケープなしの"が見つかったらスタックをポップ、という流れにすることで、複雑な正規表現を用意する必要がなくなります。具体的には下記のような形(便宜上、文字列しか解析していないサンプルです)。

const ANALYZE_STRING_PATTERNS =
{
	// 文字列の外側を解析する構文解析パターンリスト
	// 引用符を見つけたら文字列内部を解析するパターンをプッシュする
	'outside_string': [
		// 3番目の要素がこのパターンにマッチした時にプッシュする構文解析パターンリストの名前
		[RegExp('^"'), CSS_CLASS_NAME_STRING, 'inside_string_literal']
	],

	// 文字列の内側を解析する構文解析パターンリスト
	// エスケープされていない引用符が見つかったらスタックをポップして外側の解析に戻る
	'inside_string_literal': [
		// エスケープされた引用符は無視する
		[RegExp('^\\\\"'), CSS_CLASS_NAME_STRING, KEEP_STACK],
		// エスケープされていない引用符が見つかったらスタックをポップ
		[RegExp('^"'), CSS_CLASS_NAME_STRING, POP_STACK]
	]
};

以上、アルゴリズム的な仕組みは基本的にこれだけ。
実際に使用しているコードでは、一つの正規表現でマッチしたグループごとに違うCSSクラス名を割り当てられるようにしたり、結果を格納するHTMLに足す時にCSSクラス名が同じなら繋げる"class"の値が同じなのに一文字ごとに<span>で囲まれてしまうのを防ぐ)などの効率化や、前述のCSSカラー値のサンプルを表示するといった特殊な処理を加えていますが、本質的には関係ない部分なので省きました。

このシンプルなアルゴリズムで表示サンプルでお見せしたようなカラフルなシンタックスハイライトを実現しています。悪くないでしょ。😎


動的な変換にも対応

これでソースコードを<table>タグで構成されたHTMLに変換する処理が完成したので、しばらくはそのまま静的な運用をしていました。つまり、記事中にソースコードを入れたい時は手元で事前に変換 → 出力されたHTMLを記事内に貼り付けるという手順です。この方法なら記事ページのHTMLにJavaScriptは必要なく、見栄えを良くするCSSを読み込むだけで綺麗に表示されます。

しかし程なくして 「やっぱりページを表示した時に動的に変換したいな」と思うようになりました。ソースコードに修正があった時にいちいち変換の手順を踏むのは面倒くさい…… 構文解析プログラムにバグがあったり、機能を追加した時も同様です。過去にHTMLに変換したソースコードを再び変換にかけなければなりません。

というわけで、表示ページにもJavaScriptを仕込んで、その場でソースコードをHTMLに変換するようにしました。この処理は簡単で、ページのHTMLにはプレーンなソースコードを<pre class="placeholder"><code>で囲んで書いておき、document.getElementsByClassName()innerTextプロパティを使ってソースコードを文字列として取得し変換、replaceChildなりでDOMに戻すようにしました。

HTMLに書いておくソースコード

<pre><code class="placeholder">&lt;strong&gt;Hugo&lt;/strong&gt; version is: &lt;em&gt;&lbrace;&lbrace;hugo.Version&rbrace;&rbrace;&lt;/em&gt;.</code></pre>

それを構文強調表示に差し替えるJavaScript

// DOM読み込み完了時に実行するよ
window.addEventListener("DOMContentLoaded", function ()
{
	// ソースコードが書かれているすべての要素を見つける
	const placeholders = document.getElementsByClassName('placeholder');

	let targets = []
	for (let i = 0; i < placeholders.length; i++)
		targets.push(placeholders[i]);

	targets.forEach(element =>
	{
		// ソースコードを取得してアンエスケープ
		let sourceCode = element.getElementsByTagName('code')[0].innerHTML;
		sourceCode = unescapeHtml(sourceCode);

		// 構文強調表示したhtml文字列に変換
		const htmlString = '<div class="codeblock">' + sourceCodeToHighlightedHtml(sourceCode) + '</div>';

		// 元のソースコードが書かれていた要素と入れ替える
		const divTag = document.createElement('div');
		divTag.innerHTML = htmlString;
		element.parentElement.replaceChild(divTag, element);
	});
});

実際の表示

<strong>Hugo</strong> version is: <em>{{hugo.Version}}</em>.

これでHTMLには生のソースコードを書いておけば、ページ表示時に都度、構文強調処理が走って表示されます。 『なんちゃってシンタックスハイライト』にバグが見つかっても、プログラムだけ修正しておけばよく、一度変換してしまったソースコードを再び変換にかける必要はなくりました。
ただし、HTMLにソースコードを書くときは、上記のようにHTMLタグの<>や、Go Templateの命令開始の括弧である{{等をエスケープしておかなければなりません(さもないとHTMLタグやショートコードとして解釈されてしまう)。


Lighthouseを考慮した遅延機能

構文強調表示とは直接関係ない処理ですが、Lighthouseのスコアをちょっとでも稼ごうと、遅延実行にも対応させました。具体的には、ページを読み込んだ段階では構文強調処理を行わず、ユーザーがページをスクロールしてソースコードが表示された時に初めて処理するようにします。
この遅延処理はJavaScript標準のIntersectionObserverというもので簡単に対応できました。

下記のようなコードで簡単に該当要素が画面に入った(見えるようになった)時にコールバック関数(このコードではintersectCallback)を実行してくれます。

const theIntersectionObserver = new IntersectionObserver(intersectCallback, {
	root: null,			// nullの場合はブラウザのビューポートとの交差を判定
	rootMargin: '0px',	// ビューポートのマージン
	threshold: 0.1		// 要素が全体のどのくらい見えたらコールバックを実行するか
});

// DOM読み込み完了時に実行するよ
window.addEventListener("DOMContentLoaded", function ()
{
	// ソースコードが書かれているすべての要素を見つける
	const placeholders = document.getElementsByClassName('placeholder');

	let targets = []
	for (let i = 0; i < placeholders.length; i++)
		targets.push(placeholders[i]);

	// 見つかった要素を監視して見えた時にコールバックが呼び出されるようにする
	targets.forEach(element => theIntersectionObserver.observe(element));
});

このAPIを使い、 『なんちゃってシンタックスハイライト』では、ソースコードを表示している<pre><code>タグが画面に入ったタイミングで構文強調表示処理を行うJavaScriptファイルの読み込み(<body>への<script>タグの追加)、処理結果を綺麗に表示するためのCSSの読み込み(<head>要素への<link>タグの追加)を行うようにしました。DOMに追加されたJavaScriptファイルの読み込みが完了すると、構文強調表示処理が実行され、CSSが読み込まれ次第シンタックスハイライトが綺麗に表示されます。

また一つのページにソースコードが複数ある場合にJavaScriptとCSSの読み込み(同じ<script>タグと<link>タグの追加)が何度も実行されてはまずいので、フラグで2回目移行は読み込まないようにしました。


Hugoのショートコードを大いに活用

『なんちゃってシンタックスハイライト』はCSSクラス名で処理を行うべきソースコードを見つけるため、必ず既定のCSSクラス名をタイプミスせずに入力し、決められた構造のHTMLの中にソースコードを書く必要があります。
<ruby>タグを使わない自前の綺麗なルビを作った時もそうだったのですが、こういう作業はHugoのショートコード機能が大活躍します。いわばHTMLに関数的なものを使えるようになるのがHugoのショートコードなのです。HTMLの仕様に標準であればいいのにとすら思います。

というわけで、下記の様な簡単なショートコードを作りました。引数で言語を指定したりもしています。ショートコードの典型的な活用方法といった趣です。

{{- $syntax := .Get "syntax" -}}
<pre class="placeholder" data-syntax="{{$syntax}}"><code>
	{{- /* ソースコード前後の空白、タブ、改行を削除してから出力。 */ -}}
	{{- $code := trim .Inner "\r\n\t" -}}
	{{- $code | safeHTML -}}
</code></pre>

ただし、上記の書き方だと前述の通り<>や、{{などをエスケープして書かなければならない点がショートコードを使わずにベタ打ちする時と変わりません。単純な置換で済むとは言えちょっと面倒なので、ショートコードのメリットを大いに活用し、ソースコードを外部ファイルから読み込めるようにしました。

{{- $syntax := .Get "syntax" -}}
<pre class="placeholder" data-syntax="{{$syntax}}"><code>
	{{- $inner := .Inner -}}
	{{- if $inner -}}
		{{- /* ソースコード前後の空白、タブ、改行を削除してから出力。 */ -}}
		{{- $code := trim $inner "\r\n\t" -}}
		{{- $code | safeHTML -}}
	{{- else -}}
		{{- /* .Innerが空の場合、file属性に指定された外部ファイルを読み込んで展開。 */ -}}
		{{- $codeFile := .Get "file" -}}
		{{- readFile $codeFile -}}
	{{- end -}}
</code></pre>

この書き方だと、"file"に指定する外部ファイルの中身は何のエスケープ処理もない生のソースコードで大丈夫。コピペするだけで済むので楽ちんです。あちこちの記事にソースコードが点在していて、「やっぱり(構文強調表示の)HTML構造をちょっと変えよう!」となった時にショートコード化が効果てきめんに効いてきます。このショートコードだけ書き直せば、全ての記事の該当箇所が綺麗に書き換わりますからね。

Hugoは便利!


Safariリーダーへの対応

以上、ソースコードを綺麗にシンタックスハイライトできる上、ソースコードが無いページでは余分なJavaScriptもCSSも読み込まずパフォーマンスにも配慮し、Hugoの機能を存分に使って簡単に使える 『なんちゃってシンタックスハイライト』なわけですが、何かが手に入ると次の欲が出てくるのが人間というものです。何を思ったか Safariのリーダーでもソースコードが綺麗に表示できたらいいのになぁ」なんて思ってしまいました。
現状だと<table>タグを使っているので、リーダーモードではテーブルとして表示されてしまい、見るも無惨な姿です。

Safari リーダーでの表示
Safari リーダーでの表示

Safari(iPad)でリーダー表示したもの。テーブルとして表示されてしまっている。

これをリーダーモードでは普通の<code>タグのそれのように等幅のソースコードとして表示したい。
というわけで、MacやiOSでSafariをお使いの方はこのページをリーダービューで表示してみて下さい。ソースコード部分はしっかり等幅のコードとして表示され、構文強調表示用に組み立てた<table>タグは跡形も無いのがおわかりいただけると思います。
その実現方法ですが……

2021/08/15 追記[Added 2021/08/15]

ダメでした…… Safariのリーダー表示にすると見事に表示がおかしくなります(なんかね一部のソースコードだけぐちゃっと表示されるの)。
どうやら記事全体の長さの制限に引っかかってるっぽくて、一定の分量までは問題ないんですが、そこから先はLoremを足したとしても表示がおかしくなるようです……
まあ、この先に書いた方法は記事の分量を別にすれば有効なので、全く役に立たない話ではないと思います。他の要因でだめな場合もある点ご了承下さい~。

2021/08/15 追記[Added 2021/08/15]

表示できた! ※Mac(Mojave)とiPad(15)のSafariのリーダービューで確認。
いやぁ、やっぱりSafariのリーダービューは💩 そしてそのうんこを処理するためのバッドノウハウをまた一つ手に入れてしまった……

と、とにかく、この記事をSafariのリーダービューで表示してみて下さい。ソースコード部分はしっかり等幅のコードとして表示され、構文強調表示用に組み立てた<table>タグは跡形も無いのがおわかりいただけると思います。
そのクソみたいなバッドノウハウを方法をご説明します。


Safariのリーダービューは鬼門

Webページを作る側としてSafariのリーダーモードと向き合った方はおわかり頂けると思うんですが、とにかく表示アルゴリズムが謎。何に基づいてリーダービューでの表示/非表示を決定してるのかが本当にわからない。記事中の<div>で囲まれたテキストが表示されなかったり、そうかと思えばめちゃくちゃ深い階層にある記事的にはどうでも良いテキストをしっかり拾ってきたりする。<article>で囲っている記事が途中で切れていたり、<aside>タグの中身を表示してしまったり。とにかく一貫性がない。
実はこのブログ、Safariのリーダーモードにかなり真面目に取り組んだ事があって、リーダー表示してもそれなりにちゃんと記事が表示されるようになっています。リーダー表示でのみ見える文言なんかも用意してるんですが、その話はまたの機会に。

ちなみに、昨今はVivaldyにもFirefoxにも、Chromeですらリーダービューが搭載されていますが、それらはSafariのリーダーモードと比べると癖がないと言うか、素直にあるがまま表示している印象です。もっともFirefoxなどでリーダー表示を使っている人は限りなくゼロでしょうし、無視することにしました。
SafariはiPhoneやiPadでリーダー表示使ってる人が地味にいるだろうなという事で頑張ったわけです。

さて、如何いかに構文強調表示したソースコードをリーダー表示で隠し、代わりに<pre><code>で囲まれたプレーンなソースコードを表示するかですが、後者の “リーダービューでは表示されるが普段は隠す”という動作の実現は比較的簡単です。CSSでゴニョゴニョして通常のブラウザで見えないようにしてしまえばよいのです。
ただしSafariのリーダーはCSSも見やがるので、display: none;のようなあからさまな非表示を行うとリーダービューでも非表示になってしまいます(ちなみに他のブラウザのリーダービューはCSSは無視するらしくdisplay: none;でも表示される)。下記のような大きさをゼロにするCSSを使うことで、通常モードでは見えず、リーダー表示でのみ見える要素を実現できます。

.hide
{
	display: block;
	width: 0;
	height: 0;
	overflow: hidden;
}

ソースコードを<table>タグに変換しDOMに戻す処理を行っているJavaScriptを少し修正して、上記のCSSを適用した<pre class="hide"><code>を、構文強調表示した<table>とは別に残すようにしました。この状態だとリーダービューには2つのソースコード、<pre class="hide"><code>でリーダー側から正しくコードとして解釈され、等幅で表示されているものものと、構文強調表示された<table>タグのもの(先程の通り、リーダーでの見た目はぐちゃぐちゃ)が表示されることになります。
あとは<table>タグをリーダーで非表示にできればいいんですが、この方法が本当にわからず……
前述の通りdisplay: none;すれば非表示になりますが、もちろん通常表示でも見えなくなってしまいます。そもそも、リーダー表示に切り替わったことを検知するJS的なイベントも無ければ、リーダービューとそれ以外でCSSを切り替えるためのメディアクエリもないのです。

何日も悩んだ後、遂にひらめきました。 「そうだ、Safariリーダーが非表示にする広告みたく、<iframe>で囲っちゃえば表示されないんじゃん?」

Safariのリーダーで非表示にさせたいがためだけに、ページ内容と地続きのコンテンツを<iframe>で分割するというのはバッドノウハウはなはだしい気がしますが、<iframe>はリーダー表示ではまず表示されません。そして通常表示では問題なく表示されます。
構文強調表示に変換するJavaScriptでフレームをまたいだ処理が必要になるので、その点だけ若干複雑になりますが、この方法でSafariのリーダーモード対応は問題なさそうです。

とは言え、やっぱり<iframe>はなぁ…… とモヤモヤしながら実装してたんですが、ついに天啓が。
<object>タグだ!!

<object>タグは外部リソースを埋め込んで表示するためのタグですが、ブラウザが対応していなかったり、リソースが見つからない場合は<object>タグの中身がそのまま表示されます。んでもってSafariのリーダーでは<iframe>同様無視されます。
そうです、構文強調表示した<table><object>タグでサクッと囲ってしまえば、Safariのリーダービューでは非表示になるのです!

いや~、昔Flashとかiアプリとか開発してて良かったと思いました。Flashの埋め込みページとかiアプリのダウンロードページ作った経験が無かったら<object>タグに辿り着かなかったと思うのです。application/x-jam! application/x-shockwave-flash!!
彼らは死してなお我々にその知恵をお与えになっているのだ……(笑)

まとめると、<pre><code>に囲まれたプレーンなソースコードを残し、非表示にするCSSを設定。構文強調表示したソースコードの<table>タグは、"src"等を何も書かない<object>タグで囲む。
こうすることでSafariのリーダービューでは構文強調表示したものが非表示になって<pre><code>のみが表示されるようになり、リーダー表示でも違和感なくソースコードが見えるようになりました!
まあ、<object>タグ本来の使い方を完全無視してるので、<iframe>同様バッドノウハウな事は全く変わりませんが、少なくともHTMLの中で完結するだけだいぶマシかなと。自分的にはもうめでたしめでたしです。


以上、大変長くなってしまいましたが構文強調表示、シンタックスハイライトを自前で作ったよという話でした。JavaScriptはconsole.logすれば何でも表示してくれるし、正規表現とか扱いやすくて便利ですが、やっぱりカプセル化の概念が薄くてクラスの中身とかがダダ漏れなのがどーにも気持ち悪いですね。

ちなみに、当ブログでこの 『なんちゃってシンタックスハイライト』を使っている記事は下記ら辺。すごく古い記事もありますが。


参考文献とか

この記事のタグ[This article is filed under]: Hugo[Hugo] | 開発[Dev] | Safari[Safari] | JavaScript[JavaScript] | HTML[HTML] | CSS[CSS]


この記事はここで終わりです。
読んでいただきありがとうございました。
良かったらシェアしてね!

That's all for this article. Thank you for your reading.
Please share this if you like it!

Twitter | Reddit | Facebook | Pinterest | Pocket

前の記事[Prev Post]

前の記事のアイキャッチ画像

【Hugo】書き出しエラー Error copying static files: chtimes

次の記事[Next Post]

次の記事のアイキャッチ画像

VSCodeの拡張機能を公開した時の覚書き