Merge pull request #5070 from dotCooCoo/feat/kinsoku-shori-type

Paragraph: Kinsoku Shori Type chooses Push In First, Push Out First or Push Out Only
This commit is contained in:
BFlat
2026-11-09 09:22:52 +08:00
committed by GitHub
42 changed files with 694 additions and 47 deletions
+3 -3
View File
@@ -117,9 +117,9 @@ pub use text::FontHeightReference;
pub use text::GlyphChoice;
pub use text::{
AreaFit, AreaOptions, Burasagari, CharAlign, CharPosition, CharStyle, Composer, Digits, FirstBaseline, HyphenationOptions, InlineArt,
JustificationOptions, Justify, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, RubyAlign, RubyPosition,
RubySpan, ScriptMetrics, SingleWordJustify, TabAlign, TabStop, TextFlow, TextKind, TextObject, TextRun, TextStyleDef, TextWrap, VerticalAlign,
WrapShape,
JustificationOptions, Justify, Kinsoku, KinsokuShoriType, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, RubyAlign,
RubyPosition, RubySpan, ScriptMetrics, SingleWordJustify, TabAlign, TabStop, TextFlow, TextKind, TextObject, TextRun, TextStyleDef, TextWrap,
VerticalAlign, WrapShape,
};
pub use text::{CharClass, CompositeFont, CompositePart, CompositeParts};
pub use trace::TraceView;
+2 -2
View File
@@ -23,8 +23,8 @@ pub use composite::{
CompositeParts,
};
pub use para_style::{
Burasagari, Composer, FirstBaseline, HyphenationOptions, JustificationOptions, Justify, Kashida, Kinsoku, LeadingModel, Mojikumi, ParaDirection,
ParaStyle, SingleWordJustify,
Burasagari, Composer, FirstBaseline, HyphenationOptions, JustificationOptions, Justify, Kashida, Kinsoku, KinsokuShoriType, LeadingModel,
Mojikumi, ParaDirection, ParaStyle, SingleWordJustify,
};
pub use runs::{INLINE_CHAR, INLINE_MAX_SCALE, InlineArt, RubyAlign, RubyPosition, RubySpan, TextKind, TextRun, TextStyleDef, TypeAntiAlias};
pub use tabs_lists::{
+28
View File
@@ -234,6 +234,9 @@ pub struct ParaStyle {
/// Kinsoku (Paragraph panel › Kinsoku Set): which Japanese characters may not start or end a
/// line.
pub kinsoku: Kinsoku,
/// Kinsoku Shori Type (Paragraph panel menu): how a line keeps those characters off its start
/// and end, by pushing them in onto the line or out to the next one. No effect without kinsoku.
pub kinsoku_type: KinsokuShoriType,
/// Roman Hanging Punctuation (Paragraph panel menu): quotation marks, hyphens, full stops and
/// commas starting or ending a line hang outside the margin, and asterisks, tildes, ellipses,
/// dashes, colons and semicolons hang by half. East Asian punctuation hangs by burasagari.
@@ -322,6 +325,8 @@ struct ParaStyleFile {
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
kinsoku: Kinsoku,
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
kinsoku_type: KinsokuShoriType,
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
roman_hanging: bool,
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
kashida: Kashida,
@@ -353,6 +358,7 @@ impl From<ParaStyle> for ParaStyleFile {
leading_model,
burasagari,
kinsoku,
kinsoku_type,
roman_hanging,
kashida,
..
@@ -377,6 +383,7 @@ impl From<ParaStyle> for ParaStyleFile {
leading_model,
burasagari,
kinsoku,
kinsoku_type,
roman_hanging,
kashida,
}
@@ -405,6 +412,7 @@ impl From<ParaStyleFile> for ParaStyle {
leading_model,
burasagari,
kinsoku,
kinsoku_type,
roman_hanging,
kashida,
..
@@ -428,6 +436,7 @@ impl From<ParaStyleFile> for ParaStyle {
leading_model,
burasagari,
kinsoku,
kinsoku_type,
roman_hanging,
kashida,
}
@@ -492,6 +501,25 @@ pub enum Kinsoku {
Soft,
}
/// How a line keeps the characters [`Kinsoku`] rules out of its start and end (Kinsoku Shori
/// Type). Pushing in sets the characters that would start the next line (or that an opening
/// bracket ending this line would pull with it) on this line, with the line's gaps next to CJK
/// characters closed to make room, each by the same share of the em of the character before it
/// and by at most a tenth of that em; pushing out moves the character before them to the next
/// line.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum KinsokuShoriType {
/// Push in when the line can close up enough, else push out.
PushInFirst,
/// Push out, unless that would leave the line more than an em short (of the character before
/// the break): then push in when the line can close up enough.
PushOutFirst,
/// Always push out; never push in.
#[default]
PushOutOnly,
}
/// Hanging punctuation (burasagari): an East Asian comma or full stop ending a line (、。,.、。)
/// stands outside the line's measure, in the space its punctuation spacing gives it (half width
/// with Line-end Punctuation Half Width). Closing brackets and Latin punctuation don't hang. Shown
File diff suppressed because one or more lines are too long
+1
View File
@@ -226,6 +226,7 @@ pub fn para_attrs(v: &ParaStyle) -> Map<String, Value> {
("leading_model", json!(v.leading_model)),
("burasagari", json!(v.burasagari)),
("kinsoku", json!(v.kinsoku)),
("kinsoku_type", json!(v.kinsoku_type)),
("roman_hanging", json!(v.roman_hanging)),
("kashida", json!(v.kashida)),
] {
@@ -98,6 +98,43 @@ fn kinsoku_set_is_set_per_paragraph_and_saved_only_when_not_hard() {
assert_eq!(old.kinsoku, Kinsoku::Hard);
}
/// Paragraph panel menu › Kinsoku Shori Type: `text.setFormat {kinsokuType}` sets it per
/// paragraph in one undo step, refuses unknown values, saves it only when it is not Push Out Only
/// (new type's and older documents' type), reopens with the file, and paragraph styles carry it.
#[test]
fn kinsoku_shori_type_is_set_per_paragraph_and_saved_only_when_not_push_out_only() {
use vectorcraft_doc::KinsokuShoriType;
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let id = s.execute("text.create", &json!({"x": 10, "y": 10, "text": "あ」\nい", "area": {"width": 100, "height": 100}})).unwrap()["id"]
.as_u64()
.unwrap();
let text = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.clone(),
_ => panic!("text"),
};
assert_eq!(text(&s).para_at(0).kinsoku_type, KinsokuShoriType::PushOutOnly);
assert!(serde_json::to_value(text(&s).para_at(0)).unwrap().get("kinsoku_type").is_none());
s.execute("text.setFormat", &json!({"id": id, "start": 0, "end": 1, "kinsokuType": "pushInFirst"})).unwrap();
assert_eq!((text(&s).para_at(0).kinsoku_type, text(&s).para_at(1).kinsoku_type), (KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutOnly));
assert_eq!(serde_json::to_value(text(&s).para_at(0)).unwrap()["kinsoku_type"], "pushInFirst");
let reopened = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
let Some(NodeKind::Text(back)) = reopened.node(NodeId(id)).map(|n| &n.kind) else { panic!("text") };
assert_eq!((back.para_at(0).kinsoku_type, back.para_at(1).kinsoku_type), (KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutOnly));
assert!(s.execute("text.setFormat", &json!({"id": id, "kinsokuType": "pushIn"})).is_err());
assert_eq!(text(&s).para_at(0).kinsoku_type, KinsokuShoriType::PushInFirst, "an unknown value changes nothing");
s.execute("text.setFormat", &json!({"id": id, "kinsokuType": "pushOutFirst"})).unwrap();
assert_eq!(text(&s).para_at(1).kinsoku_type, KinsokuShoriType::PushOutFirst);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!((text(&s).para_at(0).kinsoku_type, text(&s).para_at(1).kinsoku_type), (KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutOnly));
// A paragraph style carries it.
s.execute("paraStyle.new", &json!({"name": "Pushed in", "attrs": {"kinsoku_type": "pushOutFirst"}})).unwrap();
s.execute("paraStyle.apply", &json!({"name": "Pushed in", "id": id})).unwrap();
assert_eq!(text(&s).para_at(1).kinsoku_type, KinsokuShoriType::PushOutFirst);
let old: vectorcraft_doc::ParaStyle = serde_json::from_value(json!({"justify": "Left"})).unwrap();
assert_eq!(old.kinsoku_type, KinsokuShoriType::PushOutOnly);
}
#[test]
fn burasagari_is_set_per_paragraph_and_saved_only_when_on() {
use vectorcraft_doc::Burasagari;
+2 -2
View File
@@ -7,8 +7,8 @@ use kurbo::{Affine, BezPath, ParamCurve, PathEl, Point, Rect, Shape, Vec2};
use unicode_bidi::{BidiInfo, Level};
use vectorcraft_doc::text::{Kashida, ListAlign};
use vectorcraft_doc::{
Burasagari, CharStyle, HyphenationOptions, InlineArt, Justify, Kinsoku, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect,
RubyAlign, RubyPosition, SingleWordJustify, TextKind, TextObject,
Burasagari, CharStyle, HyphenationOptions, InlineArt, Justify, Kinsoku, KinsokuShoriType, LeadingModel, Mojikumi, ParaDirection, ParaStyle,
PathAlign, PathEffect, RubyAlign, RubyPosition, SingleWordJustify, TextKind, TextObject,
};
use vectorcraft_geom::{ArcPath, PathData};
+146 -3
View File
@@ -1,7 +1,8 @@
use super::region::Region;
use super::vertical::stands_upright;
use super::{
Affine, BezPath, Breakpoint, Burasagari, EPS, FirstBaseline, FontDb, HyphenationOptions, Kinsoku, LeadingModel, SGlyph, hyphen_glyph,
no_line_end, no_line_start, points_for, soft_line_start,
Affine, BezPath, Breakpoint, Burasagari, EPS, FirstBaseline, FontDb, HyphenationOptions, Kinsoku, KinsokuShoriType, LeadingModel, SGlyph,
hyphen_glyph, is_cjk, no_line_end, no_line_start, points_for, soft_line_start,
};
/// Vertical metrics of a line: (ascent, descent, leading, cap height, x height).
@@ -269,7 +270,10 @@ pub(super) fn word_width(g: &[SGlyph], i: usize) -> f64 {
/// `hyphenate` holds the paragraph's Hyphenation options and the Hyphenation Zone in force when
/// the line may hyphenate a word; a line whose text before the overflowing word already ends
/// within the zone breaks before that word. A tab is as wide as it is set: up to its next stop
/// (`tabs`), with the line's content starting `x_start` from the tab origin.
/// (`tabs`), with the line's content starting `x_start` from the tab origin. Where kinsoku alone
/// keeps the line from breaking before the character that overflows, `kinsoku_type` decides
/// whether the line pushes characters in ([`push_in`]) or out; `hanging` is true when Optical
/// Margin Alignment or Roman Hanging Punctuation is on, and `vertical` in vertical type.
#[allow(clippy::too_many_arguments)]
pub(super) fn break_line(
text: &str,
@@ -281,6 +285,9 @@ pub(super) fn break_line(
hyphenate: Option<(&HyphenationOptions, f64)>,
burasagari: Burasagari,
kinsoku: Kinsoku,
kinsoku_type: KinsokuShoriType,
hanging: bool,
vertical: bool,
) -> (usize, bool) {
if !width.is_finite() {
if let Some(pos) = g[i..].iter().position(|gl| gl.ch == '\u{2028}') {
@@ -331,6 +338,11 @@ pub(super) fn break_line(
}
return (end, false);
}
if let Some(end) =
push_in(g, i, j, cluster_end, x + cluster_width, width, last_break.map(|b| b.0), kinsoku, kinsoku_type, hanging, vertical)
{
return (end, false);
}
break;
}
x += cluster_width;
@@ -493,6 +505,137 @@ fn kinsoku_between(before: char, after: Option<char>, kinsoku: Kinsoku) -> bool
}
}
/// The share of an em by which a push-in ([`push_in`]) may close each gap next to a CJK character
/// on the line, measured on the character before the gap.
pub(super) const PUSH_IN_SQUEEZE: f64 = 0.1;
/// The most glyphs after the overflowing cluster a push-in ([`push_in`]) takes onto the line; a
/// longer run of characters kinsoku keeps together pushes out.
const PUSH_IN_RUN: usize = 16;
/// Kinsoku Shori Type: the end of the line from `i` that overflows `width` at the cluster
/// `j..cluster_end` (the line is `x` wide through it) when the line pushes in, or None when it
/// pushes out. A line pushes in only where kinsoku rules out the break before `j`: a character
/// that may not end a line comes before it (an opening bracket or quotation mark, Latin ones
/// included), or one that may not start a line comes at `j`. It then takes the characters up to
/// the next break kinsoku allows (not at a soft hyphen), and the spaces after it, when closing
/// its gaps next to CJK characters, each by up to [`PUSH_IN_SQUEEZE`] of an em of the character
/// before it, makes room for them. Push In First tries this before it pushes out. Push Out First
/// tries it only when pushing out, to the line's last allowed break `pushed_out`, would leave the
/// line short by more than an em of the character before `j`. Push Out Only never pushes in, and
/// neither does a line holding a tab or right-to-left text, nor one where the characters taken in
/// reach a space after a character that may not end a line. With `hanging` (Optical Margin
/// Alignment or Roman Hanging Punctuation), the gaps that hanging punctuation keeps open are not
/// counted. `vertical`: the line is a column of vertical type ([`squeeze_em`]).
#[allow(clippy::too_many_arguments)]
fn push_in(
g: &[SGlyph],
i: usize,
j: usize,
cluster_end: usize,
x: f64,
width: f64,
pushed_out: Option<usize>,
kinsoku: Kinsoku,
kinsoku_type: KinsokuShoriType,
hanging: bool,
vertical: bool,
) -> Option<usize> {
if kinsoku == Kinsoku::None || kinsoku_type == KinsokuShoriType::PushOutOnly {
return None;
}
let before = g.get(j.checked_sub(1)?)?;
if before.len == 0 || g.get(j)?.no_break || splits_no_break(g, j) || kinsoku_allows(g, j - 1, kinsoku) {
return None;
}
let allowed = |k: usize| {
k >= g.len()
|| (g[k].byte != g[k - 1].byte
&& kinsoku_allows(g, k - 1, kinsoku)
&& (g[k].is_space() || (g[k - 1].break_after() && !g[k - 1].is_soft_hyphen())))
};
let (mut end, mut x) = (cluster_end, x);
while !allowed(end) {
let gl = &g[end];
// A space here follows a character kinsoku keeps off a line's end: the line pushes out.
if gl.is_space() || gl.ch == '\t' || gl.ch == '\u{2028}' || gl.no_break || end - cluster_end >= PUSH_IN_RUN {
return None;
}
x += gl.adv;
end += 1;
}
// A tab's width depends on where it falls; a line with one (other than a list marker's)
// pushes out.
if g[i..end].iter().any(|h| h.ch == '\t' && h.len > 0) {
return None;
}
// With `hanging`, punctuation keeps the gap after a mark that may hang at the line's start,
// and the gap before one that may hang at its end ([`optical_hangs`]): those gaps are not
// counted.
let mut last = end;
while last > i && g[last - 1].is_space() {
last -= 1;
}
// A line holding right-to-left text is reordered before it is drawn, and justification closes
// the gaps between its glyphs in that order: it pushes out.
if g[i..last].iter().any(|h| h.len > 0 && h.level.is_rtl()) {
return None;
}
let punct = |k: usize| g.get(k).is_some_and(optical_punct);
let quote = |k: usize| g.get(k).is_some_and(|h| is_quote(h.ch));
let may_hang = |a: usize| {
hanging
&& ((a == i && punct(i))
|| (a == i + 1 && punct(i) && quote(i + 1))
|| (a + 2 == last && punct(last - 1))
|| (a + 3 == last && punct(last - 2) && quote(last - 1)))
};
let room: f64 = (i..end - 1).filter(|&a| closes(g, a) && !may_hang(a)).map(|a| PUSH_IN_SQUEEZE * squeeze_em(&g[a], vertical)).sum();
if x - width > room + EPS {
return None;
}
if kinsoku_type == KinsokuShoriType::PushOutFirst {
let mut kept = pushed_out.filter(|&b| b > i).unwrap_or(i);
while kept > i && g[kept - 1].is_space() {
kept -= 1;
}
let short = width - g[i..kept].iter().map(|h| h.adv).sum::<f64>();
if short <= squeeze_em(before, vertical) + EPS {
return None;
}
}
while g.get(end).is_some_and(SGlyph::is_space) {
end += 1;
}
if g.get(end).is_some_and(|h| h.ch == '\u{2028}') {
end += 1;
}
Some(end)
}
/// May a push-in close the gap after glyph `a`? A gap between two characters, at least one of them
/// CJK, outside a tate-chu-yoko block, a list marker and a shaped cluster, and not next to a space,
/// a tab or an inline graphic before it.
fn closes(g: &[SGlyph], a: usize) -> bool {
let (Some(p), Some(n)) = (g.get(a), g.get(a + 1)) else { return false };
p.len > 0
&& p.byte != n.byte
&& p.inline.is_none()
&& !p.is_space()
&& !n.is_space()
&& p.ch != '\t'
&& !n.continues_tcy()
&& (is_cjk(p.ch) || is_cjk(n.ch))
}
/// The em a push-in measures in, along the line: `g`'s em at its Horizontal Scale, as Mojikumi's
/// spacing is, or at its Vertical Scale when it stands upright in vertical type and the font's
/// vertical metrics set its length down the column ([`super::vertical::upright_cell`]).
pub(super) fn squeeze_em(g: &SGlyph, vertical: bool) -> f64 {
let down_the_column = vertical && stands_upright(g) && g.face.vertical_glyph(g.gid).is_some();
g.face.units_per_em() * if down_the_column { g.sy } else { g.sx }
}
#[cfg(test)]
#[test]
fn kinsoku_keeps_closing_marks_off_line_starts_and_opening_ones_off_line_ends() {
+58 -20
View File
@@ -1,16 +1,22 @@
use super::bidi::{is_rtl, para_bidi_with, visual_order};
use super::breaking::{
Metrics, Pen, break_line, candidates, hangs, leader_outline, optical_hangs, optical_hangs_rtl, optical_punct, optical_share, roman_share,
set_width, tab_advance, word_width,
Metrics, PUSH_IN_SQUEEZE, Pen, break_line, candidates, hangs, leader_outline, optical_hangs, optical_hangs_rtl, optical_punct, optical_share,
roman_share, set_width, squeeze_em, tab_advance, word_width,
};
use super::region::Region;
use super::vertical::{EM_CENTER, align_shift, compress_punctuation, glyph_em, punct_half, space_japanese_and_latin};
use super::{
Affine, BidiInfo, Burasagari, CharStyle, Composer, Ctx, EPS, Flex, Justify, LeadingModel, LineFit, LineInfo, ListMarker, Mojikumi, ParaStyle,
Params, Point, Punct, Range, SGlyph, SingleWordJustify, TextObject, apply_desired, cap_x_heights, compose, fit_line, hyphen_glyph, is_cjk,
is_word_space, kashida, mark_word_ends, style_metrics,
Affine, BidiInfo, Burasagari, CharStyle, Composer, Ctx, EPS, Flex, Justify, Kinsoku, KinsokuShoriType, LeadingModel, LineFit, LineInfo,
ListMarker, Mojikumi, ParaStyle, Params, Point, Punct, Range, SGlyph, SingleWordJustify, TextObject, apply_desired, cap_x_heights, compose,
fit_line, hyphen_glyph, is_cjk, is_word_space, kashida, mark_word_ends, no_line_end, no_line_start, style_metrics,
};
/// Does paragraph `para` push characters in (Kinsoku Shori Type Push In First or Push Out First,
/// with kinsoku on)?
fn pushes_in(para: &ParaStyle) -> bool {
para.kinsoku != Kinsoku::None && para.kinsoku_type != KinsokuShoriType::PushOutOnly
}
/// Paragraphs longer than this (glyphs) are always broken line by line.
const MAX_COMPOSE_GLYPHS: usize = 200_000;
@@ -32,6 +38,8 @@ fn compose_para(
// A paragraph whose commas or full stops may hang is composed line by line (as the Japanese
// single-line composer does).
let may_hang = para.burasagari != Burasagari::None && sg.iter().any(hangs);
// So is a paragraph that may push characters kinsoku keeps off a line's ends in onto a line.
let may_push_in = pushes_in(para) && sg.iter().any(|g| no_line_start(g.ch) || no_line_end(g.ch));
// A tab's width depends on where it falls in its line, which the composer's fixed glyph widths
// can't follow: such a paragraph is broken line by line ([`break_line`] measures its tabs).
// The tab of a list marker is on the first line, with the marker, and has a fixed width.
@@ -42,6 +50,7 @@ fn compose_para(
|| sg.len() < 2
|| sg.len() > MAX_COMPOSE_GLYPHS
|| may_hang
|| may_push_in
|| has_tab
|| has_soft_break
{
@@ -285,6 +294,8 @@ struct PlacementContext<'a> {
zone: f64,
index: usize,
para_line: usize,
/// Optical Margin Alignment or Roman Hanging Punctuation is on.
hanging: bool,
}
struct LinePosition {
@@ -293,13 +304,16 @@ struct LinePosition {
x1: f64,
end: usize,
hyph: bool,
/// Kinsoku Shori Type pushed characters in onto the line ([`break_line`]).
pushed_in: bool,
m: Metrics,
ind_l: f64,
ind_r: f64,
}
fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementContext<'_>) -> Option<LinePosition> {
let PlacementContext { para, pr, sg, continued, rtl, mirror, pm, composed, hyphen_run, hyphen_limit, zone, index: i, para_line } = context;
let PlacementContext { para, pr, sg, continued, rtl, mirror, pm, composed, hyphen_run, hyphen_limit, zone, index: i, para_line, hanging } =
context;
let n = sg.len();
let est = if *i < n { Metrics::of(&sg[*i]) } else { *pm };
let first_line = *para_line == 0 && !*continued;
@@ -328,10 +342,17 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
};
let width = x1 - x0 - ind_l - ind_r;
let hyphenate = (para.hyphenate && (*hyphen_limit == 0 || *hyphen_run < *hyphen_limit)).then_some((&para.hyphenation, *zone));
let (end, hyph) = match composed.as_ref().and_then(|lines| lines.get(*para_line)) {
Some(&(end, hyph)) if end > *i => (end, hyph),
_ if *i < n => break_line(cx.text, sg, *i, width, &para.tabs, ind_l, hyphenate, para.burasagari, para.kinsoku),
_ => (n, false),
let (end, hyph, pushed_in) = match composed.as_ref().and_then(|lines| lines.get(*para_line)) {
Some(&(end, hyph)) if end > *i => (end, hyph, false),
_ if *i < n => {
let line =
|ty| break_line(cx.text, sg, *i, width, &para.tabs, ind_l, hyphenate, para.burasagari, para.kinsoku, ty, *hanging, cx.vertical);
let (end, hyph) = line(para.kinsoku_type);
// The line pushed characters in where it ends after the line that pushes out.
let pushed_in = pushes_in(para) && end > line(KinsokuShoriType::PushOutOnly).0;
(end, hyph, pushed_in)
}
_ => (n, false, false),
};
let m = Metrics::max(&sg[*i..end]).unwrap_or(*pm);
baseline += if same_baseline || (pen.regions.is_none() && first_in_region) {
@@ -353,7 +374,7 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
pen.queued.clear();
continue;
}
return Some(LinePosition { baseline, x0, x1, end, hyph, m, ind_l, ind_r });
return Some(LinePosition { baseline, x0, x1, end, hyph, pushed_in, m, ind_l, ind_r });
}
}
@@ -381,6 +402,10 @@ struct LineJustification {
per_space: f64,
per_gap: f64,
per_cjk: f64,
/// Kinsoku Shori Type: the share of an em (of the glyph before it, at most
/// [`PUSH_IN_SQUEEZE`]) each gap next to a CJK character closes by on a line that pushed
/// characters in; 0 on other lines.
squeeze: f64,
fit: LineFit,
start_x: f64,
after: Vec<Option<usize>>,
@@ -415,6 +440,7 @@ fn run_paragraph_lines(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, t: &TextObject, mut
zone: paragraph.zone,
index,
para_line: line_in_paragraph,
hanging: t.optical_margin || paragraph.para.roman_hanging,
};
let Some(position) = place_flow_line(cx, pen, &mut placement) else { return false };
hyphen_run = if position.hyph && position.end > index { hyphen_run + 1 } else { 0 };
@@ -572,12 +598,13 @@ fn drawn_after(index: usize, glyph: usize, after: &[Option<usize>]) -> Option<us
}
// Japanese (and Chinese) lines are justified between their characters (JLREQ 3.8): the
// gaps next to a CJK character, not inside a Latin word or a tate-chu-yoko block.
// gaps next to a CJK character, not inside a Latin word, a tate-chu-yoko block or a shaped
// cluster.
fn is_cjk_gap(glyph: usize, held: &[usize], after: &[Option<usize>], index: usize, sg: &[SGlyph]) -> bool {
!held.contains(&glyph)
&& drawn_after(index, glyph, after)
.and_then(|next| sg.get(glyph).zip(sg.get(next)))
.is_some_and(|(a, b)| !a.is_space() && !b.is_space() && !b.continues_tcy() && a.ch != '\t' && (is_cjk(a.ch) || is_cjk(b.ch)))
&& drawn_after(index, glyph, after).and_then(|next| sg.get(glyph).zip(sg.get(next))).is_some_and(|(a, b)| {
a.byte != b.byte && !a.is_space() && !b.is_space() && !b.continues_tcy() && a.ch != '\t' && (is_cjk(a.ch) || is_cjk(b.ch))
})
}
// What justification may change of glyph `j`: nothing of a `held` glyph, and no glyph
@@ -639,7 +666,18 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
let width = line.render_width + kashidas.iter().map(|k| k.1).sum::<f64>();
let (mut per_space, mut per_gap, mut per_cjk) = (0.0, 0.0, 0.0);
let mut fit = LineFit::default();
if justify && (room - width).abs() > EPS {
let mut squeeze = 0.0;
// Kinsoku Shori Type: a line of area type that pushed characters in is wider than its
// measure, and closes its gaps next to CJK characters to fit, however it is aligned: each by
// the same share of the em of the glyph before it, at most [`PUSH_IN_SQUEEZE`].
let pushed_in = line.position.pushed_in && paragraph.regions.is_some() && cjk_gaps > 0 && width > room + EPS;
if pushed_in {
let ems: f64 =
(line.index..line.body).filter(|&j| is_cjk_gap(j, held, &after, line.index, sg)).map(|j| squeeze_em(&sg[j], cx.vertical)).sum();
if ems > EPS {
squeeze = ((room - width) / ems).max(-PUSH_IN_SQUEEZE);
}
} else if justify && (room - width).abs() > EPS {
if cjk_gaps > 0 && room > width {
// Spread over the CJK gaps and the word spaces alike.
per_space = (room - width) / (cjk_gaps + spaces) as f64;
@@ -667,7 +705,7 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
}
}
let position = &line.position;
let start_x = if justify {
let start_x = if justify || pushed_in {
line.ax0 - line.opt_l
} else if paragraph.regions.is_none() {
match align {
@@ -682,7 +720,7 @@ fn justify_flow_line(cx: &Ctx<'_>, paragraph: &ParagraphData<'_, '_>, line: &Lin
_ => line.ax1 + line.opt_r - width,
}
};
LineJustification { per_space, per_gap, per_cjk, fit, start_x, after, kashidas }
LineJustification { per_space, per_gap, per_cjk, squeeze, fit, start_x, after, kashidas }
}
fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData<'_, '_>, line: &LineGeometry, justification: &LineJustification) {
@@ -738,9 +776,9 @@ fn emit_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, paragraph: &ParagraphData
}
if glyph.is_space() {
advance += justification.per_space;
} else if justification.per_cjk != 0.0 {
} else if justification.per_cjk != 0.0 || justification.squeeze != 0.0 {
if is_cjk_gap(glyph_index, &line.held, &justification.after, index, sg) {
advance += justification.per_cjk;
advance += justification.per_cjk + justification.squeeze * squeeze_em(glyph, cx.vertical);
}
} else if !line.held.contains(&glyph_index)
&& drawn_after(index, glyph_index, &justification.after).and_then(|next| sg.get(next)).is_some_and(|next| !next.continues_tcy())
+136 -8
View File
@@ -78,7 +78,20 @@ fn indic_hyphenation_reaches_both_composers_without_splitting_marks() {
}
let cands = candidates(&text, &glyphs, Some(&opts), Kinsoku::None);
assert_eq!(cands.iter().filter(|b| b.hyphen > 0.0).map(|b| b.end).collect::<Vec<_>>(), (2..16).step_by(2).collect::<Vec<_>>(), "{tag}");
let (end, hyphen) = break_line(&text, &glyphs, 0, 4.5, &[], 0.0, Some((&opts, 0.0)), Burasagari::None, Kinsoku::None);
let (end, hyphen) = break_line(
&text,
&glyphs,
0,
4.5,
&[],
0.0,
Some((&opts, 0.0)),
Burasagari::None,
Kinsoku::None,
KinsokuShoriType::PushOutOnly,
false,
false,
);
assert_eq!((end, hyphen), (4, true), "{tag}");
let params = Params { justify_last: false, ragged: true, tolerance: 1.0, uniform_from: 0, hyphen_limit: 0 };
let lines = compose(&glyphs, &|_| 4.5, &|_| 0.0, &cands, &[], &params).expect("every-line composition");
@@ -148,7 +161,8 @@ fn greedy_wrap_keeps_an_oversized_first_cluster_and_its_source_range() {
let text = "بَت";
let glyphs = synthetic_glyphs(&[(0, 4, 2.0, 'ب'), (0, 4, 2.0, 'ب'), (4, 2, 1.0, 'ت')]);
let (end, hyphenated) = break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard);
let (end, hyphenated) =
break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
assert!(!hyphenated);
assert_eq!(end, 2, "oversized base-plus-mark cluster must stay together");
@@ -161,11 +175,13 @@ fn greedy_wrap_keeps_an_oversized_cluster_after_a_line_start_atomic() {
let text = "بَتَث";
let glyphs = synthetic_glyphs(&[(0, 4, 1.0, 'ب'), (4, 4, 2.0, 'ت'), (4, 4, 2.0, 'ت'), (8, 2, 1.0, 'ث')]);
let (first_end, _) = break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard);
let (first_end, _) =
break_line(text, &glyphs, 0, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
assert_eq!(first_end, 1);
assert_eq!(source_range(&glyphs, 0, first_end), 0..4);
let (second_end, hyphenated) = break_line(text, &glyphs, first_end, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard);
let (second_end, hyphenated) =
break_line(text, &glyphs, first_end, 3.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, KinsokuShoriType::PushOutOnly, false, false);
assert!(!hyphenated);
assert_eq!(second_end, 3, "oversized cluster at the next line start must stay together");
@@ -220,7 +236,8 @@ fn narrow_arabic_area_wrap_keeps_base_and_mark_together_when_supported() {
fn no_break_keeps_marked_characters_on_one_line() {
let text = "A B";
let plain = synthetic_glyphs(&[(0, 1, 10.0, 'A'), (1, 1, 4.0, ' '), (2, 1, 10.0, 'B')]);
let (end, hyphen) = break_line(text, &plain, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None);
let (end, hyphen) =
break_line(text, &plain, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
assert!(!hyphen);
assert_eq!(end, 2, "without the mark the line ends after the space");
@@ -228,7 +245,8 @@ fn no_break_keeps_marked_characters_on_one_line() {
for g in &mut marked {
g.no_break = true;
}
let (end, hyphen) = break_line(text, &marked, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None);
let (end, hyphen) =
break_line(text, &marked, 0, 16.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
assert!(!hyphen);
assert_eq!(end, 3, "the marked words stay together even though they are wider than the line");
@@ -237,11 +255,121 @@ fn no_break_keeps_marked_characters_on_one_line() {
let mut glyphs = synthetic_glyphs(&[(0, 1, 8.0, 'A'), (1, 1, 2.0, ' '), (2, 3, 10.0, '日'), (5, 3, 10.0, '本')]);
glyphs[2].no_break = true;
glyphs[3].no_break = true;
let (end, _) = break_line(text, &glyphs, 0, 22.0, &[], 0.0, None, Burasagari::None, Kinsoku::None);
let (end, _) = break_line(text, &glyphs, 0, 22.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
assert_eq!(end, 2, "the marked pair moves to the next line together");
let (end, _) = break_line(text, &glyphs, end, 15.0, &[], 0.0, None, Burasagari::None, Kinsoku::None);
let (end, _) = break_line(text, &glyphs, end, 15.0, &[], 0.0, None, Burasagari::None, Kinsoku::None, KinsokuShoriType::PushOutOnly, false, false);
assert_eq!(end, 4, "on its own line the pair stays together even when it is wider than the line");
let breaks = candidates(text, &glyphs, None, Kinsoku::None);
assert!(breaks.iter().all(|b| b.end != 3), "no break between the marked characters: {breaks:?}");
}
/// Synthetic full-width characters `text`, each 10 pt wide on a 10 pt em.
fn full_width(text: &str) -> Vec<SGlyph> {
let specs: Vec<(usize, usize, f64, char)> = text.char_indices().map(|(byte, ch)| (byte, ch.len_utf8(), 10.0, ch)).collect();
let mut glyphs = synthetic_glyphs(&specs);
for g in &mut glyphs {
g.sx = 10.0 / g.face.units_per_em();
g.sy = g.sx;
}
glyphs
}
/// Kinsoku Shori Type, in a measure of 15 full-width characters: a closing bracket that would
/// start the next line is pushed in onto the line with Push In First, and the character before
/// it is pushed out with it with Push Out First and Push Out Only. Push Out First pushes in when
/// pushing out would leave the line two characters short (an opening bracket goes too). A line
/// that cannot close up enough pushes out, and with Kinsoku None the types do nothing.
#[test]
fn kinsoku_shori_type_pushes_a_closing_bracket_in_or_out() {
use KinsokuShoriType::{PushInFirst, PushOutFirst, PushOutOnly};
let hung = |text: &str, width: f64, kinsoku: Kinsoku, ty: KinsokuShoriType, hanging: bool| {
break_line(text, &full_width(text), 0, width, &[], 0.0, None, Burasagari::None, kinsoku, ty, hanging, false).0
};
let end = |text: &str, width: f64, kinsoku: Kinsoku, ty: KinsokuShoriType| hung(text, width, kinsoku, ty, false);
let text = "一二三四五六七八九十一二三四五」六";
assert_eq!(end(text, 150.0, Kinsoku::Hard, PushInFirst), 16, "」 is pushed in");
assert_eq!(end(text, 150.0, Kinsoku::Hard, PushOutFirst), 14, "五 is pushed out with 」");
assert_eq!(end(text, 150.0, Kinsoku::Hard, PushOutOnly), 14, "五 is pushed out with 」");
assert_eq!(end(text, 150.0, Kinsoku::Soft, PushInFirst), 16, "Soft keeps 」 off the line start too");
assert_eq!(end(text, 150.0, Kinsoku::None, PushInFirst), 15, "no kinsoku: 」 starts the next line");
let bracketed = "一二三四五六七八九十一二三「四」六";
assert_eq!(end(bracketed, 150.0, Kinsoku::Hard, PushOutFirst), 16, "pushing out 「四 would leave two ems");
assert_eq!(end(bracketed, 150.0, Kinsoku::Hard, PushInFirst), 16);
assert_eq!(end(bracketed, 150.0, Kinsoku::Hard, PushOutOnly), 13);
// Latin brackets and quotation marks are pushed in too: an opening one that would end the
// line, and a closing one after a Latin letter that would start the next.
for opening in ["一二三四五六七八九十一二三四(五六", "一二三四五六七八九十一二三四\u{201c}五六"] {
assert_eq!(end(opening, 150.0, Kinsoku::Hard, PushInFirst), 16, "{opening}: the bracket and 五 are pushed in");
assert_eq!(end(opening, 150.0, Kinsoku::Hard, PushOutOnly), 14, "{opening}: the bracket is pushed out with 五");
}
let latin = "一二三四五六七八九十一二三四a)";
assert_eq!(end(latin, 150.0, Kinsoku::Hard, PushInFirst), 16, ") is pushed in");
assert_eq!(end(latin, 150.0, Kinsoku::Hard, PushOutOnly), 14, "a is pushed out with )");
// A push-in does not end at a soft hyphen: taking `b` and the soft hyphen in would fit, the
// whole word `bc` does not, so the line pushes out.
let shy = format!("{}a)b\u{ad}c", "一".repeat(31));
assert_eq!(end(&shy, 320.0, Kinsoku::Hard, PushInFirst), 31);
assert_eq!(end(&shy, 320.0, Kinsoku::Hard, PushOutOnly), 31);
// Nor at a space after an opening bracket, which would leave the bracket ending the line.
let spaced = format!("{}「「 A", "一".repeat(14));
assert_eq!(end(&spaced, 150.0, Kinsoku::Hard, PushInFirst), 14);
assert_eq!(end(&spaced, 150.0, Kinsoku::Hard, PushOutOnly), 14);
// With hanging punctuation on, the gap before a full stop that may hang at the line's end is
// not counted: the other nine gaps close by 9 pt, short of the 10 pt the full stop needs. With
// it off, all ten gaps close and the full stop is pushed in.
let stop = format!("{}五.", "一".repeat(9));
assert_eq!(hung(&stop, 100.0, Kinsoku::Hard, PushInFirst, true), 9);
assert_eq!(hung(&stop, 100.0, Kinsoku::Hard, PushOutOnly, true), 9);
assert_eq!(end(&stop, 100.0, Kinsoku::Hard, PushInFirst), 11);
assert_eq!(end(&stop, 100.0, Kinsoku::Hard, PushOutOnly), 9);
// Two gaps close by at most 2 pt, not the 5 pt 」 needs.
let short = "一二」三";
assert_eq!(end(short, 25.0, Kinsoku::Hard, PushInFirst), 1, "too little to close up: 二 is pushed out");
// A line holding a tab pushes out. Here the tab reaches the default stop at 36 pt, so pushing
// out leaves the line one em short, though the tab glyph's own advance is 10 pt.
let tabbed = "一\t二三四五六七八九十一二」六";
assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushOutOnly), 12);
assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushOutFirst), 12);
assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushInFirst), 12);
}
/// Kinsoku Shori Type counts only the gaps justification closes: a line holding right-to-left
/// text pushes out, the gaps close by a tenth of the em at the characters' Horizontal Scale, and
/// no gap closes inside a shaped cluster.
#[test]
fn kinsoku_shori_type_counts_the_gaps_that_close() {
use KinsokuShoriType::{PushInFirst, PushOutOnly};
// A line holding right-to-left text is reordered before it is drawn, so the gaps that close
// differ from the ones counted here: it pushes out.
let mixed = "一二三א五六七八九十一二三四五」六";
let mut glyphs = full_width(mixed);
glyphs[3].level = unicode_bidi::Level::rtl();
let mixed_end = |ty: KinsokuShoriType| break_line(mixed, &glyphs, 0, 150.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false).0;
assert_eq!(mixed_end(PushInFirst), 14, "right-to-left text on the line: 五 is pushed out with 」");
assert_eq!(mixed_end(PushOutOnly), 14);
// The gaps close by a tenth of the em at the characters' Horizontal Scale: at 200% Vertical
// Scale five gaps still close by 5 pt, short of the 10 pt 」 needs.
let tall = "一二三四五」六";
let mut glyphs = full_width(tall);
for g in &mut glyphs {
g.sy *= 2.0;
}
let tall_end = |ty: KinsokuShoriType| break_line(tall, &glyphs, 0, 50.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false, false).0;
assert_eq!(tall_end(PushInFirst), 4, "Vertical Scale does not widen the gaps: 五 is pushed out with 」");
// No gap closes inside a shaped cluster. 二 is two glyphs here, so the line has five gaps to
// close by 5 pt, short of the 6 pt it overflows by.
let mut glyphs = full_width(tall);
glyphs[1].adv = 5.0;
let half = glyphs[1].clone();
glyphs.insert(2, half);
let split_end = |width: f64| break_line(tall, &glyphs, 0, width, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, PushInFirst, false, false).0;
assert_eq!(split_end(54.0), 5, "五 is pushed out with 」");
assert_eq!(split_end(55.0), 7, "five gaps close by 5 pt: 」 is pushed in");
}
+80
View File
@@ -855,6 +855,86 @@ fn kinsoku_set_decides_whether_a_small_kana_may_start_a_line() {
}
}
/// Kinsoku Shori Type: in a measure of fifteen ems, the closing bracket after the fifteenth
/// character ends the first line with Push In First, which closes the line's gaps to keep it inside
/// the frame whatever its alignment; Push Out Only moves the fifteenth character to the next line
/// with the bracket. Horizontal and vertical. Needs a font with full-width Japanese (skipped
/// without one).
#[test]
fn kinsoku_shori_type_pushes_a_closing_bracket_in_or_out() {
use vectorcraft_doc::KinsokuShoriType;
let text = "一二三四五六七八九十一二三四五」六";
let lay = |ty: KinsokuShoriType, justify: Justify, vertical_type: bool| {
let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() });
t.xf = Affine::IDENTITY;
t.vertical = vertical_type;
t.para.kinsoku_type = ty;
t.para.justify = justify;
let frame = if vertical_type { Rect::new(0.0, 0.0, 200.0, 300.0) } else { Rect::new(0.0, 0.0, 300.0, 200.0) };
t.kind = TextKind::Area { frame: PathData::from_bezpath(&frame.to_path(0.1)) };
layout(FontDb::global(), &t)
};
if (lay(KinsokuShoriType::PushOutOnly, Justify::Left, false).glyphs[0].advance - 20.0).abs() > 2.0 {
return; // no font with full-width Japanese here
}
for vertical_type in [false, true] {
let lines = |ty| lay(ty, Justify::Left, vertical_type).glyphs.iter().map(|g| g.line).collect::<Vec<_>>();
let mut pushed_in = vec![0; 16];
pushed_in.push(1);
assert_eq!(lines(KinsokuShoriType::PushInFirst), pushed_in, "vertical {vertical_type}");
let mut pushed_out = vec![0; 14];
pushed_out.extend([1, 1, 1]);
assert_eq!(lines(KinsokuShoriType::PushOutOnly), pushed_out, "vertical {vertical_type}");
assert_eq!(lines(KinsokuShoriType::PushOutFirst), pushed_out, "vertical {vertical_type}: a push-out leaves one em");
}
for justify in [Justify::Left, Justify::Center, Justify::Right, Justify::JustifyLeft] {
let l = lay(KinsokuShoriType::PushInFirst, justify, false);
let (first, bracket) = (&l.glyphs[0], &l.glyphs[15]);
assert!(first.origin.x.abs() < 0.01, "{justify:?}: the line starts at the frame's left: {:?}", first.origin);
assert!(bracket.origin.x + bracket.advance <= 300.01, "{justify:?}: 」 ends inside the frame: {:?}", bracket.origin);
assert!(l.glyphs[1].origin.x < 20.0 - 0.5, "{justify:?}: the gaps close up");
for g in &l.glyphs[..15] {
assert!(g.advance >= 18.0 - 0.01, "{justify:?}: a gap closes by at most a tenth of an em: {}", g.advance);
}
}
// A No Break run wider than the frame did not push anything in, and keeps its gaps.
let mut t = TextObject::point(Point::ZERO, "一二三四五六七八九十", CharStyle { size: 20.0, no_break: true, ..CharStyle::default() });
t.xf = Affine::IDENTITY;
t.para.kinsoku_type = KinsokuShoriType::PushInFirst;
t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 150.0, 200.0).to_path(0.1)) };
let l = layout(FontDb::global(), &t);
assert!((l.glyphs[1].origin.x - 20.0).abs() < 0.01, "{:?}", l.glyphs[1].origin);
}
/// Kinsoku Shori Type at 200% Vertical Scale in a column of 225 pt: the font's vertical metrics set
/// each character 40 pt long, and its five gaps close by up to 4 pt each, room for the 15 pt 」
/// overflows by, so 」 is pushed in; Push Out Only moves 五 to the next column with it. Needs
/// craft-fonts' Shippori Mincho (skipped without it).
#[test]
fn kinsoku_shori_type_closes_a_column_by_the_em_down_the_column() {
use vectorcraft_doc::KinsokuShoriType;
if FontDb::global().face("Shippori Mincho", "Regular").is_none_or(|f| f.family != "Shippori Mincho") {
eprintln!("skipped: built without craft-fonts (set CRAFT_FONTS_DIR to a craft-fonts checkout to run it)");
return;
}
let lay = |ty: KinsokuShoriType| {
let mut t = vertical_styled(
"一二三四五」六",
CharStyle { size: 20.0, font_family: "Shippori Mincho".into(), v_scale: 200.0, ..CharStyle::default() },
);
t.para.kinsoku_type = ty;
t.kind = TextKind::Area { frame: PathData::from_bezpath(&Rect::new(0.0, 0.0, 200.0, 225.0).to_path(0.1)) };
layout(FontDb::global(), &t)
};
let lines = |ty| lay(ty).glyphs.iter().map(|g| g.line).collect::<Vec<_>>();
let out = lay(KinsokuShoriType::PushOutOnly);
assert!((out.glyphs[0].advance - 40.0).abs() < 0.01, "40 pt down the column: {}", out.glyphs[0].advance);
assert_eq!(lines(KinsokuShoriType::PushInFirst), [0, 0, 0, 0, 0, 0, 1]);
let bracket = &lay(KinsokuShoriType::PushInFirst).glyphs[5];
assert!(bracket.origin.y + bracket.advance <= 225.01, "」 ends inside the column: {:?} +{}", bracket.origin, bracket.advance);
assert_eq!(lines(KinsokuShoriType::PushOutOnly), [0, 0, 0, 0, 1, 1, 1]);
}
/// Proportional Metrics in vertical type (#966): upright glyphs take the font's proportional
/// heights (`vpal`, from shaping them top to bottom) and move with them, so each glyph's ink stays in
/// its shorter cell. Line-end Punctuation Half Width takes nothing more off the punctuation `vpal`
+4
View File
@@ -1926,6 +1926,10 @@ font-technology Variable Променливи
Stylistic Стилистично
Mojikumi Set Набор Mojikumi
Kinsoku Set Набор Kinsoku
Kinsoku Shori Type Тип Kinsoku Shori
Push In First Първо прибиране
Push Out First Първо изтласкване
Push Out Only Само изтласкване
Hard Твърдо
Soft Меко
Monitor RGB RGB на монитора
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1905,6 +1905,10 @@ digits Malayalam Malajalam
Stylistic Stilsko
Mojikumi Set Mojikumi skup
Kinsoku Set Kinsoku skup
Kinsoku Shori Type Vrsta Kinsoku Shori
Push In First Prvo uvlačenje
Push Out First Prvo izbacivanje
Push Out Only Samo izbacivanje
Hard Tvrdo
Right Indent Desna uvlaka
Right-to-Left Paragraph Direction Smjer pasusa s desna nalijevo
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2136,6 +2136,7 @@ digits Malayalam Malaiàlam
Keywords: Paraules clau:
Kind Tipus
Kinsoku Set Conjunt Kinsoku
Kinsoku Shori Type Tipus de Kinsoku Shori
Kite Estel
Knife Ganivet
Knockout Group Grup knockout
@@ -3206,6 +3207,9 @@ digits Malayalam Malaiàlam
Puppet Warp Pins Pins de deformació de titella
Puppet Warp Tool Eina de deformació de titella
Purple Porpra
Push In First Primer cap a dins
Push Out First Primer cap a fora
Push Out Only Només cap a fora
Put back in the toolbar Torna a posar a la barra d’eines
Quality Qualitat
Quality {quality} Qualitat {quality}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1932,6 +1932,10 @@ digits Malayalam Malajálamské
Stylistic Stylistický
Mojikumi Set Sada Mojikumi
Kinsoku Set Sada Kinsoku
Kinsoku Shori Type Typ Kinsoku Shori
Push In First Nejprve vtáhnout
Push Out First Nejprve vysunout
Push Out Only Pouze vysunout
Hard Pevné
Right Indent Odsazení zprava
Right-to-Left Paragraph Direction Směr odstavce zprava doleva
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1738,6 +1738,10 @@ digits Malayalam Malayalam
Stylistic Stilistisk
Mojikumi Set Mojikumi-sæt
Kinsoku Set Kinsoku-sæt
Kinsoku Shori Type Kinsoku Shori-type
Push In First Skub ind først
Push Out First Skub ud først
Push Out Only Kun skub ud
Hard Hård
Right Indent Højre indrykning
Right-to-Left Paragraph Direction Afsnitsretning fra højre mod venstre
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1993,6 +1993,10 @@ file-filter Templates Vorlagen
Stylistic Stilistisch
Mojikumi Set Mojikumi-Satz
Kinsoku Set Kinsoku-Satz
Kinsoku Shori Type Kinsoku-Shori-Typ
Push In First Zuerst einziehen
Push Out First Zuerst hinausschieben
Push Out Only Nur hinausschieben
Hard Hart
Soft Weich
Monitor RGB Monitor-RGB
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2789,6 +2789,10 @@ theme Appearance Εμφάνιση
Stylistic Στιλιστικό
Mojikumi Set Σύνολο Mojikumi
Kinsoku Set Σύνολο Kinsoku
Kinsoku Shori Type Τύπος Kinsoku Shori
Push In First Πρώτα προς τα μέσα
Push Out First Πρώτα προς τα έξω
Push Out Only Μόνο προς τα έξω
Mono Μονοφωνικό
Monochrome images Μονόχρωμες εικόνες
Monospaced Σταθερού πλάτους
Can't render this file because it is too large.
+4
View File
@@ -1996,6 +1996,10 @@ font-technology Variable Variables
Stylistic Estilística
Mojikumi Set Conjunto de mojikumi
Kinsoku Set Conjunto de kinsoku
Kinsoku Shori Type Tipo de kinsoku shori
Push In First Primero hacia dentro
Push Out First Primero hacia fuera
Push Out Only Solo hacia fuera
Hard Fuerte
Soft Suave
Monitor RGB RGB del monitor
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1893,6 +1893,10 @@ digits Malayalam Malajalam
Stylistic Tyylitelty
Mojikumi Set Mojikumi-joukko
Kinsoku Set Kinsoku-joukko
Kinsoku Shori Type Kinsoku Shori -tyyppi
Push In First Ensin sisään
Push Out First Ensin ulos
Push Out Only Vain ulos
Hard Tiukka
Right Indent Oikea sisennys
Right-to-Left Paragraph Direction Kappaleen suunta oikealta vasemmalle
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1990,6 +1990,10 @@ font-technology Variable Variables
Stylistic Stylistique
Mojikumi Set Jeu de mojikumi
Kinsoku Set Jeu de kinsoku
Kinsoku Shori Type Type de kinsoku shori
Push In First D’abord vers l’intérieur
Push Out First D’abord vers l’extérieur
Push Out Only Vers l’extérieur uniquement
Hard Fort
Soft Souple
Monitor RGB RVB du moniteur
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2198,6 +2198,7 @@ file-filter Templates Sablonok
Keywords: Kulcsszavak:
Kind Típus
Kinsoku Set Kinsoku-készlet
Kinsoku Shori Type Kinsoku Shori típusa
Kite Deltoid
Knife Kés
Knockout Group Kiütő csoport
@@ -3126,6 +3127,9 @@ mark-style Japanese Japán
Puppet Warp Pins Bábtorzítás tűi
Puppet Warp Tool Bábtorzítás
Purple Lila
Push In First Először behúzás
Push Out First Először kitolás
Push Out Only Csak kitolás
Put back in the toolbar Visszahelyezés az eszköztárba
Quality Minőség
Quality {quality} Minőség: {quality}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1738,6 +1738,10 @@ digits Malayalam Malayalam
Stylistic Stilistik
Mojikumi Set Set Mojikumi
Kinsoku Set Set Kinsoku
Kinsoku Shori Type Jenis Kinsoku Shori
Push In First Dorong masuk dahulu
Push Out First Dorong keluar dahulu
Push Out Only Hanya dorong keluar
Hard Keras
Right Indent Indentasi kanan
Right-to-Left Paragraph Direction Arah paragraf kanan ke kiri
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
+4
View File
@@ -2025,6 +2025,10 @@ file-filter Templates Modelli
Stylistic Stilistica
Mojikumi Set Set mojikumi
Kinsoku Set Set kinsoku
Kinsoku Shori Type Tipo di kinsoku shori
Push In First Prima all'interno
Push Out First Prima all'esterno
Push Out Only Solo all'esterno
Hard Forte
Soft Debole
Monitor RGB RGB monitor
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -76,6 +76,10 @@ digits Malayalam マラヤーラム数字
Stylistic スタイリスティック
Mojikumi Set 文字組み
Kinsoku Set 禁則処理
Kinsoku Shori Type 禁則調整方式
Push In First 追い込み優先
Push Out First 追い出し優先
Push Out Only 追い出しのみ
Hard 強い禁則
Soft 弱い禁則
Latin 欧文
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1646,6 +1646,10 @@ digits Malayalam მალაიალამური
Stylistic სტილისტური
Mojikumi Set Mojikumi-ის ნაკრები
Kinsoku Set Kinsoku-ის ნაკრები
Kinsoku Shori Type Kinsoku Shori-ის ტიპი
Push In First ჯერ შეტანა
Push Out First ჯერ გატანა
Push Out Only მხოლოდ გატანა
Hard მკაცრი
Right Indent მარჯვენა შეწევა
Right-to-Left Paragraph Direction აბზაცის მიმართულება მარჯვნიდან მარცხნივ
Can't render this file because it is too large.
+4
View File
@@ -69,6 +69,10 @@ digits Malayalam 말라얄람어
Stylistic 스타일
Mojikumi Set 모지쿠미 세트
Kinsoku Set 긴소쿠 세트
Kinsoku Shori Type 긴소쿠 처리 유형
Push In First 밀어 넣기 우선
Push Out First 밀어내기 우선
Push Out Only 밀어내기만
Hard 강하게
Soft 약하게
Latin 라틴어
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1752,6 +1752,10 @@ digits Malayalam Malayalam
Stylistic Stilistisch
Mojikumi Set Mojikumi-set
Kinsoku Set Kinsoku-set
Kinsoku Shori Type Kinsoku Shori-type
Push In First Eerst inschuiven
Push Out First Eerst uitschuiven
Push Out Only Alleen uitschuiven
Hard Hard
Right Indent Rechts inspringen
Right-to-Left Paragraph Direction Alinearichting van rechts naar links
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1850,6 +1850,10 @@ digits Malayalam Malajalam
Stylistic Stylistyczny
Mojikumi Set Zestaw mojikumi
Kinsoku Set Zestaw kinsoku
Kinsoku Shori Type Typ kinsoku shori
Push In First Najpierw wciągaj
Push Out First Najpierw wypychaj
Push Out Only Tylko wypychaj
Hard Ścisły
Right Indent Wcięcie z prawej
Right-to-Left Paragraph Direction Kierunek akapitu od prawej do lewej
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3435,6 +3435,10 @@ font-technology Variable Variáveis
Stylistic Estilístico
Mojikumi Set Conjunto Mojikumi
Kinsoku Set Conjunto Kinsoku
Kinsoku Shori Type Tipo de Kinsoku Shori
Push In First Primeiro para dentro
Push Out First Primeiro para fora
Push Out Only Somente para fora
Hard Duro
Soft Macio
Monospaced Monoespaçado
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2022,6 +2022,10 @@ file-filter Templates Шаблоны
Stylistic Стилистические
Mojikumi Set Набор mojikumi
Kinsoku Set Набор kinsoku
Kinsoku Shori Type Тип kinsoku shori
Push In First Сначала втягивать
Push Out First Сначала выталкивать
Push Out Only Только выталкивать
Hard Строгий
Soft Мягкий
Monitor RGB RGB монитора
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -1937,6 +1937,10 @@ font-technology Variable Variabla
Stylistic Stilistisk
Mojikumi Set Mojikumi-uppsättning
Kinsoku Set Kinsoku-uppsättning
Kinsoku Shori Type Kinsoku Shori-typ
Push In First Skjut in först
Push Out First Skjut ut först
Push Out Only Skjut endast ut
Hard Hård
Soft Mjuk
Monitor RGB Bildskärms-RGB
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -1826,6 +1826,10 @@ font-technology Variable Değişken
Mojikumi Mojikumi
Mojikumi Set Mojikumi Seti
Kinsoku Set Kinsoku Seti
Kinsoku Shori Type Kinsoku Shori Türü
Push In First Önce İçeri
Push Out First Önce Dışarı
Push Out Only Yalnızca Dışarı
Hard Sert
Soft Yumuşak
Monitor RGB RGB'yi izleyin
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -2019,6 +2019,10 @@ file-filter Templates Шаблони
Stylistic Стилістичні
Mojikumi Set Набір mojikumi
Kinsoku Set Набір kinsoku
Kinsoku Shori Type Тип kinsoku shori
Push In First Спершу втягувати
Push Out First Спершу виштовхувати
Push Out Only Лише виштовхувати
Hard Суворий
Soft М’який
Monitor RGB RGB монітора
Can't render this file because it contains an unexpected character in line 182 and column 5.
+4
View File
@@ -2038,6 +2038,7 @@ digits Hindi Hindi
Keywords: Từ khóa:
Kind Loại
Kinsoku Set Bộ Kinsoku
Kinsoku Shori Type Kiểu Kinsoku Shori
Knife Dao
Knockout Group Nhóm Knockout
Lab Lab
@@ -2906,6 +2907,9 @@ mark-style Japanese Kiểu Nhật
Puppet Warp Biến dạng con rối
Puppet Warp Pins Ghim biến dạng con rối
Puppet Warp Tool Công cụ Biến dạng con rối
Push In First Đẩy vào trước
Push Out First Đẩy ra trước
Push Out Only Chỉ đẩy ra
Put back in the toolbar Đặt trở lại vào thanh công cụ
Quality Chất lượng
Quality {quality} Chất lượng {quality}
Can't render this file because it contains an unexpected character in line 204 and column 5.
+4
View File
@@ -5047,6 +5047,10 @@ font-technology Variable 可变
Stylistic 风格化
Mojikumi Set 标点挤压集
Kinsoku Set Kinsoku 集
Kinsoku Shori Type Kinsoku 处理方式
Push In First 先推入
Push Out First 先推出
Push Out Only 只推出
Hard 强
Soft 弱
Saved Selection 1 已保存的选区 1
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -2885,6 +2885,10 @@ file-filter Templates 範本
Stylistic 風格化
Mojikumi Set 標點間距
Kinsoku Set Kinsoku 集
Kinsoku Shori Type Kinsoku 處理方式
Push In First 先推入
Push Out First 先推出
Push Out Only 只推出
Hard 強
Soft 弱
Monitor RGB 顯示器 RGB
Can't render this file because it contains an unexpected character in line 6 and column 47.
+30 -7
View File
@@ -2,13 +2,14 @@
//! Bullets and Numbered List buttons and their presets (More Options opens Bullets and
//! Numbering…), indents, space before/after, Hyphenate, Insert Kashidas (with the Indic options),
//! Mojikumi Set and Kinsoku Set (with the East Asian options); the panel menu turns Roman Hanging
//! Punctuation on and off, opens Justification… and Hyphenation… and picks the Single-line or
//! Every-line Composer.
//! Punctuation on and off, opens Justification… and Hyphenation…, picks the Kinsoku Shori Type
//! (with the East Asian options) and picks the Single-line or Every-line Composer.
use egui::{Ui, vec2};
use serde_json::{Value, json};
use vectorcraft_doc::{
Composer, HyphenationOptions, JustificationOptions, Justify, Kinsoku, Mojikumi, NodeKind, ParaDirection, ParaStyle, SingleWordJustify,
Composer, HyphenationOptions, JustificationOptions, Justify, Kinsoku, KinsokuShoriType, Mojikumi, NodeKind, ParaDirection, ParaStyle,
SingleWordJustify,
};
use super::character::{Metric, Shared, text_style};
@@ -860,6 +861,22 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
}
});
});
// How a line keeps the kinsoku characters off its ends: the selected paragraphs' type is
// checked, and none is when they differ.
let kinsoku_type = shared_para(app, |p| p.kinsoku_type);
ui.add_enabled_ui(has, |ui| {
ui.menu_button(format!(" {}", tl!("Kinsoku Shori Type")), |ui| {
for (label, t, key) in [
(tl!("Push In First"), KinsokuShoriType::PushInFirst, "pushInFirst"),
(tl!("Push Out First"), KinsokuShoriType::PushOutFirst, "pushOutFirst"),
(tl!("Push Out Only"), KinsokuShoriType::PushOutOnly, "pushOutOnly"),
] {
if menu_item(ui, label, true, kinsoku_type == Some(t)) {
set_kinsoku_type(app, key);
}
}
});
});
}
ui.separator();
let composer = shared_composer(app);
@@ -876,12 +893,18 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
}
}
/// Kinsoku Shori Type (`key`: `"pushInFirst"`, `"pushOutFirst"` or `"pushOutOnly"`) for the
/// paragraphs the panel acts on ([`format`]), in one undo step.
fn set_kinsoku_type(app: &mut VectorcraftApp, key: &str) {
format(app, json!({"kinsokuType": key}));
}
/// Reset Panel: the paragraphs the panel acts on ([`format`]) take Auto alignment, no list, no
/// indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification
/// options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi
/// Set, Regular burasagari, the Hard kinsoku set, the Every-line Composer, Roman Hanging
/// Punctuation off, no kashidas and no tab stops, as one undo step named Reset Panel (inside an
/// undo group that is already open, as part of its step).
/// Set, Regular burasagari, the Hard kinsoku set, Push Out Only, the Every-line Composer, Roman
/// Hanging Punctuation off, no kashidas and no tab stops, as one undo step named Reset Panel (inside
/// an undo group that is already open, as part of its step).
fn reset_panel(app: &mut VectorcraftApp) {
let h = HyphenationOptions::default();
let j = vectorcraft_doc::JustificationOptions::default();
@@ -901,7 +924,7 @@ fn reset_panel(app: &mut VectorcraftApp) {
"wordSpacing": j.word_spacing, "letterSpacing": j.letter_spacing, "glyphScaling": j.glyph_scaling,
"autoLeading": j.auto_leading, "singleWordJustification": "fullJustify",
"direction": "auto", "leadingModel": "romanBaseline", "mojikumi": "lineEndHalf", "burasagari": "standard", "kinsoku": "hard",
"composer": "everyLine", "romanHangingPunctuation": false
"kinsokuType": "pushOutOnly", "composer": "everyLine", "romanHangingPunctuation": false
}),
);
para_cmd(app, "text.tabs.clear", json!({}));
@@ -107,6 +107,32 @@ fn paragraph_mixed_kinsoku_and_mojikumi_show_blank_dropdowns() {
assert_eq!(shared_para(&app, |p| p.mojikumi), Some(Mojikumi::LineEndHalf));
}
/// Kinsoku Shori Type: the menu's item sets the type of the paragraphs the Type tool's selection
/// touches in one undo step, and none is checked while the selected paragraphs differ.
#[test]
fn kinsoku_shori_type_sets_the_selected_paragraphs_in_one_undo_step() {
let (mut app, id) = editing_text("あ\nい\n");
app.session.prefs.show_east_asian_options = true;
let types = |app: &VectorcraftApp| {
let node = app.session.active().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap();
let NodeKind::Text(text) = &node.kind else { panic!("text") };
(text.para_at(0).kinsoku_type, text.para_at(1).kinsoku_type)
};
select_range(&mut app, 0, 3);
let history = app.session.active().unwrap().history.undo.len();
set_kinsoku_type(&mut app, "pushInFirst");
assert_eq!(types(&app), (KinsokuShoriType::PushInFirst, KinsokuShoriType::PushOutOnly));
assert_eq!(app.session.active().unwrap().history.undo.len(), history + 1);
app.session.set_tool_option("selectAll", &json!(true));
assert_eq!(shared_para(&app, |p| p.kinsoku_type), None, "mixed: no item checked");
set_kinsoku_type(&mut app, "pushOutFirst");
assert_eq!(types(&app), (KinsokuShoriType::PushOutFirst, KinsokuShoriType::PushOutFirst));
assert_eq!(shared_para(&app, |p| p.kinsoku_type), Some(KinsokuShoriType::PushOutFirst));
app.run("edit.undo", json!({})).unwrap();
app.run("edit.undo", json!({})).unwrap();
assert_eq!(types(&app), (KinsokuShoriType::PushOutOnly, KinsokuShoriType::PushOutOnly));
}
#[test]
fn paragraph_mixed_indent_fields_show_blank() {
let (mut app, id) = editing_text("A\nB\n");
+24
View File
@@ -5890,6 +5890,30 @@ JLREQ's level 3 line-breaking rules do (https://www.w3.org/TR/jlreq/#addendum_a3
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"kinsoku":"soft"}}}
```
## Kinsoku Shori Type
`text.setFormat {kinsokuType: "pushInFirst"|"pushOutFirst"|"pushOutOnly"}` (Paragraph panel menu › Kinsoku Shori Type,
shown with the East Asian options) sets how the area type lines of the paragraphs of the selected type (or `ids`, or the
paragraphs a `start` / `end` range touches) keep the characters of their Kinsoku Set off their start and end. When the
character that does not fit on a line may not start the next one, or the character before it may not end the line,
**Push In First** sets it on the line, with the characters up to the next break the Kinsoku Set allows. The line then
closes its gaps next to CJK characters to fit its measure, however it is aligned, each gap by the same share of the em
of the character before it and by at most a tenth of that em; a line that cannot close up that much, or that holds a
tab, pushes out instead. **Push Out First** pushes in
the same way only when pushing out would leave the line more than an em, of the character before the break, short of
its measure. **Push Out Only** (the default: new type and documents from before the setting) moves the
character before a prohibited line start to the next line with it. With Burasagari on, an overflowing comma or full
stop may hang outside the margin first, leaving the preceding character on the line. With Kinsoku Set None the setting
does nothing. When Push In First or Push Out First is selected and a paragraph contains a prohibited line-start or
line-end character, the whole paragraph is broken line by line, as the Single-line Composer breaks it, even if no line
ends up pushing a character in. The setting is saved
with the paragraph (`kinsoku_type` in `para`) only when it is not `pushOutOnly`, paragraph styles carry it, and Reset
Panel sets it back to Push Out Only. One undo step.
```json
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"kinsokuType":"pushInFirst"}}}
```
## Hanging punctuation (burasagari)
`text.setFormat {burasagari: "none"|"standard"|"forced"}` sets the Paragraph panel menu's Burasagari (None / Regular /
+2 -1
View File
@@ -1084,7 +1084,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
The frames of threaded text share one writing direction: Type › Type Orientation (`type.orientation.vertical`, `type.orientation.horizontal`) in one frame sets every frame, threading frames gives the thread its first frame's direction, frames taken out of a thread keep theirs, and the story flows through every frame in the thread's direction.
Type › Optical Margin Alignment (`type.opticalMarginAlignment`) hangs the punctuation and letter edges at the ends of a type object's lines outside its margins: at the left of left-aligned lines, at the right of right-aligned lines, and at both ends of centered and justified lines, in point and area type of left-to-right paragraphs. Quotation marks, hyphens, full stops and commas hang wholly; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their width. It is off by default and saved with the type object. Justification's Glyph Scaling leaves the glyphs that hang at their width, so they hang exactly their share, and Standard burasagari counts what hangs at a line's start as room before hanging a comma; `crates/text/src/tests/hang.rs` checks whole paragraphs with both composers, ragged, centred and justified, with Line-end Punctuation Half Width and bidirectional lines.
Optical Margin Alignment and Roman Hanging Punctuation hang right-to-left paragraphs of point and area type, and type on a path whose first paragraph runs right to left, too. Auto aligns a right-to-left line at its right, or at the end bracket of a path, so it hangs there. A right-to-left line is read from right to left: where it hangs at its left margin, `."` ending it hangs both glyphs.
The Paragraph panel menu's Reset Panel (`text.setStyle`, `text.setFormat` and `text.tabs.clear`) gives the selected type's paragraphs, or the paragraphs the Type tool's selection touches, Auto alignment, no indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi Set, Regular burasagari, the Hard kinsoku set, the Every-line Composer, Roman Hanging Punctuation off, no kashidas and no tab stops, as one undo step.
The Paragraph panel menu's Kinsoku Shori Type (`text.setFormat {kinsokuType}`, with the East Asian options) picks Push In First, Push Out First or Push Out Only for each paragraph: a character the Kinsoku Set keeps off a line's start or end is set on the line, with the line's gaps next to CJK characters closed by at most a tenth of an em each to fit, or moved to the next line with the character before it. Push Out First pushes in only when pushing out would leave the line more than an em short. Push Out Only is the default, the setting is saved only when it is not, and paragraph styles carry it.
The Paragraph panel menu's Reset Panel (`text.setStyle`, `text.setFormat` and `text.tabs.clear`) gives the selected type's paragraphs, or the paragraphs the Type tool's selection touches, Auto alignment, no indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi Set, Regular burasagari, the Hard kinsoku set, Push Out Only, the Every-line Composer, Roman Hanging Punctuation off, no kashidas and no tab stops, as one undo step.
The Paragraph panel menu's Hyphenation… (`ui.hyphenation`, `text.setFormat {wordsLongerThan, afterFirst, beforeLast, hyphenLimit, hyphenationZone, hyphenateCapitalized}`) sets each paragraph's hyphenation limits: the shortest word that breaks, the fewest letters on each side of the hyphen, the most hyphenated lines in a row, a zone at the right edge where the Single-line Composer breaks before a word instead of hyphenating it, and whether capitalized words break. The defaults are words longer than 5 letters, 3 letters on each side, no limit or zone, and capitalized words hyphenated; paragraph styles and the native file keep the options.
PDF export with type as text writes the hyphen that ends a line broken inside a word as a hyphen character where the line draws it, and the file opens with the hyphen in the line's text; kashidas are drawn as paths.
The Paragraph panel's Insert Kashidas (with the Indic options; `text.setFormat {kashida}`) lengthens each Arabic word of a justified horizontal area type line with a kashida, a tatweel between its last pair of joined letters, before the word spaces widen: Short, Medium and Long kashidas are at most a quarter, half and a whole em long, Stylistic ones are whole tatweels up to an em, and None (the default) leaves the line as before; paragraph styles and the native file keep the setting.
+1
View File
@@ -61,6 +61,7 @@ against Illustrator's public documentation; **estimated** unless noted.
| Middle Eastern: Middle Eastern composers, split caret | missing | Character Direction is done; editable bidirectional SVG text remains incomplete. |
| CJK: input methods (marked text, candidate window) | done | |
| CJK: kinsoku (Hard, Soft, None), Line-end Punctuation Half Width, tate-chu-yoko (auto) | done / partial | custom kinsoku sets, the Mojikumi Settings dialog and the other mojikumi sets missing |
| CJK: Kinsoku Shori Type (Push In First, Push Out First, Push Out Only) | done | Paragraph panel menu › Kinsoku Shori Type (`text.setFormat {kinsokuType}`), with the East Asian options ([Formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); Push Out Only is the default, and a push-in closes the line's gaps next to CJK characters by at most a tenth of an em each (VectorCraft's own limit). Evidence: `layout::wrapping_tests::kinsoku_shori_type_pushes_a_closing_bracket_in_or_out`, `tests::vertical::kinsoku_shori_type_pushes_a_closing_bracket_in_or_out`, `typecmd::area_tests::kinsoku_shori_type_is_set_per_paragraph_and_saved_only_when_not_push_out_only` |
| CJK: Proportional Metrics (`palt` in horizontal type, `vpal` in vertical type) | done | #966 |
| CJK: East Asian Type units (Q, Ha) | done | Preferences › Units › East Asian Type (`unitsAsianType`, Points by default) offers every unit and Q and Ha, a quarter of a millimetre each ([Scripting Guide](https://ai-scripting.docsforadobe.dev/scripting/measurementUnits/): 0.709 pt per Q; [InDesign's unit notation](https://helpx.adobe.com/in/indesign/desktop/layout-and-grid-tools/rulers-and-measure-tools/change-ruler-measurement-units.html): `q`, `h`). The public pages do not say which fields follow it, so no field uses it yet and that waits for a source. Evidence: `q_and_ha_are_a_quarter_millimetre`, `east_asian_type_unit_*` |
| CJK: manual tate-chu-yoko | done | Character panel menu › Tate-chu-yoko (`text.setFormat` / `text.setRangeStyle {tateChuYoko}`), with Show East Asian Options: marked characters stand side by side across one em of a vertical column, squeezed to the em when wider; two or three half-width digits still do so without the mark; horizontal type keeps the mark and does not move them. Tate-chu-yoko Settings… (`ui.tateChuYokoSettings`, `{tcyUpDown, tcyLeftRight}` in points) moves a marked block up or down and right or left as one piece. Evidence: `tests_vertical::manual_tate_chu_yoko_sets_marked_characters_across_one_em`, `tests_vertical::tate_chu_yoko_settings_move_a_marked_block_up_down_left_and_right`, `typecmd::tate_chu_yoko_is_a_character_attribute_saved_only_when_on`, `typecmd::tate_chu_yoko_settings_are_character_attributes_saved_only_when_set`, `dialogs::tate_chu_yoko::tests` |