Skip to content
Open
21 changes: 21 additions & 0 deletions packages/preview/auto-mando/0.2.0/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Vincent Tam

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
185 changes: 185 additions & 0 deletions packages/preview/auto-mando/0.2.0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# auto-mando

`auto-mando` is a Typst package for automatic Mandarin annotation of Chinese
characters (漢字). It leverages the high-performance Rust-based WASM plugin
from [`rust-mando`](https://crates.io/crates/rust-mando) to segment text and
apply ruby annotations in either **pīnyīn** or **zhùyīn** (bopomofo /
注音符號).

## Features

* **Automatic Segmentation**: Accurately splits Chinese sentences into words to
ensure correct annotation placement.
* **Pīnyīn Ruby**: Places pīnyīn above (or below) each character via the
`rubby` package.
* **Zhùyīn Ruby**: Places bopomofo to the right of each character by default
(traditional Taiwanese textbook style), with top/bottom/left as alternatives.
* **Tone Styles**: Supports tone marks (`nǐ`, default) and tone numbers (`ni3`)
for pīnyīn; tone marks are always used for zhùyīn.
* **Content-Aware**: Processes both plain strings and Typst content blocks,
preserving line breaks and paragraph breaks.

## Usage

### Basic Pīnyīn Example

```typst
#import "@preview/auto-mando:0.2.0": pinyin-ruby

#set text(font: ("Libertinus Serif", "Noto Serif CJK TC"), size: 18pt)
#set par(leading: 1.5em) // prevent annotation overlap between lines

#pinyin-ruby[北京歡迎你!]
```

![pinyin example](pinyin-eg.png)

### Basic Zhuyin Example

```typst
#import "@preview/auto-mando:0.2.0": zhuyin-ruby

#set text(font: ("Libertinus Serif", "Noto Serif CJK TC"), size: 18pt)
#set par(leading: 1.5em)

#zhuyin-ruby[北京歡迎你!]
```

Bopomofo is placed to the **right** of each character by default, matching
the layout used in Taiwanese elementary school textbooks.

![zhuyin example](zhuyin-eg.png)

### Customizing Word Separation

```typst
#import "@preview/auto-mando:0.2.0": pinyin-ruby
#set text(24pt, font: ("Libertinus Serif", "AR PL KaitiM Big5"))
#set par(leading: 1.5em)

#pinyin-ruby(word-sep: 0.7em)[
北京歡迎你們!

你們今天過得怎麼樣

world, 你可以幫幫我嗎?

能不能告訴我現在你在做甚麼?
]
```

![sample output](example.png)

### Pīnyīn Tone Styles

```typst
#pinyin-ruby(style: "numbers")[北京] // → bei3jing1
```

### Customising Ruby Styles

Both functions accept a `ruby-styles` dictionary that controls the appearance
of the annotation. You only need to supply the keys you want to change — the
rest are filled in from the defaults.

**Pīnyīn** defaults (`pos` must be `top` or `bottom`):

| key | default |
|-----|---------|
| `size` | `0.7em` |
| `dy` | `0pt` |
| `pos` | `top` |
| `alignment` | `"center"` |
| `auto-spacing` | `true` |

```typst
// Smaller annotation below the characters
#pinyin-ruby(
ruby-styles: (size: 0.5em, pos: bottom),
)[北京歡迎你]
```

![custom ruby example](custom-ruby.png)

**Zhùyīn** defaults (`pos` may be `top`, `bottom`, `left`, or `right`):

| key | default |
|-----|---------|
| `size` | `0.33em` |
| `dy` | `0pt` |
| `pos` | `right` |
| `alignment` | `"center"` |
| `auto-spacing` | `true` |

```typst
// Bopomofo above the characters instead of to the right
#zhuyin-ruby(
ruby-styles: (size: 0.5em, pos: top),
)[北京歡迎你]
```

> **Note on line spacing**: At larger font sizes, ruby annotations may overlap
> the line above. Add `#set par(leading: 1.5em)` (or a larger value) before
> your annotated text.

## API Reference

### `pinyin-ruby(it, style: "marks", ruby-styles: (...), word-sep: 0.25em)`

Annotates Chinese text with pīnyīn ruby.

* `it` — string or content block.
* `style` — `"marks"` (default) or `"numbers"`.
* `ruby-styles` — partial or full style dictionary (see table above).
* `word-sep` — horizontal gap between consecutive Chinese words (default
`0.25em`). Pass `0em` to disable.

### `zhuyin-ruby(it, ruby-styles: (...), word-sep: 0.25em)`

Annotates Chinese text with zhùyīn (bopomofo) ruby.

* `it` — string or content block.
* `ruby-styles` — partial or full style dictionary (see table above).
Set `pos: right` (default) for traditional right-side bopomofo, or
`pos: top` / `bottom` for above/below placement.
* `word-sep` — horizontal gap between consecutive Chinese words (default
`0.25em`). Pass `0em` to disable.

### `pinyin-to-zhuyin(syllable)`

Converts a single pinyin syllable with tone number (e.g. `"pin1"`) to a
bopomofo string (e.g. `"ㄆㄧㄣˉ"`). Useful for building custom renderers.

### `syllables-to-zhuyin(syllables)`

Converts an array of pinyin-number syllables to an array of bopomofo strings,
one per syllable.

### `flat(txt, style: "marks")`

Returns a space-separated pīnyīn string (non-Chinese tokens omitted). Useful
for metadata or search indexing.

### `segment(txt, style: "marks")`

Low-level function returning an array of `{word, pinyin}` dictionaries.
`pinyin` is `none` for non-Chinese tokens (punctuation, spaces, Latin).

### `render-segments-pinyin(segs, ruby-styles: (...), word-sep: 0.25em)`
### `render-segments-zhuyin(segs, ruby-styles: (...), word-sep: 0.25em)`

Low-level renderers that operate on the array returned by `segment()`. Use
these if you need to pre-process segments before rendering.

## Technical Details

* **WASM Backend**: Powered by `rust_mando.wasm` for fast, memory-efficient
processing.
* **Dependencies**: [`rubby:0.10.2`](https://typst.app/universe/package/rubby)
for top/bottom ruby placement.
* **Compiler Requirements**: Typst `0.14.0` or higher.
* **License**: MIT.

## Author

Vincent Tam ([GitHub](https://github.com/VincentTam)).
Binary file added packages/preview/auto-mando/0.2.0/custom-ruby.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/preview/auto-mando/0.2.0/example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/preview/auto-mando/0.2.0/pinyin-eg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/preview/auto-mando/0.2.0/rust_mando.wasm
Binary file not shown.
3 changes: 3 additions & 0 deletions packages/preview/auto-mando/0.2.0/src/lib.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#import "utils.typ": flat, segment, segment-content, pinyin-to-zhuyin, syllables-to-zhuyin
#import "pinyin-ruby.typ": _default-pinyin-ruby-styles, render-segments-pinyin, pinyin-ruby
#import "zhuyin-ruby.typ": _default-zhuyin-ruby-styles, render-segments-zhuyin, zhuyin-ruby
88 changes: 88 additions & 0 deletions packages/preview/auto-mando/0.2.0/src/pinyin-ruby.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#import "@preview/rubby:0.10.2": get-ruby
#import "utils.typ": segment-content

// ── default styles ────────────────────────────────────────────────────────────

/// Default ruby styles for pīnyīn annotation.
/// Pass a modified copy as `ruby-styles` to override individual keys.
/// `pos` must be `top` or `bottom` (without quotes, rubby constraint).
#let _default-pinyin-ruby-styles = (
size: 0.7em,
dy: 0pt,
pos: top,
alignment: "center",
delimiter: "|",
auto-spacing: true,
)

// ── segment renderer ──────────────────────────────────────────────────────────

/// Render one segment dict `{word, pinyin}` as pīnyīn-annotated content.
/// Delegates to `rubby`'s `get-ruby` for top/bottom placement.
#let _render-segment-pinyin(seg, ruby-styles: (:)) = {
if seg.pinyin == none {
if seg.word == "\n" { linebreak() }
else { seg.word }
} else {
ruby-styles = _default-pinyin-ruby-styles + ruby-styles
let ruby-fn = get-ruby(..ruby-styles)
let annotation = seg.pinyin.join(ruby-styles.delimiter)
let base = seg.word.clusters().join(ruby-styles.delimiter)
ruby-fn(annotation, base)
}
}

/// Render an array of segment dicts with pīnyīn annotations.
///
/// Parameters:
/// - `segs` — array from `segment()` or `segment-content()`
/// - `ruby-styles` — style dict (see `_default-pinyin-ruby-styles`)
/// - `word-sep` — horizontal gap between consecutive Chinese words
/// (default: 0.25em). Pass `0em` to disable.
#let render-segments-pinyin(segs, ruby-styles: _default-pinyin-ruby-styles, word-sep: 0.25em) = {
let n = segs.len()
let i = 0
while i < n {
let seg = segs.at(i)
let next = if i + 1 < n { segs.at(i + 1) } else { none }
// Two consecutive \n → paragraph break.
if seg.word == "\n" and next != none and next.word == "\n" {
parbreak(); i += 2; continue
}
// Gap between consecutive Chinese words.
if word-sep != 0em and i > 0 {
let prev = segs.at(i - 1)
if seg.pinyin != none and prev.pinyin != none { h(word-sep) }
}
_render-segment-pinyin(seg, ruby-styles: ruby-styles)
i += 1
}
}

/// High-level function: annotate Chinese text with pīnyīn ruby.
///
/// Parameters:
/// - `it` — string or content block
/// - `style` — `"marks"` (default) or `"numbers"`
/// - `ruby-styles` — style dict (see `_default-pinyin-ruby-styles`)
/// - `word-sep` — gap between consecutive Chinese words (default: 0.25em)
///
/// Example:
/// ```typst
/// #pinyin-ruby[北京歡迎你!]
/// #pinyin-ruby(style: "numbers")[北京]
/// #pinyin-ruby(ruby-styles: (size: 0.5em, dy: 0pt, pos: top,
/// alignment: "center", auto-spacing: true))[北京]
/// ```
#let pinyin-ruby(
it,
style: "marks",
ruby-styles: _default-pinyin-ruby-styles,
word-sep: 0.25em,
) = {
render-segments-pinyin(
segment-content(it, style: style),
ruby-styles: ruby-styles,
word-sep: word-sep,
)
}
Loading
Loading