mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
parent
29eb13ca62
commit
34fa8df044
@ -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(),
|
||||||
|
@ -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()
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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![];
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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<'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])
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Preparation<'_> {
|
|
||||||
/// 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,22 +332,18 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let breaks = LineBreakIterator::new(text);
|
/// An iterator over the line break opportunities in a text.
|
||||||
let mut last = 0;
|
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>>,
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(lang) = lang {
|
impl Iterator for Breakpoints<'_> {
|
||||||
Either::Left(breaks.flat_map(move |(end, mandatory)| {
|
type Item = (usize, bool, bool);
|
||||||
// We don't want to confuse the hyphenator with trailing
|
|
||||||
// punctuation, so we trim it. And if that makes the word empty, we
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
// need to return the single breakpoint manually because hypher
|
// If we're currently in a hyphenated "word", process the next syllable.
|
||||||
// would eat it.
|
if let Some(syllable) = self.syllables.as_mut().and_then(Iterator::next) {
|
||||||
let word = &text[last .. end];
|
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());
|
let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic());
|
||||||
let suffix = last + trimmed.len();
|
if !trimmed.is_empty() {
|
||||||
let mut start = std::mem::replace(&mut last, end);
|
self.suffix = self.offset + trimmed.len();
|
||||||
if trimmed.is_empty() {
|
self.syllables = Some(hypher::hyphenate(trimmed, lang));
|
||||||
Either::Left([(end, mandatory, false)].into_iter())
|
return self.next();
|
||||||
} 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)))
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let bytes = lang.as_str().as_bytes().try_into().ok()?;
|
||||||
|
hypher::Lang::from_iso(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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.
|
||||||
|
@ -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 |
@ -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)[نهاية]
|
||||||
|
|
||||||
|
@ -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) من أهم الأحماض النووية التي تُشكِّل
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
دع النص يمطر عليك
|
دع النص يمطر عليك
|
||||||
|
@ -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,
|
||||||
|
@ -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))
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
|
||||||
"المطر هو الحياة" \
|
"المطر هو الحياة" \
|
||||||
المطر هو الحياة
|
المطر هو الحياة
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user