summaryrefslogtreecommitdiff
path: root/src/library/text/par.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-03-08 19:49:26 +0100
committerLaurenz <laurmaedje@gmail.com>2022-03-08 19:49:26 +0100
commitb71113d37a29bab5c7dc4b501c33ee9afbdb8213 (patch)
tree9cab01b9e7f5930651d587473dc24468a4a5eaa1 /src/library/text/par.rs
parentd24c7030d819fd6c6e8e73663ddfe42047cca397 (diff)
Hyphenation
Diffstat (limited to 'src/library/text/par.rs')
-rw-r--r--src/library/text/par.rs76
1 files changed, 65 insertions, 11 deletions
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 812231c2..70cac1be 100644
--- a/src/library/text/par.rs
+++ b/src/library/text/par.rs
@@ -27,12 +27,17 @@ pub enum ParChild {
#[class]
impl ParNode {
+ /// An ISO 639-1 language code.
+ pub const LANG: Option<EcoString> = None;
/// The direction for text and inline objects.
pub const DIR: Dir = Dir::LTR;
/// How to align text and inline objects in their line.
pub const ALIGN: Align = Align::Left;
/// Whether to justify text in its line.
pub const JUSTIFY: bool = false;
+ /// Whether to hyphenate text to improve line breaking. When `auto`, words
+ /// will will be hyphenated if and only if justification is enabled.
+ pub const HYPHENATE: Smart<bool> = Smart::Auto;
/// The spacing between lines (dependent on scaled font size).
pub const LEADING: Linear = Relative::new(0.65).into();
/// The extra spacing between paragraphs (dependent on scaled font size).
@@ -49,13 +54,14 @@ impl ParNode {
}
fn set(args: &mut Args, styles: &mut StyleMap) -> TypResult<()> {
+ let lang = args.named::<Option<EcoString>>("lang")?;
+
let mut dir =
- args.named("lang")?
- .map(|iso: EcoString| match iso.to_lowercase().as_str() {
- "ar" | "he" | "fa" | "ur" | "ps" | "yi" => Dir::RTL,
- "en" | "fr" | "de" => Dir::LTR,
- _ => Dir::LTR,
- });
+ lang.clone().flatten().map(|iso| match iso.to_lowercase().as_str() {
+ "ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur"
+ | "yi" => Dir::RTL,
+ _ => Dir::LTR,
+ });
if let Some(Spanned { v, span }) = args.named::<Spanned<Dir>>("dir")? {
if v.axis() != SpecAxis::Horizontal {
@@ -74,9 +80,11 @@ impl ParNode {
dir.map(|dir| dir.start().into())
};
+ styles.set_opt(Self::LANG, lang);
styles.set_opt(Self::DIR, dir);
styles.set_opt(Self::ALIGN, align);
styles.set_opt(Self::JUSTIFY, args.named("justify")?);
+ styles.set_opt(Self::HYPHENATE, args.named("hyphenate")?);
styles.set_opt(Self::LEADING, args.named("leading")?);
styles.set_opt(Self::SPACING, args.named("spacing")?);
styles.set_opt(Self::INDENT, args.named("indent")?);
@@ -137,7 +145,7 @@ impl Layout for ParNode {
let par = ParLayout::new(ctx, self, bidi, regions, &styles)?;
// Break the paragraph into lines.
- let lines = break_into_lines(&mut ctx.fonts, &par, regions.first.x);
+ let lines = break_into_lines(&mut ctx.fonts, &par, regions.first.x, styles);
// Stack the lines into one frame per region.
Ok(stack_lines(&ctx.fonts, lines, regions, styles))
@@ -278,6 +286,7 @@ impl<'a> ParLayout<'a> {
fonts: &mut FontStore,
mut range: Range,
mandatory: bool,
+ hyphen: bool,
) -> LineLayout<'a> {
// Find the items which bound the text range.
let last_idx = self.find(range.end.saturating_sub(1)).unwrap();
@@ -308,7 +317,10 @@ impl<'a> ParLayout<'a> {
// empty string.
if !shifted.is_empty() || rest.is_empty() {
// Reshape that part.
- let reshaped = shaped.reshape(fonts, shifted);
+ let mut reshaped = shaped.reshape(fonts, shifted);
+ if hyphen {
+ reshaped.push_hyphen(fonts);
+ }
last = Some(ParItem::Text(reshaped));
}
@@ -524,6 +536,7 @@ fn break_into_lines<'a>(
fonts: &mut FontStore,
par: &'a ParLayout<'a>,
width: Length,
+ styles: StyleChain,
) -> Vec<LineLayout<'a>> {
// The already determined lines and the current line attempt.
let mut lines = vec![];
@@ -531,9 +544,9 @@ fn break_into_lines<'a>(
let mut last = None;
// Find suitable line breaks.
- for (end, mandatory) in LineBreakIterator::new(&par.bidi.text) {
+ for (end, mandatory, hyphen) in breakpoints(&par.bidi.text, styles) {
// Compute the line and its size.
- let mut line = par.line(fonts, start .. end, mandatory);
+ let mut line = par.line(fonts, start .. end, mandatory, hyphen);
// If the line doesn't fit anymore, we push the last fitting attempt
// into the stack and rebuild the line from its end. The resulting
@@ -542,7 +555,7 @@ fn break_into_lines<'a>(
if let Some((last_line, last_end)) = last.take() {
lines.push(last_line);
start = last_end;
- line = par.line(fonts, start .. end, mandatory);
+ line = par.line(fonts, start .. end, mandatory, hyphen);
}
}
@@ -565,6 +578,47 @@ fn break_into_lines<'a>(
lines
}
+/// Determine all possible points in the text where lines can broken.
+fn breakpoints<'a>(
+ text: &'a str,
+ styles: StyleChain,
+) -> impl Iterator<Item = (usize, bool, bool)> + 'a {
+ let mut lang = None;
+ if styles.get(ParNode::HYPHENATE).unwrap_or(styles.get(ParNode::JUSTIFY)) {
+ lang = styles
+ .get_ref(ParNode::LANG)
+ .as_ref()
+ .and_then(|iso| iso.as_bytes().try_into().ok())
+ .and_then(hypher::Lang::from_iso);
+ }
+
+ let breaks = LineBreakIterator::new(text);
+ let mut last = 0;
+
+ if let Some(lang) = lang {
+ Either::Left(breaks.flat_map(move |(end, mandatory)| {
+ let word = &text[last .. end];
+ let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic());
+ let suffix = last + trimmed.len();
+ let mut start = std::mem::replace(&mut last, end);
+ if trimmed.is_empty() {
+ Either::Left([(end, mandatory, false)].into_iter())
+ } else {
+ Either::Right(hypher::hyphenate(trimmed, lang).map(move |syllable| {
+ start += syllable.len();
+ if start == suffix {
+ start = end;
+ }
+ let hyphen = start < end;
+ (start, mandatory && !hyphen, hyphen)
+ }))
+ }
+ }))
+ } else {
+ Either::Right(breaks.map(|(e, m)| (e, m, false)))
+ }
+}
+
/// Combine the lines into one frame per region.
fn stack_lines(
fonts: &FontStore,