📦

Typstの日本語Lipsumパッケージを作ってみた件

2024/01/27に公開

Typstで色々設定いじって試している時とか、人に説明するためにサンプルを見せる時とか、テスト用の日本語テキスト欲しくならない?

そう!そんな時はroremuのお出番でございます!ロレム様は360度文字数を自由設定、しかも公式のラテン語lorem()以上の機能を持ち、offsetのずらし指定さえできちゃいま~す!

更に更に、将来には段落単位の生成も追加されるかもしれないですって?!試すなら、今すぐお使いになれます!こちらのコマンドで、なんと無料で今すぐ使えちゃうよ!

#import "@preview/roremu:0.1.0": roremu

という茶番は置いといて、ついに先日採択いただきましたので、リポの公開と実装詳細・手順や注意事項などについてお話します。

TL;DR

https://github.com/mkpoli/roremu

#set text(font: "Noto Sans CJK JP")
#import "@preview/roremu:0.1.0": roremu
#roremu(128)

要件

一番重要な実装要件は以下でしょう。その他の追加機能は、これらの中核的な機能ができてから考えることとします。

  • 日本語のダミーテキストを生成する。
  • ダミーテキストの字数を指定できる。
  • テキストのずらし指定を設定できる。

実装

テキストの選定

日本語のダミーテキストといえば、著作権が切れた近代小説の一節がよく使われます。夏目漱石・宮沢賢治等の文豪の作品は特に一般的です[1]。こうした作品はとっくに著作権の保護期間を過ぎ、パブリック・ドメインに放出されたので、自由に利用できます。その中でも、本パッケージは夏目漱石の処女作小説である『吾輩は猫である』を採用します。

最近では、特定な作品の一節には内容に目を奪われてしまいがちであるという指摘に基づいて、マルコフ連鎖などを用いて、ぱっと見自然な日本語だが、意味は通じないという物もでてきています[2]。また、現代的な組版において、特に科学技術分野の論文などでは、和欧混植・インライン数式が含まれることが一般的であるため、Wikipediaの理科系記事を用いた方がいいのではないかという意見もできている[3]

いずれにしても、実装の難易度などやライセンスの煩雑さなどから少なくとも現時点では行わないことにします。その代りに、自由にテキストを指定できる機能を設けておきます。

  • 正しく定義された
  • ドキュメントをできる。

テキストの取得

前章では、『吾輩は猫である』と決めたので、テキストを取得していきますが、原本はどこから取ってくればよいのか、というと、「青空文庫」というとてもありがたいウェブサイトがあります。簡単に言えば、著作権が消滅または著者が許諾した文学作品を、原本より有志が無償で翻刻し公開した電子書籍文庫です[4]

図書カード:No.789よりXHTMLファイル形式のテキストをすぐに取得できましたが、問題はルビ(振り仮名)があることです。Typstは確かにルビを振れなくはないのですが、サードパーティーの実装しかないこと[5][6][7]、段落のスペーシングを邪魔しかねない、現代の組版において一般的な用途でルビを振ることをすくないこと、Lipsumは文字よりも段落全体像を掴むために使われやすいことなどから、ルビを除去することにしました。

ルビを除去するのに色んな方法がありますが、コードを書いたりライブラリを使ったりするのがめんどくさいので、こちらのブックマークレットを使うことにします。しかし、現代的なブラウザーはセキュリティ上の理由からか、ブックマークレットはアドレスバーに入れても効果がないので、Consoleにそのまま入れてみたら動いたので、それを青空文庫のXHTMLページ使いました。段落を長めのとってコピーします。

それから、一個一個の段落が短いので、一回の呼び出しで一段落にしたいので、とりあえず改行をすべて空文字列に置換することで、段落をすべて潰しました。

テキストの表示

Typstの開発環境を用意して、新たにフォルダを作ります。Typstでは、文章などを記述した物でも、機能(スタイル・関数など)のみを記述した物でも、どちらも同じ.typファイルで区別されないため、とりあえず、lib.typという名前のTypstファイルでも作っておきましょう。

得られたテキストを使いたいので、変数として保存しておく必要があります。もちろん、#let text = "吾輩は猫である。…"のようにそのままリテラル文字列にテキストを入れて変数を定義しても良いのですが、なにせ長いし内容と論理を分離させたいので、単独でneko.txtを作って、その中に格納しておきました。

read()関数を使って格納したテキストファイルを読み込みます。また、フォントがおかしいのでとりあえず源ノ角ゴシック(Noto Sans CJK JP)でも指定しておきます。このファイルをコンパイルしてみると、テキストが表示されます。

lib.typ
#set text(font: "Noto Sans CJK JP")
#let text = read("neko.txt")
#text

文字数の指定

それでは、指定された文字数だけを表示させるという関数がほしいので、作ります。ついでにコメントに用途や引数の説明も書いておきます。text.slice(0, words)と書きたいところですが、Typstにおいて文字列strの中身がUTF-8のバイト配列なので、日本語テキストのようにマルチバイト文字を使う場合、こうやってしまうとerror: string index 5 is not a character boundaryと怒られてしまいます。対処法としては、text.clusters()を使ってUnicodeの書記素クラスタ(grapheme cluster)を単位に分割しておきます。そうして得られた書記素を表すstrの配列arrayが得られます。これにslice()してまた合併join()すると日本語の文字単位で切り出せます。

lib.typ
#set text(font: "Noto Sans CJK JP")
/// 日本語ダミーテキスト生成
///
/// - words (int): 生成するテキストの長さ(文字数)
/// -> str
#let roremu(words) = {
  let text = read("neko.txt")
  text.clusters().slice(0, words).join("")
}

= 8文字
#roremu(8)
= 10文字
#roremu(10)
= 16文字
#roremu(16)
= 33文字
#roremu(33)

残りの問題として、もし百億文字を要求されたら、文字が足りなくなってしまいます。その場合は、繰り返させておくる必要があります。方法は簡単です。例えば5文字しかない時に22文字が要求されると、(5+5+5+5)+2 = 5*4 + 2 = \lfloor22\div5\rfloor + (22 \bmod 5)のように、必要とされる文字数を今ある文字数で割った商を繰り返した上であまりの文字数を足しておくと足ります。実装には、Python同様文字列に整数を掛ければその回数分繰り返されるのを利用します。calc.div-euclid()整除法を使って、整数商に1を足すと繰り返す回数になります(あるいは単に小数で割ったあとに天井関数を入れるのも良いcalc.ceil(words / length))。繰り返した後に、足りた物を更に必要の長さにslice()すればよい。

#let roremu(words) = {
  let text = read("neko.txt")
  let length = text.clusters().len()
  let times = calc.div-euclid(words, length) + 1
  (text * times).clusters().slice(0, words).join("")
}

ずらしの指定

ずらし考え方も簡単で、例えばまずずらさずに「いろはにほへと」の7文字から、5文字を取る場合は、単純に0から5を「いろはにほへと」と取れば良いのですが、1ずらすと「いろはにほへと」と、1から5+1=6を取れば良い。繰り返す場合を考えると1ずらしで7を取る場合、「いろはにほへといろはにほへと」のように繰り返した後に、1ずらしのまま、1から7+1=8を取ればよい。繰り返しの回数の計算には、ずらしも考慮を入れる必要があるので、「いろはにほへといろはにほへと」という1+78文字をまずひとつの塊と考えて、5あって8必要なので、85で割った結果1回分に、最後に残りの部分3語を足すと、必要の長さ8になる。その上に、最初から8語取れば、「いろはにほへとい」を切り取れる。その後に最初の1文字を落とせば、欲しかった「ろはにほへとい」を得られるので、slice(offset, offset + words)にすれば良い。

/// 日本語ダミーテキスト生成
///
/// - words (int): 生成するテキストの長さ(文字数)
/// - offset (int): テキストの開始位置(文字数)
/// -> str
#let roremu(words) = {
  let text = read("neko.txt")
  let length = text.clusters().len()
  let times = calc.div-euclid(offset + words, length) + 1
  (text * times).clusters().slice(offset, offset + words).join("")
}

カスタムなテキストを指定する

以上のように、ほしかったコア機能をすべて実装できたので、前述の理由もあってテキストの指定をできるようにしたい。新たに引数を追加して、もし指定されていなければ『吾輩は猫である』を使いますが、指定され場合はそれを使います。

lib.typ
/// 日本語ダミーテキスト生成
///
/// - words (int): 生成するテキストの長さ(文字数)
/// - offset (int): テキストの開始位置(文字数)
/// - custom-text (str, none): デフォルト『吾輩は猫である』の代わりに使用する文字列
/// -> str
#let roremu(words, offset: 0, custom-text: none) = {
  let text = if custom-text == none { read("neko.txt") } else { custom-text }
  let length = text.clusters().len()
  let times = calc.div-euclid(offset + words, length) + 1
  (text * times).clusters().slice(offset, offset + words).join("")
}
#roremu(100, custom-text: "猫")

テストケースを付ける

テスト用の表示を付けております。

test.typ
#set text(font: "Noto Serif CJK JP")
#show raw: set text(font: "Noto Sans Mono CJK JP")
#import "./lib.typ": roremu

#show raw.where(block: true): it => {
  box(fill: luma(240), inset: 10pt, radius: 4pt, it)
}

#raw("#roremu(8)", lang: "typst", block: true)

#roremu(8)

#v(1em)

// ...

ドキュメントを付ける

公式にはまだ決められているドキュメントのスタイルがないのですが、今回はtidyを利用してドキュメントを作ります。tidyは自動的にコメントからドキュメントを取り出してくれます。manual.typを作って、manual.pdfにコンパイルさせます。

manual.typ
#import "@preview/tidy:0.2.0"

#set text(font: "Noto Serif CJK JP", size: 10pt)
#show raw: set text(font: ("Fira Code", "Noto Sans Mono CJK JP"))

#set heading(numbering: "1.1")
#set page(paper: "jis-b5")

#let title(body) = {
  set text(font: "Noto Serif CJK JP", size: 20pt)
  align(center, body)
}

#title("roremu")
#outline(title: "目次")

= 概要
// ...

= 使い方
// ...

== 例
#include("test.typ")

= API Reference
#let docs = tidy.parse-module(read("lib.typ"))
#tidy.show-module(docs)

公開

パッケージ名

最初は lorem-ja とそのまま名前を付けていましたが、Typstのパッケージ命名規則によれば、機能の直接な名前を付けてはいけません。つまり、表を作るパッケージがtableと、画像を作るパッケージがgraphicなどと付けてはならない、というルールです。このルールにした理由は、ユーザーがその機能を使いたい場合、真っ先に正規な名前のパッケージを見つけてしまうから、同じ機能の他のパッケージがあると公平ではなくなるということらしいです。なので、今回は「ロレム」のroremuと名を改めました。

パッケージ化

パッケージとして公開するには、同じフォルダにtypst.tomlを作ってメタデータを記入する必要があります。ライセンスはOSI認定されたものである必要があり、そのSPDX IDlicense = に記入します。それから、ドキュメントのPDFや画像データなど、ダウンロード時に不必要なものをexclude = に記述する必要があります。それ以外は適切なものを入れてください。

typst.toml
[package]
name = "roremu"
version = "0.1.0"
entrypoint = "lib.typ"
authors = ["mkpoli"]
license = "Unlicense"
description = "Generate blind text (lorem ipsum) for Japanese"
repository = "https://github.com/mkpoli/roremu"
keywords = ["lorem", "lorem ipsum", "lipsum", "dummy text", "blind text", "placeholder", "japanese"]
exclude = ["*.pdf", "manual.typ", "test.typ"]

ドキュメントの自動コンパイル

パッケージができたら、gitレポジトリにして、Githubに挙げます。ただし毎回手動でドキュメントをPDFにコンパイルするのがめんどくさいので、Github ActionsというCI/CDツールを用いて、自動的にコンパイルしてもらいます。やっていることはまずレポをcloneして、必要フォントをダウンロードして、Typstのドキュメントをコンパイルしてもらって、ダウンロードしてフォントを消して、できたmanual.pdfをコミット&プッシュしてもらいます。

.github/workflows
/compile-docs.yml
name: Compile and Commit documentation file.
on:
  push:
    branches:
      - master
  workflow_dispatch:

permissions:
  contents: write
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Create fonts directory in the repository
        run: mkdir -p $GITHUB_WORKSPACE/fonts

      - name: Download and Extract Fonts
        run: |
          wget -P $GITHUB_WORKSPACE/fonts https://github.com/tonsky/FiraCode/releases/download/5.2/Fira_Code_v5.2.zip
          unzip $GITHUB_WORKSPACE/fonts/Fira_Code_v5.2.zip -d $GITHUB_WORKSPACE/fonts/firacode
          wget -P $GITHUB_WORKSPACE/fonts https://noto-website-2.storage.googleapis.com/pkgs/NotoSerifCJKjp-hinted.zip
          unzip $GITHUB_WORKSPACE/fonts/NotoSerifCJKjp-hinted.zip -d $GITHUB_WORKSPACE/fonts/notoserifcjkjp
          wget -P $GITHUB_WORKSPACE/fonts https://github.com/notofonts/noto-cjk/releases/download/Sans2.004/11_NotoSansMonoCJKjp.zip
          unzip $GITHUB_WORKSPACE/fonts/11_NotoSansMonoCJKjp.zip -d $GITHUB_WORKSPACE/fonts/notosansmonocjkjp

      - name: Compile Typst to PDF
        uses: mkpoli/compile-typst-action@main
        with:
          source_paths: 'manual.typ'
          output_paths: 'manual.pdf'
          fonts_path: 'fonts'
          root_path: '.'

      - name: Remove Fonts from Repository
        run: rm -rf $GITHUB_WORKSPACE/fonts

      - name: Commit compiled PDFs
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: '[continuous deployment]: Compiled docs to PDF'
          file_pattern: 'manual.pdf'

typst/packagesへPRする

すべて用意が整いましたら、パッケージをTypst公式に提出します。今はまだ正式なパッケージ管理システムができておらず、Githubのtypst/packagesレポジトリにPull Requestを出す形で提出するシステムになっています。提出が認可されたら、#import "@preview/rorem:0.1.0"のように使えるようになります。

PRする際には、まずtypst/packagesをForkします。Forkしたレポジトリに今までのファイル(例えばtypst.tomlneko.txtlib.typmanual.pdfmanual.typtest.typLICENSEREADME.mdなど)をpackages/preview/roremu/0.1.0/に貼り付けます。注意すべきのは、かならずパッケージ名/バージョン番号というファイル構造にします。その後に、コミット&プッシュします。した後に、typst/packagesにPull Requestを提出します。テンプレートに従って、確認をしたら、後は認可されるのを待ちます。もし何か問題があれば指摘されるので、焦らず議論・対応していきましょう。

おわりに

以上でroremuパッケージの開発・公開までの経緯を紹介してみました。いかかがでしたでしょうか。何か質問があればぜひコメント欄にお願いします。

コミュニティ

「くみはんクラブ」というコミュニティを2024年1月に創設し、そこでTypstを始めとする様々な組版の話について議論されています。何かTypstなどを使う上で質問や不明点があれば、ぜひいらしてください。また、そのメンバーが中心となって、Typst Japan Communityを結成し、公式の認可を得てドキュメントの非公式日本語翻訳プロジェクトも始動しました。

https://discord.gg/dHRaAsBeRY

https://x.com/mkpoli/status/1751193383085973670

https://x.com/mkpoli/status/1827242838880408036

https://x.com/mkpoli/status/1858311758517326002

脚注
  1. https://ja.wikipedia.org/wiki/Lorem_ipsum ↩︎

  2. https://zenn.dev/sabigara/articles/88757a61fdba8e ↩︎

  3. https://xar.sh/post/95166529739/ ↩︎

  4. https://ja.wikipedia.org/wiki/青空文庫 ↩︎

  5. https://zenn.dev/saito_atsushi/articles/ff9490458570e1 ↩︎

  6. https://github.com/rinmyo/ruby-typ ↩︎

  7. https://github.com/Andrew15-5/rubby ↩︎

Discussion