Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 35 additions & 12 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

/// How to render the `tu` mora / `t` onset in katakana.
/// How to render the `tu` mora in katakana. Ainu `tu` has several attested
/// spellings; pick the one your materials use.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TuStyle {
/// `ainconv`'s convention: ト゚.
/// `ainconv`'s default: ト゚ (ト with a combining handakuten).
#[default]
To,
/// Alternative convention: ツ゚.
/// ツ゚ — ツ with a combining handakuten.
Tsu,
/// トゥ — ト followed by a small ゥ.
Twu,
/// ツ — a plain katakana tsu, no handakuten.
PlainTsu,
}

/// Mode a fresh composition starts in.
Expand Down Expand Up @@ -56,14 +61,26 @@ impl Default for Input {
}
}

/// Katakana notation options (post-processed over `ainconv`).
/// Katakana notation options, applied on top of the default `ainconv`
/// conversion. Mirrors the canonical option set in `ainconv-tests`
/// (`options.schema.json`): `useWi`/`useWe`/`useWo`, `useSmallI`/`U`/`N`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Orthography {
/// Rendering of the `tu`/`t` sound.
/// How to render the `tu` mora (ト゚ / ツ゚ / トゥ / ツ).
pub tu_style: TuStyle,
/// Keep the small glide codas (`y`→ィ, `w`→ゥ) instead of collapsing to イ/ウ.
pub small_glides: bool,
/// Render the `-y` coda as a small ィ instead of イ.
pub use_small_i: bool,
/// Render the `-w` coda as a small ゥ instead of ウ.
pub use_small_u: bool,
/// Render the `-n` coda as a small ㇴ instead of ン.
pub use_small_n: bool,
/// Render onset `wi` as ヰ instead of ウィ.
pub use_wi: bool,
/// Render onset `we` as ヱ instead of ウェ.
pub use_we: bool,
/// Render onset `wo` as ヲ instead of ウォ.
pub use_wo: bool,
/// Show the `=` morpheme boundary in the output (`ainconv` strips it).
pub show_equals_boundary: bool,
}
Expand All @@ -72,7 +89,12 @@ impl Default for Orthography {
fn default() -> Self {
Self {
tu_style: TuStyle::To,
small_glides: false,
use_small_i: false,
use_small_u: false,
use_small_n: false,
use_wi: false,
use_we: false,
use_wo: false,
show_equals_boundary: false,
}
}
Expand Down Expand Up @@ -216,8 +238,8 @@ mod tests {
#[test]
fn toml_round_trips() {
let mut c = Config::default();
c.orthography.tu_style = TuStyle::Tsu;
c.orthography.small_glides = true;
c.orthography.tu_style = TuStyle::Twu;
c.orthography.use_small_i = true;
c.input.default_mode = InputMode::Latin;
c.suggestions.max_candidates = 5;
assert_eq!(Config::parse(&c.to_toml()), c);
Expand All @@ -231,8 +253,9 @@ mod tests {
#[test]
fn partial_file_fills_missing_with_defaults() {
// Only one field of one section given; everything else defaults.
let c = Config::parse("[orthography]\nsmall_glides = true\n");
assert!(c.orthography.small_glides);
let c = Config::parse("[orthography]\nuse_small_i = true\n");
assert!(c.orthography.use_small_i);
assert!(!c.orthography.use_small_u); // not given → defaulted
assert_eq!(c.orthography.tu_style, TuStyle::To); // defaulted
assert_eq!(c.suggestions.max_candidates, 9); // whole section defaulted
assert_eq!(c.input.default_mode, InputMode::Kana);
Expand Down
94 changes: 70 additions & 24 deletions src/kana.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ pub fn convert(latn: &str) -> String {
/// Convert romaji to katakana, applying notation options on top of the default
/// conversion:
/// * `show_equals_boundary` — keep the `=` morpheme boundary (ainconv strips it),
/// * `small_glides` — render coda `y`/`w` as small ィ/ゥ (ainconv collapses to イ/ウ),
/// * `tu_style` — render the `tu` mora as ツ゚ instead of ト゚.
/// * `use_wi`/`use_we`/`use_wo` — render onset `wi`/`we`/`wo` as ヰ/ヱ/ヲ,
/// * `use_small_i`/`use_small_u` — render coda `y`/`w` as small ィ/ゥ (else イ/ウ),
/// * `use_small_n` — render coda `n` as small ㇴ (else ン),
/// * `tu_style` — render the `tu` mora as ト゚ / ツ゚ / トゥ / ツ.
pub fn convert_with(latn: &str, ortho: &Orthography) -> String {
let chars: Vec<char> = latn.chars().collect();
let mut out = String::new();
Expand Down Expand Up @@ -151,9 +153,9 @@ pub fn convert_with(latn: &str, ortho: &Orthography) -> String {
continue;
}
// Coda glide: `y`/`w` after a vowel and not before a vowel. ainconv
// collapses these to イ/ウ; with `small_glides` we keep the small kana.
if ortho.small_glides
&& (c == 'y' || c == 'w')
// collapses these to イ/ウ; the small-kana options keep ィ/ゥ.
let small_glide = (c == 'y' && ortho.use_small_i) || (c == 'w' && ortho.use_small_u);
if small_glide
&& i > 0
&& is_vowel(chars[i - 1])
&& (i + 1 >= chars.len() || !is_vowel(chars[i + 1]))
Expand All @@ -168,9 +170,32 @@ pub fn convert_with(latn: &str, ortho: &Orthography) -> String {
}
flush(&mut run, &mut out);

// `tu` is the only source of ト゚, so this swap is unambiguous.
if ortho.tu_style == TuStyle::Tsu {
out = out.replace("ト゚", "ツ゚");
// Onset `wi`/`we`/`wo` — ainconv spells these out as ウィ/ウェ/ウォ; the options
// keep the dedicated kana. Post-processed so a following coda stays intact
// (e.g. `wen` → ウェン → ヱン).
if ortho.use_wi {
out = out.replace("ウィ", "ヰ");
}
if ortho.use_we {
out = out.replace("ウェ", "ヱ");
}
if ortho.use_wo {
out = out.replace("ウォ", "ヲ");
}
// Coda `n` renders as ン by default; ㇴ is the small-kana alternative. Every ン
// in Ainu output is a coda (onset n+V is ナ…ノ), so the swap is unambiguous.
if ortho.use_small_n {
out = out.replace('ン', "ㇴ");
}
// `tu` is the only source of ト゚, so remapping it is unambiguous.
let tu = match ortho.tu_style {
TuStyle::To => None,
TuStyle::Tsu => Some("ツ゚"),
TuStyle::Twu => Some("トゥ"),
TuStyle::PlainTsu => Some("ツ"),
};
if let Some(rep) = tu {
out = out.replace("ト゚", rep);
}
out
}
Expand Down Expand Up @@ -405,37 +430,58 @@ mod tests {
}

#[test]
fn tu_style_tsu() {
// ainconv renders `tu` as ト゚; the Tsu option swaps it to ツ゚.
fn tu_style_variants() {
// ainconv renders `tu` as ト゚; each option remaps it.
assert_eq!(convert("tu"), "ト゚");
assert_eq!(convert_with("tu", &ortho(|o| o.tu_style = TuStyle::Tsu)), "ツ゚");
assert_eq!(convert_with("tu", &ortho(|o| o.tu_style = TuStyle::Twu)), "トゥ");
assert_eq!(
convert_with("tu", &ortho(|o| o.tu_style = TuStyle::Tsu)),
"ツ゚"
convert_with("tu", &ortho(|o| o.tu_style = TuStyle::PlainTsu)),
""
);
assert_eq!(
convert_with("tumpu", &ortho(|o| o.tu_style = TuStyle::Tsu)),
"ツ゚ㇺプ"
convert_with("tumpu", &ortho(|o| o.tu_style = TuStyle::Twu)),
"トゥㇺプ"
);
}

#[test]
fn small_glides() {
// Coda y/w collapse to イ/ウ by default; the option keeps the small kana.
fn small_glide_codas() {
// Coda y/w collapse to イ/ウ by default; the small-kana options keep ィ/ゥ.
assert_eq!(convert("kamuy"), "カムイ");
assert_eq!(
convert_with("kamuy", &ortho(|o| o.small_glides = true)),
convert_with("kamuy", &ortho(|o| o.use_small_i = true)),
"カムィ"
);
assert_eq!(convert_with("ay", &ortho(|o| o.use_small_i = true)), "アィ");
// use_small_i affects only the `y` coda, not the `w` coda.
assert_eq!(
convert_with("ay", &ortho(|o| o.small_glides = true)),
"アィ"
);
// onset y/w (followed by a vowel) is unaffected
assert_eq!(convert_with("ya", &ortho(|o| o.small_glides = true)), "ヤ");
assert_eq!(
convert_with("yaywa", &ortho(|o| o.small_glides = true)),
convert_with("yaywa", &ortho(|o| o.use_small_i = true)),
"ヤィワ"
);
// onset y (followed by a vowel) is unaffected
assert_eq!(convert_with("ya", &ortho(|o| o.use_small_i = true)), "ヤ");
}

#[test]
fn small_n_coda() {
// Coda `n` is ン by default; use_small_n keeps ㇴ.
assert_eq!(convert("pon"), "ポン");
assert_eq!(convert_with("pon", &ortho(|o| o.use_small_n = true)), "ポㇴ");
// onset n+vowel (ナ…ノ) is never ン, so it is untouched.
assert_eq!(convert_with("nina", &ortho(|o| o.use_small_n = true)), convert("nina"));
}

#[test]
fn w_onset_kana() {
// ainconv spells onset wi/we/wo as ウィ/ウェ/ウォ; the options keep ヰ/ヱ/ヲ.
assert_eq!(convert_with("wi", &ortho(|o| o.use_wi = true)), "ヰ");
assert_eq!(convert_with("we", &ortho(|o| o.use_we = true)), "ヱ");
assert_eq!(convert_with("wo", &ortho(|o| o.use_wo = true)), "ヲ");
// a following coda stays intact (post-processed): wen → ウェン → ヱン.
assert_eq!(convert_with("wen", &ortho(|o| o.use_we = true)), "ヱン");
// off by default
assert_eq!(convert_with("wi", &Orthography::default()), convert("wi"));
}

#[test]
Expand Down
13 changes: 11 additions & 2 deletions src/settings_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ fn checkbox_specs(cfg: &Config) -> [(i32, PCWSTR, bool); 6] {
(
ID_GLIDES,
w!("y/w の半母音を ィ/ゥ で表記 / Small glides ィ/ゥ"),
cfg.orthography.small_glides,
// Checked only when both codas are on (the box represents both-or-none).
cfg.orthography.use_small_i && cfg.orthography.use_small_u,
),
(
ID_EQUALS,
Expand Down Expand Up @@ -290,7 +291,15 @@ unsafe fn apply(hwnd: HWND) {
} else {
TuStyle::To
};
cfg.orthography.small_glides = checked(ID_GLIDES);
// The single "small glides" checkbox can only represent both-on or both-off,
// so it must not clobber an asymmetric per-coda state set via the config file
// (e.g. use_small_i=true, use_small_u=false). Write both fields only when they
// already agree, or when the user turns the option on.
let glides = checked(ID_GLIDES);
if glides || cfg.orthography.use_small_i == cfg.orthography.use_small_u {
cfg.orthography.use_small_i = glides;
cfg.orthography.use_small_u = glides;
}
cfg.orthography.show_equals_boundary = checked(ID_EQUALS);
cfg.suggestions.enabled = checked(ID_SUGGEST);

Expand Down
Loading