DoxygenのHTML出力をウェブサイトに公開する際の追加処理を説明する。
ドキュメンテーションツールであるDoxygenは修飾機能が豊富で本来のソースコード解析を目的としなくてもリッチなドキュメントを作成できる。本サイトもDoxygen修飾を付加したプレーンテキストとして原稿を作成し、マークダウン形式(拡張子*.md)ファイルとしてDoxygenに処理させたHTML出力を用いている。このHTML出力はそのままでパソコン上のブラウザで過不足なく表示できるが、モバイル表示を念頭に簡単なチューニングを施している。なお本記事は検索ボックス非表示を前提とする。
本記事はDoxygenバージョン1.8.15を前提に作成して1.9.2まで対応してきたが、1.11.0の導入は一部の修正を必要とした。記事の最後にバージョン依存で修正した箇所をまとめる。
追加処理
Doxygen出力に以下の処理を追加した。
- モバイル画面への対応
- ページロードの高速化
- 転送データの圧縮
- メタディスクリプションの追加
- !DOCTYPEタグとhtmlタグの修正
モバイル画面への対応のみ必須とするべきで、Doxygenカスタマイズに示したヘッダHTMLファイルもこの処理だけは追加している。その他はグーグルLighthouseのスコア改善のみで使用上の効果を実感するものではない。
モバイル画面への対応
DoxygenのHTML出力はモバイル画面を考慮していないので、タブレットやスマホの表示でフォントサイズが不適切となる。最低限の対策として<head>エレメントに<meta name="viewport">タグを追加する。
<head>
...
<meta name="viewport" content="width=device-width, initial-scale=1"/>
...
</head>
ページロードの高速化
外部Javaスクリプトのロードをページロードと並列化してアクセスからページ表示までの時間を短縮する。この目的で<head>エレメント内<script>エレメントで外部スクリプトを読み込むもの全てにdefer属性を加える。defer属性を持つ外部スクリプトは並列ロードされるがページロード完了するまで実行されない。これをasync属性とすれば並列ロード後直ちにに並列実行するが、依存関係にあるスクリプトの実行順が確定せずエラーを起こす可能性がある。
<head>
...
<script type="text/javascript" src="jquery.js" defer></script>
<script type="text/javascript" src="dynsections.js" defer></script>
...
<script type="text/javascript" src="resize.js" defer></script>
<script type="text/javascript" src="navtreedata.js" defer></script>
<script type="text/javascript" src="navtree.js" defer></script>
...
</head>
ただし以下のインラインスクリプトもロードされる外部スクリプトが定義する関数を用いるため、このままでは左側ツリービューペインが表示されないなどの不具合となる。
<head>
...
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt GPL-v2 */
$(document).ready(initResizable);
/* @license-end */</script>
...
</head>
<body>
...
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt GPL-v2 */
$(document).ready(function(){initNavTree('index.html','');});
/* @license-end */
</script>
...
</body>
$関数はjquery.js、initResizable関数はresize.js、initNavTree関数はnavtree.jsが定義する。$(document).ready関数はページロード後に実引数として与えた関数(イベントハンドラ)を実行するが、$関数自体がjquery.js未実行で未定義エラーとなる。
代わりに外部スクリプトに依存しないdocument.addEventListner関数とDOMContentLoadedイベントを用いれば良い。
ただしイベントハンドラとして渡すinitResizable関数も外部スクリプト定義で、addEventListener関数実行時にバインドされてエラーとなる。代わりに無名関数をハンドラとして渡し無名関数の中でinitResizable関数をコールすればバインドは遅延されエラーとならない。initNavTree関数も外部スクリプト定義だが、こちらは最初から無名関数の中にある。
<head>
...
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt GPL-v2 */
document.addEventListener('DOMContentLoaded',function(){initResizable();});
/* @license-end */</script>
...
</head>
<body>
...
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt GPL-v2 */
document.addEventListener('DOMContentLoaded',function(){initNavTree('index.html','');});
/* @license-end */
</script>
...
</body>
- 覚え書き
- Javaスクリプトにおけるイベントハンドラへの関数バインドの詳細、特に無名関数をバインドした場合の無名関数内部スコープ(あるいはクロージャ)について正確な定義を発見できていない。このため無名関数でバインドが遅延されると記述したものの全く自信が無いが、少なくともエラーは発生せず期待通りに動く。
転送データの圧縮
テキストファイル(*.html、*.xml、*.css、*.jsなど)の転送データを圧縮してページ表示時間をさらに短縮する。Apacheサーバーならその目的で記述された.htaccessファイルをウェブサイトディレクトリに置くだけで良い。ただし.htaccessファイルはサーバー主設定をディレクトリ単位で上書きするもので、使用には十分な注意が必要とされる。
内容を理解しないまま参考リンクから.htaccessをコピーして配置する。既に.htaccessが存在する場合は既存ファイルに追記する。
.htaccess
# https://www.cloud9works.net/seo/tutorial-gzip-with-htaccess/
<IfModule mod_deflate.c>
SetOutputFilter DEFLATE
# For old browsers
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch \bMSIE\s(7|8) !no-gzip !gzip-only-text/html
# Execlude image data (GIF, JPEG, PNG, ICO)
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|ico)$ no-gzip dont-vary
# Files to deflate
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/js
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/atom_xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/x-httpd-php
AddOutputFilterByType DEFLATE application/x-font-ttf
AddOutputFilterByType DEFLATE application/x-font-woff
AddOutputFilterByType DEFLATE application/x-font-opentype
</IfModule>
メタディスクリプションの追加
<head>エレメントに<meta name="description">タグでメタディスクリプションを設定すれば、グーグルなどの検索エンジンが表示する概要を記述できる。これを設定しなければ検索エンジンが概要を自動生成する。
本サイトトップページのメタディスクリプション定義を例示する。
<head>
...
<meta name="description" content="N-BASICで初めてプログラミングに触れた世代に属する者として、ウィンドウズパソコンにC++プログラミング環境を金をかけずに構築します。ツールとして、MSYS2、mingw-w64、Code::Blocks、Git for Windows、GNU gettext、Doxygen、Inno Setupを利用します。">
...
</head>
!DOCTYPEタグとhtmlタグの修正
<!DOCTYPE>タグはPUBLIC識別子などを含む旧式のものが用いられているので、これらの識別子を除外する。
<html>タグにlang属性が設定されていないので追加する。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja">
<head>
...
グーグルLighthouseによる評価
グーグルLighthouseの監査(Audits)機能により各処理の効果を評価した。Lighthouseはグーグルクロームブラウザのアドレスバー再右端の︙から[Google Chromeの設定|その他のツール|デベロッパーツール]で開くクロームDevToolsのメニュー[Audits]で起動する。
任意のウェブページへ移動しAudits画面下部の[Run audits]を押せば監査が行われ結果が表示される。引き続き新たな監査を行う場合はAuditsメニューバー(上から二段目のメニューバー)最左端の+、[Perform an audit]を押して監査前の画面に戻す。監査は全て以下のデフォルト設定で行った。
項目 | 設定 |
Device | Mobile |
Audits | Performance, Progressive Web App, Best practices, Accessibility, SEO |
Throttling | Simulated Slow 4G, 4x CPU Slowdown |
Clear storage | チェック |
以下に本サイトトップページ相当のテストページで計測した結果を示す。各レベルは前レベルまでの処理に新たな処理を追加したものとする。Performanceスコアのみ変動するため5回平均値とした。
レベル | 追加処理 | Performance | Accessibility | Best Practices | SEO |
0 | 無し | 91.6 | 92 | 64 | 64 |
1 | モバイル画面への対応 | 91.4 | 94 | 64 | 83 |
2 | ページロードの高速化 | 98.6 | 94 | 64 | 83 |
3 | 転送データの圧縮 | 99.8 | 94 | 64 | 83 |
4 | メタディスクリプションの追加 | 99.6 | 94 | 64 | 93 |
5 | !DOCTYPEタグとhtmlタグの修正 | 99.8 | 100 | 71 | 93 |
処理の実装
以下に全ての追加処理を実装する方法を説明する。
header.htmlの修正
<head>エレメントの単純な書き換えは、Doxygenカスタマイズファイルの一つheader.htmlを修正すれば良い。例えばmy_header.htmlはそのままウェブサイト公開用としても用いる事ができるが、<!DOCTYPE>タグと<html>タグを記述に従い直接修正する。
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=9"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="generator" content="Doxygen $doxygenversion"/>
<title>$projectname: $title</title>
<title>$title</title>
...
外部スクリプトを読み込む<script>タグも同様に修正できる。しかし$treeviewなどのマーカーがGENERATE_TREEVIEWなどのオプション設定でそういった<script>タグを挿入するため、後述の自動処理バッチファイルに委ねる方が適当であろう。
自動処理バッチファイル
自動処理バッチファイルは出力されたHTMLファイル全てに以下を施す。
- 外部スクリプトを読み込む<script>タグにdefer属性を追加する。
- $(document).ready関数をdocument.addEventListner関数に置換する。
- <head>エレメントに<meta name="description">タグでメタディスクリプションを追加する。
これらの処理を行うためMSYS2のsedツールを利用する。
第一、第二は容易だが第三はかなり複雑になる。header.htmlは全HTMLファイルに共通でページ毎に異なるメタディスクリプションを<head>エレメントに書き込む手段が無い。そこで以下のテクニックを用いる。
- Doxygenコマンドの\htmlonlyと\endhtmlonlyで<body>エレメントにコメントアウトされた<meta name="description">タグを書き込む。
- sedで<meta name="description">タグをアンコメントして<body>エレメントから<head>エレメントへ移す。
原稿となるマークダウン形式ファイル(*.md)でメタディスクリプションを追加するページ(Doxygenの\mainpageまたは\page)の任意行に以下を記述する。
...
\htmlonly
<!-- head inserter
<meta name="description" content="N-BASICで初めてプログラミングに触れた世代に属する者として、ウィンドウズパソコンにC++プログラミング環境を金をかけずに構築します。ツールとして、MSYS2、mingw-w64、Code::Blocks、Git for Windows、GNU gettext、Doxygen、Inno Setupを利用します。">
head inserter -->
\endhtmlonly
...
Doxygenは\htmlonlyコマンドと\endhtmlonlyコマンドの間の行をHTMLファイルの<body>エレメントにそのまま挿入する。挿入行の開始行と終了行はコメントの開始行と終了行でsedの検索マーカー行を兼ねる。マーカー行間は任意のHTML文なので、必要なら<meta name="keywords">タグも追加できる。これら挿入行は今のところコメントアウトされ仮に放置しても表示に影響しない。
sedでマーカー行を検索し、挿入行全てをアンコメントして<head>エレメント先頭へ移動する。
<head>
<meta name="description" content="N-BASICで初めてプログラミングに触れた世代に属する者として、ウィンドウズパソコンにC++プログラミング環境を金をかけずに構築します。ツールとして、MSYS2、mingw-w64、Code::Blocks、Git for Windows、GNU gettext、Doxygen、Inno Setupを利用します。">
...
</head>
以下にカレントディレクトリの全HTMLファイルに処理を施すバッチファイルを示す。
k_modify_html_files.bat
@rem k_modify_html_files.bat
@rem Modify doxygen output html files in the current directory for mobile
@rem browsing.
@SETLOCAL
@SET PATH=C:\msys64\usr\bin;%PATH%
@rem Temporary files. To get an absolute path of them which is in the same
@rem directory of this batch file, %~dp0 batch parameter is placed before the
@rem names.
@rem https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/call#batch-parameters
@set _SCRIPTFILE_HEAD_INSERTER_CUT=%~dp0_head_inserter.sed
@set _CLIPBOARD_FILE=%~dp0_clipboard.dat
@set _INTERMEDIATE_FILE=%~dp0_intermediate.dat
@rem Sed command to make java script loadings parallel. Note that the
@rem escaped backslash in the sed expression, which must be distinguished with
@rem the backslashs in sed "s/regexp/replacement/[flags]" option formula.
@set _COMMAND_MAKE_PARALLEL="
@set _COMMAND_MAKE_PARALLEL=%_COMMAND_MAKE_PARALLEL%s/(<script type=\"text\/javascript\" src=\".*\")>/\1 defer>/g;
@set _COMMAND_MAKE_PARALLEL=%_COMMAND_MAKE_PARALLEL%s/\$\(document\)\.ready\((function\(\).*)\)/document.addEventListener('DOMContentLoaded',\1)/g;
@set _COMMAND_MAKE_PARALLEL=%_COMMAND_MAKE_PARALLEL%s/\$\(document\)\.ready\((.*)\)/document.addEventListener('DOMContentLoaded',function(){\1();})/g;
@set _COMMAND_MAKE_PARALLEL=%_COMMAND_MAKE_PARALLEL%"
@rem Sed script file to cut head inserter lines from <body>, that is
@rem too complex for windows batch syntax to express as the -e argument.
@rem Note that the first command line is redirected with ">", while the other
@rem lines are with ">>". Because the "echo" places space characters before
@rem redirection operators to the standard output such the space must be
@rem avoided, especially for the "w filename" option otherwise it might append
@rem the space characters to the file name.
@set _LABEL_HEAD_INSERTER=head inserter
@echo /^^\s*^<!-- %_LABEL_HEAD_INSERTER%\s*$/,/^^\s*%_LABEL_HEAD_INSERTER% --^>\s*$/{> %_SCRIPTFILE_HEAD_INSERTER_CUT%
@echo s/.*%_LABEL_HEAD_INSERTER%.*/^<!-- %_LABEL_HEAD_INSERTER% --^>/>> %_SCRIPTFILE_HEAD_INSERTER_CUT%
@echo w %_CLIPBOARD_FILE%>> %_SCRIPTFILE_HEAD_INSERTER_CUT%
@echo d>> %_SCRIPTFILE_HEAD_INSERTER_CUT%
@echo }>> %_SCRIPTFILE_HEAD_INSERTER_CUT%
@rem Sed command to paste clipboard contents to <head>.
@set _COMMAND_HEAD_INSERTER_PASTE="/^<head>$/r %_CLIPBOARD_FILE%"
@rem Html file modifications. The two sed executions cannot be connected by a
@rem pipe because there is another data transfer than the standard io through
@rem the file denoted as %_CLIPBOARD_FILE% which might not be passed correctly
@rem if the commands run in parallel on the same line.
for %%j in (*.html) do (
sed -E -e %_COMMAND_MAKE_PARALLEL% -f %_SCRIPTFILE_HEAD_INSERTER_CUT% %%j > %_INTERMEDIATE_FILE%
sed -E -e %_COMMAND_HEAD_INSERTER_PASTE% %_INTERMEDIATE_FILE% > %%j
)
@del %_SCRIPTFILE_HEAD_INSERTER_CUT%
@del %_CLIPBOARD_FILE%
@del %_INTERMEDIATE_FILE%
三つのsedスクリプトで処理される。"sedスクリプト"とは1個以上のsedコマンドによるシーケンスで実引数(-eオプション)あるいはスクリプトファイル(-fオプション)で与え、複数の-eあるいは-fオプションは全て結合される(sed, a stream editor 3.1 sed script overview)。このバッチファイルはsedスクリプトをバッチローカルな環境変数あるいは一時的スクリプトファイルとして作成する。
オプション | 処理 |
-e %_COMMAND_MAKE_PARALLEL% | <script>にdeferを追加し$(document).readyをdocument.addEventListnerに置換する |
-f %_SCRIPTFILE_HEAD_INSERTER_CUT% | マーカー行を検索し挿入行をアンコメントしてクリップボードファイルに移動する |
-e %_COMMAND_HEAD_INSERTER_PASTE% | クリップボードファイルの内容を<head>エレメントに挿入する |
第二のみコマンドプロンプト実引数での記述が難しく、一時的スクリプトファイル(-fオプション)を使用している。第一と第二は単一のsed実行で結合処理できる。一方で第二の出力するクリップボードファイルを第三が読み込むため、第二と第三は結合処理もパイプ接続もできない。
Doxyfileの修正
.htaccessファイルをウェブサイトルートに置くためDoxyfileを修正する。これを含む3点の修正を施す。
- .htaccessファイルをHTML_EXTRA_FILESオプションに追加する。
- favicon.icoファイルをHTML_EXTRA_FILESオプションに追加する。
- アライアスhead_inserterとendhead_inserterをALIASESオプションに追加する。
favicon.icoファイルはウェブサイトシンボルマークとしてブラウザのタイトルバーに表示される。アライアスhead_inserterとendhead_inserterは原稿での挿入行マーカー記述を簡略化するが、それほど複雑なマーカーでもないので必要性は大きくない。
.htaccessファイル、favicon.icoファイルは以下に配置する。全てのウェブサイト公開用ファイルはdoxygen\htmlディレクトリに出力またはコピーされる。
ディレクトリ | ファイル | 内容 |
[プロジェクトディレクトリ] | *.md | ウェブサイト原稿 |
├ htaccess | .htaccess | サーバー設定 |
├ iconimages | favicon.ico | ウェブサイトシンボルマーク |
└ doxygen | Doxyfile | ウェブサイト公開用Doxygen設定 |
│ | my_header.html | ヘッダHTML |
│ | my_footer.html | フッタHTML |
│ | my_customstylesheet.css | カスケードスタイルシート |
│ | my_layout.xml | レイアウト |
│ | doxygen.log | ウェブサイト公開用ログ出力 |
└ html | index.html | ウェブサイト公開用HTMLトップページ |
| *.html, *.js, *.cssなど | ウェブサイト公開用HTML |
| .htaccess, favicon.ico | HTML_EXTRA_FILESによるコピー |
Doxyfile(追加部分)はそのままウェブサイト公開用として使えるので、これを例として修正箇所を示す。
#------------------------------------------------ [Additional Setting Start]
#---------------------------------------------------------------------------
PROJECT_NAME = "パソコンでプログラミングしよう"
PROJECT_NUMBER = X.X.X.X(X)
PROJECT_BRIEF = "金をかけずにウィンドウズC++プログラミング環境を構築する"
...
HTML_EXTRA_FILES += ../htaccess/.htaccess \
../iconimages/favicon.ico
ALIASES += "head_inserter=\htmlonly ^^<!-- head inserter" \
"endhead_inserter=head inserter --> ^^\endhtmlonly"
#---------------------------------------------------------------------------
#-------------------------------------------------- [Additional Setting End]
ALIASESオプションの^^は物理改行(アライアス置換時の改行)を行う。^^の前に空白を置かないと認識されない場合がある。
- 覚え書き
- Doxygen1.8.XまでALIASESオプションで^^は機能せず内部コマンド\_linebrで代用する必要があった。1.9.1以降では逆に\_linebrは機能しない。
アライアスhead_inserterとendhead_inserterを用いれば原稿(*.md)へのメタディスクリプション追加は以下となる。
...
\head_inserter
<meta name="description" content="N-BASICで初めてプログラミングに触れた世代に属する者として、ウィンドウズパソコンにC++プログラミング環境を金をかけずに構築します。ツールとして、MSYS2、mingw-w64、Code::Blocks、Git for Windows、GNU gettext、Doxygen、Inno Setupを利用します。">
\endhead_inserter
...
バージョン依存の修正
本記事はDoxygenバージョン1.8.5を前提とする。本項目は以降のDoxygenアップデートにより修正した処理をまとめる。
バージョン1.10.0
バージョン1.10.0変更対応であるが本サイトはこれをスキップしてアップデートしたため確認は1.11.0による。挿入されるページロード後イベントハンドラの登録が$(document).ready()から省略形式の$()に変更された。ハンドラが全て無名関数となっているのを確認した。
<head>
...
</head>
<body>
...
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT */
$(function() { codefold.init(0); });
/* @license-end */
</script>
...
<script type="text/javascript">
/* @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT */
$(function(){initNavTree('index.html',''); initResizable(true); });
/* @license-end */
</script>
...
</body>
これに対応するためk_modify_html_files.batのsedコマンドを修正した。
k_modify_html_files.bat(修正)
...
@rem Sed command to make java script loadings parallel.
@set _COMMAND_MAKE_PARALLEL="
@set _COMMAND_MAKE_PARALLEL=%_COMMAND_MAKE_PARALLEL%s/(<script type=\"text\/javascript\" src=\".*\")>/\1 defer>/g;
@set _COMMAND_MAKE_PARALLEL=%_COMMAND_MAKE_PARALLEL%s/\$\((function\(\).*)\)/document.addEventListener('DOMContentLoaded',\1)/g;
@set _COMMAND_MAKE_PARALLEL=%_COMMAND_MAKE_PARALLEL%"
...