Skip to content

Instantly share code, notes, and snippets.

@zr-tex8r
Last active May 26, 2024 05:16
Show Gist options
  • Save zr-tex8r/d758ec18e69165e601436e0efd93e0d1 to your computer and use it in GitHub Desktop.
Save zr-tex8r/d758ec18e69165e601436e0efd93e0d1 to your computer and use it in GitHub Desktop.
bxjaref: Typstの参照の出力を日本語の習慣に合わせて調整する

bxjaref

Typst:参照の出力を日本語の習慣に合わせて調整する

基本的な使い方

全項目importの形でパッケージを読み込む。

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

その直後に以下のshow ruleを設定する。

#show: use-ja-ref

これにより、参照の出力(ref関数またはその簡易記法の@«ラベル名»)において、例えば節の参照は“1.2節”、図の参照は“図3”のようにsupplementの文言(“節”や“図”)が適切な位置に付けられるようになる。番号の部分(“1.2”など)の出力は従来の仕様が維持され、対象要素のnumberingパラメタの設定に従う。

set text(lang: "ja")の設定が自動的に行わるため、supplementの文言は日本語になる。
※Typstの標準の仕様では、節の参照は“節 1.2”、図の参照は“図 3”のようになり、supplementは常に番号の前に欧文空白を挟んで付けられる。和文の入力ではこの空白は不要なので、bxjarefでは文言の位置を調整するとともにこの余計な空白を除去している。ただし実際に版面に出力する際には欧文と和文の間に“四分空き”が自動的に挿入されることに注意。

Typst標準のref関数ではsupplement引数(簡易記法においては@«ラベル名»[«内容»]と書くと[«内容»]の部分がsupplementになる)で個別にsupplementの文言を指定できる。bxjarefの拡張機能として、supplement引数に?を含めることで番号の入る位置を明示することが可能になる。

// 例えばレベル1の節見出しの参照の場合
@secA        //-->"1節" (※標準では"節 1")
@secA[章]    //-->"1章" ("節"や"章"は後ろに付く)
@secA[セクション] //-->"セクション1"
@secA[第?章] //-->"第1章" ("?"の部分に番号が入る)

機能説明

  • use-ja-ref(«本体»)(function): bxjarefの機能である「参照出力の日本語用調整」を有効にする。以下のshow ruleの形式での使用を想定している。

    #show: use-ja-ref
    // パラメタを指定したいなら次のようにする
    #show: use-ja-ref.with(placeholder: "!")
    • «本体»(content): 適用対象となる内容。前述のshow ruleの形で呼び出す場合は“以降の文章全部”となる。

    オプション引数:

    • target(auto | array | function; 既定値auto): 日本語用調整の対象となる参照種別を限定する。次の何れかを指定する。

      • auto: 全ての参照種別が対象となる。
      • 参照種別の配列: 配列に含まれる参照種別だけが対象となる。
      • 参照種別を受け取って真偽値を返す関数: 返り値が真となる参照種別だけが対象となる。
      • 参照種別は要素関数(節見出しはheading、数式はmath.equation)で表す。ただしfigureについてはkindの値(imagetableまたは文字列)で表す。
    • footnote-supplement(str | content | none; 既定値none): 脚注に対するsupplement値の既定値。
      ※詳細は「脚注に対する参照」節を参照。

    • placeholder(str | none; 既定値"?"): supplement値の中のプレースホルダー記号。
      ※詳細については「supplement値の取扱」節を参照。

    • add-space(bool; 既定値false): カウンタ出力とsupplement値の間に欧文空白を挿入するか。
      ※詳細については「supplement値の取扱」節を参照。

    • lang(str | none; 既定値"ja"): none以外を指定した場合、text.langにその値を設定するset ruleを実行する。

  • no-trim(«パターン»)(function): 「numbering関数の第1引数に指定すると、«パターン»を指定したのと同等になるような関数」を返す。
    ※つまりnumbering.with(«パターン»)と同等である。詳細については「no-trim関数」節を参照。

supplement値の取扱

参照の出力(例えば“図5”)は参照の種別を表す文言(“図”)とカウンタ出力(“5”)からなる。この参照の種別を表す文言を指定するのがref関数のsupplementオプション引数である。refの簡易記法である@«ラベル名»では、直後に内容ブロック[...]を書けばそれがsupplement引数と見なされる。

supplement引数の既定値はautoでこの場合は参照対象(例えばheading)のsupplement引数の値が使われ、それもautoである場合は言語により決まる既定値(日本語ではheadingは“節”)が使われる。以下では“実際にsupplementとして使われる値”のことを「supplement値」と呼ぶことにする。

Typst標準の仕様では参照出力は「supplement値+欧文空白+カウンタ出力」で構成される。(ただしsupplement値が空内容[]である場合は欧文空白もなくカウンタ出力のみが参照出力となる。)ところが、日本語の文書ではそもそも欧文空白は余計であるし、また“5節”や“第5節”のように番号の後ろにもsupplement値を付けたい場合もあるので、この仕様では対応できない。そこで、bxjarefの日本語用調整においては、supplement値の記法を拡張して「カウンタ出力が入るべき場所をプレースホルダー記号?で指定する」ようにした。拡張された仕様では、参照出力は以下のように構成される:

  • supplement値が?を含まない場合:
    • 既定では「supplement値+カウンタ出力」となる。欧文空白は入らない。
    • use-ja-refのオプションadd-spaceがtrueの場合は従来通り「supplement値+欧文空白+カウンタ出力」となる。ただしsupplement値が空(""または[])の場合はカウンタ出力のみとなる。
    • 特例として、supplement値が“部”・“章”・“節”の何れかである場合は「カウンタ出力+supplement値」となる。この場合もadd-spaceがtrueの場合は間に欧文空白が入る。
  • supplement値が?を(1つだけ)含む場合:
    • supplement値の?をカウンタ出力に置換したテキストとなる。
      add-spaceの設定は無関係である。

以下に「supplement値」と「カウンタ出力が“1”である場合の参照出力」の組み合わせの例をいくつか挙げておく。

  • → “図1”(add-spaceがtrueなら“図 1”)
  • → “1節”(add-spaceがtrueなら“1 節”)
  • 第?章 → “第1章”
  • ?項 → “1項”
  • 章? → “章1”
  • Section ? → “Section 1”
  • 空 → “1”

※補足

  • supplement値が複数の?を含む場合はエラーになる。
  • プレースホルダー記号は既定では?であるが、use-ja-refのオプション引数placeholderで変更できる。

脚注に対する参照

Typst標準の動作では脚注に対する参照は他のものと異なっていて、「ある場所に付けた脚注を再度別の場所に付けるために脚注番号(合印)を出力する」という目的をもつ。

#set footnote(numbering: "1)")

春はあけぼの#footnote[脚注だよ]<fnA>// "1)"の合印

夏はばけもの@fnA// ここにも"1)"の合印が出力される

bxjarefでは、脚注に対する参照を他の参照種別と同じく「参照のための文言を出力する」目的に切り替える機能を提供する。bxjarefを有効にした上で脚注に対する参照のsupplement値をnone以外にすると出力が切り替わる。

#show: use-ja-ref
#set footnote(numbering: "1)")

春はあけぼの#footnote[脚注だよ]<fnA>// "1)"の合印

@fnA[注]を参照せよ。// "注1"と出力(合印でない)

夏はばけもの@fnA// "1)"の合印(変更なし)

なお、脚注に対するrefsupplement引数がautoの場合は(footnoteにはsupplement引数はないので)use-ja-ref関数のfootnote-supplement引数に指定した値がsupplement値となる。このfootnote-supplement引数の既定値はnoneである。このため“既定の場合”の動作は従来のものから変わらない。

no-trim関数

Typst標準の仕様では、参照出力中のカウンタ書式は参照種別(例えば数式はmath.equation)に設定されたカウンタ書式(numberingパラメタ)を継承するが、ここでnumbering文字列(パターン)として指定した場合は、結果のカウンタ出力の際に「最初の接頭辞の最後の接尾辞の部分を除去する」という調整が行われる。use-ja-refを有効化した場合もこの仕様は維持される。

例えば、数式に対するカウンタ書式を以下のように設定したとする。

#set footnote(numbering: "1)")
#set math.equation(numbering: "(1)")
$ a + b = c $<eqA> // 数式番号"(1)"が出る

@eqA // "式1"と出力

このとき数式番号は“(1)”となるが参照の出力は“式1”となる(Typst標準の挙動では“式 1”)。ここで「接頭辞・接尾辞の除去」を回避して“式(1)”と出力させたい場合には「numberingを敢えて関数で指定する」という方策が利用できる。

#set math.equation(numbering: numbering.with("(1)"))

関数で指定した場合は「除去」が行われないからである。

しかし上記の書き方には“敢えて面倒な書き方をする”意図が判りにくいという欠点がある。そこでbxjarefでは、意図が明確にするためnumbering.withと同じ機能をもつno-trim関数を用意した。

#set math.equation(numbering: no-trim("(1)"))

@eqA // "式(1)"と出力

supplementの既定値の変更

(TODO)

注意事項

  • bxjarefが動作を変更するのは「参照(ref関数)の出力」の範囲に限られる。特に、参照対象の要素自体の出力(例えばfigureのキャプションの中の番号など)には何の影響も及ぼさないことに注意してほしい。
  • 特に、参照対象要素の側のsupplement引数を指定する際には「supplement値の拡張機能(?の使用など)は参照出力でのみ有効である」ことに留意する必要がある。
// パッケージ読込と有効化
#import "@local/bxjaref:0.1.0": *
#show: use-ja-ref
#set page(paper: "a5")
#set text(lang: "ja", font: "Harano Aji Mincho")
#set math.equation(numbering: "(i)")
#set heading(numbering: "§1.a.")
= bxjarefしてみた
// "@eqA"の後に区切りが必要なので"#{}"を入れている
@secA#{}はすごい。\ // "§"や末尾"."は外れる
@ssecA#{}はすごい。\ // (これはTypst標準の仕様)
@eqA#{}はすごい。\
#ref(<figA>)はすごい。\ // 関数形式
// supplementを指定した例
@secA[章]はすごい。\ //"節"や"章"は自動的に後側になる
@ssecA[☃]はすごい。\ // それ以外は前側
@eqA[*式*]はすごい。\
@figA[]はすごい。\ //番号だけ
// supplementでプレースホルダー"?"を利用した例
@secA[第?章]はすごい。\
@ssecA[節?]はすごい。\
@eqA[式(?)]はすごい。\
#ref(<figA>, supplement: "図[?]")はすごい。// 関数形式
#[四分空きを無効にしてみる。
#set text(cjk-latin-spacing: none)
@secA[第?章]はすごい。\ // 空白は入っていない
@ssecA#{}はすごい。\ // やはり空白は入っていない
@eqA#{}はすごい。\
@figA[]はすごい。
]
#pagebreak()
= メッチャすごい節 <secA>
== 超すごい小節 <ssecA>
$ 1+1=2 $<eqA>
#figure(caption: "メッチャすごい図")[
#block(
width: 6cm, height: 2cm, fill: aqua,
align(center + horizon, text(size: 40pt)[図]),
)
] <figA>
// パッケージ読込と有効化
#import "@local/bxjaref:0.1.0": *
#show: use-ja-ref
#set page(paper: "a5")
#set text(lang: "ja", font: "Harano Aji Mincho")
// "no-trim"を付けると括弧が外れなくなる
// ※関数指定の場合は接頭辞・接尾辞が外れないことを利用している.
#set math.equation(numbering: no-trim("(i)"))
// 節見出しのnumberingについて,
// レベル1は"第1章", それ以外は"1.a"とする.
// ※これも関数指定なのでrefで接頭辞・接尾辞が外れない.
#set heading(numbering: (..args) => {
let counts = args.pos()
let format = if counts.len() == 1 { "第1章" } else { "1.a" }
numbering(format, ..counts)
})
// ref側のsupplementを関数で指定する
#set ref(supplement: it => { // it は参照対象の要素
if it.func() == heading { // 節見出し
// レベル1以外は後に"節"を補う
if it.level == 1 { "" } else { "?節" }
} else { // 節見出し以外
// (独自拡張) 関数がautoを返した場合は
// "supplement自体がauto"のときと同じ処理になる
auto
}
})
= もっとbxjarefしてみた
// "@eqA"の後に区切りが必要なので"#{}"を入れている
@secA#{}はすごい。\ // "§"や末尾"."は外れる
@ssecA#{}はすごい。\ // (これはTypst標準の仕様)
@eqA#{}はすごい。\ // 括弧が残る
#ref(<figA>)はすごい。
#pagebreak()
= メッチャすごい節 <secA>
== 超すごい小節 <ssecA>
$ 1+1=2 $<eqA>
#figure(caption: "メッチャすごい図")[
#block(
width: 6cm, height: 2cm, fill: aqua,
align(center + horizon, text(size: 40pt)[図]),
)
] <figA>
#let (
use-ja-ref,
no-trim,
) = {
let default-lang = "ja"
let default-numbering = "1.1"
let default-placeholder = "?"
let postfix-supplements = ("部", "部分", "章", "節", "节", "\u{FA56}")
//---------------------------------------------
let is-std-symbol(char) = { // check if char is a standard symbol
(char in "1aAiI" or not ("2" in numbering(char + "1", 2, 1)))
}
let trim-pattern(check, pattern) = {
let chrs = pattern.codepoints()
let poss = range(chrs.len()).filter(k => check(chrs.at(k)))
let (nchr, nsym) = (chrs.len(), poss.len(),)
let (fp, lp) = (poss.first(), poss.last())
let p = if nsym == 1 { 0 } else { poss.at(-2) + 1 }
if (lp + 1 < nchr and p == lp) {
chrs.push(chrs.at(lp))
(nchr, nsym) = (nchr + 1, nsym + 1)
} else {
nchr = lp + 1
}
if nsym == 1 and fp > 0 {
return chrs.at(fp) + chrs.slice(0, nchr).join()
}
chrs.slice(fp, nchr).join()
}
//---------------------------------------------
// Converts content to string if possible.
let stringize(v) = {
if type(v) != content { v }
else if v == [] { "" }
else { v.at("text", default: v) }
}
// Retrieves the necessary parameters.
let retrieve(it, footnote-sup, placeholder) = {
let elem = it.element
let kind = (
if elem.func() == figure { elem.at("kind", default: none) }
else { elem.func() }
)
let supplement = stringize(
if type(it.supplement) == function {
let elem-sup = elem.at("supplement", default: none)
(..args) => { // auto is changed to elem-sup
let stext = (it.supplement)(..args)
if stext != auto { stext } else { elem-sup }
}
}
else if it.supplement != auto { it.supplement }
else if kind == footnote { footnote-sup }
else { elem.at("supplement", default: none) }
)
if supplement in postfix-supplements {
supplement = placeholder + supplement
}
let count = elem.at("counter", default: none)
if kind in (heading, math.equation, footnote) {
count = counter(kind)
}
let number = elem.at("numbering", default: default-numbering)
// If the numbering is a pattern string,
// trim it just as the standard procedure does.
if type(number) == str {
number = trim-pattern(is-std-symbol, number)
}
(kind, supplement, number, count)
}
// Resolves a function-value supplement
// just as the standard procedure does.
let get-supplement(supplement, element) = {
if type(supplement) != function { supplement }
else { supplement(element) }
}
// Adorns the counter text with the supplement.
let add-supplement(stext, ctext, space, placeholder) = {
if ( // stext has a placeholder
type(stext) == str and
placeholder != none and placeholder != "" and
stext.find(placeholder) != none
) {
let ps = stext.split(placeholder)
assert(ps.len() == 2,
message: "supplement has multiple placeholder characters",
)
return { ps.first(); ctext; ps.last() }
}
// Otherwise prefix with stext, but without space by default.
let sp = if space and stext not in ("", none) { " " } else { "" }
{ stext; sp; ctext }
}
let elements = (heading, math.equation, image, table, footnote)
let is-function(v) = {
type(v) == function and v not in elements
}
let is-target(kind, spec) = {
if spec == auto { true }
else if type(spec) == array { kind in spec }
else if is-function(spec) { spec(kind) }
else { spec == kind }
}
let use-ja-ref(
body,
target: auto,
footnote-supplement: none,
placeholder: default-placeholder,
add-space: false,
lang: default-lang,
) = {
assert(
type(placeholder) == str and placeholder.codepoints().len() == 1,
message: "placeholder must be a single character",
)
set text(lang: lang) if lang != none
show ref: it => {
let elem = it.element
if type(elem) != content { return it }
let (kind, supplement, number, count) = (
retrieve(it, footnote-supplement, placeholder)
)
if (
(count == none) or
(kind == footnote and supplement == none) or
not is-target(kind, target)
) { return it }
let ctext = numbering(number, ..count.at(elem.location()))
let stext = get-supplement(supplement, elem)
add-supplement(stext, ctext, add-space, placeholder)
}
body
}
let no-trim(pattern) = {
numbering.with(pattern)
}
(// exports
use-ja-ref,
no-trim,
)
}
[package]
name = "bxjaref"
version = "0.1.0"
entrypoint = "bxjaref.typ"
compiler = "0.11.0"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment