Move language-related properties from par to text

Closes #67
This commit is contained in:
Laurenz 2022-04-10 23:23:50 +02:00
parent 29eb13ca62
commit 34fa8df044
22 changed files with 368 additions and 179 deletions

View File

@ -6,7 +6,7 @@ use super::{Fold, Resolve, Smart, StyleChain, Value};
use crate::geom::{
Align, Em, Get, Length, Numeric, Paint, Relative, Spec, SpecAxis, Stroke,
};
use crate::library::text::{ParNode, TextNode};
use crate::library::text::TextNode;
/// The unresolved alignment representation.
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
@ -23,7 +23,7 @@ impl Resolve for RawAlign {
type Output = Align;
fn resolve(self, styles: StyleChain) -> Self::Output {
let dir = styles.get(ParNode::DIR);
let dir = styles.get(TextNode::DIR);
match self {
Self::Start => dir.start().into(),
Self::End => dir.end().into(),

View File

@ -66,6 +66,14 @@ impl StyleMap {
self.0.push(Entry::Recipe(Recipe::new::<T>(func, span)));
}
/// Whether the map contains a style property for the given key.
pub fn contains<'a, K: Key<'a>>(&self, _: K) -> bool {
self.0
.iter()
.filter_map(|entry| entry.property())
.any(|property| property.key == TypeId::of::<K>())
}
/// Make `self` the first link of the `tail` chain.
///
/// The resulting style chain contains styles from `self` as well as
@ -261,7 +269,7 @@ where
///
/// This trait is not intended to be implemented manually, but rather through
/// the `#[node]` proc-macro.
pub trait Key<'a>: 'static {
pub trait Key<'a>: Copy + 'static {
/// The unfolded type which this property is stored as in a style map. For
/// example, this is [`Toggle`](crate::geom::Length) for the
/// [`STRONG`](TextNode::STRONG) property.
@ -680,6 +688,15 @@ impl<T> StyleVec<T> {
self.items.len()
}
/// Iterate over the contained maps. Note that zipping this with `items()`
/// does not yield the same result as calling `iter()` because this method
/// only returns maps once that are shared by consecutive items. This method
/// is designed for use cases where you want to check, for example, whether
/// any of the maps fulfills a specific property.
pub fn maps(&self) -> impl Iterator<Item = &StyleMap> {
self.maps.iter().map(|(map, _)| map)
}
/// Iterate over the contained items.
pub fn items(&self) -> std::slice::Iter<'_, T> {
self.items.iter()

View File

@ -26,6 +26,7 @@ impl<const S: ShapeKind> ShapeNode<S> {
/// How to stroke the shape.
#[property(resolve, fold)]
pub const STROKE: Smart<Option<RawStroke>> = Smart::Auto;
/// How much to pad the shape's content.
pub const PADDING: Relative<RawLength> = Relative::zero();

View File

@ -1,5 +1,5 @@
use crate::library::prelude::*;
use crate::library::text::ParNode;
use crate::library::text::{HorizontalAlign, ParNode};
/// Align a node along the layouting axes.
#[derive(Debug, Hash)]
@ -33,7 +33,7 @@ impl Layout for AlignNode {
// Align paragraphs inside the child.
let mut passed = StyleMap::new();
if let Some(align) = self.aligns.x {
passed.set(ParNode::ALIGN, align);
passed.set(ParNode::ALIGN, HorizontalAlign(align));
}
// Layout the child.

View File

@ -1,5 +1,5 @@
use crate::library::prelude::*;
use crate::library::text::ParNode;
use crate::library::text::TextNode;
/// Separate a region into multiple equally sized columns.
#[derive(Debug, Hash)]
@ -59,7 +59,7 @@ impl Layout for ColumnsNode {
// Layout the children.
let mut frames = self.child.layout(ctx, &pod, styles)?.into_iter();
let dir = styles.get(ParNode::DIR);
let dir = styles.get(TextNode::DIR);
let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
let mut finished = vec![];

View File

@ -17,6 +17,7 @@ impl PageNode {
pub const HEIGHT: Smart<RawLength> = Smart::Custom(Paper::A4.height().into());
/// Whether the page is flipped into landscape orientation.
pub const FLIPPED: bool = false;
/// The left margin.
pub const LEFT: Smart<Relative<RawLength>> = Smart::Auto;
/// The right margin.
@ -25,10 +26,12 @@ impl PageNode {
pub const TOP: Smart<Relative<RawLength>> = Smart::Auto;
/// The bottom margin.
pub const BOTTOM: Smart<Relative<RawLength>> = Smart::Auto;
/// The page's background color.
pub const FILL: Option<Paint> = None;
/// How many columns the page has.
pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap();
/// The page's background color.
pub const FILL: Option<Paint> = None;
/// The page's header.
#[property(referenced)]
pub const HEADER: Marginal = Marginal::None;

View File

@ -25,6 +25,7 @@ impl HeadingNode {
let upscale = (1.6 - 0.1 * level as f64).max(0.75);
TextSize(Em::new(upscale).into())
});
/// Whether text in the heading is strengthend.
#[property(referenced)]
pub const STRONG: Leveled<bool> = Leveled::Value(true);
@ -34,12 +35,14 @@ impl HeadingNode {
/// Whether the heading is underlined.
#[property(referenced)]
pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
/// The extra padding above the heading.
#[property(referenced)]
pub const ABOVE: Leveled<RawLength> = Leveled::Value(Length::zero().into());
/// The extra padding below the heading.
#[property(referenced)]
pub const BELOW: Leveled<RawLength> = Leveled::Value(Length::zero().into());
/// Whether the heading is block-level.
#[property(referenced)]
pub const BLOCK: Leveled<bool> = Leveled::Value(true);

View File

@ -33,6 +33,7 @@ impl<const L: ListKind> ListNode<L> {
/// How the list is labelled.
#[property(referenced)]
pub const LABEL: Label = Label::Default;
/// The spacing between the list items of a non-wide list.
#[property(resolve)]
pub const SPACING: RawLength = RawLength::zero();
@ -42,6 +43,7 @@ impl<const L: ListKind> ListNode<L> {
/// The space between the label and the body of each item.
#[property(resolve)]
pub const BODY_INDENT: RawLength = Em::new(0.5).into();
/// The extra padding above the list.
#[property(resolve)]
pub const ABOVE: RawLength = RawLength::zero();
@ -137,7 +139,7 @@ pub const UNORDERED: ListKind = 0;
/// Ordered list labelling style.
pub const ORDERED: ListKind = 1;
/// Either content or a closure mapping to content.
/// How to label a list or enumeration.
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Label {
/// The default labelling.

View File

@ -21,6 +21,7 @@ impl TableNode {
/// How to stroke the cells.
#[property(resolve, fold)]
pub const STROKE: Option<RawStroke> = Some(RawStroke::default());
/// How much to pad the cells's content.
pub const PADDING: Relative<RawLength> = Length::pt(5.0).into();

View File

@ -24,6 +24,7 @@ impl<const L: DecoLine> DecoNode<L> {
/// tables if `auto`.
#[property(shorthand, resolve, fold)]
pub const STROKE: Smart<RawStroke> = Smart::Auto;
/// Position of the line relative to the baseline, read from the font tables
/// if `auto`.
#[property(resolve)]
@ -31,6 +32,7 @@ impl<const L: DecoLine> DecoNode<L> {
/// Amount that the line will be longer or shorter than its associated text.
#[property(resolve)]
pub const EXTENT: RawLength = RawLength::zero();
/// Whether the line skips sections in which it would collide
/// with the glyphs. Does not apply to strikethrough.
pub const EVADE: bool = true;

View File

@ -61,6 +61,18 @@ impl TextNode {
/// The bottom end of the text bounding box.
pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline);
/// An ISO 639-1 language code.
#[property(referenced)]
pub const LANG: Option<Lang> = None;
/// The direction for text and inline objects. When `auto`, the direction is
/// automatically inferred from the language.
#[property(resolve)]
pub const DIR: Smart<HorizontalDir> = Smart::Auto;
/// Whether to hyphenate text to improve line breaking. When `auto`, words
/// will will be hyphenated if and only if justification is enabled.
#[property(resolve)]
pub const HYPHENATE: Smart<Hyphenate> = Smart::Auto;
/// Whether to apply kerning ("kern").
pub const KERNING: bool = true;
/// Whether small capital glyphs should be used. ("smcp")
@ -241,6 +253,80 @@ castable! {
}),
}
/// A natural language.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Lang(EcoString);
impl Lang {
/// The default direction for the language.
pub fn dir(&self) -> Dir {
match self.0.as_str() {
"ar" | "dv" | "fa" | "he" | "ks" | "pa" | "ps" | "sd" | "ug" | "ur"
| "yi" => Dir::RTL,
_ => Dir::LTR,
}
}
/// Return the language code as a string slice.
pub fn as_str(&self) -> &str {
&self.0
}
}
castable! {
Lang,
Expected: "string",
Value::Str(string) => Self(string.to_lowercase()),
}
/// The direction of text and inline objects in their line.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalDir(pub Dir);
castable! {
HorizontalDir,
Expected: "direction",
@dir: Dir => match dir.axis() {
SpecAxis::Horizontal => Self(*dir),
SpecAxis::Vertical => Err("must be horizontal")?,
},
}
impl Resolve for Smart<HorizontalDir> {
type Output = Dir;
fn resolve(self, styles: StyleChain) -> Self::Output {
match self {
Smart::Auto => match styles.get(TextNode::LANG) {
Some(lang) => lang.dir(),
None => Dir::LTR,
},
Smart::Custom(dir) => dir.0,
}
}
}
/// Whether to hyphenate text.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Hyphenate(pub bool);
castable! {
Hyphenate,
Expected: "boolean",
Value::Bool(v) => Self(v),
}
impl Resolve for Smart<Hyphenate> {
type Output = bool;
fn resolve(self, styles: StyleChain) -> Self::Output {
match self {
Smart::Auto => styles.get(ParNode::JUSTIFY),
Smart::Custom(v) => v.0,
}
}
}
/// A stylistic set in a font face.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StylisticSet(u8);

View File

@ -1,10 +1,9 @@
use std::sync::Arc;
use either::Either;
use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
use super::{shape, ShapedText, TextNode};
use super::{shape, Lang, ShapedText, TextNode};
use crate::font::FontStore;
use crate::library::layout::Spacing;
use crate::library::prelude::*;
@ -27,21 +26,6 @@ pub enum ParChild {
#[node]
impl ParNode {
/// An ISO 639-1 language code.
#[property(referenced)]
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.
#[property(resolve)]
pub const ALIGN: RawAlign = RawAlign::Start;
/// Whether to justify text in its line.
pub const JUSTIFY: bool = false;
/// How to determine line breaks.
pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto;
/// 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.
#[property(resolve)]
pub const LEADING: RawLength = Em::new(0.65).into();
@ -52,6 +36,15 @@ impl ParNode {
#[property(resolve)]
pub const INDENT: RawLength = RawLength::zero();
/// How to align text and inline objects in their line.
#[property(resolve)]
pub const ALIGN: HorizontalAlign = HorizontalAlign(RawAlign::Start);
/// Whether to justify text in its line.
pub const JUSTIFY: bool = false;
/// How to determine line breaks.
#[property(resolve)]
pub const LINEBREAKS: Smart<Linebreaks> = Smart::Auto;
fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph
// since that happens automatically through markup. Instead, it just
@ -59,45 +52,6 @@ impl ParNode {
// adjacent stuff and it styles the contained paragraphs.
Ok(Content::Block(args.expect("body")?))
}
fn set(args: &mut Args) -> TypResult<StyleMap> {
let mut styles = StyleMap::new();
let lang = args.named::<Option<EcoString>>("lang")?;
let mut dir =
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 {
bail!(span, "must be horizontal");
}
dir = Some(v);
}
let mut align = None;
if let Some(Spanned { v, span }) = args.named::<Spanned<RawAlign>>("align")? {
if v.axis() != SpecAxis::Horizontal {
bail!(span, "must be horizontal");
}
align = Some(v);
};
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::LINEBREAKS, args.named("linebreaks")?);
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")?);
Ok(styles)
}
}
impl ParNode {
@ -147,7 +101,7 @@ impl Layout for ParNode {
let p = prepare(ctx, self, &text, regions, &styles)?;
// Break the paragraph into lines.
let lines = linebreak(&p, &mut ctx.fonts, regions.first.x, styles);
let lines = linebreak(&p, &mut ctx.fonts, regions.first.x);
// Stack the lines into one frame per region.
Ok(stack(&lines, &ctx.fonts, regions, styles))
@ -182,6 +136,27 @@ impl Merge for ParChild {
}
}
/// A horizontal alignment.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalAlign(pub RawAlign);
castable! {
HorizontalAlign,
Expected: "alignment",
@align: RawAlign => match align.axis() {
SpecAxis::Horizontal => Self(*align),
SpecAxis::Vertical => Err("must be horizontal")?,
},
}
impl Resolve for HorizontalAlign {
type Output = Align;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.0.resolve(styles)
}
}
/// How to determine line breaks in a paragraph.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Linebreaks {
@ -201,6 +176,20 @@ castable! {
},
}
impl Resolve for Smart<Linebreaks> {
type Output = Linebreaks;
fn resolve(self, styles: StyleChain) -> Self::Output {
self.unwrap_or_else(|| {
if styles.get(ParNode::JUSTIFY) {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
})
}
}
/// A paragraph break.
pub struct ParbreakNode;
@ -233,17 +222,35 @@ type Range = std::ops::Range<usize>;
struct Preparation<'a> {
/// Bidirectional text embedding levels for the paragraph.
bidi: BidiInfo<'a>,
/// The paragraph's children.
children: &'a StyleVec<ParChild>,
/// Spacing, separated text runs and layouted nodes.
items: Vec<ParItem<'a>>,
/// The ranges of the items in `bidi.text`.
ranges: Vec<Range>,
/// The shared styles.
styles: StyleChain<'a>,
}
impl Preparation<'_> {
impl<'a> Preparation<'a> {
/// Find the item whose range contains the `text_offset`.
fn find(&self, text_offset: usize) -> Option<&ParItem<'a>> {
self.find_idx(text_offset).map(|idx| &self.items[idx])
}
/// Find the index of the item whose range contains the `text_offset`.
fn find(&self, text_offset: usize) -> Option<usize> {
fn find_idx(&self, text_offset: usize) -> Option<usize> {
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
}
/// Get a style property, but only if it is the same for all children of the
/// paragraph.
fn get_shared<K: Key<'a>>(&self, key: K) -> Option<K::Output> {
self.children
.maps()
.all(|map| !map.contains(key))
.then(|| self.styles.get(key))
}
}
/// A prepared item in a paragraph layout.
@ -258,6 +265,16 @@ enum ParItem<'a> {
Frame(Frame),
}
impl<'a> ParItem<'a> {
/// If this a text item, return it.
fn text(&self) -> Option<&ShapedText<'a>> {
match self {
Self::Text(shaped) => Some(shaped),
_ => None,
}
}
}
/// A layouted line, consisting of a sequence of layouted paragraph items that
/// are mostly borrowed from the preparation phase. This type enables you to
/// measure the size of a line in a range before comitting to building the
@ -315,10 +332,8 @@ impl<'a> Line<'a> {
// How many justifiable glyphs the line contains.
fn justifiables(&self) -> usize {
let mut count = 0;
for item in self.items() {
if let ParItem::Text(shaped) = item {
count += shaped.justifiables();
}
for shaped in self.items().filter_map(ParItem::text) {
count += shaped.justifiables();
}
count
}
@ -326,10 +341,8 @@ impl<'a> Line<'a> {
/// How much of the line is stretchable spaces.
fn stretch(&self) -> Length {
let mut stretch = Length::zero();
for item in self.items() {
if let ParItem::Text(shaped) = item {
stretch += shaped.stretch();
}
for shaped in self.items().filter_map(ParItem::text) {
stretch += shaped.stretch();
}
stretch
}
@ -344,7 +357,7 @@ fn prepare<'a>(
regions: &Regions,
styles: &'a StyleChain,
) -> TypResult<Preparation<'a>> {
let bidi = BidiInfo::new(&text, match styles.get(ParNode::DIR) {
let bidi = BidiInfo::new(&text, match styles.get(TextNode::DIR) {
Dir::LTR => Some(Level::ltr()),
Dir::RTL => Some(Level::rtl()),
_ => None,
@ -358,7 +371,7 @@ fn prepare<'a>(
let styles = map.chain(styles);
match child {
ParChild::Text(_) => {
// TODO: Also split by language and script.
// TODO: Also split by language.
let mut cursor = range.start;
for (level, count) in bidi.levels[range].group() {
let start = cursor;
@ -402,7 +415,13 @@ fn prepare<'a>(
}
}
Ok(Preparation { bidi, items, ranges })
Ok(Preparation {
bidi,
children: &par.0,
items,
ranges,
styles: *styles,
})
}
/// Find suitable linebreaks.
@ -410,22 +429,13 @@ fn linebreak<'a>(
p: &'a Preparation<'a>,
fonts: &mut FontStore,
width: Length,
styles: StyleChain,
) -> Vec<Line<'a>> {
let breaks = styles.get(ParNode::LINEBREAKS).unwrap_or_else(|| {
if styles.get(ParNode::JUSTIFY) {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
});
let breaker = match breaks {
let breaker = match p.styles.get(ParNode::LINEBREAKS) {
Linebreaks::Simple => linebreak_simple,
Linebreaks::Optimized => linebreak_optimized,
};
breaker(p, fonts, width, styles)
breaker(p, fonts, width)
}
/// Perform line breaking in simple first-fit style. This means that we build
@ -435,13 +445,12 @@ fn linebreak_simple<'a>(
p: &'a Preparation<'a>,
fonts: &mut FontStore,
width: Length,
styles: StyleChain,
) -> Vec<Line<'a>> {
let mut lines = vec![];
let mut start = 0;
let mut last = None;
for (end, mandatory, hyphen) in breakpoints(&p.bidi.text, styles) {
for (end, mandatory, hyphen) in breakpoints(p) {
// Compute the line and its size.
let mut attempt = line(p, fonts, start .. end, mandatory, hyphen);
@ -496,7 +505,6 @@ fn linebreak_optimized<'a>(
p: &'a Preparation<'a>,
fonts: &mut FontStore,
width: Length,
styles: StyleChain,
) -> Vec<Line<'a>> {
/// The cost of a line or paragraph layout.
type Cost = f64;
@ -515,8 +523,8 @@ fn linebreak_optimized<'a>(
const MIN_COST: Cost = -MAX_COST;
const MIN_RATIO: f64 = -0.15;
let em = styles.get(TextNode::SIZE);
let justify = styles.get(ParNode::JUSTIFY);
let em = p.styles.get(TextNode::SIZE);
let justify = p.styles.get(ParNode::JUSTIFY);
// Dynamic programming table.
let mut active = 0;
@ -526,7 +534,7 @@ fn linebreak_optimized<'a>(
line: line(p, fonts, 0 .. 0, false, false),
}];
for (end, mandatory, hyphen) in breakpoints(&p.bidi.text, styles) {
for (end, mandatory, hyphen) in breakpoints(p) {
let k = table.len();
let eof = end == p.bidi.text.len();
let mut best: Option<Entry> = None;
@ -611,47 +619,104 @@ fn linebreak_optimized<'a>(
/// Returns for each breakpoint the text index, whether the break is mandatory
/// (after `\n`) and whether a hyphen is required (when breaking inside of a
/// word).
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(ParNode::LANG)
.as_ref()
.and_then(|iso| iso.as_bytes().try_into().ok())
.and_then(hypher::Lang::from_iso);
fn breakpoints<'a>(p: &'a Preparation) -> impl Iterator<Item = (usize, bool, bool)> + 'a {
Breakpoints {
p,
linebreaks: LineBreakIterator::new(p.bidi.text),
syllables: None,
offset: 0,
suffix: 0,
end: 0,
mandatory: false,
hyphenate: p.get_shared(TextNode::HYPHENATE),
lang: p.get_shared(TextNode::LANG).map(Option::as_ref),
}
}
/// An iterator over the line break opportunities in a text.
struct Breakpoints<'a> {
/// The paragraph's items.
p: &'a Preparation<'a>,
/// The inner iterator over the unicode line break opportunities.
linebreaks: LineBreakIterator<'a>,
/// Iterator over syllables of the current word.
syllables: Option<hypher::Syllables<'a>>,
/// The current text offset.
offset: usize,
/// The trimmed end of the current word.
suffix: usize,
/// The untrimmed end of the current word.
end: usize,
/// Whether the break after the current word is mandatory.
mandatory: bool,
/// Whether to hyphenate if it's the same for all children.
hyphenate: Option<bool>,
/// The text language if it's the same for all children.
lang: Option<Option<&'a Lang>>,
}
impl Iterator for Breakpoints<'_> {
type Item = (usize, bool, bool);
fn next(&mut self) -> Option<Self::Item> {
// If we're currently in a hyphenated "word", process the next syllable.
if let Some(syllable) = self.syllables.as_mut().and_then(Iterator::next) {
self.offset += syllable.len();
if self.offset == self.suffix {
self.offset = self.end;
}
// Filter out hyphenation opportunities where hyphenation was
// actually disabled.
let hyphen = self.offset < self.end;
if hyphen && !self.hyphenate_at(self.offset) {
return self.next();
}
return Some((self.offset, self.mandatory && !hyphen, hyphen));
}
// Get the next "word".
(self.end, self.mandatory) = self.linebreaks.next()?;
// Hyphenate the next word.
if self.hyphenate != Some(false) {
if let Some(lang) = self.lang_at(self.offset) {
let word = &self.p.bidi.text[self.offset .. self.end];
let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic());
if !trimmed.is_empty() {
self.suffix = self.offset + trimmed.len();
self.syllables = Some(hypher::hyphenate(trimmed, lang));
return self.next();
}
}
}
self.offset = self.end;
Some((self.end, self.mandatory, false))
}
}
impl Breakpoints<'_> {
/// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(&self, offset: usize) -> bool {
self.hyphenate
.or_else(|| {
let shaped = self.p.find(offset)?.text()?;
Some(shaped.styles.get(TextNode::HYPHENATE))
})
.unwrap_or(false)
}
let breaks = LineBreakIterator::new(text);
let mut last = 0;
/// The text language at the given offset.
fn lang_at(&self, offset: usize) -> Option<hypher::Lang> {
let lang = self.lang.unwrap_or_else(|| {
let shaped = self.p.find(offset)?.text()?;
shaped.styles.get(TextNode::LANG).as_ref()
})?;
if let Some(lang) = lang {
Either::Left(breaks.flat_map(move |(end, mandatory)| {
// We don't want to confuse the hyphenator with trailing
// punctuation, so we trim it. And if that makes the word empty, we
// need to return the single breakpoint manually because hypher
// would eat it.
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)))
let bytes = lang.as_str().as_bytes().try_into().ok()?;
hypher::Lang::from_iso(bytes)
}
}
@ -664,11 +729,11 @@ fn line<'a>(
hyphen: bool,
) -> Line<'a> {
// Find the items which bound the text range.
let last_idx = p.find(range.end.saturating_sub(1)).unwrap();
let last_idx = p.find_idx(range.end.saturating_sub(1)).unwrap();
let first_idx = if range.is_empty() {
last_idx
} else {
p.find(range.start).unwrap()
p.find_idx(range.start).unwrap()
};
// Slice out the relevant items.

View File

@ -3,7 +3,7 @@ use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet};
use syntect::parsing::SyntaxSet;
use super::{FontFamily, TextNode, Toggle};
use super::{FontFamily, Hyphenate, TextNode, Toggle};
use crate::library::prelude::*;
use crate::source::SourceId;
use crate::syntax::{self, RedNode};
@ -29,6 +29,7 @@ impl RawNode {
/// The raw text's font family. Just the normal text family if `none`.
#[property(referenced)]
pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono"));
/// The language to syntax-highlight in.
#[property(referenced)]
pub const LANG: Option<EcoString> = None;
@ -97,6 +98,9 @@ impl Show for RawNode {
};
let mut map = StyleMap::new();
map.set(TextNode::OVERHANG, false);
map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false)));
if let Smart::Custom(family) = styles.get(Self::FAMILY) {
map.set_family(family.clone(), styles);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -25,11 +25,11 @@
// Test start and end alignment.
#rotate(-30deg, origin: end + horizon)[Hello]
#set par(lang: "de")
#set text(lang: "de")
#align(start)[Start]
#align(end)[Ende]
#set par(lang: "ar")
#set text(lang: "ar")
#align(start)[يبدأ]
#align(end)[نهاية]

View File

@ -3,9 +3,8 @@
---
// Test normal operation and RTL directions.
#set page(height: 3.25cm, width: 7.05cm, columns: 2)
#set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Serif")
#set columns(gutter: 30pt)
#set text("Noto Sans Arabic", "IBM Plex Serif")
#set par(lang: "ar")
#rect(fill: conifer, height: 8pt, width: 6pt) وتحفيز
العديد من التفاعلات الكيميائية. (DNA) من أهم الأحماض النووية التي تُشكِّل

View File

@ -2,37 +2,35 @@
---
// Test reordering with different top-level paragraph directions.
#let content = [Text טֶקסט]
#let content = par[Text טֶקסט]
#set text("IBM Plex Serif")
#par(lang: "he", content)
#par(lang: "de", content)
#text(lang: "he", content)
#text(lang: "de", content)
---
// Test that consecutive, embedded LTR runs stay LTR.
// Here, we have two runs: "A" and italic "B".
#let content = [أنت A#emph[B]مطرC]
#let content = par[أنت A#emph[B]مطرC]
#set text("IBM Plex Serif", "Noto Sans Arabic")
#par(lang: "ar", content)
#par(lang: "de", content)
#text(lang: "ar", content)
#text(lang: "de", content)
---
// Test that consecutive, embedded RTL runs stay RTL.
// Here, we have three runs: "גֶ", bold "שֶׁ", and "ם".
#let content = [Aגֶ#strong[שֶׁ]םB]
#let content = par[Aגֶ#strong[שֶׁ]םB]
#set text("IBM Plex Serif", "Noto Serif Hebrew")
#par(lang: "he", content)
#par(lang: "de", content)
#text(lang: "he", content)
#text(lang: "de", content)
---
// Test embedding up to level 4 with isolates.
#set text("IBM Plex Serif")
#set par(dir: rtl)
#set text(dir: rtl, "IBM Plex Serif")
א\u{2066}A\u{2067}Bב\u{2069}?
---
// Test hard line break (leads to two paragraphs in unicode-bidi).
#set text("Noto Sans Arabic", "IBM Plex Serif")
#set par(lang: "ar")
#set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Serif")
Life المطر هو الحياة \
الحياة تمطر is rain.
@ -44,13 +42,12 @@ Lריווח #h(1cm) R
---
// Test inline object.
#set text("IBM Plex Serif")
#set par(lang: "he")
#set text(lang: "he", "IBM Plex Serif")
קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים
---
// Test setting a vertical direction.
// Ref: false
// Error: 15-18 must be horizontal
#set par(dir: ttb)
// Error: 16-19 must be horizontal
#set text(dir: ttb)

View File

@ -1,23 +1,33 @@
// Test hyphenation.
---
// Hyphenate english.
#set page(width: 70pt)
#set par(lang: "en", hyphenate: true)
Warm welcomes to Typst.
// Test hyphenating english and greek.
#set text(hyphenate: true)
#set page(width: auto)
#grid(
columns: (70pt, 60pt),
text(lang: "en")[Warm welcomes to Typst.],
text(lang: "el")[διαμερίσματα. \ λατρευτός],
)
---
// Hyphenate greek.
#set page(width: 60pt)
#set par(lang: "el", hyphenate: true)
διαμερίσματα. \
λατρευτός
// Test disabling hyphenation for short passages.
#set text(lang: "en", hyphenate: true)
Welcome to wonderful experiences. \
Welcome to `wonderful` experiences. \
Welcome to #text(hyphenate: false)[wonderful] experiences. \
Welcome to wonde#text(hyphenate: false)[rf]ul experiences. \
// Test enabling hyphenation for short passages.
#set text(lang: "en", hyphenate: false)
Welcome to wonderful experiences. \
Welcome to wo#text(hyphenate: true)[nd]erful experiences. \
---
// Hyphenate between shape runs.
#set par(lang: "en", hyphenate: true)
#set page(width: 80pt)
#set text(lang: "en", hyphenate: true)
It's a #emph[Tree]beard.
---
@ -26,5 +36,5 @@ It's a #emph[Tree]beard.
// do that. The test passes if there's just one hyphenation between
// "net" and "works".
#set page(width: 70pt)
#set par(lang: "en", hyphenate: true)
#set text(lang: "en", hyphenate: true)
#h(6pt) networks, the rest.

View File

@ -19,8 +19,8 @@ starts a paragraph without indent.
Except if you have another paragraph in them.
#set text(8pt, "Noto Sans Arabic", "IBM Plex Sans")
#set par(lang: "ar", leading: 8pt)
#set text(8pt, lang: "ar", "Noto Sans Arabic", "IBM Plex Sans")
#set par(leading: 8pt)
= Arabic
دع النص يمطر عليك

View File

@ -1,8 +1,8 @@
---
#set page(width: 180pt)
#set text(lang: "en")
#set par(
lang: "en",
justify: true,
indent: 14pt,
spacing: 0pt,

View File

@ -1,6 +1,6 @@
#set page(width: auto, height: auto)
#set par(lang: "en", leading: 4pt, justify: true)
#set text(family: "Latin Modern Roman")
#set par(leading: 4pt, justify: true)
#set text(lang: "en", family: "Latin Modern Roman")
#let story = [
In olden times when wishing still helped one, there lived a king whose
@ -16,7 +16,7 @@
#let column(title, linebreaks, hyphenate) = {
rect(width: 132pt, fill: rgb("eee"))[
#strong(title)
#par(linebreaks: linebreaks, hyphenate: hyphenate, story)
#par(linebreaks: linebreaks, text(hyphenate: hyphenate, story))
]
}

View File

@ -23,8 +23,7 @@ A#box["]B
Book quotes are even smarter. \
---
#set par(lang: "ar")
#set text("Noto Sans Arabic", "IBM Plex Sans")
#set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Sans")
"المطر هو الحياة" \
المطر هو الحياة