summaryrefslogtreecommitdiff
path: root/library/src/text
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-29 13:37:25 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-29 14:18:13 +0100
commit0efe669278a5e1c3f2985eba2f3360e91159c54a (patch)
tree502712857c48f0decb5e698257c0a96d358a436e /library/src/text
parent836692e73cff0356e409a9ba5b4887b86809d4ca (diff)
Reorganize library and tests
Diffstat (limited to 'library/src/text')
-rw-r--r--library/src/text/deco.rs4
-rw-r--r--library/src/text/link.rs66
-rw-r--r--library/src/text/misc.rs146
-rw-r--r--library/src/text/mod.rs179
-rw-r--r--library/src/text/par.rs1215
-rw-r--r--library/src/text/quotes.rs20
-rw-r--r--library/src/text/shaping.rs7
7 files changed, 179 insertions, 1458 deletions
diff --git a/library/src/text/deco.rs b/library/src/text/deco.rs
index 33c13e90..e81b219c 100644
--- a/library/src/text/deco.rs
+++ b/library/src/text/deco.rs
@@ -12,7 +12,7 @@ pub struct DecoNode<const L: DecoLine>(pub Content);
pub type UnderlineNode = DecoNode<UNDERLINE>;
/// Typeset stricken-through text.
-pub type StrikethroughNode = DecoNode<STRIKETHROUGH>;
+pub type StrikeNode = DecoNode<STRIKETHROUGH>;
/// Typeset overlined text.
pub type OverlineNode = DecoNode<OVERLINE>;
@@ -65,7 +65,7 @@ impl<const L: DecoLine> Show for DecoNode<L> {
///
/// For more details, see [`DecoNode`].
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub(super) struct Decoration {
+pub struct Decoration {
pub line: DecoLine,
pub stroke: PartialStroke<Abs>,
pub offset: Smart<Abs>,
diff --git a/library/src/text/link.rs b/library/src/text/link.rs
deleted file mode 100644
index 1b87def2..00000000
--- a/library/src/text/link.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-use super::TextNode;
-use crate::prelude::*;
-
-/// Link text and other elements to a destination.
-#[derive(Debug, Hash)]
-pub struct LinkNode {
- /// The destination the link points to.
- pub dest: Destination,
- /// How the link is represented.
- pub body: Content,
-}
-
-impl LinkNode {
- /// Create a link node from a URL with its bare text.
- pub fn from_url(url: EcoString) -> Self {
- let mut text = url.as_str();
- for prefix in ["mailto:", "tel:"] {
- text = text.trim_start_matches(prefix);
- }
- let shorter = text.len() < url.len();
- let body = TextNode::packed(if shorter { text.into() } else { url.clone() });
- Self { dest: Destination::Url(url), body }
- }
-}
-
-#[node(Show, Finalize)]
-impl LinkNode {
- /// A destination the text should be linked to.
- #[property(skip, referenced)]
- pub(crate) const DEST: Option<Destination> = None;
-
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- let dest = args.expect::<Destination>("destination")?;
- Ok(match dest {
- Destination::Url(url) => match args.eat()? {
- Some(body) => Self { dest: Destination::Url(url), body },
- None => Self::from_url(url),
- },
- Destination::Internal(_) => Self { dest, body: args.expect("body")? },
- }
- .pack())
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "url" => Some(match &self.dest {
- Destination::Url(url) => Value::Str(url.clone().into()),
- Destination::Internal(loc) => Value::Dict(loc.encode()),
- }),
- "body" => Some(Value::Content(self.body.clone())),
- _ => None,
- }
- }
-}
-
-impl Show for LinkNode {
- fn show(&self, _: Tracked<dyn World>, _: StyleChain) -> Content {
- self.body.clone()
- }
-}
-
-impl Finalize for LinkNode {
- fn finalize(&self, realized: Content) -> Content {
- realized.styled(Self::DEST, Some(self.dest.clone()))
- }
-}
diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs
new file mode 100644
index 00000000..15ef9a63
--- /dev/null
+++ b/library/src/text/misc.rs
@@ -0,0 +1,146 @@
+use super::TextNode;
+use crate::prelude::*;
+
+/// A text space.
+#[derive(Debug, Hash)]
+pub struct SpaceNode;
+
+#[node(Unlabellable, Behave)]
+impl SpaceNode {
+ fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
+ Ok(Self.pack())
+ }
+}
+
+impl Unlabellable for SpaceNode {}
+
+impl Behave for SpaceNode {
+ fn behaviour(&self) -> Behaviour {
+ Behaviour::Weak(2)
+ }
+}
+
+/// A line break.
+#[derive(Debug, Hash)]
+pub struct LinebreakNode {
+ pub justify: bool,
+}
+
+#[node(Behave)]
+impl LinebreakNode {
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ let justify = args.named("justify")?.unwrap_or(false);
+ Ok(Self { justify }.pack())
+ }
+}
+
+impl Behave for LinebreakNode {
+ fn behaviour(&self) -> Behaviour {
+ Behaviour::Destructive
+ }
+}
+
+/// Strong content, rendered in boldface by default.
+#[derive(Debug, Hash)]
+pub struct StrongNode(pub Content);
+
+#[node(Show)]
+impl StrongNode {
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self(args.expect("body")?).pack())
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "body" => Some(Value::Content(self.0.clone())),
+ _ => None,
+ }
+ }
+}
+
+impl Show for StrongNode {
+ fn show(&self, _: Tracked<dyn World>, _: StyleChain) -> Content {
+ self.0.clone().styled(TextNode::BOLD, Toggle)
+ }
+}
+
+/// Emphasized content, rendered with an italic font by default.
+#[derive(Debug, Hash)]
+pub struct EmphNode(pub Content);
+
+#[node(Show)]
+impl EmphNode {
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ Ok(Self(args.expect("body")?).pack())
+ }
+
+ fn field(&self, name: &str) -> Option<Value> {
+ match name {
+ "body" => Some(Value::Content(self.0.clone())),
+ _ => None,
+ }
+ }
+}
+
+impl Show for EmphNode {
+ fn show(&self, _: Tracked<dyn World>, _: StyleChain) -> Content {
+ self.0.clone().styled(TextNode::ITALIC, Toggle)
+ }
+}
+
+/// A toggle that turns on and off alternatingly if folded.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub struct Toggle;
+
+impl Fold for Toggle {
+ type Output = bool;
+
+ fn fold(self, outer: Self::Output) -> Self::Output {
+ !outer
+ }
+}
+
+/// Convert a string or content to lowercase.
+pub fn lower(_: &Vm, args: &mut Args) -> SourceResult<Value> {
+ case(Case::Lower, args)
+}
+
+/// Convert a string or content to uppercase.
+pub fn upper(_: &Vm, args: &mut Args) -> SourceResult<Value> {
+ case(Case::Upper, args)
+}
+
+/// Change the case of text.
+fn case(case: Case, args: &mut Args) -> SourceResult<Value> {
+ let Spanned { v, span } = args.expect("string or content")?;
+ Ok(match v {
+ Value::Str(v) => Value::Str(case.apply(&v).into()),
+ Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))),
+ v => bail!(span, "expected string or content, found {}", v.type_name()),
+ })
+}
+
+/// A case transformation on text.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Case {
+ /// Everything is lowercased.
+ Lower,
+ /// Everything is uppercased.
+ Upper,
+}
+
+impl Case {
+ /// Apply the case to a string.
+ pub fn apply(self, text: &str) -> String {
+ match self {
+ Self::Lower => text.to_lowercase(),
+ Self::Upper => text.to_uppercase(),
+ }
+ }
+}
+
+/// Display text in small capitals.
+pub fn smallcaps(_: &Vm, args: &mut Args) -> SourceResult<Value> {
+ let body: Content = args.expect("content")?;
+ Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true)))
+}
diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs
index 3fd30e9e..c9298fee 100644
--- a/library/src/text/mod.rs
+++ b/library/src/text/mod.rs
@@ -1,16 +1,15 @@
-//! Text handling and paragraph layout.
+//! Text handling.
mod deco;
-mod link;
-mod par;
+mod misc;
mod quotes;
mod raw;
mod shaping;
mod shift;
pub use self::deco::*;
-pub use self::link::*;
-pub use self::par::*;
+pub use self::misc::*;
+pub use self::quotes::*;
pub use self::raw::*;
pub use self::shaping::*;
pub use self::shift::*;
@@ -21,7 +20,7 @@ use rustybuzz::Tag;
use typst::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
use typst::util::EcoString;
-use self::quotes::*;
+use crate::layout::ParNode;
use crate::prelude::*;
/// A single run of text with the same style.
@@ -114,19 +113,19 @@ impl TextNode {
/// Whether the font weight should be increased by 300.
#[property(skip, fold)]
- const BOLD: Toggle = false;
+ pub const BOLD: Toggle = false;
/// Whether the font style should be inverted.
#[property(skip, fold)]
- const ITALIC: Toggle = false;
+ pub const ITALIC: Toggle = false;
/// A case transformation that should be applied to the text.
#[property(skip)]
- const CASE: Option<Case> = None;
+ pub const CASE: Option<Case> = None;
/// Whether small capital glyphs should be used. ("smcp")
#[property(skip)]
- const SMALLCAPS: bool = false;
+ pub const SMALLCAPS: bool = false;
/// Decorative lines.
#[property(skip, fold)]
- const DECO: Decoration = vec![];
+ pub const DECO: Decoration = vec![];
fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
// The text constructor is special: It doesn't create a text node.
@@ -409,161 +408,3 @@ impl Fold for FontFeatures {
self
}
}
-
-/// A text space.
-#[derive(Debug, Hash)]
-pub struct SpaceNode;
-
-#[node(Unlabellable, Behave)]
-impl SpaceNode {
- fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
- Ok(Self.pack())
- }
-}
-
-impl Unlabellable for SpaceNode {}
-
-impl Behave for SpaceNode {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Weak(2)
- }
-}
-
-/// A line break.
-#[derive(Debug, Hash)]
-pub struct LinebreakNode {
- pub justify: bool,
-}
-
-#[node(Behave)]
-impl LinebreakNode {
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- let justify = args.named("justify")?.unwrap_or(false);
- Ok(Self { justify }.pack())
- }
-}
-
-impl Behave for LinebreakNode {
- fn behaviour(&self) -> Behaviour {
- Behaviour::Destructive
- }
-}
-
-/// A smart quote.
-#[derive(Debug, Hash)]
-pub struct SmartQuoteNode {
- pub double: bool,
-}
-
-#[node]
-impl SmartQuoteNode {
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- let double = args.named("double")?.unwrap_or(true);
- Ok(Self { double }.pack())
- }
-}
-
-/// Convert a string or content to lowercase.
-pub fn lower(_: &Vm, args: &mut Args) -> SourceResult<Value> {
- case(Case::Lower, args)
-}
-
-/// Convert a string or content to uppercase.
-pub fn upper(_: &Vm, args: &mut Args) -> SourceResult<Value> {
- case(Case::Upper, args)
-}
-
-/// Change the case of text.
-fn case(case: Case, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v, span } = args.expect("string or content")?;
- Ok(match v {
- Value::Str(v) => Value::Str(case.apply(&v).into()),
- Value::Content(v) => Value::Content(v.styled(TextNode::CASE, Some(case))),
- v => bail!(span, "expected string or content, found {}", v.type_name()),
- })
-}
-
-/// A case transformation on text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Case {
- /// Everything is uppercased.
- Upper,
- /// Everything is lowercased.
- Lower,
-}
-
-impl Case {
- /// Apply the case to a string.
- pub fn apply(self, text: &str) -> String {
- match self {
- Self::Upper => text.to_uppercase(),
- Self::Lower => text.to_lowercase(),
- }
- }
-}
-
-/// Display text in small capitals.
-pub fn smallcaps(_: &Vm, args: &mut Args) -> SourceResult<Value> {
- let body: Content = args.expect("content")?;
- Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true)))
-}
-
-/// Strong content, rendered in boldface by default.
-#[derive(Debug, Hash)]
-pub struct StrongNode(pub Content);
-
-#[node(Show)]
-impl StrongNode {
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "body" => Some(Value::Content(self.0.clone())),
- _ => None,
- }
- }
-}
-
-impl Show for StrongNode {
- fn show(&self, _: Tracked<dyn World>, _: StyleChain) -> Content {
- self.0.clone().styled(TextNode::BOLD, Toggle)
- }
-}
-
-/// Emphasized content, rendered with an italic font by default.
-#[derive(Debug, Hash)]
-pub struct EmphNode(pub Content);
-
-#[node(Show)]
-impl EmphNode {
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "body" => Some(Value::Content(self.0.clone())),
- _ => None,
- }
- }
-}
-
-impl Show for EmphNode {
- fn show(&self, _: Tracked<dyn World>, _: StyleChain) -> Content {
- self.0.clone().styled(TextNode::ITALIC, Toggle)
- }
-}
-
-/// A toggle that turns on and off alternatingly if folded.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Toggle;
-
-impl Fold for Toggle {
- type Output = bool;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- !outer
- }
-}
diff --git a/library/src/text/par.rs b/library/src/text/par.rs
deleted file mode 100644
index 3c722d84..00000000
--- a/library/src/text/par.rs
+++ /dev/null
@@ -1,1215 +0,0 @@
-use unicode_bidi::{BidiInfo, Level as BidiLevel};
-use unicode_script::{Script, UnicodeScript};
-use xi_unicode::LineBreakIterator;
-
-use typst::model::Key;
-
-use super::{
- shape, Lang, LinebreakNode, Quoter, Quotes, ShapedText, SmartQuoteNode, SpaceNode,
- TextNode,
-};
-use crate::layout::{HNode, Spacing};
-use crate::prelude::*;
-
-/// Arrange text, spacing and inline-level nodes into a paragraph.
-#[derive(Hash)]
-pub struct ParNode(pub StyleVec<Content>);
-
-#[node]
-impl ParNode {
- /// The indent the first line of a consecutive paragraph should have.
- #[property(resolve)]
- pub const INDENT: Length = Length::zero();
- /// The spacing between lines.
- #[property(resolve)]
- pub const LEADING: Length = Em::new(0.65).into();
- /// How to align text and inline objects in their line.
- #[property(resolve)]
- pub const ALIGN: HorizontalAlign = HorizontalAlign(GenAlign::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;
-
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- // The paragraph constructor is special: It doesn't create a paragraph
- // node. Instead, it just ensures that the passed content lives in a
- // separate paragraph and styles it.
- Ok(Content::sequence(vec![
- ParbreakNode.pack(),
- args.expect("body")?,
- ParbreakNode.pack(),
- ]))
- }
-}
-
-impl ParNode {
- /// Layout the paragraph into a collection of lines.
- #[comemo::memoize]
- pub fn layout(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- regions: &Regions,
- consecutive: bool,
- ) -> SourceResult<Fragment> {
- // Collect all text into one string for BiDi analysis.
- let (text, segments) = collect(self, &styles, consecutive);
-
- // Perform BiDi analysis and then prepare paragraph layout by building a
- // representation on which we can do line breaking without layouting
- // each and every line from scratch.
- let p = prepare(world, self, &text, segments, regions, styles)?;
-
- // Break the paragraph into lines.
- let lines = linebreak(&p, regions.first.x);
-
- // Stack the lines into one frame per region.
- finalize(&p, &lines, regions)
- }
-}
-
-impl Debug for ParNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_str("Par ")?;
- self.0.fmt(f)
- }
-}
-
-/// A horizontal alignment.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct HorizontalAlign(pub GenAlign);
-
-castable! {
- HorizontalAlign,
- Expected: "alignment",
- @align: GenAlign => match align.axis() {
- Axis::X => Self(*align),
- Axis::Y => 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 {
- /// Determine the linebreaks in a simple first-fit style.
- Simple,
- /// Optimize the linebreaks for the whole paragraph.
- Optimized,
-}
-
-castable! {
- Linebreaks,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "simple" => Self::Simple,
- "optimized" => Self::Optimized,
- _ => Err(r#"expected "simple" or "optimized""#)?,
- },
-}
-
-/// A paragraph break.
-#[derive(Debug, Hash)]
-pub struct ParbreakNode;
-
-#[node(Unlabellable)]
-impl ParbreakNode {
- fn construct(_: &Vm, _: &mut Args) -> SourceResult<Content> {
- Ok(Self.pack())
- }
-}
-
-impl Unlabellable for ParbreakNode {}
-
-/// Repeats content to fill a line.
-#[derive(Debug, Hash)]
-pub struct RepeatNode(pub Content);
-
-#[node(Layout, Inline)]
-impl RepeatNode {
- fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl Layout for RepeatNode {
- fn layout(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- regions: &Regions,
- ) -> SourceResult<Fragment> {
- self.0.layout(world, styles, regions)
- }
-}
-
-impl Inline for RepeatNode {}
-
-/// Range of a substring of text.
-type Range = std::ops::Range<usize>;
-
-// The characters by which spacing, inline content and pins are replaced in the
-// paragraph's full text.
-const SPACING_REPLACE: char = ' '; // Space
-const NODE_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
-
-/// A paragraph representation in which children are already layouted and text
-/// is already preshaped.
-///
-/// In many cases, we can directly reuse these results when constructing a line.
-/// Only when a line break falls onto a text index that is not safe-to-break per
-/// rustybuzz, we have to reshape that portion.
-struct Preparation<'a> {
- /// The compilation environment.
- world: Tracked<'a, dyn World>,
- /// Bidirectional text embedding levels for the paragraph.
- bidi: BidiInfo<'a>,
- /// Text runs, spacing and layouted nodes.
- items: Vec<Item<'a>>,
- /// The styles shared by all children.
- styles: StyleChain<'a>,
- /// 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<Lang>,
- /// The paragraph's resolved alignment.
- align: Align,
- /// Whether to justify the paragraph.
- justify: bool,
-}
-
-impl<'a> Preparation<'a> {
- /// Find the item that contains the given `text_offset`.
- fn find(&self, text_offset: usize) -> Option<&Item<'a>> {
- let mut cursor = 0;
- for item in &self.items {
- let end = cursor + item.len();
- if (cursor..end).contains(&text_offset) {
- return Some(item);
- }
- cursor = end;
- }
- None
- }
-
- /// Return the items that intersect the given `text_range`.
- ///
- /// Returns the expanded range around the items and the items.
- fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) {
- let mut cursor = 0;
- let mut start = 0;
- let mut end = 0;
- let mut expanded = text_range.clone();
-
- for (i, item) in self.items.iter().enumerate() {
- if cursor <= text_range.start {
- start = i;
- expanded.start = cursor;
- }
-
- let len = item.len();
- if cursor < text_range.end || cursor + len <= text_range.end {
- end = i + 1;
- expanded.end = cursor + len;
- } else {
- break;
- }
-
- cursor += len;
- }
-
- (expanded, &self.items[start..end])
- }
-}
-
-/// A segment of one or multiple collapsed children.
-#[derive(Debug, Copy, Clone)]
-enum Segment<'a> {
- /// One or multiple collapsed text or text-equivalent children. Stores how
- /// long the segment is (in bytes of the full text string).
- Text(usize),
- /// Horizontal spacing between other segments.
- Spacing(Spacing),
- /// Arbitrary inline-level content.
- Inline(&'a Content),
-}
-
-impl Segment<'_> {
- /// The text length of the item.
- fn len(&self) -> usize {
- match *self {
- Self::Text(len) => len,
- Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
- Self::Inline(_) => NODE_REPLACE.len_utf8(),
- }
- }
-}
-
-/// A prepared item in a paragraph layout.
-#[derive(Debug)]
-enum Item<'a> {
- /// A shaped text run with consistent style and direction.
- Text(ShapedText<'a>),
- /// Absolute spacing between other items.
- Absolute(Abs),
- /// Fractional spacing between other items.
- Fractional(Fr),
- /// Layouted inline-level content.
- Frame(Frame),
- /// A repeating node that fills the remaining space in a line.
- Repeat(&'a RepeatNode, StyleChain<'a>),
-}
-
-impl<'a> Item<'a> {
- /// If this a text item, return it.
- fn text(&self) -> Option<&ShapedText<'a>> {
- match self {
- Self::Text(shaped) => Some(shaped),
- _ => None,
- }
- }
-
- /// The text length of the item.
- fn len(&self) -> usize {
- match self {
- Self::Text(shaped) => shaped.text.len(),
- Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(),
- Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(),
- }
- }
-
- /// The natural layouted width of the item.
- fn width(&self) -> Abs {
- match self {
- Self::Text(shaped) => shaped.width,
- Self::Absolute(v) => *v,
- Self::Frame(frame) => frame.width(),
- Self::Fractional(_) | Self::Repeat(_, _) => Abs::zero(),
- }
- }
-}
-
-/// 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
-/// line's frame.
-///
-/// At most two paragraph items must be created individually for this line: The
-/// first and last one since they may be broken apart by the start or end of the
-/// line, respectively. But even those can partially reuse previous results when
-/// the break index is safe-to-break per rustybuzz.
-struct Line<'a> {
- /// Bidi information about the paragraph.
- bidi: &'a BidiInfo<'a>,
- /// The trimmed range the line spans in the paragraph.
- trimmed: Range,
- /// The untrimmed end where the line ends.
- end: usize,
- /// A reshaped text item if the line sliced up a text item at the start.
- first: Option<Item<'a>>,
- /// Inner items which don't need to be reprocessed.
- inner: &'a [Item<'a>],
- /// A reshaped text item if the line sliced up a text item at the end. If
- /// there is only one text item, this takes precedence over `first`.
- last: Option<Item<'a>>,
- /// The width of the line.
- width: Abs,
- /// Whether the line should be justified.
- justify: bool,
- /// Whether the line ends with a hyphen or dash, either naturally or through
- /// hyphenation.
- dash: bool,
-}
-
-impl<'a> Line<'a> {
- /// Iterate over the line's items.
- fn items(&self) -> impl Iterator<Item = &Item<'a>> {
- self.first.iter().chain(self.inner).chain(&self.last)
- }
-
- /// Return items that intersect the given `text_range`.
- fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> {
- let mut cursor = self.trimmed.start;
- let mut start = 0;
- let mut end = 0;
-
- for (i, item) in self.items().enumerate() {
- if cursor <= text_range.start {
- start = i;
- }
-
- let len = item.len();
- if cursor < text_range.end || cursor + len <= text_range.end {
- end = i + 1;
- } else {
- break;
- }
-
- cursor += len;
- }
-
- self.items().skip(start).take(end - start)
- }
-
- /// How many justifiable glyphs the line contains.
- fn justifiables(&self) -> usize {
- let mut count = 0;
- for shaped in self.items().filter_map(Item::text) {
- count += shaped.justifiables();
- }
- count
- }
-
- /// How much of the line is stretchable spaces.
- fn stretch(&self) -> Abs {
- let mut stretch = Abs::zero();
- for shaped in self.items().filter_map(Item::text) {
- stretch += shaped.stretch();
- }
- stretch
- }
-
- /// The sum of fractions in the line.
- fn fr(&self) -> Fr {
- self.items()
- .filter_map(|item| match item {
- Item::Fractional(fr) => Some(*fr),
- Item::Repeat(_, _) => Some(Fr::one()),
- _ => None,
- })
- .sum()
- }
-}
-
-/// Collect all text of the paragraph into one string. This also performs
-/// string-level preprocessing like case transformations.
-fn collect<'a>(
- par: &'a ParNode,
- styles: &'a StyleChain<'a>,
- consecutive: bool,
-) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) {
- let mut full = String::new();
- let mut quoter = Quoter::new();
- let mut segments = vec![];
- let mut iter = par.0.iter().peekable();
-
- if consecutive {
- let indent = styles.get(ParNode::INDENT);
- if !indent.is_zero()
- && par
- .0
- .items()
- .find_map(|child| {
- if child.is::<TextNode>() || child.is::<SmartQuoteNode>() {
- Some(true)
- } else if child.has::<dyn Inline>() {
- Some(false)
- } else {
- None
- }
- })
- .unwrap_or_default()
- {
- full.push(SPACING_REPLACE);
- segments.push((Segment::Spacing(indent.into()), *styles));
- }
- }
-
- while let Some((child, map)) = iter.next() {
- let styles = styles.chain(map);
- let segment = if child.is::<SpaceNode>() {
- full.push(' ');
- Segment::Text(1)
- } else if let Some(node) = child.to::<TextNode>() {
- let prev = full.len();
- if let Some(case) = styles.get(TextNode::CASE) {
- full.push_str(&case.apply(&node.0));
- } else {
- full.push_str(&node.0);
- }
- Segment::Text(full.len() - prev)
- } else if let Some(node) = child.to::<LinebreakNode>() {
- let c = if node.justify { '\u{2028}' } else { '\n' };
- full.push(c);
- Segment::Text(c.len_utf8())
- } else if let Some(node) = child.to::<SmartQuoteNode>() {
- let prev = full.len();
- if styles.get(TextNode::SMART_QUOTES) {
- let lang = styles.get(TextNode::LANG);
- let region = styles.get(TextNode::REGION);
- let quotes = Quotes::from_lang(lang, region);
- let peeked = iter.peek().and_then(|(child, _)| {
- if let Some(node) = child.to::<TextNode>() {
- node.0.chars().next()
- } else if child.is::<SmartQuoteNode>() {
- Some('"')
- } else if child.is::<SpaceNode>() || child.is::<HNode>() {
- Some(SPACING_REPLACE)
- } else {
- Some(NODE_REPLACE)
- }
- });
-
- full.push_str(quoter.quote(&quotes, node.double, peeked));
- } else {
- full.push(if node.double { '"' } else { '\'' });
- }
- Segment::Text(full.len() - prev)
- } else if let Some(&node) = child.to::<HNode>() {
- full.push(SPACING_REPLACE);
- Segment::Spacing(node.amount)
- } else if child.has::<dyn Inline>() {
- full.push(NODE_REPLACE);
- Segment::Inline(child)
- } else {
- panic!("unexpected par child: {child:?}");
- };
-
- if let Some(last) = full.chars().last() {
- quoter.last(last);
- }
-
- if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) =
- (segments.last_mut(), segment)
- {
- if *last_styles == styles {
- *last_len += len;
- continue;
- }
- }
-
- segments.push((segment, styles));
- }
-
- (full, segments)
-}
-
-/// Prepare paragraph layout by shaping the whole paragraph and layouting all
-/// contained inline-level content.
-fn prepare<'a>(
- world: Tracked<'a, dyn World>,
- par: &'a ParNode,
- text: &'a str,
- segments: Vec<(Segment<'a>, StyleChain<'a>)>,
- regions: &Regions,
- styles: StyleChain<'a>,
-) -> SourceResult<Preparation<'a>> {
- let bidi = BidiInfo::new(
- text,
- match styles.get(TextNode::DIR) {
- Dir::LTR => Some(BidiLevel::ltr()),
- Dir::RTL => Some(BidiLevel::rtl()),
- _ => None,
- },
- );
-
- let mut cursor = 0;
- let mut items = vec![];
-
- // Shape / layout the children and collect them into items.
- for (segment, styles) in segments {
- let end = cursor + segment.len();
- match segment {
- Segment::Text(_) => {
- shape_range(&mut items, world, &bidi, cursor..end, styles);
- }
- Segment::Spacing(spacing) => match spacing {
- Spacing::Relative(v) => {
- let resolved = v.resolve(styles).relative_to(regions.base.x);
- items.push(Item::Absolute(resolved));
- }
- Spacing::Fractional(v) => {
- items.push(Item::Fractional(v));
- }
- },
- Segment::Inline(inline) => {
- if let Some(repeat) = inline.to::<RepeatNode>() {
- items.push(Item::Repeat(repeat, styles));
- } else {
- let size = Size::new(regions.first.x, regions.base.y);
- let pod = Regions::one(size, regions.base, Axes::splat(false));
- let mut frame = inline.layout(world, styles, &pod)?.into_frame();
- frame.translate(Point::with_y(styles.get(TextNode::BASELINE)));
- items.push(Item::Frame(frame));
- }
- }
- }
-
- cursor = end;
- }
-
- Ok(Preparation {
- world,
- bidi,
- items,
- styles,
- hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE),
- lang: shared_get(styles, &par.0, TextNode::LANG),
- align: styles.get(ParNode::ALIGN),
- justify: styles.get(ParNode::JUSTIFY),
- })
-}
-
-/// Group a range of text by BiDi level and script, shape the runs and generate
-/// items for them.
-fn shape_range<'a>(
- items: &mut Vec<Item<'a>>,
- world: Tracked<dyn World>,
- bidi: &BidiInfo<'a>,
- range: Range,
- styles: StyleChain<'a>,
-) {
- let mut process = |text, level: BidiLevel| {
- let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
- let shaped = shape(world, text, styles, dir);
- items.push(Item::Text(shaped));
- };
-
- let mut prev_level = BidiLevel::ltr();
- let mut prev_script = Script::Unknown;
- let mut cursor = range.start;
-
- // Group by embedding level and script.
- for i in cursor..range.end {
- if !bidi.text.is_char_boundary(i) {
- continue;
- }
-
- let level = bidi.levels[i];
- let script =
- bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script());
-
- if level != prev_level || !is_compatible(script, prev_script) {
- if cursor < i {
- process(&bidi.text[cursor..i], prev_level);
- }
- cursor = i;
- prev_level = level;
- prev_script = script;
- } else if is_generic_script(prev_script) {
- prev_script = script;
- }
- }
-
- process(&bidi.text[cursor..range.end], prev_level);
-}
-
-/// Whether this is not a specific script.
-fn is_generic_script(script: Script) -> bool {
- matches!(script, Script::Unknown | Script::Common | Script::Inherited)
-}
-
-/// Whether these script can be part of the same shape run.
-fn is_compatible(a: Script, b: Script) -> bool {
- is_generic_script(a) || is_generic_script(b) || a == b
-}
-
-/// Get a style property, but only if it is the same for all children of the
-/// paragraph.
-fn shared_get<'a, K: Key>(
- styles: StyleChain<'a>,
- children: &StyleVec<Content>,
- key: K,
-) -> Option<K::Output<'a>> {
- children
- .styles()
- .all(|map| !map.contains(key))
- .then(|| styles.get(key))
-}
-
-/// Find suitable linebreaks.
-fn linebreak<'a>(p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- let linebreaks = p.styles.get(ParNode::LINEBREAKS).unwrap_or_else(|| {
- if p.styles.get(ParNode::JUSTIFY) {
- Linebreaks::Optimized
- } else {
- Linebreaks::Simple
- }
- });
-
- match linebreaks {
- Linebreaks::Simple => linebreak_simple(p, width),
- Linebreaks::Optimized => linebreak_optimized(p, width),
- }
-}
-
-/// Perform line breaking in simple first-fit style. This means that we build
-/// lines greedily, always taking the longest possible line. This may lead to
-/// very unbalanced line, but is fast and simple.
-fn linebreak_simple<'a>(p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- let mut lines = vec![];
- let mut start = 0;
- let mut last = None;
-
- for (end, mandatory, hyphen) in breakpoints(p) {
- // Compute the line and its size.
- let mut attempt = line(p, start..end, mandatory, hyphen);
-
- // If the line doesn't fit anymore, we push the last fitting attempt
- // into the stack and rebuild the line from the attempt's end. The
- // resulting line cannot be broken up further.
- if !width.fits(attempt.width) {
- if let Some((last_attempt, last_end)) = last.take() {
- lines.push(last_attempt);
- start = last_end;
- attempt = line(p, start..end, mandatory, hyphen);
- }
- }
-
- // Finish the current line if there is a mandatory line break (i.e.
- // due to "\n") or if the line doesn't fit horizontally already
- // since then no shorter line will be possible.
- if mandatory || !width.fits(attempt.width) {
- lines.push(attempt);
- start = end;
- last = None;
- } else {
- last = Some((attempt, end));
- }
- }
-
- if let Some((line, _)) = last {
- lines.push(line);
- }
-
- lines
-}
-
-/// Perform line breaking in optimized Knuth-Plass style. Here, we use more
-/// context to determine the line breaks than in the simple first-fit style. For
-/// example, we might choose to cut a line short even though there is still a
-/// bit of space to improve the fit of one of the following lines. The
-/// Knuth-Plass algorithm is based on the idea of "cost". A line which has a
-/// very tight or very loose fit has a higher cost than one that is just right.
-/// Ending a line with a hyphen incurs extra cost and endings two successive
-/// lines with hyphens even more.
-///
-/// To find the layout with the minimal total cost the algorithm uses dynamic
-/// programming: For each possible breakpoint it determines the optimal
-/// paragraph layout _up to that point_. It walks over all possible start points
-/// for a line ending at that point and finds the one for which the cost of the
-/// line plus the cost of the optimal paragraph up to the start point (already
-/// computed and stored in dynamic programming table) is minimal. The final
-/// result is simply the layout determined for the last breakpoint at the end of
-/// text.
-fn linebreak_optimized<'a>(p: &'a Preparation<'a>, width: Abs) -> Vec<Line<'a>> {
- /// The cost of a line or paragraph layout.
- type Cost = f64;
-
- /// An entry in the dynamic programming table.
- struct Entry<'a> {
- pred: usize,
- total: Cost,
- line: Line<'a>,
- }
-
- // Cost parameters.
- const HYPH_COST: Cost = 0.5;
- const CONSECUTIVE_DASH_COST: Cost = 30.0;
- const MAX_COST: Cost = 1_000_000.0;
- const MIN_COST: Cost = -MAX_COST;
- const MIN_RATIO: f64 = -0.15;
-
- // Dynamic programming table.
- let mut active = 0;
- let mut table = vec![Entry {
- pred: 0,
- total: 0.0,
- line: line(p, 0..0, false, false),
- }];
-
- let em = p.styles.get(TextNode::SIZE);
-
- for (end, mandatory, hyphen) in breakpoints(p) {
- let k = table.len();
- let eof = end == p.bidi.text.len();
- let mut best: Option<Entry> = None;
-
- // Find the optimal predecessor.
- for (i, pred) in table.iter_mut().enumerate().skip(active) {
- // Layout the line.
- let start = pred.line.end;
- let attempt = line(p, start..end, mandatory, hyphen);
-
- // Determine how much the line's spaces would need to be stretched
- // to make it the desired width.
- let delta = width - attempt.width;
- let mut ratio = delta / attempt.stretch();
- if ratio.is_infinite() {
- ratio = delta / (em / 2.0);
- }
-
- // At some point, it doesn't matter any more.
- ratio = ratio.min(10.0);
-
- // Determine the cost of the line.
- let min_ratio = if attempt.justify { MIN_RATIO } else { 0.0 };
- let mut cost = if ratio < min_ratio {
- // The line is overfull. This is the case if
- // - justification is on, but we'd need to shrink too much
- // - justification is off and the line just doesn't fit
- // Since any longer line will also be overfull, we can deactive
- // this breakpoint.
- active = i + 1;
- MAX_COST
- } else if mandatory || eof {
- // This is a mandatory break and the line is not overfull, so it
- // has minimum cost. All breakpoints before this one become
- // inactive since no line can span above the mandatory break.
- active = k;
- MIN_COST + if attempt.justify { ratio.powi(3).abs() } else { 0.0 }
- } else {
- // Normal line with cost of |ratio^3|.
- ratio.powi(3).abs()
- };
-
- // Penalize hyphens.
- if hyphen {
- cost += HYPH_COST;
- }
-
- // Penalize two consecutive dashes (not necessarily hyphens) extra.
- if attempt.dash && pred.line.dash {
- cost += CONSECUTIVE_DASH_COST;
- }
-
- // The total cost of this line and its chain of predecessors.
- let total = pred.total + cost;
-
- // If this attempt is better than what we had before, take it!
- if best.as_ref().map_or(true, |best| best.total >= total) {
- best = Some(Entry { pred: i, total, line: attempt });
- }
- }
-
- table.push(best.unwrap());
- }
-
- // Retrace the best path.
- let mut lines = vec![];
- let mut idx = table.len() - 1;
- while idx != 0 {
- table.truncate(idx + 1);
- let entry = table.pop().unwrap();
- lines.push(entry.line);
- idx = entry.pred;
- }
-
- lines.reverse();
- lines
-}
-
-/// Determine all possible points in the text where lines can broken.
-///
-/// 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>(p: &'a Preparation) -> Breakpoints<'a> {
- Breakpoints {
- p,
- linebreaks: LineBreakIterator::new(p.bidi.text),
- syllables: None,
- offset: 0,
- suffix: 0,
- end: 0,
- mandatory: false,
- }
-}
-
-/// 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,
-}
-
-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(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.p.hyphenate != Some(false) {
- if let Some(lang) = self.lang(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(&self, offset: usize) -> bool {
- self.p
- .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(&self, offset: usize) -> Option<hypher::Lang> {
- let lang = self.p.lang.or_else(|| {
- let shaped = self.p.find(offset)?.text()?;
- Some(shaped.styles.get(TextNode::LANG))
- })?;
-
- let bytes = lang.as_str().as_bytes().try_into().ok()?;
- hypher::Lang::from_iso(bytes)
- }
-}
-
-/// Create a line which spans the given range.
-fn line<'a>(
- p: &'a Preparation,
- mut range: Range,
- mandatory: bool,
- hyphen: bool,
-) -> Line<'a> {
- let end = range.end;
- let mut justify = p.justify && end < p.bidi.text.len() && !mandatory;
-
- if range.is_empty() {
- return Line {
- bidi: &p.bidi,
- end,
- trimmed: range,
- first: None,
- inner: &[],
- last: None,
- width: Abs::zero(),
- justify,
- dash: false,
- };
- }
-
- // Slice out the relevant items.
- let (expanded, mut inner) = p.slice(range.clone());
- let mut width = Abs::zero();
-
- // Reshape the last item if it's split in half or hyphenated.
- let mut last = None;
- let mut dash = false;
- if let Some((Item::Text(shaped), before)) = inner.split_last() {
- // Compute the range we want to shape, trimming whitespace at the
- // end of the line.
- let base = expanded.end - shaped.text.len();
- let start = range.start.max(base);
- let text = &p.bidi.text[start..range.end];
- let trimmed = text.trim_end();
- range.end = start + trimmed.len();
-
- // Deal with hyphens, dashes and justification.
- let shy = trimmed.ends_with('\u{ad}');
- dash = hyphen || shy || trimmed.ends_with(['-', '–', '—']);
- justify |= text.ends_with('\u{2028}');
-
- // Usually, we don't want to shape an empty string because:
- // - We don't want the height of trimmed whitespace in a different
- // font to be considered for the line height.
- // - Even if it's in the same font, its unnecessary.
- //
- // There is one exception though. When the whole line is empty, we
- // need the shaped empty string to make the line the appropriate
- // height. That is the case exactly if the string is empty and there
- // are no other items in the line.
- if hyphen || start + shaped.text.len() > range.end {
- if hyphen || start < range.end || before.is_empty() {
- let shifted = start - base..range.end - base;
- let mut reshaped = shaped.reshape(p.world, shifted);
- if hyphen || shy {
- reshaped.push_hyphen(p.world);
- }
- width += reshaped.width;
- last = Some(Item::Text(reshaped));
- }
-
- inner = before;
- }
- }
-
- // Reshape the start item if it's split in half.
- let mut first = None;
- if let Some((Item::Text(shaped), after)) = inner.split_first() {
- // Compute the range we want to shape.
- let base = expanded.start;
- let end = range.end.min(base + shaped.text.len());
-
- // Reshape if necessary.
- if range.start + shaped.text.len() > end {
- if range.start < end {
- let shifted = range.start - base..end - base;
- let reshaped = shaped.reshape(p.world, shifted);
- width += reshaped.width;
- first = Some(Item::Text(reshaped));
- }
-
- inner = after;
- }
- }
-
- // Measure the inner items.
- for item in inner {
- width += item.width();
- }
-
- Line {
- bidi: &p.bidi,
- trimmed: range,
- end,
- first,
- inner,
- last,
- width,
- justify,
- dash,
- }
-}
-
-/// Combine layouted lines into one frame per region.
-fn finalize(
- p: &Preparation,
- lines: &[Line],
- regions: &Regions,
-) -> SourceResult<Fragment> {
- // Determine the paragraph's width: Full width of the region if we
- // should expand or there's fractional spacing, fit-to-width otherwise.
- let mut width = regions.first.x;
- if !regions.expand.x && lines.iter().all(|line| line.fr().is_zero()) {
- width = lines.iter().map(|line| line.width).max().unwrap_or_default();
- }
-
- // Stack the lines into one frame per region.
- lines
- .iter()
- .map(|line| commit(p, line, regions.base, width))
- .collect::<SourceResult<_>>()
- .map(Fragment::frames)
-}
-
-/// Commit to a line and build its frame.
-fn commit(p: &Preparation, line: &Line, base: Size, width: Abs) -> SourceResult<Frame> {
- let mut remaining = width - line.width;
- let mut offset = Abs::zero();
-
- // Reorder the line from logical to visual order.
- let reordered = reorder(line);
-
- // Handle hanging punctuation to the left.
- if let Some(Item::Text(text)) = reordered.first() {
- if let Some(glyph) = text.glyphs.first() {
- if !text.dir.is_positive()
- && text.styles.get(TextNode::OVERHANG)
- && (reordered.len() > 1 || text.glyphs.len() > 1)
- {
- let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
- offset -= amount;
- remaining += amount;
- }
- }
- }
-
- // Handle hanging punctuation to the right.
- if let Some(Item::Text(text)) = reordered.last() {
- if let Some(glyph) = text.glyphs.last() {
- if text.dir.is_positive()
- && text.styles.get(TextNode::OVERHANG)
- && (reordered.len() > 1 || text.glyphs.len() > 1)
- {
- let amount = overhang(glyph.c) * glyph.x_advance.at(text.size);
- remaining += amount;
- }
- }
- }
-
- // Determine how much to justify each space.
- let fr = line.fr();
- let mut justification = Abs::zero();
- if remaining < Abs::zero() || (line.justify && fr.is_zero()) {
- let justifiables = line.justifiables();
- if justifiables > 0 {
- justification = remaining / justifiables as f64;
- remaining = Abs::zero();
- }
- }
-
- let mut top = Abs::zero();
- let mut bottom = Abs::zero();
-
- // Build the frames and determine the height and baseline.
- let mut frames = vec![];
- for item in reordered {
- let mut push = |offset: &mut Abs, frame: Frame| {
- let width = frame.width();
- top.set_max(frame.baseline());
- bottom.set_max(frame.size().y - frame.baseline());
- frames.push((*offset, frame));
- *offset += width;
- };
-
- match item {
- Item::Absolute(v) => {
- offset += *v;
- }
- Item::Fractional(v) => {
- offset += v.share(fr, remaining);
- }
- Item::Text(shaped) => {
- let frame = shaped.build(p.world, justification);
- push(&mut offset, frame);
- }
- Item::Frame(frame) => {
- push(&mut offset, frame.clone());
- }
- Item::Repeat(repeat, styles) => {
- let before = offset;
- let fill = Fr::one().share(fr, remaining);
- let size = Size::new(fill, base.y);
- let pod = Regions::one(size, base, Axes::new(false, false));
- let frame = repeat.layout(p.world, *styles, &pod)?.into_frame();
- let width = frame.width();
- let count = (fill / width).floor();
- let remaining = fill % width;
- let apart = remaining / (count - 1.0);
- if count == 1.0 {
- offset += p.align.position(remaining);
- }
- if width > Abs::zero() {
- for _ in 0..(count as usize).min(1000) {
- push(&mut offset, frame.clone());
- offset += apart;
- }
- }
- offset = before + fill;
- }
- }
- }
-
- // Remaining space is distributed now.
- if !fr.is_zero() {
- remaining = Abs::zero();
- }
-
- let size = Size::new(width, top + bottom);
- let mut output = Frame::new(size);
- output.set_baseline(top);
-
- // Construct the line's frame.
- for (offset, frame) in frames {
- let x = offset + p.align.position(remaining);
- let y = top - frame.baseline();
- output.push_frame(Point::new(x, y), frame);
- }
-
- Ok(output)
-}
-
-/// Return a line's items in visual order.
-fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> {
- let mut reordered = vec![];
-
- // The bidi crate doesn't like empty lines.
- if line.trimmed.is_empty() {
- return line.slice(line.trimmed.clone()).collect();
- }
-
- // Find the paragraph that contains the line.
- let para = line
- .bidi
- .paragraphs
- .iter()
- .find(|para| para.range.contains(&line.trimmed.start))
- .unwrap();
-
- // Compute the reordered ranges in visual order (left to right).
- let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone());
-
- // Collect the reordered items.
- for run in runs {
- // Skip reset L1 runs because handling them would require reshaping
- // again in some cases.
- if line.bidi.levels[run.start] != levels[run.start] {
- continue;
- }
-
- let prev = reordered.len();
- reordered.extend(line.slice(run.clone()));
-
- if levels[run.start].is_rtl() {
- reordered[prev..].reverse();
- }
- }
-
- reordered
-}
-
-/// How much a character should hang into the end margin.
-///
-/// For more discussion, see:
-/// https://recoveringphysicist.com/21/
-fn overhang(c: char) -> f64 {
- match c {
- // Dashes.
- '–' | '—' => 0.2,
- '-' => 0.55,
-
- // Punctuation.
- '.' | ',' => 0.8,
- ':' | ';' => 0.3,
-
- // Arabic and Ideographic
- '\u{60C}' | '\u{6D4}' => 0.4,
- '\u{3001}' | '\u{3002}' => 1.0,
-
- _ => 0.0,
- }
-}
diff --git a/library/src/text/quotes.rs b/library/src/text/quotes.rs
index 87a965af..95cf1ad9 100644
--- a/library/src/text/quotes.rs
+++ b/library/src/text/quotes.rs
@@ -1,10 +1,24 @@
use typst::syntax::is_newline;
-use super::{Lang, Region};
+use crate::prelude::*;
+
+/// A smart quote.
+#[derive(Debug, Hash)]
+pub struct SmartQuoteNode {
+ pub double: bool,
+}
+
+#[node]
+impl SmartQuoteNode {
+ fn construct(_: &Vm, args: &mut Args) -> SourceResult<Content> {
+ let double = args.named("double")?.unwrap_or(true);
+ Ok(Self { double }.pack())
+ }
+}
/// State machine for smart quote subtitution.
#[derive(Debug, Clone)]
-pub(super) struct Quoter {
+pub struct Quoter {
/// How many quotes have been opened.
quote_depth: usize,
/// Whether an opening quote might follow.
@@ -68,7 +82,7 @@ fn is_opening_bracket(c: char) -> bool {
}
/// Decides which quotes to subtitute smart quotes with.
-pub(super) struct Quotes<'s> {
+pub struct Quotes<'s> {
/// The opening single quote.
pub single_open: &'s str,
/// The closing single quote.
diff --git a/library/src/text/shaping.rs b/library/src/text/shaping.rs
index 6ce4d671..687f2a87 100644
--- a/library/src/text/shaping.rs
+++ b/library/src/text/shaping.rs
@@ -6,6 +6,7 @@ use typst::font::{Font, FontVariant};
use typst::util::SliceExt;
use super::*;
+use crate::meta::LinkNode;
use crate::prelude::*;
/// The result of shaping text.
@@ -13,7 +14,7 @@ use crate::prelude::*;
/// This type contains owned or borrowed shaped text runs, which can be
/// measured, used to reshape substrings more quickly and converted into a
/// frame.
-pub(super) struct ShapedText<'a> {
+pub struct ShapedText<'a> {
/// The text that was shaped.
pub text: &'a str,
/// The text direction.
@@ -32,7 +33,7 @@ pub(super) struct ShapedText<'a> {
/// A single glyph resulting from shaping.
#[derive(Debug, Clone)]
-pub(super) struct ShapedGlyph {
+pub struct ShapedGlyph {
/// The font the glyph is contained in.
pub font: Font,
/// The glyph's index in the font.
@@ -314,7 +315,7 @@ struct ShapingContext<'a> {
}
/// Shape text into [`ShapedText`].
-pub(super) fn shape<'a>(
+pub fn shape<'a>(
world: Tracked<dyn World>,
text: &'a str,
styles: StyleChain<'a>,