Skip to content

Instantly share code, notes, and snippets.

@zr-tex8r
Last active May 26, 2024 05:16
Show Gist options
  • Save zr-tex8r/15fbdc6a694eaf6b8d0ca6cf9052f8b1 to your computer and use it in GitHub Desktop.
Save zr-tex8r/15fbdc6a694eaf6b8d0ca6cf9052f8b1 to your computer and use it in GitHub Desktop.
Typst:日本語用にnumberingを拡張したやつ

bxbango

Typst:日本語用にnumberingを拡張したやつ

Typstのnumbering関数の拡張版にあたるja-numbering関数を提供する。この関数は、標準のnumberingがサポートするカウンタスタイルに加えて、日本でよく使われる他のカウンタスタイル(例えば丸数字“①、②、③……”など)をサポートする。

同様の機能をもつ既存パッケージとしてnumberingxがあるが、numberingxが独自の書式文字列の文法を採用しているのに対して、本パッケージはnumbering関数の書式文字列の文法をそのまま踏襲している。すなわち、書式文字列内ではカウンタスタイルの部分は特定のUnicode文字1つ(例えば“①”)で表される。

パッケージ読込

全項目インポートの形式を想定している。

#import "@local/bxbango:0.1.0": *

使用法

機能一覧

  • ja-numbering(«書式», «カウンタ値»...)(function): 指定の書式文字列に従ってカウンタ値を出力する。

    引数:

    • «書式»(str): 書式文字列。Typst標準のnumbering関数のformatと同様の文法に従うが、日本語用にカウンタ記号(counting symbol)がいくつか追加されている。
    • «カウンタ値»(int; 可変長): カウンタの値。実際には任意の整数が指定できる。

    オプション引数:

    • custum(dictionary; 既定値(:)): カスタムのカウンタ記号を指定する。引数には「カウンタ記号(Unicode文字1つからなる文字列)をキー、スタイル関数を値とする辞書」を指定する。スタイル関数とは「カウンタ値の整数をカウンタ表現の文字列(またはcontent)に変換する関数」のことである。
    • base(string; 既定値"japanese"): 有効なカウンタ記号のセットを限定する。
      • "japanese": カスタムのもの、本パッケージ提供のもの、Typst標準のものが(この優先順で)全て有効になる。
      • "standard": カスタムのもの、Typst標準のものだけが有効になる。
      • "none": カスタムのものだけ有効になる。

    返り値(str | content): カウンタ値の表現。
    ※contentを出力するようなカスタム記号を利用しない限り、返り値は文字列になるはずである。

  • bango(module): より高度な機能を提供するためのサブモジュール。

カウンタ記号

Typst標準のnumberingで使えるカウンタ記号はja-numberingでも有効である。それらに加えて以下のカウンタ記号が利用できる。

  • W3C Group Note「Ready-made Counter Styles」(以下では“Ready-made”と呼ぶ)で規定されているスタイル: a A
  • LaTeXの[japanese-otfパッケージ]で規定されているスタイル: 🅰 🄐 🅐 🄰
  • その他のスタイル: 亿

規則が明らかな文字が多いが、いくつか補足しておく。

  • : 位取り記数法に基づく漢数字表記(2024→“二〇二四”)。いわゆる“一〇方式”。
    ※例外的に、これはTypst標準のカウンタ記号を非互換な形で置き換えている。Typst標準のは簡体字中国語の読みに基づく漢数字表記である。
  • : 日本語の読みに基づく漢数字表記(2024→“二千二十四”)。いわゆる“十字方式”。
  • : 日本語の大字を用いた漢数字表記(2024→“弐阡弐拾四”)。
    の細則は“Ready-made”に従う。ただし有効範囲は「Typstの整数型の全範囲」に拡大されている。
  • : 十干(甲、乙、丙、丁、…)。
  • : これらは五十音順(あ、い、う、…)に基づく。
  • : これらはイロハ順(い、ろ、は、…)に基づく。
  • : これらは曜日順(日、月、火、…)に基づく。
  • 亿: これはTypst標準のの書式で、簡体字中国語の読みに基づく漢数字表記である。

要素関数のnumberingとして利用する方法

Typstのカウンタの書式設定はnumbering関数を使う代わりに、要素関数(element function)の引数(numberingという名であることが多い)に書式文字列を指定する方法で行われることがある。

#set heading(numbering: "1.a")

Typstの仕様として書式文字列の引数には代わりに関数を渡すことができる。書式設定としてja-numberingの機能を利用したい場合は、以下のような記述をすればよい。

#set heading(numbering: ja-numbering.with("①.a"))

bangoサブパッケージの機能

スタイル関数

単純に1つの整数を指定のスタイルの文字列に変換する関数である。

“Ready-made”で規定されているスタイル。“Ready-made”でのスタイル名をそのまま関数名としている。

  • cjk-decimal(2024→“二〇二四”)
  • cjk-heavenly-stem(甲、乙、丙⋯)
  • circled-ideograph(㊀、㊁、㊂⋯)
  • parenthesized-ideograph(㈠、㈡、㈢⋯)
  • hiragana(あ、い、う⋯)
  • hiragana-iroha(い、ろ、は⋯)
  • katakana(ア、イ、ウ⋯)
  • katakana-iroha(イ、ロ、ハ⋯)
  • circled-katakana(㋐、㋑、㋒⋯)
  • japanese-informal(2024→“二千二十四”)
  • japanese-formal(2024→“弐阡弐拾四”)
  • lower-alpha(a、b、c⋯)
  • upper-alpha(A、B、C⋯)
  • fullwidth-lower-alpha(a、b、c⋯)
  • fullwidth-upper-alpha(A、B、C⋯)
  • parenthesized-lower-latin(⒜、⒝、⒞⋯)
  • circled-decimal(①、②、③⋯)
  • circled-lower-latin(ⓐ、ⓑ、ⓒ⋯)
  • circled-upper-latin(Ⓐ、Ⓑ、Ⓒ⋯)
  • dotted-decimal(⒈、⒉、⒊⋯)
  • double-circled-decimal(⓵、⓶、⓷⋯)
  • filled-circled-decimal(❶、❷、❸⋯)
  • fullwidth-decimal(1、2、3⋯)
  • fullwidth-lower-roman(ⅰ、ⅱ、ⅲ⋯)
  • fullwidth-upper-roman(Ⅰ、Ⅱ、Ⅲ⋯)
  • parenthesized-decimal(⑴、⑵、⑶⋯)

japanese-otfで規定されているスタイル。

  • parenthesized-yobi(㈰、㈪、㈫⋯)※\ajKakkoYobi
  • filled-squared-upper-latin(🅰、🅱、🅲⋯)※\ajKuroKakuAlph
  • recycle-symbol(♳、♴、♵⋯)※\ajRecycle
  • parenthesized-upper-latin(🄐、🄑、🄒⋯)※\ajKakkoAlph
  • circled-yobi(㊐、㊊、㊋⋯)※\ajMaruYobi
  • filled-circled-upper-latin(🅐、🅑、🅒⋯)※\ajKuroMaruAlph
  • squared-upper-latin(🄰、🄱、🄲⋯)※\ajKakuAlph

その他のスタイル。

  • circled-katakana-iroha(㋑、㋺、㋩⋯)

汎用スタイル関数

CSS Counter Styles Level 3で規定するアルゴリズムの実装。詳細については当該文献を参照されたい。

  • cyclic(«記号リスト», «値»)

    • «記号リスト»(array): symbolsの値。
    • «値»(int): 入力の整数値。
    • 返り値(str): カウンタ表現。
  • fixed(«記号リスト», «値»)

    • «記号リスト»(array): symbolsの値。
    • «値»(int): 入力の整数値。
    • first(int): first symbol valueの値。
    • 返り値(str): カウンタ表現。
  • numeric(«記号リスト», «値»)

    • «記号リスト»(array): symbolsの値。
    • «値»(int): 入力の整数値。
    • negative(str): negativeの値。
    • 返り値(str): カウンタ表現。
  • alphabetic(«記号リスト», «値»)

    • «記号リスト»(array): symbolsの値。
    • «値»(int): 入力の整数値。
    • negative(str): negativeの値。
    • 返り値(str): カウンタ表現。

numbering関数

「Typstと同様の文法の書式文字列」を利用してカウンタ値を書式化する関数を作るための補助関数。

  • numbering(«検査器», «変換器», «書式», «値»...)
    • «検査器»(function): 文字1つからなる文字列を受け取って、その文字がカウンタ記号であるかの真偽値を返す関数。
    • «変換器»(function): 単一のカウンタ記号からなる文字列と整数値を受け取って、カウンタ表現を返す関数。
    • «書式»(str): Typstと同様の文法の書式文字列。
    • «値»(int); 可変長): カウンタ値。
    • 返り値(str|content): カウンタ値の表現。
#import "@local/bxbango:0.2.0": *
#set page(paper: "a5")
#set text(lang: "ja", font: "Harano Aji Mincho")
// 節見出しの番号
#set heading(numbering: ja-numbering.with("§壱.㋑"))
// 番号付箇条書きの番号
#set enum(numbering: ja-numbering.with("♳."))
= ja-numbering関数
#ja-numbering(
"十-甲.㈰.🅐.①", // ".①"が反復される
5000000000000000, 5, 3, 26, 0, 19, 42
) \
#ja-numbering(
custom: ("☃": n => "☃" * n),
"【☃】", 8
)
= 要素の書式設定
+ これは
+ ひどい
+ タイプスツ
+ ですね
= bangoサブパッケージ
#bango.japanese-informal(9087611000050401132) \
#bango.upper-alpha(9590250)
#import "./sub.typ" as bango
#let (
// ja-numbering(
// custom: dictionry, // the custom symbols [(:)]
// base: string, // the base mode ["japanese"]
// str, // the pattern string
// ..int, // the counter values
// ) -> str
ja-numbering,
) = {
// Counter symbols for styles provided by this package
let ja-symbols = (
// W3C ready-made styles
"一": bango.cjk-decimal,
"甲": bango.cjk-heavenly-stem,
"㊀": bango.circled-ideograph,
"㈠": bango.parenthesized-ideograph,
"あ": bango.hiragana,
"い": bango.hiragana-iroha,
"ア": bango.katakana,
"イ": bango.katakana-iroha,
"㋐": bango.circled-katakana,
"十": bango.japanese-informal,
"壱": bango.japanese-formal,
"a": bango.lower-alpha,
"A": bango.upper-alpha,
"a": bango.fullwidth-lower-alpha,
"A": bango.fullwidth-upper-alpha,
"⒜": bango.parenthesized-lower-latin,
"①": bango.circled-decimal,
"ⓐ": bango.circled-lower-latin,
"Ⓐ": bango.circled-upper-latin,
"⒈": bango.dotted-decimal,
"⓵": bango.double-circled-decimal,
"❶": bango.filled-circled-decimal,
"1": bango.fullwidth-decimal,
"ⅰ": bango.fullwidth-lower-roman,
"Ⅰ": bango.fullwidth-upper-roman,
"⑴": bango.parenthesized-decimal,
// Compatible with LaTeX japanese-otf
"㈰": bango.parenthesized-yobi,
"🅰": bango.filled-squared-upper-latin,
"♳": bango.recycle-symbol,
"🄐": bango.parenthesized-upper-latin,
"㊐": bango.circled-yobi,
"🅐": bango.filled-circled-upper-latin,
"🄰": bango.squared-upper-latin,
// Others
"㋑": bango.circled-katakana-iroha,
"亿": numbering.with("一"), // moved
)
let checker(standard: true, japanese: true, custom, char) = {
let f = custom.at(char, default: none)
if f != none { return true }
if japanese {
let f = ja-symbols.at(char, default: none)
if f != none { return true }
}
(standard and // check if char is a standard symbol
(char in "1aAiI" or not ("2" in numbering(char + "1", 2, 1))))
}
let formatter(japanese: true, custom, char, value) = {
let f = custom.at(char, default: none)
if f != none { return f(value) }
if japanese {
let f = ja-symbols.at(char, default: none)
if f != none { return f(value) }
} // then char must be a standard symbol
numbering(char, value)
}
let base-mode = ( // (japanese, standard)
"none": (false, false),
"standard": (true, false),
"japanese": (true, true),
)
let ja-numbering(custom: (:), base: "japanese", pattern, ..args) = {
let (std, ja) = base-mode.at(base)
bango.numbering(
checker.with(standard: std, japanese: ja, custom),
formatter.with(japanese: ja, custom),
str(pattern), ..args.pos(),
)
}
( // exports
ja-numbering,
)
}
#let (
numbering,
// Generic style functions
cyclic,
fixed,
alphabetic,
numeric,
// Style functions
cjk-decimal,
cjk-heavenly-stem,
circled-ideograph,
parenthesized-ideograph,
hiragana,
hiragana-iroha,
katakana,
katakana-iroha,
circled-katakana,
japanese-informal,
japanese-formal,
lower-alpha,
upper-alpha,
fullwidth-lower-alpha,
fullwidth-upper-alpha,
parenthesized-lower-latin,
circled-decimal,
circled-lower-latin,
circled-upper-latin,
dotted-decimal,
double-circled-decimal,
filled-circled-decimal,
fullwidth-decimal,
fullwidth-lower-roman,
fullwidth-upper-roman,
parenthesized-decimal,
parenthesized-yobi,
filled-squared-upper-latin,
recycle-symbol,
parenthesized-upper-latin,
circled-yobi,
filled-circled-upper-latin,
squared-upper-latin,
circled-katakana-iroha,
) = {
let m-numbering(check, format, pattern, ..args) = {
let (chrs, vals) = (pattern.codepoints(), args.pos())
let poss = range(chrs.len()).filter(k => check(chrs.at(k)))
let (nsym, nval, nchr) = (poss.len(), vals.len(), chrs.len())
let lp = poss.last()
if nsym > nval {
chrs = chrs.slice(0, poss.at(nval - 1) + 1) + chrs.slice(lp + 1)
} else if nsym < nval {
let f = if nsym == 1 { 0 } else { poss.at(-2) + 1 }
let (sp, ep) = if lp == f { (lp, nchr) } else { (f, lp + 1) }
let (rep, len) = (nval - nsym, ep - sp)
chrs = (chrs.slice(0, ep) + chrs.slice(sp, ep) * rep
+ chrs.slice(ep))
poss = poss + range(lp + len, lp + len * rep + 1, step: len)
}
for k in range(nval) {
chrs.at(poss.at(k)) = format(chrs.at(poss.at(k)), vals.at(k))
}
chrs.join("")
}
//////// Generic style functions
let prepare(symbols, value, min-len) = {
assert(type(symbols) == array,
message: "symbols must be an array")
assert(symbols.len() >= min-len,
message: "too few symbols are given")
int(value) // fail if not convertible
}
let resolve-sign(negative, value) = {
if value < 0 { (negative, -value) } else { ("", value) }
}
let cyclic(symbols, value) = {
value = prepare(symbols, value, 1)
symbols.at(rem-euclid(value - 1, symbols.len()))
}
let fixed(first: 1, symbols, value) = {
value = prepare(symbols, value, 1)
let v = if value < first { symbols.len() } else { value - first }
let ret = symbols.at(v, default: none)
if ret == none { str(value) } else { ret }
}
let numeric(negative: "-", symbols, value) = {
value = prepare(symbols, value, 2)
let (sgn, v) = resolve-sign(negative, value)
if v == 0 { return symbols.at(0) }
let slen = symbols.len()
let ret = while v != 0 {
symbols.at(calc.rem(v, slen))
v = calc.quo(v, slen)
}
sgn + ret.rev()
}
let alphabetic(negative: "-", symbols, value) = {
value = prepare(symbols, value, 2)
if value <= 0 { return str(value) } // range is [1,∞)
let slen = symbols.len()
let ret = while value != 0 {
value = value - 1
symbols.at(calc.rem(value, slen))
value = calc.quo(value, slen)
}
ret.rev()
}
let sym(..args) = {
let input = args.pos()
if type(input.at(0)) == str { return input.at(0).codepoints() }
for k in range(0, input.len(), step: 2) {
let (c, cc) = (input.at(k), str.to-unicode(input.at(k + 1)))
range(cc, cc + c).map(str.from-unicode)
}
}
//////// Style functions for Japanese kanji
let ja-neg = "マイナス"
let ija-symbols = (
"〇", "一", "二", "三", "四", "五", "六", "七", "八", "九",
"十", "百", "千", "万", "億", "兆", "京",
)
let fja-symbols = (
"零", "壱", "弐", "参", "四", "伍", "六", "七", "八", "九",
"拾", "百", "阡", "萬", "億", "兆", "京",
)
let ja-div(value, quo1, quo2) = {
calc.rem(calc.quo(value, quo1), quo2)
}
let ja-unit(value, uni-si, formal, syms) = {
if value == 0 { return "" }
if value > 1 or uni-si == 0 or formal { syms.at(value) }
if uni-si > 0 { syms.at(uni-si) }
}
let ja-block(value, blk-si, formal, syms) = {
if value == 0 { return "" }
ja-unit(ja-div(value, 1000, 10), 12, formal, syms)
ja-unit(ja-div(value, 100, 10), 11, formal, syms)
ja-unit(ja-div(value, 10, 10), 10, formal, syms)
ja-unit(ja-div(value, 1, 10), 0, formal, syms)
if blk-si > 0 { syms.at(blk-si) }
}
let ja-form(value, formal, negative, syms) = {
value = prepare(syms, value, 17)
let (sgn, v) = resolve-sign(negative, value)
if v == 0 { return syms.at(0) }
sgn
ja-block(ja-div(v, 10000000000000000, 10000), 16, formal, syms)
ja-block(ja-div(v, 1000000000000, 10000), 15, formal, syms)
ja-block(ja-div(v, 100000000, 10000), 14, formal, syms)
ja-block(ja-div(v, 10000, 10000), 13, formal, syms)
ja-block(ja-div(v, 1, 10000), 0, formal, syms)
}
let japanese-informal(value, negative: ja-neg, symbols: ija-symbols) = {
ja-form(value, false, negative, symbols)
}
let japanese-formal(value, negative: ja-neg, symbols: fja-symbols) = {
ja-form(value, true, negative, symbols)
}
( // exports
m-numbering,
//////// Generic style functions
// NB. 'symbolic' and 'additive' are not supported.
cyclic,
fixed,
alphabetic,
numeric,
//////// Style functions
// adapted from W3C Group Note "Ready-made Counter Styles":
// 一 cjk-decimal (numeric)
numeric.with(sym("〇一二三四五六七八九")),
// 甲 cjk-heavenly-stem (fixed)
fixed.with(sym("甲乙丙丁戊己庚辛壬癸")),
// ㊀ circled-ideograph (fixed)
fixed.with(sym(10, "㊀")),
// ㈠ parenthesized-ideograph (fixed)
fixed.with(sym(10, "㈠")),
// あ hiragana (alphabetic)
alphabetic.with(sym({
"あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみ"
"むめもやゆよらりるれろわゐゑをん"})),
// い hiragana-iroha (alphabetic)
alphabetic.with(sym({
"いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふ"
"こえてあさきゆめみしゑひもせす"})),
// ア katakana (alphabetic)
alphabetic.with(sym({
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミ"
"ムメモヤユヨラリルレロワヰヱヲン"})),
// イ katakana-iroha (alphabetic)
alphabetic.with(sym({
"イロハニホヘトチリヌルヲワカヨタレソツネナラムウヰノオクヤマケフ"
"コエテアサキユメミシヱヒモセス"})),
// ㋐ circled-katakana (fixed)
fixed.with(sym(48, "㋐")),
// 十 japanese-informal (complex)
japanese-informal,
// 壱 japanese-formal (complex)
japanese-formal,
// a lower-alpha (alphabetic)
alphabetic.with(sym(26, "a")),
// A upper-alpha (alphabetic)
alphabetic.with(sym(26, "A")),
// a fullwidth-lower-alpha (alphabetic)
alphabetic.with(sym(26, "a")),
// A fullwidth-upper-alpha (alphabetic)
alphabetic.with(sym(26, "A")),
// ⒜ parenthesized-lower-latin (fixed)
fixed.with(sym(26, "⒜")),
// ① circled-decimal (fixed-0)
fixed.with(first: 0, sym(1, "⓪", 20, "①", 15, "㉑", 15, "㊱")),
// ⓐ circled-lower-latin (fixed)
fixed.with(sym(26, "ⓐ")),
// Ⓐ circled-upper-latin (fixed)
fixed.with(sym(26, "Ⓐ")),
// ⒈ dotted-decimal (fixed-0) *NB. 0 added*
fixed.with(first: 0, sym(1, "🄀", 20, "⒈")),
// ⓵ double-circled-decimal (fixed)
fixed.with(sym(10, "⓵")),
// ❶ filled-circled-decimal (fixed-0) *NB. 0 added*
fixed.with(first: 0, sym(1, "⓿", 10, "❶", 10, "⓫")),
// 1 fullwidth-decimal (numeric)
numeric.with(sym("0123456789")),
// ⅰ fullwidth-lower-roman (fixed)
fixed.with(sym(12, "ⅰ")),
// Ⅰ fullwidth-upper-roman (fixed)
fixed.with(sym(12, "Ⅰ")),
// ⑴ parenthesized-decimal (fixed)
fixed.with(sym(20, "⑴")),
// adapted from LaTeX japanese-otf package:
// ㈰ parenthesized-yobi (fixed) {\ajKakkoYobi}
fixed.with(sym("㈰㈪㈫㈬㈭㈮㈯㈷㉁")),
// 🅰 filled-squared-upper-latin (fixed) {\ajKuroKakuAlph}
fixed.with(sym(26, "🅰")),
// ♳ recycle-symbol (fixed-0) {\ajRecycle}
fixed.with(first: 0, sym(12, "♲")),
// 🄐 parenthesized-upper-latin (fixed) {\ajKakkoAlph}
fixed.with(sym(26, "🄐")),
// ㊐ circled-yobi (fixed) {\ajMaruYobi}
fixed.with(sym(1, "㊐", 6, "㊊")),
// 🅐 filled-circled-upper-latin (fixed) {\ajKuroMaruAlph}
fixed.with(sym(26, "🅐")),
// 🄰 squared-upper-latin (fixed) {\ajKakuAlph}
fixed.with(sym(26, "🄰")),
// other styles:
// ㋑ circled-katakana-iroha (fixed)
fixed.with(sym({
"㋑㋺㋩㋥㋭㋬㋣㋠㋷㋦㋸㋾㋻㋕㋵㋟㋹㋞㋡㋧㋤㋶㋰㋒㋼㋨㋔㋗㋳㋮㋘㋫"
"㋙㋓㋢㋐㋚㋖㋴㋱㋯㋛㋽㋪㋲㋝㋜"})),
)
}
[package]
name = "bxbango"
version = "0.2.0"
entrypoint = "lib.typ"
compiler = "0.9.0"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment