Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scala製のドキュメンテーションジェネレータLaikaに入門した

個人的に興味があって触る機会があったので、公式サイトで勉強しながらまとめた。

typelevel.org

Laikaとは

  • Laika*1とは、Scalaで記述された静的サイトジェネレータ。
    • LaikaのサイトもLaikaで生成されている。
  • 高度にモジュール化されているため、例えばプログラム上から変換処理だけ呼び出すといったこともできる。このため一部だけ拡張させるといったことが容易にできる。
  • Markdown入力に標準で対応している。
  • HTML/PDF/EPUB出力に標準で対応している。
  • sbtのプラグインとして動作させるとScalaのコードを書かずに最小限の設定で静的サイトを生成できる。
  • Scala.js上でも動作するため、フロントエンドサイドで動的にHTMLを生成することもできる。

Laikaを試す

Scala CLIを使ったScala Scriptを書いて最小限のHTML生成を行うには、laika.scala.scというファイルを作成し、以下のように書く:

//> using scala 3.3.1
//> using dep "org.typelevel::laika-core::1.0.0"

import laika.api._
import laika.format._

val transformer = Transformer
  .from(Markdown)
  .to(HTML)
  .build

val md = """# Hello, Laika!

This is exercise for **Laika**.
"""

val result = transformer.transform(md)

println(result.right.get)

このコードを実行する:

% scala-cli laika.scala.sc

すると、Markdownが変換されたHTMLが標準出力に出てくる:

<h1 id="hello-laika" class="section">Hello, Laika!</h1>
<p>This is exercise for <strong>Laika</strong>.</p>

Laikaがデフォルトで対応しているフォーマット

  • Laikaは入出力をFormatという概念で抽象化している。
  • 入力、出力に対してFormatを指定することで、Laikaはこれを利用して入力形式から中間形式を生成し、そして出力形式に変換する。
  • Laikaが提供するFormatは、laika.format以下で提供される。
  • 入力として使えるFormatとして、LaikaはデフォルトでMarkdownReStructuredTextを提供する。
  • 出力として使えるFormatとして、LaikaはデフォルトでASTEPUBHTMLPDFXSLFOを提供する。
    • ASTはデバッグ用。

例えば、MarkdownのASTを出力してみる:

//> using scala 3.3.1
//> using dep "org.typelevel::laika-core::1.0.0"

import laika.api._
import laika.format._

val transformer = Transformer
  .from(Markdown)
  .to(AST)
  .build

val md = """# Hello, Laika!

This is exercise for **Laika**.
"""

val result = transformer.transform(md)

println(result.right.get)

コードを実行すると、Laikaが内部で扱っている内部表現が得られる:

RootElement - Blocks: 1
. Section
. . Header(1,Id(hello-laika) + Styles(section)) - Spans: 1
. . . Text - 'Hello, Laika!'
. . Content - Blocks: 1
. . . Paragraph - Spans: 3
. . . . Text - 'This is exercise for '
. . . . Strong - Spans: 1
. . . . . Text - 'Laika'
. . . . Text - '.'

デバッグするときはこれを利用する。

ExtensionBundle

  • Laikaでは、Formatが文書をパースした後の抽象構文木に対して、追加で後処理をかけることができる仕組み(ExtensionBundle)がある。
  • ExtensionBundleを利用するには、.using(extensionBundle)を使う。
  • Laikaは、デフォルトでMarkdown.GitHubFlavorSyntaxHighlightingなどのExtensionBundleを提供する。

例えば、Scalaのコードをシンタックスハイライトしてもらう:

//> using scala 3.3.1
//> using dep "org.typelevel::laika-core::1.0.0"

import laika.api._
import laika.format._
import laika.config.SyntaxHighlighting

val transformer = Transformer
  .from(Markdown)
  .to(HTML)
  .using(Markdown.GitHubFlavor)
  .using(SyntaxHighlighting)
  .build

val md = """# Hello, Laika!

```scala
val to = "Laika"
println(s"Hello, $to!")
```
"""

val result = transformer.transform(md)

println(result.right.get)

SyntaxHighlightingを使うにはいったん言語を認識させる必要があるのでGitHubFlavorも必要になる。

Laikaと副作用

  • Laikaは関数型のスタイルで記述されているため、ファイルに対する入出力やテーマの適用といった副作用を伴う仕組みは追加のモジュールで提供される。
  • Laikaで副作用まわりを扱うには、laika-ioを利用する。
  • laika-ioは、Cats Effectという副作用を専門に扱うライブラリをその基盤としている。

例えば、変換結果をファイルに出力するには以下のように書く:

//> using scala 3.3.1
//> using dep "org.typelevel::laika-core::1.0.0"
//> using dep "org.typelevel::laika-io::1.0.0"

import cats.effect.{IO, IOApp, ExitCode}
import laika.api._
import laika.format._
import laika.io.syntax._

// Laikaのテーマを使うために必要
import laika.helium.Helium
import laika.helium.config._
import laika.ast.Path.Root

// IOApp.Simpleを使うとIO[Unit]を返すrunメソッドを実装するだけで良い
object Main extends IOApp.Simple:
  // テーマが必要なので、Laikaがデフォルトで提供するHeliumテーマを使う
  val theme = Helium.defaults.site
    .topNavigationBar(
      // ホームを設定しないとテーマが動作しないのでここで設定する
      homeLink = IconLink.internal(Root / "example.md", HeliumIcon.home)
    )
    .build

  def makeTransformer = Transformer
    .from(Markdown)
    .to(HTML)
    // Cats Effectが提供するIO型を使って副作用を扱う
    .parallel[IO]
    // テーマを設定する
    .withTheme(theme)
    .build

  def run: IO[Unit] = makeTransformer.use { transformer =>
    transformer
      .fromDirectory("laikaFrom")
      .toDirectory("laikaTarget")
      .transform
  }.void // voidはIO[Unit]に変換する(runメソッドの戻り値がIO[Unit]でないとコンパイルエラーになる)

// 明示的に呼び出してやる
Main.main(args)

./laikaFrom/example.mdに以下のようなMarkdownファイルを書く:

# Example Document

This is example document to describe Laika.

スクリプトを実行すると./laikaTarget/example.htmlにHTMLが生成されている:

.parallel[IO]によってCats Effect向けの型に包まれてTransformerが返されるので、直接これを利用するのではなく、.useメソッドを使ってその中で利用する形になる。

Laikaをsbtプラグインから利用する

  • Laikaはsbtプラグインとしても利用できる。
  • MarkdownからHTMLサイトを生成するといった一般的なドキュメント生成のために、sbtからLaikaを呼び出すために利用する。

下準備として、sbtプロジェクトを作成する。

Laikaをsbtプラグインとして呼び出すには、sbtプロジェクト(空のディレクトリでもよい)のproject/plugins.sbtに以下のように記載する:

addSbtPlugin("org.typelevel" % "laika-sbt" % "1.0.0")

次に、build.sbtに以下のように追記する:

import laika.helium.Helium
import laika.helium.config._
import laika.ast.Path.Root

enablePlugins(LaikaPlugin)

laikaTheme := Helium.defaults.site
  .topNavigationBar(
    // ホームを設定しないとテーマが動作しないのでここで設定する
    homeLink = IconLink.internal(Root / "hello.md", HeliumIcon.home)
  )
  .build

デフォルトではLaika sbtプラグインはsrc/docsをドキュメントディレクトリだと認識する(設定によって変更可)ので、src/docs/hello.mdを作成する:

# Hello, Laika!

This is example file for _Laika sbt plugin_.

最後にsbt laikaSiteを実行するとHTMLがtarget/docs/site/以下に生成される。

% sbt laikaSite
...
% cat target/docs/site/hello.html
<!DOCTYPE html>
...

今日のところはここまで。

応用

Laikaは拡張性の高い構造になっているため、以下のような拡張ができる:

  • テーマを自作する
  • Formatを自作して好きなフォーマットから好きなフォーマットへと出力する
  • ExtensionBundleを自作する
  • Scaladocへのリンクを作る

今回はまだ勉強しきっていないので割愛するが、これらの内容についてもおいおい触れていこうと思う。

*1:公式サイトを見るに、名前はたぶん宇宙犬から来ている

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?