mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
parent
29eb13ca62
commit
34fa8df044
@ -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(),
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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![];
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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 |
@ -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)[نهاية]
|
||||
|
||||
|
@ -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) من أهم الأحماض النووية التي تُشكِّل
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
دع النص يمطر عليك
|
||||
|
@ -1,8 +1,8 @@
|
||||
|
||||
---
|
||||
#set page(width: 180pt)
|
||||
#set text(lang: "en")
|
||||
#set par(
|
||||
lang: "en",
|
||||
justify: true,
|
||||
indent: 14pt,
|
||||
spacing: 0pt,
|
||||
|
@ -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))
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
"المطر هو الحياة" \
|
||||
المطر هو الحياة
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user