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 &lt;- RAT.new(1 2)
stdout.print_line(Rat.repr)  # =&gt; (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 &lt;- RAT.new(1 2)
stdout.print_line(Rat.repr)  # =&gt; (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.numerR.denom のコメントの塊は、 type rat のコメントの塊よりもインデントが深い。このため、 R.numerR.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;
}

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コメントの塊の最初の行のコメントテキストから、次のように抽出される。

  1. 最初の行のコメントテキストを正規表現 [ ]*(?<Title>.*?)[ ]*(\{\{\{.*)? にマッチする。

  2. グループ Title をタイトルとして使う。

8.3.3. ブロック

Kinkdocコメントの塊の2行目以降は、空のコメント行によってブロックに分割される。

ブロックには三種類ある:

  • コードブロック

  • 見出しブロック

  • 段落ブロック

8.3.3.1. コードブロック

ブロックのすべての行が、3つ以上の空白文字で始まる場合、そのブロックはコードブロックである。

コードブロックのテキストは、 次のように、コメントテキストから生成される。

  1. それぞれの行のコメントテキストのの先頭から、3つの空白文字を取り除く。

  2. それぞれの行のコメントテキストの末尾にラインフィード文字 (U+000a) を付け足す。

  3. コメントテキストを順番通りに結合する。

コードブロックの直後が別のコードブロックである場合、単一のコードブロックにまとめられる。そのテキストは、まとめられるふたつのコードブロックのテキストを、ひとつのラインフィード文字 (U+000a) を挟んで結合したものとなる。

8.3.3.2. 見出しブロック

ブロックが次の条件を満たすとき、そのブロックは見出しブロックである。

  • ブロックがコードブロックでない。

  • ブロックががひとつの行だけからなる。

  • 行のコメントテキストが正規表現 [ ]+==[ ]+(?<Heading>[^ ].*?)[ ]+== にマッチする。

グループ Heading を、見出しブロックのテキストとして使う。

8.3.3.3. 段落ブロック

ブロックがコードブロックでも見出しブロックでもない場合、それは段落ブロックである。

段落ブロックのテキストは次のように生成される。

  1. それぞれの行のコメントテキストのの先頭から、空白文字の連なりを取り除く。

  2. すべての行のコメントテキストを順番通りに、空白文字を挟んで結合する。

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]
}