jsdocコメントにある複数行「*」の意味

最近jsdoc3を使い始めたのですが…

ドキュメントコメントを開始する/**は良いとしても、その後で複数行に渡って存在する*は何なんだろうって思ってたんでですが、ようやく理由がわかりました。

複数行でドキュメント出力する際の行頭位置だったんですね…

例えばjsdoc3の@exampleを使って

/**
  @example
  var hoge = "foo!";
  var hige = "haa!!";
*/

なんてやると、@example以下は(サンプルコードとして表示させるため)white-spacepreで出力されますが、それだと二行目の「var hige = "haa!!";」だけ先頭に余計な空白が空いてしまう。

でもそれぞれの行頭に*を付けて

/**
  @example
  * var hoge = "foo";
  * var hige = "haa!!";
*/

とやっておけば、それぞれの行頭位置が同じになるわけですね。

GADTというものを知った

最近OCamlをようやくversion4系列にアップデートしたのですが。

その際にGADTとかいう言葉が気になったので調べていたら、ちょうどわかりやすいエントリーが。

Detecting use-cases for GADTs in OCaml

上のエントリによると、どうやらGADTというのは「代数型に型の強制を付けるためのもの」らしいです。

どいういうことかというと、例えばとあるexprという代数型を

type expr = 
  | Tint of int
  | Tbool of bool

と宣言した時、exprは中身がintの時もあれば、boolの時もある型ですよ、と宣言しているわけですが…

つまりTint 10Tbool falseも、同じexpr型に属することになりますよ、としているわけですが…

このexpr型を使って構成される「別の代数型」というものを考えた時、単にexpr型とだけ宣言されると、表現としてイマイチになることがあるのですよね。

例えば次のようなAbstract Syntax Treeを定義したとき…

type ast = 
  | Value of expr
  | IfExpr of expr * expr * expr

なんかIfExprexpr * expr * exprの部分の表現力が乏しくないですか?

exprが3つ並んでるんですけど、それぞれがどういうexpr型を要求しているか、よくわからないですよね。

もしこのIfExpr

IfExpr of (条件部なのでboolのexpr) * (真の場合の値でIntのexpr) * (偽の場合の値でIntのexpr)

みたいなニュアンスで型を制限できたら、安全だし、わかりやすくないですか?

つまり代数型のexprに、型制限を付けたいってことなんですけど。

しかしこれを、まさに可能にするのがGADTというやつらしく。

まずGADTというのは、言葉としては「G + ADT」です。

ADTはAlgebraic Data Typeなので、代数型。

GはGeneralizedの頭文字。

なので合わせると「汎用代数型」とでも訳すのでしょうか?

実際にexprとastをそれぞれGADTを使った型宣言(expr'とast'とする)に書き直すと、こういう感じになります。

type expr' = 
  | Gint: int -> expr' 
  | Gbool: bool -> expr'

type _ ast' = 
  | GValue: int expr'
  | GIfExpr: bool expr' -> int expr' -> int expr' -> int expr'

普通の代数型だとofが来る部分に:が来て、右側が型の関数みたいな記述になるのが特徴。

で、構成される代数型に型の制約を付けたい場合はtype _ ast' = ...みたいに宣言します。

アンダースコアの部分は「代数型に何かしらの型アノテーションがつくよ」みたいなニュアンスなんでしょうかね?

こうやって作られたGADTを使うと、

let statement_by_gadt : ast' = GIfExpr (Gint 10) (Gint 20) (Gint int 30)

は、IfExprの一つ目のexpr'が「bool expr'」じゃないので、コンパイルの段階でエラーになります。

一方で、GADTじゃない、普通の代数型のastを使った場合、次の

let statement_no_gadt : ast = IfExpr (Tint 10) (Tint 20) (Tint 30)

は(型の上では)エラーではありません。

IfExprを構成するメンバーは全てexpr型と規定されているだけなので、型の上では正しいからです。

これを構文エラーにするには、evalするときにundefned patternとして、型エラーを自前で書くしかないわけですが、GADTで制約していればコンパイルの段階でエラーが出てしまう!ということですね。

こんな便利なものを知らなかったとは…と反省なのです。

nanocのほうがjekyllより良いかも

nanocとは

nanocRuby製の静的サイトジェネレーターです。

静的サイトジェネレーターとは、固定的な内容のウェブサイトを作るのに使われるサイトジェネレーターです。

ホームページ制作ツールのような複雑なIDEではなく、コマンドラインとエディタでサクっとサイトが作れるので、プログラマーには人気があります。

昨今はそうしたツールでブログを公開する人も珍しくなくなりました。

ちなみにこの記事ではnanocの使い方は説明しません。本家に丁寧な説明があるので、そちらを参照して下さい。

jekyllとの違い

Ruby製の静的サイトジェネレーターといえば、jekyllも有名ですが、最近はnanocを主に使っています。

理由は記事の変換処理を柔軟に扱えるからです。

jekyllでも記事を変換する場合に、独自の処理を設定することは出来ますが、変換エンジン「そのもの」の切り替えしかできません。

しかしnanocでは、変換処理をfilterという単位で分割定義できて、それらを記事の「メタデータ」に応じて自由に組み合わせて適用させることができます。

例えば「コードハイライトの前処理は、プログラミング系の記事にだけ適用する」といった具合にです。

サンプルケース

マークダウンをCSSフレームワークと合わせて使った場合、デフォルトの出力では困ることがありますよね。

例えばタグに、そのCSSフレームワークで使う特別なクラス属性を付けたかったりとか。

でもマークダウンで次のように書くと、

## これはH2

もちろん次のように変換されるわけですが、

<h2>これはH2</h2>

こういう風に出したい時もあるわけじゃないですか。

<h2 class="my-header">これはH2</h2>

こういう場合、そのまま生でタグを打ってもいいのですが、せっかくマークダウンを使っているのですから、楽をしたいものです。

というわけで、nanocのfilterを定義しちゃいましょう。

filterを定義する

要件定義として、次のように宣言したら目的のコードが出力される、としましょう。

[myheader2 これはH2]

要はこのコードをマークダウンにかける前に、正規表現で目的のコードに変換すればいいのです。

そこでlib/filters.rbなどというファイルを作り、次のような処理を書いて、my_headerという識別子のフィルタを定義します。

class MyHeader < Nanoc::Filter
  identifier :my_header
  type :text
  def run(content, params={})
    content.gsub(/\[myheader(\d)\s+([^\]]+)\]/, "<h\\1 class='my-header'>\\2</h\\1>")
  end
end

次にRulesというファイルで、今作ったmy_headerというfilterを、マークダウン変換の前に差し込みます。

compile '*' do
  if item[:extension] == 'md'
    filter :my_header # <- これを追加!
    filter :kramdown # マークダウン変換処理
    layout 'default'
  end
end

しかしこれだと、全てのマークダウンファイルにmy_headerフィルタを通してしまいますよね。

なので、itemにuse_my_syntaxというメタデータが設定されているときだけ適用させるようにしてみます。

compile '*' do
  if item[:extension] == 'md'
    if item[:use_my_syntax] then # 独自文法使う?
      filter :my_header # <- これを追加!
    end
    filter :kramdown # マークダウン変換
    layout 'default'
  end
end

比較としてjekyllの独自コンバーター定義の方法を抜粋しておきます。

class Jekyll::Converters::Markdown::MyCustomProcessor
  def initialize(config)
    require 'funky_markdown'
    @config = config

  def convert(content)
    ::FunkyMarkdown.new(content).convert
  end
end

上記のconvertという関数を拡張するわけですが、見たらわかるように引数として元記事のcontentしかもらえないわけです。

しかしnanocでは記事に付いたメタデータを見ながら処理を分岐できるので、これは大きなアドバンテージです。

ちなみに記事のメタデータを付ける方法は、記事の先頭に次のようなヘッダーを書くだけです。

---
use_my_syntax: true
---

ヘッダーはYAMLで記述します。

最後に

最後に、nanoc.ymlのpruneについて。

prune:
  auto_prune: true
  exclude: [ '.git', '.hg', '.svn', 'CVS', ".gitignore", "push.sh" ]

auto_pruneを有効にすると、使用されていないファイルを削除してくれます。

デフォルトでは有効になっていませんが、trueに設定することをおすすめします。

これを有効にしてないと、元記事のファイル名を変えた時に、古いファイルが出力先に残り続けてしまいます。

例えばあるときにtop.mdというファイルを、welcome.mdに変えたくなったとしましょう。

そのまま放置してしまうと、出力先にtop/index.htmlというファイルが残ってしまいます(a.mdはa/index.htmlに出力されるのがデフォルトのルール)。

しかしprunetrueにしておくとtop/index.htmlは自動で削除されます。

段落ごとにコメントできる縦書き文章の作成サービスを公開しました

Brushup! について

段落ごとにコメントできる縦書き文章の作成サービスBrushup!を作りました。

f:id:convertical:20141223135626p:plain

パスワードを付けることもできるので、特定の人にだけ見せる使い方も可能です。

主に編集や校正の依頼に使えるツールを想定しています。

段落へのコメントを試してみたい方は、お試し用のエントリからどうぞ(コメントを投稿するにはユーザー登録が必要)。

ユーザー登録について

グーグルアカウントで登録できます。

マークアップについて

マークダウンという文法ですが、普通に文章を書く感覚で使っても、大体は思ったとおりの出力になるんじゃないかと思います。

マークダウンについては、ヘルプにも少しだけ説明しましたが、細かいことは各自で調べて下さい。

ちなみに縦書き文庫の記法は基本的にはそのまま使えるはず(全部はまだ試していませんが…)

チップ記法とか台詞記法とかは大丈夫でした。

またルビの記述ですが、Brushup!では簡易記法を用意しました。

[ruby 漢字 かんじ]にルビをふる。

みたいに書けます。多少はタイプ量が減るかと…

活動メモについて

次にプロフィールページにある「活動メモ」というものですが、自分の活動の中で報告したいものを勝手に書き込んで下さい。

f:id:convertical:20141223135723p:plain

サービスの性質上、プロフィールページに公開投稿を自動で一覧表示、みたいな機能は敢えて用意していません。

活動メモを使って、自分の言葉で報告したりしなかったりすればいいと思います。

ちなみに投稿した文章のURLは、連番じゃなくてハッシュ値で作られるので、自分でURLを公開しない限りはアクセスされないと思います。

もちろん非公開にすることも出来ますし、パスワードを付けて公開することも可能です。

注意事項

最後に注意事項ですが、現在のところ絶賛テスト中なクオリティなので、ユーザー数を100人までに制限しています。各ユーザーが投稿できる数も3つまで。字数制限は縦書き文庫と同じく10万字です。

あと今回は、スマホでもそれなりに閲覧できるように、全てのページをワンカラムで作りました。

なので、スマホだとちょっと小さくてクリックしにくいところがあるだろうなと思います。

datasetにセットしたboolean値について

ちょっとハマったのでメモ。

例えば、

<div id="foo" data-hoge="true"></div>

とかあったとき、以下の結果はどちらもfalse

$("#foo").data("hoge") == "true" // false
$("#foo").data("hoge") === "true"  // false

理由はdatasetにセットした"true" もしくは "false"は、boolean型として評価されるから…

typeof $("#foo").data("hoge"); // "boolean"
$("#foo").data("hoge") === true; // true

ちなみにnehan.jsも、datasetの値はズボラぶっこいて、一律で文字列型のまま返しちゃっていたので、今さっき直しました。

脚本を台詞記法で書く

ちょっとした小ネタですが、縦書き文庫の台詞記法はキャラクタ登録なしでも使えます。

キャラクタ名の部分に、人物の名前をそのまま書けば、キャラクタ画像じゃなくて文字列で表示されます。

なので、ちょっとした脚本は次のような感じで書けます。

[speak 太郎 今日は天皇杯の決勝だね。]
[speak 次郎 去年まで同じJ2で戦ってた同士が決勝って凄いね。]

詳しくは縦書き文庫ヘルプ | 台詞記法を参照して下さい。

チップ記法

わざわざtipタグを書くのが煩わしいので、チップ用の記法を用意しました。

チップについては 縦書き文庫 | チップ記法 を参照のこと

文法

次のように記述します。

[tip タイトル:内容]

注意

  • tipタイトルの間には一つ以上の半角スペースが必要です。
  • タイトル内容の間には半角のコロン:が必要です。

台詞記法で吹き出しの有無を切り替えられるようになりました

台詞記法で、吹き出しの有無を選べるようになりました。

声に出す台詞はspeakを使います。

[speak taro これは声に出して言う台詞!]

心のなかの台詞はthinkを使います。

[think taro これは心のなかの台詞…]

実際に表示させるとこんな風になります。

f:id:convertical:20141101083004p:plain

詳細は縦書き文庫ヘルプ | 台詞記法についてを参照のこと。