Typstの日本語Lipsumパッケージを作ってみた件
Typstで色々設定いじって試している時とか、人に説明するためにサンプルを見せる時とか、テスト用の日本語テキスト欲しくならない?
そう!そんな時はroremu
のお出番でございます!ロレム様は360度文字数を自由設定、しかも公式のラテン語lorem()
以上の機能を持ち、offset
のずらし指定さえできちゃいま~す!
更に更に、将来には段落単位の生成も追加されるかもしれないですって?!試すなら、今すぐお使いになれます!こちらのコマンドで、なんと無料で今すぐ使えちゃうよ!
#import "@preview/roremu:0.1.0": roremu
という茶番は置いといて、ついに先日採択いただきましたので、リポの公開と実装詳細・手順や注意事項などについてお話します。
TL;DR
#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)でも指定しておきます。このファイルをコンパイルしてみると、テキストが表示されます。
#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()
すると日本語の文字単位で切り出せます。
#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)
残りの問題として、もし百億文字を要求されたら、文字が足りなくなってしまいます。その場合は、繰り返させておくる必要があります。方法は簡単です。例えばcalc.div-euclid()
の整除法を使って、整数商に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("")
}
ずらしの指定
ずらし考え方も簡単で、例えばまずずらさずに「いろはにほへと」の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("")
}
カスタムなテキストを指定する
以上のように、ほしかったコア機能をすべて実装できたので、前述の理由もあってテキストの指定をできるようにしたい。新たに引数を追加して、もし指定されていなければ『吾輩は猫である』を使いますが、指定され場合はそれを使います。
/// 日本語ダミーテキスト生成
///
/// - 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: "猫")
テストケースを付ける
テスト用の表示を付けております。
#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
にコンパイルさせます。
#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 IDをlicense =
に記入します。それから、ドキュメントのPDFや画像データなど、ダウンロード時に不必要なものをexclude =
に記述する必要があります。それ以外は適切なものを入れてください。
[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
をコミット&プッシュしてもらいます。
/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.toml
、neko.txt
、lib.typ
、manual.pdf
、manual.typ
、test.typ
、LICENSE
、README.md
など)をpackages/preview/roremu/0.1.0/
に貼り付けます。注意すべきのは、かならずパッケージ名/バージョン番号
というファイル構造にします。その後に、コミット&プッシュします。した後に、typst/packagesにPull Requestを提出します。テンプレートに従って、確認をしたら、後は認可されるのを待ちます。もし何か問題があれば指摘されるので、焦らず議論・対応していきましょう。
おわりに
以上でroremu
パッケージの開発・公開までの経緯を紹介してみました。いかかがでしたでしょうか。何か質問があればぜひコメント欄にお願いします。
コミュニティ
「くみはんクラブ」というコミュニティを創設しました。そこで、Typstに関する話もされますので、何か質問や不明点があれば、ぜひいらしてください。
Discussion