Paragraph: Asian Typography settings, with kinsoku and mid-word Latin wrapping in layout (#269)

Paragraphs gain the Asian Typography flags (line-breaking rules, wrapping
Latin text in the middle of a word, hanging punctuation, compressing
punctuation at the start of a line, and automatic space between Asian and
Latin text and between Asian text and numbers). They inherit through styles,
round-trip through .docx pPr, are set by the new para.asianTypography
command and appear on a new Asian Typography tab of the Paragraph dialog.

Layout applies the kinsoku lists (no line starts with closing East Asian
punctuation or ends with opening punctuation), breaks between any East Asian
characters when kinsoku is off, and breaks Latin words anywhere when word
wrap is off. Text without East Asian characters breaks as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76 2026-10-10 19:07:29 +02:00
parent 7b8c38fa62
commit 147a055468
18 changed files with 388 additions and 48 deletions

View File

@ -622,13 +622,34 @@ pub struct ParaProps {
/// Drop cap: lines to drop (0 = none) — applies to the first character(s).
#[serde(skip_serializing_if = "Option::is_none")]
pub drop_cap: Option<u8>,
/// Asian typography (`w:kinsoku`): East Asian line-breaking rules — no line starts with
/// closing punctuation such as 、。」 or ends with opening punctuation such as 「(.
/// Unset = on.
#[serde(skip_serializing_if = "Option::is_none")]
pub kinsoku: Option<bool>,
/// `w:wordWrap`: Latin words wrap whole; `false` lets them break at any character (Word's
/// "Allow Latin text to wrap in the middle of a word"). Unset = on.
#[serde(skip_serializing_if = "Option::is_none")]
pub word_wrap: Option<bool>,
/// `w:overflowPunct`: punctuation may hang past the line end. Unset = on.
#[serde(skip_serializing_if = "Option::is_none")]
pub overflow_punct: Option<bool>,
/// `w:topLinePunct`: compress punctuation at the start of a line. Unset = off.
#[serde(skip_serializing_if = "Option::is_none")]
pub top_line_punct: Option<bool>,
/// `w:autoSpaceDE`: automatic space between Asian and Latin text. Unset = on.
#[serde(rename = "autoSpaceDE", skip_serializing_if = "Option::is_none")]
pub auto_space_de: Option<bool>,
/// `w:autoSpaceDN`: automatic space between Asian text and numbers. Unset = on.
#[serde(rename = "autoSpaceDN", skip_serializing_if = "Option::is_none")]
pub auto_space_dn: Option<bool>,
}
impl ParaProps {
pub fn overlay(&mut self, patch: &ParaProps) {
overlay_fields!(self, patch; style, align, indent_left, indent_right, indent_first, space_before, space_after, line_spacing,
contextual_spacing, keep_next, keep_lines, page_break_before, widow_control, outline_level, numbering, tabs, shading, borders,
suppress_hyphens, suppress_line_numbers, bidi, drop_cap);
suppress_hyphens, suppress_line_numbers, bidi, drop_cap, kinsoku, word_wrap, overflow_punct, top_line_punct, auto_space_de, auto_space_dn);
}
pub fn overlaid(mut self, patch: &ParaProps) -> ParaProps {
self.overlay(patch);

View File

@ -141,6 +141,14 @@ pub struct ResolvedPara {
pub bidi: bool,
pub drop_cap: u8,
pub suppress_hyphens: bool,
/// Asian typography (see `ParaProps`): East Asian line-breaking rules.
pub kinsoku: bool,
/// Latin words wrap whole (`false`: they may break at any character).
pub word_wrap: bool,
pub overflow_punct: bool,
pub top_line_punct: bool,
pub auto_space_de: bool,
pub auto_space_dn: bool,
}
const MAX_INDENT: f32 = 1584.0; // 22"
@ -186,6 +194,12 @@ impl ResolvedPara {
bidi: p.bidi.unwrap_or(false),
drop_cap: p.drop_cap.unwrap_or(0).min(10),
suppress_hyphens: p.suppress_hyphens.unwrap_or(false),
kinsoku: p.kinsoku.unwrap_or(true),
word_wrap: p.word_wrap.unwrap_or(true),
overflow_punct: p.overflow_punct.unwrap_or(true),
top_line_punct: p.top_line_punct.unwrap_or(false),
auto_space_de: p.auto_space_de.unwrap_or(true),
auto_space_dn: p.auto_space_dn.unwrap_or(true),
}
}
}

View File

@ -142,6 +142,12 @@ impl PropCtx {
"w:suppressAutoHyphens" => p.suppress_hyphens = Some(on_off(k)),
"w:suppressLineNumbers" => p.suppress_line_numbers = Some(on_off(k)),
"w:bidi" => p.bidi = Some(on_off(k)),
"w:kinsoku" => p.kinsoku = Some(on_off(k)),
"w:wordWrap" => p.word_wrap = Some(on_off(k)),
"w:overflowPunct" => p.overflow_punct = Some(on_off(k)),
"w:topLinePunct" => p.top_line_punct = Some(on_off(k)),
"w:autoSpaceDE" => p.auto_space_de = Some(on_off(k)),
"w:autoSpaceDN" => p.auto_space_dn = Some(on_off(k)),
"w:framePr" => {
if matches!(k.attr("w:dropCap"), Some("drop") | Some("margin")) {
p.drop_cap = Some(k.attr("w:lines").and_then(u32_of).unwrap_or(3).clamp(1, 10) as u8);

View File

@ -230,6 +230,12 @@ pub fn ppr_inner(w: &mut W, p: &ParaProps, framed: bool) {
w.close("w:tabs");
}
toggle(w, "w:suppressAutoHyphens", p.suppress_hyphens);
toggle(w, "w:kinsoku", p.kinsoku);
toggle(w, "w:wordWrap", p.word_wrap);
toggle(w, "w:overflowPunct", p.overflow_punct);
toggle(w, "w:topLinePunct", p.top_line_punct);
toggle(w, "w:autoSpaceDE", p.auto_space_de);
toggle(w, "w:autoSpaceDN", p.auto_space_dn);
toggle(w, "w:bidi", p.bidi);
if p.space_before.is_some() || p.space_after.is_some() || p.line_spacing.is_some() {
let before = p.space_before.map(|v| twips(v.max(0.0)));

View File

@ -136,11 +136,18 @@ fn every_para_prop_round_trips() {
suppress_line_numbers: Some(true),
bidi: Some(false),
drop_cap: None,
kinsoku: Some(false),
word_wrap: Some(false),
overflow_punct: Some(false),
top_line_punct: Some(true),
auto_space_de: Some(false),
auto_space_dn: Some(true),
};
let variants = [
ParaProps { line_spacing: Some(LineSpacing::AtLeast(14.0)), indent_first: Some(24.0), align: Some(Align::Center), ..Default::default() },
ParaProps { line_spacing: Some(LineSpacing::Exactly(20.0)), align: Some(Align::Right), ..Default::default() },
ParaProps { align: Some(Align::Distribute), ..Default::default() },
ParaProps { kinsoku: Some(true), word_wrap: Some(true), auto_space_dn: Some(false), ..Default::default() },
ParaProps::default(),
];
let mut ps = vec![Paragraph::with_text("main", CharProps::default())];

View File

@ -129,6 +129,9 @@ pub fn specs() -> Vec<CommandSpec> {
CommandSpec::new("para.widowControl", "Widow/Orphan Control", "Home › Paragraph › Line and Page Breaks", |s, v| {
tog(s, v, |p| p.widow_control, |p, b| p.widow_control = Some(b))
}),
CommandSpec::new("para.asianTypography", "Asian Typography", "Home › Paragraph › Asian Typography", asian_typography).params(
r#"{"kinsoku"?: bool (East Asian line-breaking rules), "wordWrap"?: bool (false: Latin words may break at any character), "overflowPunct"?: bool (hanging punctuation), "topLinePunct"?: bool (compress punctuation at the start of a line), "autoSpaceDE"?: bool (space between Asian and Latin text), "autoSpaceDN"?: bool (space between Asian text and numbers)} — flags left out stay as they are; returns the flags at the caret"#,
),
CommandSpec::new("para.tabs", "Tabs", "Home › Paragraph › Paragraph", |s, v| {
let tabs: Vec<TabStop> = serde_json::from_value(v.get("tabs").cloned().unwrap_or(Value::Null)).map_err(|e| CmdError::Params(e.to_string()))?;
if tabs.len() > 64 {
@ -240,6 +243,42 @@ fn tog(s: &mut Session, v: &Value, get: fn(&ParaProps) -> Option<bool>, set: fn(
fmt(s, &|p| set(p, on))
}
/// Asian Typography (the Paragraph dialog's tab): sets the given flags on every selected
/// paragraph and returns the resolved flags at the caret.
fn asian_typography(s: &mut Session, v: &Value) -> CmdResult {
let flag = |k: &str| p::bool(v, k);
let (kinsoku, word_wrap, overflow, top_line, de, dn) =
(flag("kinsoku"), flag("wordWrap"), flag("overflowPunct"), flag("topLinePunct"), flag("autoSpaceDE"), flag("autoSpaceDN"));
let mut out = if [kinsoku, word_wrap, overflow, top_line, de, dn].iter().any(Option::is_some) {
fmt(s, &|p| {
let set = |field: &mut Option<bool>, val: Option<bool>| {
if val.is_some() {
*field = val;
}
};
set(&mut p.kinsoku, kinsoku);
set(&mut p.word_wrap, word_wrap);
set(&mut p.overflow_punct, overflow);
set(&mut p.top_line_punct, top_line);
set(&mut p.auto_space_de, de);
set(&mut p.auto_space_dn, dn);
})?
} else {
sel_result(s)?
};
let r = cur(s);
if let Some(o) = out.as_object_mut() {
o.insert(
"asianTypography".into(),
json!({
"kinsoku": r.kinsoku, "wordWrap": r.word_wrap, "overflowPunct": r.overflow_punct,
"topLinePunct": r.top_line_punct, "autoSpaceDE": r.auto_space_de, "autoSpaceDN": r.auto_space_dn,
}),
);
}
Ok(out)
}
pub fn indent(s: &mut Session, _: &Value) -> CmdResult {
let (a, b) = s.sel.ordered();
let paths = s.doc.paths_between(&a, &b);

View File

@ -1652,3 +1652,24 @@ fn timestamps_come_from_the_clock() {
// Fixed-width ISO strings sort by time.
assert!(date >= cmd::iso_from_unix_secs(before) && date <= cmd::iso_from_unix_secs(cmd::now_unix()), "{date}");
}
#[test]
fn asian_typography_sets_flags_on_selected_paragraphs_and_undoes() {
let mut s = s();
run(&mut s, "document.setText", json!({"text": "一\n二\n三"}));
run(&mut s, "select.all", json!({}));
let r = run(&mut s, "para.asianTypography", json!({"kinsoku": false, "wordWrap": false, "topLinePunct": true}));
assert_eq!(r["asianTypography"]["kinsoku"], false);
assert_eq!(r["asianTypography"]["autoSpaceDE"], true, "a flag left out keeps its default");
for p in s.doc.body.iter().filter_map(|b| b.as_para()) {
assert_eq!(
(p.props.kinsoku, p.props.word_wrap, p.props.top_line_punct, p.props.overflow_punct),
(Some(false), Some(false), Some(true), None)
);
}
run(&mut s, "edit.undo", json!({}));
assert!(s.doc.body.iter().filter_map(|b| b.as_para()).all(|p| p.props.kinsoku.is_none() && p.props.word_wrap.is_none()));
// Without flags it only reports them.
let r = run(&mut s, "para.asianTypography", json!({}));
assert_eq!(r["asianTypography"]["kinsoku"], true);
}

View File

@ -0,0 +1,103 @@
//! Asian typography line-breaking rules (the paragraph's `kinsoku` and `word_wrap` flags), applied
//! on top of the Unicode line-break opportunities (UAX #14).
//!
//! - Kinsoku on (the default): no line starts with closing East Asian punctuation (、。,)」…)
//! and none ends with opening punctuation ((「【…). UAX #14 already forbids most of these
//! breaks; the lists make sure of it for the standard Japanese/Chinese sets.
//! - Kinsoku off: a line may break between any two East Asian characters, punctuation included.
//! - Word wrap off: Latin words may break at any character.
//!
//! Only East Asian characters are in the lists, and the extra breaks need letters on both sides,
//! so text with no East Asian characters breaks exactly as before unless word wrap is off.
use unicode_segmentation::UnicodeSegmentation;
/// East Asian characters a line may not start with: closing brackets and quotes, the
/// ideographic comma and full stop, fullwidth and halfwidth punctuation, the middle dot,
/// iteration and sound marks.
const NO_START: &str = "、。,.・:;?!゛゜ヽヾゝゞ々〻〉》」』】〕〗〙〛〞〟)]}⦆。」、・゙゚%¢";
/// East Asian characters a line may not end with: opening brackets and quotes, currency signs.
const NO_END: &str = "〈《「『【〔〖〘〚〝([{⦅「$£¥";
/// Whether `c` is East Asian text: CJK ideographs, kana, bopomofo, CJK punctuation, and
/// fullwidth/halfwidth forms (not spaces, not combining sound marks).
pub fn is_east_asian(c: char) -> bool {
matches!(c as u32,
0x2E80..=0x2FFF // radicals, ideographic description
| 0x3001..=0x303F // CJK symbols and punctuation (not the ideographic space)
| 0x3040..=0x3098 // hiragana
| 0x309B..=0x30FF // sound marks, katakana
| 0x3100..=0x312F // bopomofo
| 0x3190..=0x31FF // kanbun, CJK strokes, katakana extensions
| 0x3400..=0x4DBF // CJK extension A
| 0x4E00..=0x9FFF // CJK unified ideographs
| 0xF900..=0xFAFF // compatibility ideographs
| 0xFE30..=0xFE4F // CJK compatibility forms
| 0xFF01..=0xFFEF // fullwidth and halfwidth forms
| 0x20000..=0x3FFFF) // supplementary ideographs
}
/// Adjust the break opportunities `opps` (byte offsets into `text`: a line may start there) for
/// the paragraph's kinsoku and word-wrap settings. The result is sorted and has no duplicates.
pub fn apply(text: &str, opps: &mut Vec<usize>, kinsoku: bool, word_wrap: bool) {
let next_char = |i: usize| text.get(i..).and_then(|t| t.chars().next());
let prev_char = |i: usize| text.get(..i).and_then(|t| t.chars().next_back());
if kinsoku {
opps.retain(|&i| !next_char(i).is_some_and(|c| NO_START.contains(c)) && !prev_char(i).is_some_and(|c| NO_END.contains(c)));
} else {
// Between any two East Asian characters.
for (i, c) in text.char_indices().skip(1) {
if is_east_asian(c) && prev_char(i).is_some_and(is_east_asian) {
opps.push(i);
}
}
}
if !word_wrap {
// Between any two letters or digits that are not East Asian (whole graphemes, so accents
// stay with their letters).
let latin = |g: &str| g.chars().next().is_some_and(|c| c.is_alphanumeric() && !is_east_asian(c));
let mut prev: Option<&str> = None;
for (i, g) in text.grapheme_indices(true) {
if prev.is_some_and(latin) && latin(g) {
opps.push(i);
}
prev = Some(g);
}
}
opps.sort_unstable();
opps.dedup();
}
#[cfg(test)]
mod tests {
use super::apply;
/// Where a line may start, as the text from there to the next opportunity.
fn breaks(text: &str, kinsoku: bool, word_wrap: bool) -> Vec<String> {
let mut opps: Vec<usize> = unicode_linebreak::linebreaks(text).map(|(i, _)| i).filter(|&i| i < text.len()).collect();
apply(text, &mut opps, kinsoku, word_wrap);
opps.iter().map(|&i| text.get(i..).and_then(|t| t.chars().next()).map(String::from).unwrap_or_default()).collect()
}
#[test]
fn kinsoku_and_word_wrap_break_opportunities() {
let ja = "日本語、「漢字」。テスト";
let on = breaks(ja, true, true);
for c in ["、", "」", "。", "漢"] {
assert!(!on.iter().any(|s| s == c), "kinsoku: no line starts with {c}: {on:?}");
}
assert!(on.iter().any(|s| s == "「"), "a line may start at an opening bracket: {on:?}");
assert!(on.iter().any(|s| s == "テ"), "{on:?}");
let off = breaks(ja, false, true);
for c in ["、", "」", "。", "漢"] {
assert!(off.iter().any(|s| s == c), "kinsoku off: a line may start with {c}: {off:?}");
}
// Latin text: unchanged by kinsoku, broken anywhere without word wrap.
let en = "Hello wonderful (world), 50%.";
assert_eq!(breaks(en, true, true), breaks(en, false, true));
assert_eq!(breaks(en, true, true), ["w", "(", "5"]);
let any = breaks("Hello café", true, false);
assert_eq!(any, ["e", "l", "l", "o", "c", "a", "f", "é"]);
}
}

View File

@ -11,6 +11,7 @@
pub mod display;
pub mod fields;
pub mod hit;
pub mod kinsoku;
pub mod math;
pub mod para;
mod table;

View File

@ -803,19 +803,19 @@ pub fn layout_para(p: &Paragraph, env: &ParaEnv) -> ParaLayout {
let to_para = |i: usize| -> Option<usize> {
if left.is_empty() { Some(i) } else { ends.binary_search_by_key(&i, |e| e.0).ok().and_then(|k| ends.get(k)).map(|e| e.1) }
};
let mut opps = std::collections::HashSet::new();
let mut found = Vec::new();
for (i, o) in unicode_linebreak::linebreaks(&text) {
// Word keeps "and/or" and web addresses whole: no break right after a slash (a word
// too long for the line still breaks anywhere).
let after_slash =
text.get(..i).is_some_and(|t| t.ends_with('/')) && text.get(i..).and_then(|t| t.chars().next()).is_some_and(char::is_alphanumeric);
if (o == unicode_linebreak::BreakOpportunity::Allowed || i < text.len())
&& !after_slash
&& let Some(end) = to_para(i)
{
opps.insert(end);
if (o == unicode_linebreak::BreakOpportunity::Allowed || i < text.len()) && !after_slash {
found.push(i);
}
}
// Asian typography: kinsoku and breaking Latin words anywhere.
crate::kinsoku::apply(&text, &mut found, rp.kinsoku, rp.word_wrap);
let opps: std::collections::HashSet<usize> = found.into_iter().filter_map(to_para).collect();
for c in &mut b.clusters {
c.break_after = opps.contains(&c.end) || matches!(c.kind, ClKind::Object(_) | ClKind::Tab);
}

View File

@ -43,6 +43,15 @@ pub enum Dialog {
keep_lines: bool,
page_break: bool,
widow: bool,
/// Page: 0 Indents and Spacing, 1 Line and Page Breaks, 2 Asian Typography.
tab: u8,
/// Asian Typography as the model has it (`para.asianTypography`): kinsoku, word wrap,
/// hanging punctuation, compress punctuation at line start, space between Asian and
/// Latin text, space between Asian text and numbers.
asian: [bool; 6],
/// `asian` when the dialog opened: OK sets only the flags that changed.
#[serde(skip)]
asian_was: [bool; 6],
},
Find {
query: String,
@ -163,7 +172,7 @@ impl Dialog {
color: s("color"),
spacing: 0.0,
},
"paragraph" => {
"paragraph" | "asianTypography" => {
let rp = app.session.doc.para_at(&app.session.sel.focus).map(|p| app.session.doc.styles.resolve_para(&p.props));
let rp = rp?;
let line = match rp.line_spacing {
@ -184,6 +193,9 @@ impl Dialog {
keep_lines: rp.keep_lines,
page_break: rp.page_break_before,
widow: rp.widow_control,
tab: if name == "asianTypography" { 2 } else { 0 },
asian: [rp.kinsoku, rp.word_wrap, rp.overflow_punct, rp.top_line_punct, rp.auto_space_de, rp.auto_space_dn],
asian_was: [rp.kinsoku, rp.word_wrap, rp.overflow_punct, rp.top_line_punct, rp.auto_space_de, rp.auto_space_dn],
}
}
"find" | "replace" => Dialog::Find {
@ -426,48 +438,94 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
ok || cancel
}
Dialog::Paragraph { rtl, align, left, right, first, before, after, line, keep_next, keep_lines, page_break, widow } => {
ui.label(egui::RichText::new(tl!("General")).font(semibold(12.5)));
Dialog::Paragraph {
rtl,
align,
left,
right,
first,
before,
after,
line,
keep_next,
keep_lines,
page_break,
widow,
tab,
asian,
asian_was,
} => {
ui.horizontal(|ui| {
ui.label(tl!("Direction:"));
ui.radio_value(rtl, true, tl!("Right-to-left"));
ui.radio_value(rtl, false, tl!("Left-to-right"));
});
egui::ComboBox::from_label(tl!("Alignment")).selected_text(align.clone()).show_ui(ui, |ui| {
for a in ["left", "center", "right", "justify"] {
ui.selectable_value(align, a.to_string(), a);
for (k, name) in ["Indents and Spacing", "Line and Page Breaks", "Asian Typography"].into_iter().enumerate() {
if ui.selectable_label(*tab as usize == k, tl!(name)).clicked() {
*tab = k as u8;
}
}
});
ui.label(egui::RichText::new(tl!("Indentation")).font(semibold(12.5)));
egui::Grid::new("ind").num_columns(4).show(ui, |ui| {
ui.label(tl!("Left:"));
ui.add(egui::DragValue::new(left).speed(0.05).suffix("\"").max_decimals(2));
ui.label(tl!("Right:"));
ui.add(egui::DragValue::new(right).speed(0.05).suffix("\"").max_decimals(2));
ui.end_row();
ui.label(tl!("First line:"));
ui.add(egui::DragValue::new(first).speed(0.05).suffix("\"").max_decimals(2));
ui.end_row();
});
ui.label(egui::RichText::new(tl!("Spacing")).font(semibold(12.5)));
egui::Grid::new("sp").num_columns(4).show(ui, |ui| {
ui.label(tl!("Before:"));
ui.add(egui::DragValue::new(before).speed(1.0).range(0.0..=1584.0).suffix(" pt"));
ui.label(tl!("Line spacing:"));
ui.add(egui::DragValue::new(line).speed(0.05).range(0.5..=5.0));
ui.end_row();
ui.label(tl!("After:"));
ui.add(egui::DragValue::new(after).speed(1.0).range(0.0..=1584.0).suffix(" pt"));
ui.end_row();
});
ui.label(egui::RichText::new(tl!("Line and Page Breaks")).font(semibold(12.5)));
ui.checkbox(widow, tl!("Widow/Orphan control"));
ui.checkbox(keep_next, tl!("Keep with next"));
ui.checkbox(keep_lines, tl!("Keep lines together"));
ui.checkbox(page_break, tl!("Page break before"));
ui.separator();
match *tab {
0 => {
ui.label(egui::RichText::new(tl!("General")).font(semibold(12.5)));
ui.horizontal(|ui| {
ui.label(tl!("Direction:"));
ui.radio_value(rtl, true, tl!("Right-to-left"));
ui.radio_value(rtl, false, tl!("Left-to-right"));
});
egui::ComboBox::from_label(tl!("Alignment")).selected_text(align.clone()).show_ui(ui, |ui| {
for a in ["left", "center", "right", "justify"] {
ui.selectable_value(align, a.to_string(), a);
}
});
ui.label(egui::RichText::new(tl!("Indentation")).font(semibold(12.5)));
egui::Grid::new("ind").num_columns(4).show(ui, |ui| {
ui.label(tl!("Left:"));
ui.add(egui::DragValue::new(left).speed(0.05).suffix("\"").max_decimals(2));
ui.label(tl!("Right:"));
ui.add(egui::DragValue::new(right).speed(0.05).suffix("\"").max_decimals(2));
ui.end_row();
ui.label(tl!("First line:"));
ui.add(egui::DragValue::new(first).speed(0.05).suffix("\"").max_decimals(2));
ui.end_row();
});
ui.label(egui::RichText::new(tl!("Spacing")).font(semibold(12.5)));
egui::Grid::new("sp").num_columns(4).show(ui, |ui| {
ui.label(tl!("Before:"));
ui.add(egui::DragValue::new(before).speed(1.0).range(0.0..=1584.0).suffix(" pt"));
ui.label(tl!("Line spacing:"));
ui.add(egui::DragValue::new(line).speed(0.05).range(0.5..=5.0));
ui.end_row();
ui.label(tl!("After:"));
ui.add(egui::DragValue::new(after).speed(1.0).range(0.0..=1584.0).suffix(" pt"));
ui.end_row();
});
}
1 => {
ui.label(egui::RichText::new(tl!("Line and Page Breaks")).font(semibold(12.5)));
ui.checkbox(widow, tl!("Widow/Orphan control"));
ui.checkbox(keep_next, tl!("Keep with next"));
ui.checkbox(keep_lines, tl!("Keep lines together"));
ui.checkbox(page_break, tl!("Page break before"));
}
_ => {
let [kinsoku, word_wrap, overflow, top_line, de, dn] = asian;
ui.label(egui::RichText::new(tl!("Line breaking")).font(semibold(12.5)));
ui.checkbox(kinsoku, tl!("Apply Asian line-breaking rules (kinsoku)"));
// The model's flag is "wrap whole words"; the box offers the opposite.
let mut mid_word = !*word_wrap;
if ui.checkbox(&mut mid_word, tl!("Allow Latin text to wrap in the middle of a word")).changed() {
*word_wrap = !mid_word;
}
ui.checkbox(overflow, tl!("Allow hanging punctuation"));
ui.label(egui::RichText::new(tl!("Character Spacing")).font(semibold(12.5)));
ui.checkbox(top_line, tl!("Compress punctuation at the start of a line"));
ui.checkbox(de, tl!("Add space between Asian and Latin text"));
ui.checkbox(dn, tl!("Add space between Asian text and numbers"));
}
}
let (ok, cancel) = buttons(ui, tl!("OK"));
if ok {
apply_paragraph(app, *rtl, align, *left, *right, *first, *before, *after, *line, [*keep_next, *keep_lines, *page_break, *widow]);
apply_asian(app, *asian, *asian_was);
}
ok || cancel
}
@ -858,6 +916,17 @@ fn apply_paragraph(
);
}
/// The Paragraph dialog's Asian Typography: sets the flags that changed since it opened, so an
/// untouched tab adds no direct formatting.
fn apply_asian(app: &mut WordApp, asian: [bool; 6], was: [bool; 6]) {
let keys = ["kinsoku", "wordWrap", "overflowPunct", "topLinePunct", "autoSpaceDE", "autoSpaceDN"];
let changed: serde_json::Map<String, Value> =
keys.iter().zip(asian.iter().zip(was)).filter(|(_, (now, was))| **now != *was).map(|(k, (now, _))| (k.to_string(), json!(now))).collect();
if !changed.is_empty() {
let _ = app.run("para.asianTypography", Value::Object(changed));
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -876,7 +945,7 @@ mod tests {
/// Open the Paragraph dialog and press OK without changing anything.
fn ok_unchanged(app: &mut WordApp) {
let Some(Dialog::Paragraph { rtl, align, left, right, first, before, after, line, keep_next, keep_lines, page_break, widow }) =
let Some(Dialog::Paragraph { rtl, align, left, right, first, before, after, line, keep_next, keep_lines, page_break, widow, .. }) =
Dialog::open("paragraph", app)
else {
panic!("no paragraph dialog")

View File

@ -25,6 +25,8 @@
Accessibility Accesibilidad
Add Añadir
Add Space Before Paragraph Añadir espacio antes del párrafo
Add space between Asian and Latin text Añadir espacio entre texto asiático y latino
Add space between Asian text and numbers Añadir espacio entre texto asiático y números
Add Text Añadir texto
Add a Table Añadir una tabla
Add a comment Añadir un comentario
@ -45,13 +47,17 @@
All Caps Mayúsculas
All Markup Todas las marcas
All caps Mayúsculas
Allow hanging punctuation Permitir puntuación colgante
Allow Latin text to wrap in the middle of a word Permitir que el texto latino se divida en mitad de una palabra
Alt Text Texto alternativo
Apply Asian line-breaking rules (kinsoku) Aplicar las reglas asiáticas de división de líneas (kinsoku)
Apply Character Style Aplicar estilo de carácter
Apply Style Aplicar estilo
Arrange Organizar
Arrange\nAll Organizar\ntodo
Arrow Flecha
Artistic Effects Efectos artísticos
Asian Typography Tipografía asiática
Author Autor
AutoCorrect Options Opciones de autocorrección
AutoFit Ajustar automáticamente
@ -140,6 +146,7 @@
Compact Compacto
Compare Comparar
Compress Pictures Comprimir imágenes
Compress punctuation at the start of a line Comprimir la puntuación al inicio de la línea
Continuous Continuo
Convert Convertir
Convert Text to Table Convertir texto en tabla
@ -311,6 +318,7 @@
Indent Sangría
Indentation Sangría
Indents Sangrías
Indents and Spacing Sangría y espaciado
Index Índice
Info Información
Ink to\nMath Tinta a\nfórmula
@ -364,6 +372,7 @@
Letter Carta
Line Línea
Line Break Salto de línea
Line breaking División de líneas
Line End Fin de línea
Line Numbers Números de línea
Line Spacing Interlineado

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -13,6 +13,8 @@
Add a comment コメントを追加
Add a Table 表を追加
Add Space Before Paragraph 段落前に間隔を追加
Add space between Asian and Latin text アジア文字と欧文の間隔を空ける
Add space between Asian text and numbers アジア文字と数字の間隔を空ける
Add Text テキストの追加
Add to Dictionary 辞書に追加
Add to Style Gallery スタイル ギャラリーに追加
@ -31,7 +33,10 @@
All Caps すべて大文字
All caps すべて大文字
All Markup すべての変更履歴
Allow hanging punctuation 句読点のぶら下げを行う
Allow Latin text to wrap in the middle of a word 欧文を単語の途中で折り返す
Alt Text 代替テキスト
Apply Asian line-breaking rules (kinsoku) アジア文字の禁則処理を行う
Apply Character Style 文字スタイルを適用
Apply Style スタイルを適用
Apply Styles スタイルを選んで適用
@ -39,6 +44,7 @@
Arrange\nAll 整列
Arrow 矢印
Artistic Effects アート効果
Asian Typography アジア文字の組版
Author 作成者
AutoCorrect Options オートコレクトのオプション
AutoFit 自動調整
@ -52,7 +58,10 @@
AutoSave 自動保存
AutoSave documents you have saved in WordCraft WordCraft で保存した文書を自動保存する
AutoSave failed: {error}. Save the document to turn AutoSave back on. 自動保存できませんでした: {error}。文書を保存し直すと、自動保存が再開します。
Compress punctuation at the start of a line 行頭の約物を詰める
Dark page (white text on black) ダーク ページ (黒地に白い文字)
Indents and Spacing インデントと間隔
Line breaking 行の分割
More templates → その他のテンプレート →
No templates match your search. 検索に一致するテンプレートはありません。
Search for templates テンプレートを検索

Can't render this file because it has a wrong number of fields in line 3.

View File

@ -24,6 +24,8 @@
Add a comment Adicionar um comentário
Add a Table Adicionar uma tabela
Add Space Before Paragraph Adicionar espaço antes do parágrafo
Add space between Asian and Latin text Adicionar espaço entre texto asiático e latino
Add space between Asian text and numbers Adicionar espaço entre texto asiático e números
Add Text Adicionar texto
Add to Dictionary Adicionar ao dicionário
Add to Style Gallery Adicionar à galeria de estilos
@ -42,13 +44,17 @@
All Caps Tudo em maiúsculas
All caps Tudo em maiúsculas
All Markup Todas as marcações
Allow hanging punctuation Permitir pontuação pendente
Allow Latin text to wrap in the middle of a word Permitir que o texto latino quebre no meio de uma palavra
Alt Text Texto alternativo
Apply Asian line-breaking rules (kinsoku) Aplicar as regras asiáticas de quebra de linha (kinsoku)
Apply Character Style Aplicar estilo de caractere
Apply Style Aplicar estilo
Arrange Organizar
Arrange\nAll Organizar\ntudo
Arrow Seta
Artistic Effects Efeitos artísticos
Asian Typography Tipografia asiática
Author Autor
AutoCorrect Options Opções de autocorreção
AutoFit Autoajuste
@ -137,6 +143,7 @@
Compact Compacto
Compare Comparar
Compress Pictures Compactar imagens
Compress punctuation at the start of a line Comprimir a pontuação no início da linha
Continuous Contínua
Convert Converter
Convert Text to Table Converter texto em tabela
@ -179,6 +186,8 @@
Delete Style Excluir estilo
Delete Table Excluir tabela
Design Design
Indents and Spacing Recuos e espaçamento
Line breaking Quebra de linhas
More templates → Mais modelos →
No templates match your search. Nenhum modelo corresponde à pesquisa.
Search for templates Pesquisar modelos

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -24,6 +24,8 @@
Add a comment Додати коментар
Add a Table Додати таблицю
Add Space Before Paragraph Додати інтервал перед абзацом
Add space between Asian and Latin text Додавати інтервал між азійським і латинським текстом
Add space between Asian text and numbers Додавати інтервал між азійським текстом і числами
Add Text Додати текст
Add to Dictionary Додати до словника
Add to Style Gallery Додати до колекції стилів
@ -42,13 +44,17 @@
All Caps Усі великі літери
All caps Усі великі літери
All Markup Усі позначки
Allow hanging punctuation Дозволити висячу пунктуацію
Allow Latin text to wrap in the middle of a word Дозволити розрив латинського тексту посеред слова
Alt Text Альтернативний текст
Apply Asian line-breaking rules (kinsoku) Застосовувати азійські правила розриву рядків (кінсоку)
Apply Character Style Застосувати стиль символів
Apply Style Застосувати стиль
Arrange Упорядкувати
Arrange\nAll Упорядкувати\nвсі
Arrow Стрілка
Artistic Effects Художні ефекти
Asian Typography Азійська типографіка
Author Автор
AutoCorrect Options Параметри автовиправлення
AutoFit Добір розміру
@ -137,6 +143,7 @@
Compact Компактний
Compare Порівняти
Compress Pictures Стиснути зображення
Compress punctuation at the start of a line Стискати розділові знаки на початку рядка
Continuous Безперервний
Convert Перетворити
Convert Text to Table Перетворити текст на таблицю
@ -179,6 +186,8 @@
Delete Style Видалити стиль
Delete Table Видалити таблицю
Design Дизайн
Indents and Spacing Відступи й інтервали
Line breaking Розрив рядків
More templates → Більше шаблонів →
No templates match your search. Немає шаблонів, що відповідають пошуку.
Search for templates Пошук шаблонів

Can't render this file because it contains an unexpected character in line 6 and column 31.

View File

@ -24,6 +24,8 @@
Add a comment 添加批注
Add a Table 添加表格
Add Space Before Paragraph 在段前添加间距
Add space between Asian and Latin text 在亚洲文字与西文之间添加间距
Add space between Asian text and numbers 在亚洲文字与数字之间添加间距
Add Text 添加文字
Add to Dictionary 添加到词典
Add to Style Gallery 添加到样式库
@ -42,7 +44,10 @@
All Caps 全部大写
All caps 全部大写
All Markup 所有标记
Allow hanging punctuation 允许标点溢出边界
Allow Latin text to wrap in the middle of a word 允许西文在单词中间换行
Alt Text 替代文字
Apply Asian line-breaking rules (kinsoku) 应用亚洲文字的断行规则(禁则)
Apply Character Style 应用字符样式
Apply Style 应用样式
Apply Styles 选择并应用样式
@ -50,6 +55,7 @@
Arrange\nAll 全部重排
Arrow 箭头
Artistic Effects 艺术效果
Asian Typography 亚洲版式
Author 作者
AutoCorrect Options 自动更正选项
AutoFit 自动调整
@ -63,7 +69,10 @@
AutoSave 自动保存
AutoSave documents you have saved in WordCraft 自动保存在 WordCraft 中保存过的文档
AutoSave failed: {error}. Save the document to turn AutoSave back on. 自动保存失败:{error}。重新保存文档后将恢复自动保存。
Compress punctuation at the start of a line 压缩行首标点
Dark page (white text on black) 深色页面(黑底白字)
Indents and Spacing 缩进和间距
Line breaking 断行
More templates → 更多模板 →
No templates match your search. 没有与搜索匹配的模板。
Search for templates 搜索模板

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -14,6 +14,8 @@
Add a comment 新增註解
Add a Table 新增表格
Add Space Before Paragraph 在段落前新增間距
Add space between Asian and Latin text 在亞洲文字與西文之間加入間距
Add space between Asian text and numbers 在亞洲文字與數字之間加入間距
Add Text 新增文字
Add to Dictionary 新增至字典
Add to Style Gallery 新增至樣式庫
@ -32,7 +34,10 @@
All Caps 全部大寫
All caps 全部大寫
All Markup 所有標記
Allow hanging punctuation 允許標點溢出邊界
Allow Latin text to wrap in the middle of a word 允許西文在單字中間換行
Alt Text 替代文字
Apply Asian line-breaking rules (kinsoku) 套用亞洲文字的斷行規則(禁則)
Apply Character Style 套用字元樣式
Apply Style 套用樣式
Apply Styles 選擇並套用樣式
@ -40,6 +45,7 @@
Arrange\nAll 全部排列
Arrow 箭號
Artistic Effects 美術效果
Asian Typography 亞洲版式
Author 作者
AutoCorrect Options 自動校正選項
AutoFit 自動調整
@ -53,7 +59,10 @@
AutoSave 自動儲存
AutoSave documents you have saved in WordCraft 自動儲存在 WordCraft 中儲存過的文件
AutoSave failed: {error}. Save the document to turn AutoSave back on. 自動儲存失敗:{error}。重新儲存文件後將恢復自動儲存。
Compress punctuation at the start of a line 壓縮行首標點
Dark page (white text on black) 深色頁面(黑底白字)
Indents and Spacing 縮排和間距
Line breaking 斷行
More templates → 更多範本 →
No templates match your search. 沒有符合搜尋的範本。
Search for templates 搜尋範本

Can't render this file because it has a wrong number of fields in line 4.

View File

@ -2,13 +2,13 @@
Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-processor feature catalog (`crates/engine/src/catalog.rs`) compared with the live command registry.
**378 of 428 catalog features have commands (88.3%).**
**379 of 428 catalog features have commands (88.6%).**
| Tab | Live | Total |
|---|---|---|
| File | 20 | 20 |
| Quick Access | 4 | 4 |
| Home | 92 | 101 |
| Home | 93 | 101 |
| Insert | 42 | 53 |
| Draw | 0 | 11 |
| Design | 10 | 10 |
@ -34,7 +34,6 @@ Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-
- Clipboard › Clipboard Pane (`edit.clipboardPane`)
- Font › Phonetic Guide (`format.phonetic`)
- Font › Enclose Characters (`format.enclose`)
- Paragraph › Asian Typography (`para.asianTypography`)
- Styles › Style Inspector (`styles.inspector`)
- Styles › Manage Styles (`styles.manage`)
- Editing › Advanced Find (`edit.advancedFind`)