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

View File

@ -66,6 +66,14 @@ impl StyleMap {
self.0.push(Entry::Recipe(Recipe::new::<T>(func, span))); 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. /// Make `self` the first link of the `tail` chain.
/// ///
/// The resulting style chain contains styles from `self` as well as /// 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 /// This trait is not intended to be implemented manually, but rather through
/// the `#[node]` proc-macro. /// 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 /// The unfolded type which this property is stored as in a style map. For
/// example, this is [`Toggle`](crate::geom::Length) for the /// example, this is [`Toggle`](crate::geom::Length) for the
/// [`STRONG`](TextNode::STRONG) property. /// [`STRONG`](TextNode::STRONG) property.
@ -680,6 +688,15 @@ impl<T> StyleVec<T> {
self.items.len() 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. /// Iterate over the contained items.
pub fn items(&self) -> std::slice::Iter<'_, T> { pub fn items(&self) -> std::slice::Iter<'_, T> {
self.items.iter() self.items.iter()

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ impl PageNode {
pub const HEIGHT: Smart<RawLength> = Smart::Custom(Paper::A4.height().into()); pub const HEIGHT: Smart<RawLength> = Smart::Custom(Paper::A4.height().into());
/// Whether the page is flipped into landscape orientation. /// Whether the page is flipped into landscape orientation.
pub const FLIPPED: bool = false; pub const FLIPPED: bool = false;
/// The left margin. /// The left margin.
pub const LEFT: Smart<Relative<RawLength>> = Smart::Auto; pub const LEFT: Smart<Relative<RawLength>> = Smart::Auto;
/// The right margin. /// The right margin.
@ -25,10 +26,12 @@ impl PageNode {
pub const TOP: Smart<Relative<RawLength>> = Smart::Auto; pub const TOP: Smart<Relative<RawLength>> = Smart::Auto;
/// The bottom margin. /// The bottom margin.
pub const BOTTOM: Smart<Relative<RawLength>> = Smart::Auto; 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. /// How many columns the page has.
pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); pub const COLUMNS: NonZeroUsize = NonZeroUsize::new(1).unwrap();
/// The page's background color.
pub const FILL: Option<Paint> = None;
/// The page's header. /// The page's header.
#[property(referenced)] #[property(referenced)]
pub const HEADER: Marginal = Marginal::None; 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); let upscale = (1.6 - 0.1 * level as f64).max(0.75);
TextSize(Em::new(upscale).into()) TextSize(Em::new(upscale).into())
}); });
/// Whether text in the heading is strengthend. /// Whether text in the heading is strengthend.
#[property(referenced)] #[property(referenced)]
pub const STRONG: Leveled<bool> = Leveled::Value(true); pub const STRONG: Leveled<bool> = Leveled::Value(true);
@ -34,12 +35,14 @@ impl HeadingNode {
/// Whether the heading is underlined. /// Whether the heading is underlined.
#[property(referenced)] #[property(referenced)]
pub const UNDERLINE: Leveled<bool> = Leveled::Value(false); pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
/// The extra padding above the heading. /// The extra padding above the heading.
#[property(referenced)] #[property(referenced)]
pub const ABOVE: Leveled<RawLength> = Leveled::Value(Length::zero().into()); pub const ABOVE: Leveled<RawLength> = Leveled::Value(Length::zero().into());
/// The extra padding below the heading. /// The extra padding below the heading.
#[property(referenced)] #[property(referenced)]
pub const BELOW: Leveled<RawLength> = Leveled::Value(Length::zero().into()); pub const BELOW: Leveled<RawLength> = Leveled::Value(Length::zero().into());
/// Whether the heading is block-level. /// Whether the heading is block-level.
#[property(referenced)] #[property(referenced)]
pub const BLOCK: Leveled<bool> = Leveled::Value(true); 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. /// How the list is labelled.
#[property(referenced)] #[property(referenced)]
pub const LABEL: Label = Label::Default; pub const LABEL: Label = Label::Default;
/// The spacing between the list items of a non-wide list. /// The spacing between the list items of a non-wide list.
#[property(resolve)] #[property(resolve)]
pub const SPACING: RawLength = RawLength::zero(); 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. /// The space between the label and the body of each item.
#[property(resolve)] #[property(resolve)]
pub const BODY_INDENT: RawLength = Em::new(0.5).into(); pub const BODY_INDENT: RawLength = Em::new(0.5).into();
/// The extra padding above the list. /// The extra padding above the list.
#[property(resolve)] #[property(resolve)]
pub const ABOVE: RawLength = RawLength::zero(); pub const ABOVE: RawLength = RawLength::zero();
@ -137,7 +139,7 @@ pub const UNORDERED: ListKind = 0;
/// Ordered list labelling style. /// Ordered list labelling style.
pub const ORDERED: ListKind = 1; 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)] #[derive(Debug, Clone, PartialEq, Hash)]
pub enum Label { pub enum Label {
/// The default labelling. /// The default labelling.

View File

@ -21,6 +21,7 @@ impl TableNode {
/// How to stroke the cells. /// How to stroke the cells.
#[property(resolve, fold)] #[property(resolve, fold)]
pub const STROKE: Option<RawStroke> = Some(RawStroke::default()); pub const STROKE: Option<RawStroke> = Some(RawStroke::default());
/// How much to pad the cells's content. /// How much to pad the cells's content.
pub const PADDING: Relative<RawLength> = Length::pt(5.0).into(); 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`. /// tables if `auto`.
#[property(shorthand, resolve, fold)] #[property(shorthand, resolve, fold)]
pub const STROKE: Smart<RawStroke> = Smart::Auto; pub const STROKE: Smart<RawStroke> = Smart::Auto;
/// Position of the line relative to the baseline, read from the font tables /// Position of the line relative to the baseline, read from the font tables
/// if `auto`. /// if `auto`.
#[property(resolve)] #[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. /// Amount that the line will be longer or shorter than its associated text.
#[property(resolve)] #[property(resolve)]
pub const EXTENT: RawLength = RawLength::zero(); pub const EXTENT: RawLength = RawLength::zero();
/// Whether the line skips sections in which it would collide /// Whether the line skips sections in which it would collide
/// with the glyphs. Does not apply to strikethrough. /// with the glyphs. Does not apply to strikethrough.
pub const EVADE: bool = true; pub const EVADE: bool = true;

View File

@ -61,6 +61,18 @@ impl TextNode {
/// The bottom end of the text bounding box. /// The bottom end of the text bounding box.
pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline); 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"). /// Whether to apply kerning ("kern").
pub const KERNING: bool = true; pub const KERNING: bool = true;
/// Whether small capital glyphs should be used. ("smcp") /// 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. /// A stylistic set in a font face.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct StylisticSet(u8); pub struct StylisticSet(u8);

View File

@ -1,10 +1,9 @@
use std::sync::Arc; use std::sync::Arc;
use either::Either;
use unicode_bidi::{BidiInfo, Level}; use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator; use xi_unicode::LineBreakIterator;
use super::{shape, ShapedText, TextNode}; use super::{shape, Lang, ShapedText, TextNode};
use crate::font::FontStore; use crate::font::FontStore;
use crate::library::layout::Spacing; use crate::library::layout::Spacing;
use crate::library::prelude::*; use crate::library::prelude::*;
@ -27,21 +26,6 @@ pub enum ParChild {
#[node] #[node]
impl ParNode { 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. /// The spacing between lines.
#[property(resolve)] #[property(resolve)]
pub const LEADING: RawLength = Em::new(0.65).into(); pub const LEADING: RawLength = Em::new(0.65).into();
@ -52,6 +36,15 @@ impl ParNode {
#[property(resolve)] #[property(resolve)]
pub const INDENT: RawLength = RawLength::zero(); 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> { fn construct(_: &mut Context, args: &mut Args) -> TypResult<Content> {
// The paragraph constructor is special: It doesn't create a paragraph // The paragraph constructor is special: It doesn't create a paragraph
// since that happens automatically through markup. Instead, it just // since that happens automatically through markup. Instead, it just
@ -59,45 +52,6 @@ impl ParNode {
// adjacent stuff and it styles the contained paragraphs. // adjacent stuff and it styles the contained paragraphs.
Ok(Content::Block(args.expect("body")?)) 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 { impl ParNode {
@ -147,7 +101,7 @@ impl Layout for ParNode {
let p = prepare(ctx, self, &text, regions, &styles)?; let p = prepare(ctx, self, &text, regions, &styles)?;
// Break the paragraph into lines. // 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. // Stack the lines into one frame per region.
Ok(stack(&lines, &ctx.fonts, regions, styles)) 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. /// How to determine line breaks in a paragraph.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Linebreaks { 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. /// A paragraph break.
pub struct ParbreakNode; pub struct ParbreakNode;
@ -233,17 +222,35 @@ type Range = std::ops::Range<usize>;
struct Preparation<'a> { struct Preparation<'a> {
/// Bidirectional text embedding levels for the paragraph. /// Bidirectional text embedding levels for the paragraph.
bidi: BidiInfo<'a>, bidi: BidiInfo<'a>,
/// The paragraph's children.
children: &'a StyleVec<ParChild>,
/// Spacing, separated text runs and layouted nodes. /// Spacing, separated text runs and layouted nodes.
items: Vec<ParItem<'a>>, items: Vec<ParItem<'a>>,
/// The ranges of the items in `bidi.text`. /// The ranges of the items in `bidi.text`.
ranges: Vec<Range>, 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`. /// 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() 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. /// A prepared item in a paragraph layout.
@ -258,6 +265,16 @@ enum ParItem<'a> {
Frame(Frame), 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 /// A layouted line, consisting of a sequence of layouted paragraph items that
/// are mostly borrowed from the preparation phase. This type enables you to /// 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 /// 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. // How many justifiable glyphs the line contains.
fn justifiables(&self) -> usize { fn justifiables(&self) -> usize {
let mut count = 0; let mut count = 0;
for item in self.items() { for shaped in self.items().filter_map(ParItem::text) {
if let ParItem::Text(shaped) = item { count += shaped.justifiables();
count += shaped.justifiables();
}
} }
count count
} }
@ -326,10 +341,8 @@ impl<'a> Line<'a> {
/// How much of the line is stretchable spaces. /// How much of the line is stretchable spaces.
fn stretch(&self) -> Length { fn stretch(&self) -> Length {
let mut stretch = Length::zero(); let mut stretch = Length::zero();
for item in self.items() { for shaped in self.items().filter_map(ParItem::text) {
if let ParItem::Text(shaped) = item { stretch += shaped.stretch();
stretch += shaped.stretch();
}
} }
stretch stretch
} }
@ -344,7 +357,7 @@ fn prepare<'a>(
regions: &Regions, regions: &Regions,
styles: &'a StyleChain, styles: &'a StyleChain,
) -> TypResult<Preparation<'a>> { ) -> 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::LTR => Some(Level::ltr()),
Dir::RTL => Some(Level::rtl()), Dir::RTL => Some(Level::rtl()),
_ => None, _ => None,
@ -358,7 +371,7 @@ fn prepare<'a>(
let styles = map.chain(styles); let styles = map.chain(styles);
match child { match child {
ParChild::Text(_) => { ParChild::Text(_) => {
// TODO: Also split by language and script. // TODO: Also split by language.
let mut cursor = range.start; let mut cursor = range.start;
for (level, count) in bidi.levels[range].group() { for (level, count) in bidi.levels[range].group() {
let start = cursor; 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. /// Find suitable linebreaks.
@ -410,22 +429,13 @@ fn linebreak<'a>(
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
fonts: &mut FontStore, fonts: &mut FontStore,
width: Length, width: Length,
styles: StyleChain,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
let breaks = styles.get(ParNode::LINEBREAKS).unwrap_or_else(|| { let breaker = match p.styles.get(ParNode::LINEBREAKS) {
if styles.get(ParNode::JUSTIFY) {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
});
let breaker = match breaks {
Linebreaks::Simple => linebreak_simple, Linebreaks::Simple => linebreak_simple,
Linebreaks::Optimized => linebreak_optimized, 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 /// 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>, p: &'a Preparation<'a>,
fonts: &mut FontStore, fonts: &mut FontStore,
width: Length, width: Length,
styles: StyleChain,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
let mut lines = vec![]; let mut lines = vec![];
let mut start = 0; let mut start = 0;
let mut last = None; 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. // Compute the line and its size.
let mut attempt = line(p, fonts, start .. end, mandatory, hyphen); let mut attempt = line(p, fonts, start .. end, mandatory, hyphen);
@ -496,7 +505,6 @@ fn linebreak_optimized<'a>(
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
fonts: &mut FontStore, fonts: &mut FontStore,
width: Length, width: Length,
styles: StyleChain,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
/// The cost of a line or paragraph layout. /// The cost of a line or paragraph layout.
type Cost = f64; type Cost = f64;
@ -515,8 +523,8 @@ fn linebreak_optimized<'a>(
const MIN_COST: Cost = -MAX_COST; const MIN_COST: Cost = -MAX_COST;
const MIN_RATIO: f64 = -0.15; const MIN_RATIO: f64 = -0.15;
let em = styles.get(TextNode::SIZE); let em = p.styles.get(TextNode::SIZE);
let justify = styles.get(ParNode::JUSTIFY); let justify = p.styles.get(ParNode::JUSTIFY);
// Dynamic programming table. // Dynamic programming table.
let mut active = 0; let mut active = 0;
@ -526,7 +534,7 @@ fn linebreak_optimized<'a>(
line: line(p, fonts, 0 .. 0, false, false), 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 k = table.len();
let eof = end == p.bidi.text.len(); let eof = end == p.bidi.text.len();
let mut best: Option<Entry> = None; 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 /// 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 /// (after `\n`) and whether a hyphen is required (when breaking inside of a
/// word). /// word).
fn breakpoints<'a>( fn breakpoints<'a>(p: &'a Preparation) -> impl Iterator<Item = (usize, bool, bool)> + 'a {
text: &'a str, Breakpoints {
styles: StyleChain, p,
) -> impl Iterator<Item = (usize, bool, bool)> + 'a { linebreaks: LineBreakIterator::new(p.bidi.text),
let mut lang = None; syllables: None,
if styles.get(ParNode::HYPHENATE).unwrap_or(styles.get(ParNode::JUSTIFY)) { offset: 0,
lang = styles suffix: 0,
.get(ParNode::LANG) end: 0,
.as_ref() mandatory: false,
.and_then(|iso| iso.as_bytes().try_into().ok()) hyphenate: p.get_shared(TextNode::HYPHENATE),
.and_then(hypher::Lang::from_iso); 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); /// The text language at the given offset.
let mut last = 0; 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 { let bytes = lang.as_str().as_bytes().try_into().ok()?;
Either::Left(breaks.flat_map(move |(end, mandatory)| { hypher::Lang::from_iso(bytes)
// 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)))
} }
} }
@ -664,11 +729,11 @@ fn line<'a>(
hyphen: bool, hyphen: bool,
) -> Line<'a> { ) -> Line<'a> {
// Find the items which bound the text range. // 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() { let first_idx = if range.is_empty() {
last_idx last_idx
} else { } else {
p.find(range.start).unwrap() p.find_idx(range.start).unwrap()
}; };
// Slice out the relevant items. // 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::highlighting::{FontStyle, Highlighter, Style, Theme, ThemeSet};
use syntect::parsing::SyntaxSet; use syntect::parsing::SyntaxSet;
use super::{FontFamily, TextNode, Toggle}; use super::{FontFamily, Hyphenate, TextNode, Toggle};
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::source::SourceId; use crate::source::SourceId;
use crate::syntax::{self, RedNode}; use crate::syntax::{self, RedNode};
@ -29,6 +29,7 @@ impl RawNode {
/// The raw text's font family. Just the normal text family if `none`. /// The raw text's font family. Just the normal text family if `none`.
#[property(referenced)] #[property(referenced)]
pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono")); pub const FAMILY: Smart<FontFamily> = Smart::Custom(FontFamily::new("IBM Plex Mono"));
/// The language to syntax-highlight in. /// The language to syntax-highlight in.
#[property(referenced)] #[property(referenced)]
pub const LANG: Option<EcoString> = None; pub const LANG: Option<EcoString> = None;
@ -97,6 +98,9 @@ impl Show for RawNode {
}; };
let mut map = StyleMap::new(); 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) { if let Smart::Custom(family) = styles.get(Self::FAMILY) {
map.set_family(family.clone(), styles); 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. // Test start and end alignment.
#rotate(-30deg, origin: end + horizon)[Hello] #rotate(-30deg, origin: end + horizon)[Hello]
#set par(lang: "de") #set text(lang: "de")
#align(start)[Start] #align(start)[Start]
#align(end)[Ende] #align(end)[Ende]
#set par(lang: "ar") #set text(lang: "ar")
#align(start)[يبدأ] #align(start)[يبدأ]
#align(end)[نهاية] #align(end)[نهاية]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
#set page(width: auto, height: auto) #set page(width: auto, height: auto)
#set par(lang: "en", leading: 4pt, justify: true) #set par(leading: 4pt, justify: true)
#set text(family: "Latin Modern Roman") #set text(lang: "en", family: "Latin Modern Roman")
#let story = [ #let story = [
In olden times when wishing still helped one, there lived a king whose In olden times when wishing still helped one, there lived a king whose
@ -16,7 +16,7 @@
#let column(title, linebreaks, hyphenate) = { #let column(title, linebreaks, hyphenate) = {
rect(width: 132pt, fill: rgb("eee"))[ rect(width: 132pt, fill: rgb("eee"))[
#strong(title) #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. \ Book quotes are even smarter. \
--- ---
#set par(lang: "ar") #set text(lang: "ar", "Noto Sans Arabic", "IBM Plex Sans")
#set text("Noto Sans Arabic", "IBM Plex Sans")
"المطر هو الحياة" \ "المطر هو الحياة" \
المطر هو الحياة المطر هو الحياة