8. Kinkdoc: API文書化システム¶
Kinkdoc はKinkプログラムのAPI文書化のシステムである。Javadoc, godoc, Doxygenなどと同じように、文書化テキストは、プログラムのコメントとして書かれる。
8.1. チュートリアル¶
モジュールをいくつか作って、それらのkinkdoc文書を書いてみよう。このチュートリアルでは、次のモジュールを作る。
math/RAT : 有理数
math/COMPLEX : 複素数
次のディレクトリ構造でライブラリを作ろう。
mathlib/ : プロジェクトディレクトリ
モジュールのルート
math/
RAT.kn : math/RATモジュールのプログラムファイル
COMPLEX.kn : math/COMPLEXモジュールのプログラムファイル
build/ : 生成されたファイルのディレクトリ
src/math/RAT.knから見ていこう。
:NUM.require_from('kink/')
:new <- {(:Numer :Denom)
:Desc = 'RAT.new(Numer Denom)'
NUM.is?(Numer) && Numer.int? || raise(
'{}: Numer must be an int num, but was {}'.format(Desc Numer.repr))
NUM.is?(Denom) && Denom.int? && Denom != 0 || raise(
'{}: Denom must be a non-zero int num, but was {}'.format(Desc Denom.repr))
new_val(
.. Rat_trait
'Numer' Numer
'Denom' Denom
)
}
:Rat_trait <- [
'numer' {[:R] R.Numer }
'denom' {[:R] R.Denom }
'repr' {[:R] '(rat {} {})'.format(R.numer R.denom) }
]
8.1.1. 関数¶
まず、new関数の説明を書こう。kinkdocコメントの塊の最初の行は、 ## からはじまり、後に続く行は # から始まる。
## RAT.new(Numer Denom)
#
# `new` makes a `rat` value
# which represents a rational number.
#
# `Numer` is the numerator, and `Denom` is the denominator.
:new <- {(:Numer :Denom)
,,,
}
最初の行の、 ## と空白に続くテキストは、節のタイトルとして扱われる。後に続く行は節の本体として扱われる。本体のテキストは、空のコメント行をはさんで、段落ブロックに分けられている。
上記のkinkdocコメントの塊は、次のようなHTMLに変換できる。
<h3>RAT.new(Numer Denom)</h3>
<p>`new` makes a `rat` value which represents a rational number.</p>
<p>`Numer` is the numerator, and `Denom` is the denominator.</p>
引用のためにバックティック ` を使うのは、単なる習慣である。kinkdocシステムにとって、バックティックは特別な意味を持たない。
例示のコードを節に追加することもできる。コードブロックは、ふたつの空白でインデントされる。
## RAT.new(Numer Denom)
#
# `new` makes a `rat` value
# which represents a rational number.
#
# `Numer` is the numerator, and `Denom` is the denominator.
#
# :RAT.require_from('org/example/')
#
# :Rat <- RAT.new(1 2)
# stdout.print_line(Rat.repr) # => (rat 1 2)
:new <- {(:Numer :Denom)
,,,
}
上記のkinkdocコメントの塊は、次のようなHTMLに変換できる。
<h3>RAT.new(Numer Denom)</h3>
<p>`new` makes a `rat` value which represents a rational number.</p>
<p>`Numer` is the numerator, and `Denom` is the denominator.</p>
<pre>
:RAT.require_from('org/example/')
:Rat <- RAT.new(1 2)
stdout.print_line(Rat.repr) # => (rat 1 2)
</pre>
節の中には、見出しも追加できる。ブロックが == と空白で始まり、空白と == で終わる場合、そのブロックは見出しとして扱われる。
## RAT.new(Numer Denom)
#
# `new` makes a `rat` value
# which represents a rational number.
#
# `Numer` is the numerator, and `Denom` is the denominator.
#
# == Usage ==
#
# :RAT.require_from('org/example/')
#
# :Rat <- RAT.new(1 2)
# stdout.print_line(Rat.repr) # => (rat 1 2)
:new <- {(:Numer :Denom)
,,,
}
上記のkinkdocコメントの塊は、次のようなHTMLに変換できる。
<h3>RAT.new(Numer Denom)</h3>
<p>`new` makes a `rat` value which represents a rational number.</p>
<p>`Numer` is the numerator, and `Denom` is the denominator.</p>
<p><em class="heading">Usage</em></p>
<pre>
:RAT.require_from('org/example/')
:Rat <- RAT.new(1 2)
stdout.print_line(Rat.repr) # => (rat 1 2)
</pre>
見出しが、 h3, h4, h5 などの要素ではなく、 p 要素に変換されていることに注意しよう。これは、見出しは文書の木構造に影響しないからである。
8.1.2. 型とメソッド¶
次に、 rat 型を文書化しよう。
kinkdocシステムは、コメント行だけをパースする。だから、kinkdocコメントはプログラムファイル中のどこにでも書ける。今回の場合、 rat 型を文書化するのに最適な場所は、 Rat_trait 変数の代入の直前だ。
## type rat
#
# `rat` is a type of rational numbers.
:Rat_trait <- [
'numer' {[:R] R.Numer }
'denom' {[:R] R.Denom }
'repr' {[:R] '(rat {} {})'.format(R.numer R.denom) }
]
ついで、メソッドにもkinkdocコメントを書こう。
## type rat
#
# `rat` is a type of rational numbers.
:Rat_trait <- [
## R.numer
#
# `numer` returns the numerator of the rational number `R`.
'numer' {[:R] R.Numer }
## R.denom
#
# `denom` returns the denominator of the rational number `R`.
'denom' {[:R] R.Denom }
'repr' {[:R] '(rat {} {})'.format(R.numer R.denom) }
]
ここで、 R.numer と R.denom のコメントの塊は、 type rat のコメントの塊よりもインデントが深い。このため、 R.numer と R.denom の節は、 type rat の節の子として扱われる。したがって、上記の例は、次のようなHTMLに変換できる。
<h3>type rat</h3>
<p>`rat` is a type of rational numbers.</p>
<h4>R.numer</h4>
<p>`numer` returns the numerator of the rational number `R`.</p>
<h4>R.denom</h4>
<p>`denom` returns the denominator of the rational number `R`.</p>
8.1.3. モジュール¶
モジュールを文書化するには、プログラムファイルの最初にkinkdocコメントの塊を追加し、タイトルは空のままにしよう。
##
# This module provides calculation of rational numbers.
:NUM.require_from('kink/')
## RAT.new(Numer Denom)
#
# `new` makes a `rat` value
# which represents a rational number.
#
# `Numer` is the numerator, and `Denom` is the denominator.
#
# == Usage ==
#
# :RAT.require_from('org/example/')
#
# :Rat <- RAT.new(1 2)
# stdout.print_line(Rat.repr) # => (rat 1 2)
:new <- {(:Numer :Denom)
:Desc = 'RAT.new(Numer Denom)'
NUM.is?(Numer) && Numer.int? || raise(
'{}: Numer must be an int num, but was {}'.format(Desc Numer.repr))
NUM.is?(Denom) && Denom.int? && Denom != 0 || raise(
'{}: Denom must be a non-zero int num, but was {}'.format(Desc Denom.repr))
new_val(
.. Rat_trait
'Numer' Numer
'Denom' Denom
)
}
## type rat
#
# `rat` is a type of rational numbers.
:Rat_trait <- [
## R.numer
#
# `numer` returns the numerator of the rational number `R`.
'numer' {[:R] R.Numer }
## R.denom
#
# `denom` returns the denominator of the rational number `R`.
'denom' {[:R] R.Denom }
'repr' {[:R] '(rat {} {})'.format(R.numer R.denom) }
]
8.1.4. Kinkdocツールチェーン¶
文書生成は2段階で行われる。
パース: DOC_PARSE_TOOL モジュールが、プログラムファイルのkinkdocコメントから JSON ファイルを生成する。
レンダリング: レンダラが、JSONファイルから最終的な結果を生成する。たとえば、 HTML_RENDER_TOOL モジュールはHTMLファイルを、 SPHINX_RENDER_TOOL はSphinxのソースファイル群を生成する。
ツールチェーンのデータフロー:
![digraph kinkdocflow {
node [fontname = "Noto Sans"];
program [label = "program files", shape = box];
json [label = "JSON file", shape = box];
html [label = "HTML file", shape = box];
sphinx [label = "Sphinx files", shape = box];
other [label = "Other formats", shape = box];
parser [label = "DOC_PARSE_TOOL"];
htmlrender [label = "HTML_RENDER_TOOL"];
sphinxrender [label = "SPHINX_RENDER_TOOL"];
otherrender [label = "Other renderers"];
program -> parser -> json;
json -> htmlrender -> html;
json -> sphinxrender -> sphinx;
json -> otherrender -> other;
}](_images/graphviz-1d14ed8438b4f41abbe379f2505af9ad3fdf880e.png)
HTMLファイルを生成するには、次のようにコマンドチェーンを実行する:
$ kink mod:kink/doc/DOC_PARSE_TOOL src | kink mod:kink/doc/render/html/HTML_RENDER_TOOL > doc.html
DOC_PARSE_TOOL は、指定されたディレクトリ、ここでは src 以下にあるすべての公開モジュールのプログラムファイルをパースして、標準出力にJSONデータを書き出す。 HTML_RENDER_TOOL モジュールは、標準入力からJSONデータを読み取って、標準出力に次のようなHTML文書を書き出す。
<!DOCTYPE html>
<html>
<head>
<title>API documentation</title>
</head>
<body>
<h1>API documentation</h1>
<h2>math/COMPLEX</h2>
<p>This module provides calculation of complex numbers.</p>
,,,
<h2>math/RAT</h2>
<p>This module provides calculation of rational numbers.</p>
<h3>RAT.new(Numer Denom)</h3>
,,,
</body>
</html>
8.1.5. 文書のタイトルと概要¶
文書全体のタイトルは、 DOC_PARSE_TOOL モジュールの --title オプションで指定できる。 --title が指定されなければ、「API documentation」がデフォルトのタイトルとして使われる。
文書の概要テキストを指定することもできる。kinkdocコメントの塊を含むプログラムファイルを作って、それを DOC_PARSE_TOOL モジュールの --overview オプションに指定しよう。プログラムファイル中のトップレベルの節が概要テキストになる。二番目以降のレベルの節は、モジュールのレベル以下の節として使われる。
次の内容のsrc/overview.knを作ろう。
##
# This library provides various systems of numbers.
次のコマンドチェーンを実行しよう。
$ kink mod:kink/doc/DOC_PARSE_TOOL src \
--title 'Math library' \
--overview src/overview.kn \
| kink mod:kink/doc/render/html/HTML_RENDER_TOOL \
> doc.html
doc.htmlは次のようになる。
<!DOCTYPE html>
<html>
<head>
<title>Math library</title>
</head>
<body>
<h1>Math library</h1>
<p>This library provides various systems of numbers.</p>
<h2>math/COMPLEX</h2>
,,,
<h2>math/RAT</h2>
,,,
</body>
</html>
8.1.6. Vim畳み込みのサポート¶
Vimの畳み込みの開始マーカーは、デフォルトで {{{ が使われる。kinkdocは、タイトル行が {{{ を含んでいる場合、この開始マーカーと、それ以降のテキストを無視する。
## type rat {{{
#
# `rat` is a type of rational numbers.
:Rat_trait <- [
## R.numer {{{
#
# `numer` returns the numerator of the rational number `R`.
'numer' {[:R]
R.Numer
} # }}}
## R.denom {{{
#
# `denom` returns the denominator of the rational number `R`.
'denom' {[:R]
R.Denom
} # }}}
'repr' {[:R] '(rat {} {})'.format(R.numer R.denom) }
] # }}}
# vim: fdm=marker
8.2. データモデル¶
Kinkdoc文書は、プログラムファイルからパースされて、ネストした節になる。節は三つの属性からなる:
title: 節のタイトルの文字列
blocks: 節の中のブロックの配列
subsections: 子の小節の配列
一番外側の節は、文書自体に対応する。二番目の節は、通常はモジュールに対応する。二番目以降の節は通常、型、関数、メソッドなどに対応する。
8.2.1. JSONスキーマ¶
Kinkdocの文書は、次のJSONスキーマにしたがってJSONにエンコードされる。
{ "type": "object",
"required": ["title", "blocks", "subsections"],
"properties": {
"title": {
"type": "string",
"pattern": "[^\\u0000-\\u0020\\u007f]( *[^\\u0000-\\u0020\\u007f])*"
},
"blocks": {
"type": "array",
"items": {
"type": "object",
"required": ["type", "text"],
"properties": {
"type": {
"enum": ["paragraph", "code", "heading"]
},
},
"if": { "properties": { "type": { "const": "code" } } },
"then": {
"properties": {
"text": {
"type": "string",
"pattern": "( *[^\\u0000-\\u0020\\u007f]+( +[^\\u0000-\\u0020\\u007f]+)*\\n)(\\n?( *[^\\u0000-\\u0020\\u007f]+( +[^\\u0000-\\u0020\\u007f]+)*\\n))*"
}
}
},
"else": {
"properties": {
"text": {
"type": "string",
"pattern": "[^\\u0000-\\u0020\\u007f]( *[^\\u0000-\\u0020\\u007f])*"
}
}
}
}
},
"subsections": {
"type": "array",
"items": { "$ref": "#" }
}
}
}
8.3. プログラムファイルからのkinkdocのパース¶
この節では、プログラムファイルからkinkdoc文書をパースする手続きを定義する。
この節で使われる用語:
プログラムファイル
プログラムファイルは、UTF-8でエンコードされた、有効なKinkのプログラムを含むファイルである。
ソースプログラム
ソースプログラムは、プログラムファイルから読み取られて、UTF-8でデコードされたUnicode文字列である。ソースプログラムは、有効なKinkプログラムである。
空白文字
空白文字はコードポイントU+0020である。
改行シーケンス
改行シーケンスは、CR+LF (U+000d, U+000a)か、LF (U+000a)である。
行
行は、ソースプログラムの中で、次の内いずれかを満たす範囲である。
ソースプログラムがひとつ以上の改行シーケンスを含む場合:
ソースプログラムの先頭から、最初の改行シーケンスの直前の位置まで。
最後ではない改行シーケンスの直後の位置から、次の改行シーケンスの直前の位置まで。
最後の改行シーケンスの直後の位置から、ソースプログラムの末尾まで。
ソースプログラムがひとつ以上の改行シーケンスを含む場合:
ソースプログラムの先頭から末尾まで。
番号記号
番号記号は、コードポイント # (U+0023)である。
コメント区切り
コメント区切りは、コメントの先頭の番号記号から、最長の番号記号の連なりである。
コメントのみの行
コメントのみの行は、次の条件を満たす行である。
行がコメントを含む。
行の中で、コメント区切りの前のコードポイントのすべてが空白文字である。
コメントテキスト
コメントテキストは、コメント行から次のようにして抜き出される。
コメント区切りの直後の位置から、行の末尾までのコードポイントを抜き出す。
すべてのASCII制御文字 (U+0000-U+001f, U+007f) を空白文字に置き換える。
末尾から、もっとも長い空白文字の連なりを取り除く。
空のコメント行
空のコメント行は、コメントテキストが空であるようなコメント行である。
コメントの塊
コメントの塊は、直前がコメントのみの行でなく、直後がコメントのみの行でないような、コメントのみの行の連なりである。
8.3.1. Kinkdocコメントの塊¶
コメントの塊が次の条件を満たす場合、それはkinkdocコメントの塊と呼ばれる。
行頭の空白文字の数が、すべての行について同じである。
最初の行のコメント区切りが、正確に2つの番号記号
##である。2行目以降のコメント区切りが、正確に1つの番号記号
#である。行のコメントテキストが空でない場合、コメントテキストの最初のコードポイントが空白文字である。
Kinkdocコメントの塊から、節が生成される。
8.3.2. タイトル¶
節のタイトルは、kinkdocコメントの塊の最初の行のコメントテキストから、次のように抽出される。
最初の行のコメントテキストを正規表現
[ ]*(?<Title>.*?)[ ]*(\{\{\{.*)?にマッチする。グループ
Titleをタイトルとして使う。
8.3.3. ブロック¶
Kinkdocコメントの塊の2行目以降は、空のコメント行によってブロックに分割される。
ブロックには三種類ある:
コードブロック
見出しブロック
段落ブロック
8.3.3.1. コードブロック¶
ブロックのすべての行が、3つ以上の空白文字で始まる場合、そのブロックはコードブロックである。
コードブロックのテキストは、 次のように、コメントテキストから生成される。
それぞれの行のコメントテキストのの先頭から、3つの空白文字を取り除く。
それぞれの行のコメントテキストの末尾にラインフィード文字 (U+000a) を付け足す。
コメントテキストを順番通りに結合する。
コードブロックの直後が別のコードブロックである場合、単一のコードブロックにまとめられる。そのテキストは、まとめられるふたつのコードブロックのテキストを、ひとつのラインフィード文字 (U+000a) を挟んで結合したものとなる。
8.3.3.2. 見出しブロック¶
ブロックが次の条件を満たすとき、そのブロックは見出しブロックである。
ブロックがコードブロックでない。
ブロックががひとつの行だけからなる。
行のコメントテキストが正規表現
[ ]+==[ ]+(?<Heading>[^ ].*?)[ ]+==にマッチする。
グループ Heading を、見出しブロックのテキストとして使う。
8.3.3.3. 段落ブロック¶
ブロックがコードブロックでも見出しブロックでもない場合、それは段落ブロックである。
段落ブロックのテキストは次のように生成される。
それぞれの行のコメントテキストのの先頭から、空白文字の連なりを取り除く。
すべての行のコメントテキストを順番通りに、空白文字を挟んで結合する。
8.3.4. プログラムレベルの節の構造¶
DOC_PARSE_TOOL モジュールは、プログラムファイルのkinkdocコメントの塊の列から、プログラムレベルの節を生成する。
8.3.4.1. プログラムレベルの節¶
ソースプログラムがひとつ以上のkinkdocコメントの塊を含み、最初のkinkdocコメントの塊のタイトルが空である場合、そのブロックの配列が、プログラムレベルの節のブロックの配列として使われる。
ソースプログラムがkinkdocコメントの塊をひとつも含まない場合、または最初のkinkdocコメントの塊のタイトルが空でない場合、プログラムレベルの節は、ブロックなしで作られる。
プログラムファイルが --overview オプションで指定されたものである場合には、 --title オプションで指定されたタイトル、あるいはデフォルトのタイトルが使われる。
プログラムファイルがモジュールのソースである場合には、そのモジュールの名前がタイトルとして使われる。
8.3.4.2. ネストした小節¶
プログラムレベルの節以外のkinkdocコメントの塊は、次の疑似コードのように、ネストした小節としてパースされる。
program_level_section := «the program level section»
chunks := «queue of kinkdoc comment chunks not of the program-level section»
read_children(program_level_section, 0)
def read_children(parent, min_whitespaces) {
loop {
if empty?(chunks)
return
chunk := peek(chunks)
if chunk.leading_whitespaces < min_whitespaces
return
dequeue(chunks)
section := new_section(
title: if empty?(chunk.title) then «replacement title» else chunk.title,
blocks: chunk.blocks,
subsections: []
)
parent.subsections := parent.subsections + [section]
read_children(section, chunk.leading_whitespaces + 1)
}
}
8.3.5. 文書レベルの節の構造¶
DOC_PARSE_TOOL モジュールは、実行ごとに、次の疑似コードのように、文書レベルの節を生成する。
doc_title := if option_given?('--title') then option('--title') else 'API documentation'
doc_section := ( if option_given?('--overview')
then parse_page_level_section(title: doc_title, file: option('--overview'))
else new_section(title: doc_title, blocks: [], subsections: [])
)
mod_pages := «tuples of (mod_name, file_name)»
mod_pages := sort(mod_pages by mod_name alphabetically)
for page in mod_pages {
page_section := parse_page_level_section(title: page.mod_name, file: page.file_name)
doc_section.subsections := doc_section.subsections + [page_section]
}