summaryrefslogtreecommitdiff
path: root/src/library
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-03 11:44:53 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-03 13:35:39 +0100
commit37a7afddfaffd44cb9bc013c9506599267e08983 (patch)
tree20e7d62d3c5418baff01a21d0406b91bf3096214 /src/library
parent56342bd972a13ffe21beaf2b87ab7eb1597704b4 (diff)
Split crates
Diffstat (limited to 'src/library')
-rw-r--r--src/library/ext.rs179
-rw-r--r--src/library/graphics/hide.rs27
-rw-r--r--src/library/graphics/image.rs120
-rw-r--r--src/library/graphics/line.rs80
-rw-r--r--src/library/graphics/mod.rs11
-rw-r--r--src/library/graphics/shape.rs201
-rw-r--r--src/library/layout/align.rs62
-rw-r--r--src/library/layout/columns.rs117
-rw-r--r--src/library/layout/container.rs80
-rw-r--r--src/library/layout/flow.rs267
-rw-r--r--src/library/layout/grid.rs593
-rw-r--r--src/library/layout/mod.rs814
-rw-r--r--src/library/layout/pad.rs83
-rw-r--r--src/library/layout/page.rs421
-rw-r--r--src/library/layout/place.rs56
-rw-r--r--src/library/layout/spacing.rs100
-rw-r--r--src/library/layout/stack.rs321
-rw-r--r--src/library/layout/transform.rs116
-rw-r--r--src/library/math/frac.rs17
-rw-r--r--src/library/math/mod.rs295
-rw-r--r--src/library/math/script.rs31
-rw-r--r--src/library/mod.rs183
-rw-r--r--src/library/prelude.rs27
-rw-r--r--src/library/raw.rs149
-rw-r--r--src/library/structure/doc.rs29
-rw-r--r--src/library/structure/heading.rs176
-rw-r--r--src/library/structure/list.rs371
-rw-r--r--src/library/structure/mod.rs13
-rw-r--r--src/library/structure/reference.rs29
-rw-r--r--src/library/structure/table.rs160
-rw-r--r--src/library/text/deco.rs249
-rw-r--r--src/library/text/link.rs114
-rw-r--r--src/library/text/mod.rs593
-rw-r--r--src/library/text/par.rs1266
-rw-r--r--src/library/text/quotes.rs148
-rw-r--r--src/library/text/raw.rs205
-rw-r--r--src/library/text/shaping.rs654
-rw-r--r--src/library/text/shift.rs187
-rw-r--r--src/library/utility/color.rs66
-rw-r--r--src/library/utility/data.rs131
-rw-r--r--src/library/utility/math.rs144
-rw-r--r--src/library/utility/mod.rs54
-rw-r--r--src/library/utility/string.rs140
43 files changed, 0 insertions, 9079 deletions
diff --git a/src/library/ext.rs b/src/library/ext.rs
deleted file mode 100644
index 07b55a7b..00000000
--- a/src/library/ext.rs
+++ /dev/null
@@ -1,179 +0,0 @@
-use super::*;
-use crate::library::prelude::*;
-
-/// Additional methods on content.
-pub trait ContentExt {
- /// Make this content strong.
- fn strong(self) -> Self;
-
- /// Make this content emphasized.
- fn emph(self) -> Self;
-
- /// Underline this content.
- fn underlined(self) -> Self;
-
- /// Add weak vertical spacing above and below the content.
- fn spaced(self, above: Option<Abs>, below: Option<Abs>) -> Self;
-
- /// Force a size for this content.
- fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self;
-
- /// Set alignments for this content.
- fn aligned(self, aligns: Axes<Option<RawAlign>>) -> Self;
-
- /// Pad this content at the sides.
- fn padded(self, padding: Sides<Rel<Length>>) -> Self;
-
- /// Transform this content's contents without affecting layout.
- fn moved(self, delta: Axes<Rel<Length>>) -> Self;
-
- /// Fill the frames resulting from a content.
- fn filled(self, fill: Paint) -> Self;
-
- /// Stroke the frames resulting from a content.
- fn stroked(self, stroke: Stroke) -> Self;
-}
-
-impl ContentExt for Content {
- fn strong(self) -> Self {
- text::StrongNode(self).pack()
- }
-
- fn emph(self) -> Self {
- text::EmphNode(self).pack()
- }
-
- fn underlined(self) -> Self {
- text::DecoNode::<{ text::UNDERLINE }>(self).pack()
- }
-
- fn spaced(self, above: Option<Abs>, below: Option<Abs>) -> Self {
- if above.is_none() && below.is_none() {
- return self;
- }
-
- let mut seq = vec![];
- if let Some(above) = above {
- seq.push(
- layout::VNode {
- amount: above.into(),
- weak: true,
- generated: true,
- }
- .pack(),
- );
- }
-
- seq.push(self);
- if let Some(below) = below {
- seq.push(
- layout::VNode {
- amount: below.into(),
- weak: true,
- generated: true,
- }
- .pack(),
- );
- }
-
- Content::sequence(seq)
- }
-
- fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self {
- layout::BoxNode { sizing, child: self }.pack()
- }
-
- fn aligned(self, aligns: Axes<Option<RawAlign>>) -> Self {
- layout::AlignNode { aligns, child: self }.pack()
- }
-
- fn padded(self, padding: Sides<Rel<Length>>) -> Self {
- layout::PadNode { padding, child: self }.pack()
- }
-
- fn moved(self, delta: Axes<Rel<Length>>) -> Self {
- layout::MoveNode { delta, child: self }.pack()
- }
-
- fn filled(self, fill: Paint) -> Self {
- FillNode { fill, child: self }.pack()
- }
-
- fn stroked(self, stroke: Stroke) -> Self {
- StrokeNode { stroke, child: self }.pack()
- }
-}
-
-/// Additional methods for the style chain.
-pub trait StyleMapExt {
- /// Set a font family composed of a preferred family and existing families
- /// from a style chain.
- fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain);
-}
-
-impl StyleMapExt for StyleMap {
- fn set_family(&mut self, preferred: text::FontFamily, existing: StyleChain) {
- self.set(
- text::TextNode::FAMILY,
- std::iter::once(preferred)
- .chain(existing.get(text::TextNode::FAMILY).iter().cloned())
- .collect(),
- );
- }
-}
-
-/// Fill the frames resulting from content.
-#[derive(Debug, Hash)]
-struct FillNode {
- /// How to fill the frames resulting from the `child`.
- fill: Paint,
- /// The content whose frames should be filled.
- child: Content,
-}
-
-#[node(LayoutBlock)]
-impl FillNode {}
-
-impl LayoutBlock for FillNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames = self.child.layout_block(world, regions, styles)?;
- for frame in &mut frames {
- let shape = Geometry::Rect(frame.size()).filled(self.fill);
- frame.prepend(Point::zero(), Element::Shape(shape));
- }
- Ok(frames)
- }
-}
-
-/// Stroke the frames resulting from content.
-#[derive(Debug, Hash)]
-struct StrokeNode {
- /// How to stroke the frames resulting from the `child`.
- stroke: Stroke,
- /// The content whose frames should be stroked.
- child: Content,
-}
-
-#[node(LayoutBlock)]
-impl StrokeNode {}
-
-impl LayoutBlock for StrokeNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames = self.child.layout_block(world, regions, styles)?;
- for frame in &mut frames {
- let shape = Geometry::Rect(frame.size()).stroked(self.stroke);
- frame.prepend(Point::zero(), Element::Shape(shape));
- }
- Ok(frames)
- }
-}
diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs
deleted file mode 100644
index d320b06c..00000000
--- a/src/library/graphics/hide.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use crate::library::prelude::*;
-
-/// Hide content without affecting layout.
-#[derive(Debug, Hash)]
-pub struct HideNode(pub Content);
-
-#[node(LayoutInline)]
-impl HideNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl LayoutInline for HideNode {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames = self.0.layout_inline(world, regions, styles)?;
- for frame in &mut frames {
- frame.clear();
- }
- Ok(frames)
- }
-}
diff --git a/src/library/graphics/image.rs b/src/library/graphics/image.rs
deleted file mode 100644
index e27ea488..00000000
--- a/src/library/graphics/image.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-use std::ffi::OsStr;
-
-use crate::image::{Image, ImageFormat, RasterFormat, VectorFormat};
-use crate::library::prelude::*;
-use crate::library::text::TextNode;
-
-/// Show a raster or vector graphic.
-#[derive(Debug, Hash)]
-pub struct ImageNode(pub Image);
-
-#[node(LayoutInline)]
-impl ImageNode {
- /// How the image should adjust itself to a given area.
- pub const FIT: ImageFit = ImageFit::Cover;
-
- fn construct(vm: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let Spanned { v: path, span } =
- args.expect::<Spanned<EcoString>>("path to image file")?;
-
- let full = vm.locate(&path).at(span)?;
- let buffer = vm.world.file(&full).at(span)?;
- let ext = full.extension().and_then(OsStr::to_str).unwrap_or_default();
- let format = match ext.to_lowercase().as_str() {
- "png" => ImageFormat::Raster(RasterFormat::Png),
- "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg),
- "gif" => ImageFormat::Raster(RasterFormat::Gif),
- "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg),
- _ => bail!(span, "unknown image format"),
- };
-
- let image = Image::new(buffer, format).at(span)?;
- let width = args.named("width")?;
- let height = args.named("height")?;
-
- Ok(ImageNode(image).pack().boxed(Axes::new(width, height)))
- }
-}
-
-impl LayoutInline for ImageNode {
- fn layout_inline(
- &self,
- _: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let pxw = self.0.width() as f64;
- let pxh = self.0.height() as f64;
- let px_ratio = pxw / pxh;
-
- // Find out whether the image is wider or taller than the target size.
- let &Regions { first, expand, .. } = regions;
- let region_ratio = first.x / first.y;
- let wide = px_ratio > region_ratio;
-
- // The space into which the image will be placed according to its fit.
- let target = if expand.x && expand.y {
- first
- } else if expand.x || (!expand.y && wide && first.x.is_finite()) {
- Size::new(first.x, first.y.min(first.x.safe_div(px_ratio)))
- } else if first.y.is_finite() {
- Size::new(first.x.min(first.y * px_ratio), first.y)
- } else {
- Size::new(Abs::pt(pxw), Abs::pt(pxh))
- };
-
- // Compute the actual size of the fitted image.
- let fit = styles.get(Self::FIT);
- let fitted = match fit {
- ImageFit::Cover | ImageFit::Contain => {
- if wide == (fit == ImageFit::Contain) {
- Size::new(target.x, target.x / px_ratio)
- } else {
- Size::new(target.y * px_ratio, target.y)
- }
- }
- ImageFit::Stretch => target,
- };
-
- // First, place the image in a frame of exactly its size and then resize
- // the frame to the target size, center aligning the image in the
- // process.
- let mut frame = Frame::new(fitted);
- frame.push(Point::zero(), Element::Image(self.0.clone(), fitted));
- frame.resize(target, Align::CENTER_HORIZON);
-
- // Create a clipping group if only part of the image should be visible.
- if fit == ImageFit::Cover && !target.fits(fitted) {
- frame.clip();
- }
-
- // Apply link if it exists.
- if let Some(url) = styles.get(TextNode::LINK) {
- frame.link(url.clone());
- }
-
- Ok(vec![frame])
- }
-}
-
-/// How an image should adjust itself to a given area.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum ImageFit {
- /// The image should completely cover the area.
- Cover,
- /// The image should be fully contained in the area.
- Contain,
- /// The image should be stretched so that it exactly fills the area.
- Stretch,
-}
-
-castable! {
- ImageFit,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "cover" => Self::Cover,
- "contain" => Self::Contain,
- "stretch" => Self::Stretch,
- _ => Err(r#"expected "cover", "contain" or "stretch""#)?,
- },
-}
diff --git a/src/library/graphics/line.rs b/src/library/graphics/line.rs
deleted file mode 100644
index ee7813a5..00000000
--- a/src/library/graphics/line.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use crate::library::prelude::*;
-
-/// Display a line without affecting the layout.
-#[derive(Debug, Hash)]
-pub struct LineNode {
- /// Where the line starts.
- origin: Axes<Rel<Length>>,
- /// The offset from the `origin` where the line ends.
- delta: Axes<Rel<Length>>,
-}
-
-#[node(LayoutInline)]
-impl LineNode {
- /// How to stroke the line.
- #[property(resolve, fold)]
- pub const STROKE: RawStroke = RawStroke::default();
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let origin = args.named("origin")?.unwrap_or_default();
-
- let delta = match args.named::<Axes<Rel<Length>>>("to")? {
- Some(to) => to.zip(origin).map(|(to, from)| to - from),
- None => {
- let length =
- args.named::<Rel<Length>>("length")?.unwrap_or(Abs::cm(1.0).into());
-
- let angle = args.named::<Angle>("angle")?.unwrap_or_default();
- let x = angle.cos() * length;
- let y = angle.sin() * length;
-
- Axes::new(x, y)
- }
- };
-
- Ok(Self { origin, delta }.pack())
- }
-}
-
-impl LayoutInline for LineNode {
- fn layout_inline(
- &self,
- _: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let stroke = styles.get(Self::STROKE).unwrap_or_default();
-
- let origin = self
- .origin
- .resolve(styles)
- .zip(regions.base)
- .map(|(l, b)| l.relative_to(b));
-
- let delta = self
- .delta
- .resolve(styles)
- .zip(regions.base)
- .map(|(l, b)| l.relative_to(b));
-
- let target = regions.expand.select(regions.first, Size::zero());
- let mut frame = Frame::new(target);
-
- let shape = Geometry::Line(delta.to_point()).stroked(stroke);
- frame.push(origin.to_point(), Element::Shape(shape));
-
- Ok(vec![frame])
- }
-}
-
-castable! {
- Axes<Rel<Length>>,
- Expected: "array of two relative lengths",
- Value::Array(array) => {
- let mut iter = array.into_iter();
- match (iter.next(), iter.next(), iter.next()) {
- (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
- _ => Err("point array must contain exactly two entries")?,
- }
- },
-}
diff --git a/src/library/graphics/mod.rs b/src/library/graphics/mod.rs
deleted file mode 100644
index 34182121..00000000
--- a/src/library/graphics/mod.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-//! Graphical elements and effects.
-
-mod hide;
-mod image;
-mod line;
-mod shape;
-
-pub use self::image::*;
-pub use hide::*;
-pub use line::*;
-pub use shape::*;
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
deleted file mode 100644
index 4804cd68..00000000
--- a/src/library/graphics/shape.rs
+++ /dev/null
@@ -1,201 +0,0 @@
-use std::f64::consts::SQRT_2;
-
-use crate::library::prelude::*;
-use crate::library::text::TextNode;
-
-/// A sizable and fillable shape with optional content.
-#[derive(Debug, Hash)]
-pub struct ShapeNode<const S: ShapeKind>(pub Option<Content>);
-
-/// A square with optional content.
-pub type SquareNode = ShapeNode<SQUARE>;
-
-/// A rectangle with optional content.
-pub type RectNode = ShapeNode<RECT>;
-
-/// A circle with optional content.
-pub type CircleNode = ShapeNode<CIRCLE>;
-
-/// A ellipse with optional content.
-pub type EllipseNode = ShapeNode<ELLIPSE>;
-
-#[node(LayoutInline)]
-impl<const S: ShapeKind> ShapeNode<S> {
- /// How to fill the shape.
- pub const FILL: Option<Paint> = None;
- /// How to stroke the shape.
- #[property(skip, resolve, fold)]
- pub const STROKE: Smart<Sides<Option<RawStroke>>> = Smart::Auto;
-
- /// How much to pad the shape's content.
- #[property(resolve, fold)]
- pub const INSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
- /// How much to extend the shape's dimensions beyond the allocated space.
- #[property(resolve, fold)]
- pub const OUTSET: Sides<Option<Rel<Length>>> = Sides::splat(Rel::zero());
-
- /// How much to round the shape's corners.
- #[property(skip, resolve, fold)]
- pub const RADIUS: Corners<Option<Rel<Length>>> = Corners::splat(Rel::zero());
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let size = match S {
- SQUARE => args.named::<Length>("size")?.map(Rel::from),
- CIRCLE => args.named::<Length>("radius")?.map(|r| 2.0 * Rel::from(r)),
- _ => None,
- };
-
- let width = match size {
- None => args.named("width")?,
- size => size,
- };
-
- let height = match size {
- None => args.named("height")?,
- size => size,
- };
-
- Ok(Self(args.eat()?).pack().boxed(Axes::new(width, height)))
- }
-
- fn set(...) {
- if is_round(S) {
- styles.set_opt(
- Self::STROKE,
- args.named::<Smart<Option<RawStroke>>>("stroke")?
- .map(|some| some.map(Sides::splat)),
- );
- } else {
- styles.set_opt(Self::STROKE, args.named("stroke")?);
- styles.set_opt(Self::RADIUS, args.named("radius")?);
- }
- }
-}
-
-impl<const S: ShapeKind> LayoutInline for ShapeNode<S> {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames;
- if let Some(child) = &self.0 {
- let mut inset = styles.get(Self::INSET);
- if is_round(S) {
- inset = inset.map(|side| side + Ratio::new(0.5 - SQRT_2 / 4.0));
- }
-
- // Pad the child.
- let child = child.clone().padded(inset.map(|side| side.map(Length::from)));
-
- let mut pod = Regions::one(regions.first, regions.base, regions.expand);
- frames = child.layout_inline(world, &pod, styles)?;
-
- for frame in frames.iter_mut() {
- frame.apply_role(Role::GenericBlock);
- }
-
- // Relayout with full expansion into square region to make sure
- // the result is really a square or circle.
- if is_quadratic(S) {
- let length = if regions.expand.x || regions.expand.y {
- let target = regions.expand.select(regions.first, Size::zero());
- target.x.max(target.y)
- } else {
- let size = frames[0].size();
- let desired = size.x.max(size.y);
- desired.min(regions.first.x).min(regions.first.y)
- };
-
- pod.first = Size::splat(length);
- pod.expand = Axes::splat(true);
- frames = child.layout_inline(world, &pod, styles)?;
- }
- } else {
- // The default size that a shape takes on if it has no child and
- // enough space.
- let mut size = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(regions.first);
-
- if is_quadratic(S) {
- let length = if regions.expand.x || regions.expand.y {
- let target = regions.expand.select(regions.first, Size::zero());
- target.x.max(target.y)
- } else {
- size.x.min(size.y)
- };
- size = Size::splat(length);
- } else {
- size = regions.expand.select(regions.first, size);
- }
-
- frames = vec![Frame::new(size)];
- }
-
- let frame = &mut frames[0];
-
- // Add fill and/or stroke.
- let fill = styles.get(Self::FILL);
- let stroke = match styles.get(Self::STROKE) {
- Smart::Auto if fill.is_none() => Sides::splat(Some(Stroke::default())),
- Smart::Auto => Sides::splat(None),
- Smart::Custom(strokes) => {
- strokes.map(|s| s.map(RawStroke::unwrap_or_default))
- }
- };
-
- let outset = styles.get(Self::OUTSET).relative_to(frame.size());
- let size = frame.size() + outset.sum_by_axis();
-
- let radius = styles
- .get(Self::RADIUS)
- .map(|side| side.relative_to(size.x.min(size.y) / 2.0));
-
- let pos = Point::new(-outset.left, -outset.top);
-
- if fill.is_some() || stroke.iter().any(Option::is_some) {
- if is_round(S) {
- let shape = ellipse(size, fill, stroke.left);
- frame.prepend(pos, Element::Shape(shape));
- } else {
- frame.prepend_multiple(
- rounded_rect(size, radius, fill, stroke)
- .into_iter()
- .map(|x| (pos, Element::Shape(x))),
- )
- }
- }
-
- // Apply link if it exists.
- if let Some(url) = styles.get(TextNode::LINK) {
- frame.link(url.clone());
- }
-
- Ok(frames)
- }
-}
-
-/// A category of shape.
-pub type ShapeKind = usize;
-
-/// A rectangle with equal side lengths.
-const SQUARE: ShapeKind = 0;
-
-/// A quadrilateral with four right angles.
-const RECT: ShapeKind = 1;
-
-/// An ellipse with coinciding foci.
-const CIRCLE: ShapeKind = 2;
-
-/// A curve around two focal points.
-const ELLIPSE: ShapeKind = 3;
-
-/// Whether a shape kind is curvy.
-fn is_round(kind: ShapeKind) -> bool {
- matches!(kind, CIRCLE | ELLIPSE)
-}
-
-/// Whether a shape kind has equal side length.
-fn is_quadratic(kind: ShapeKind) -> bool {
- matches!(kind, SQUARE | CIRCLE)
-}
diff --git a/src/library/layout/align.rs b/src/library/layout/align.rs
deleted file mode 100644
index 2ee565cc..00000000
--- a/src/library/layout/align.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use crate::library::prelude::*;
-use crate::library::text::{HorizontalAlign, ParNode};
-
-/// Align content along the layouting axes.
-#[derive(Debug, Hash)]
-pub struct AlignNode {
- /// How to align the content horizontally and vertically.
- pub aligns: Axes<Option<RawAlign>>,
- /// The content to be aligned.
- pub child: Content,
-}
-
-#[node(LayoutBlock)]
-impl AlignNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let aligns: Axes<Option<RawAlign>> = args.find()?.unwrap_or_default();
- let body: Content = args.expect("body")?;
-
- if let Axes { x: Some(x), y: None } = aligns {
- if !body.has::<dyn LayoutBlock>() {
- return Ok(body.styled(ParNode::ALIGN, HorizontalAlign(x)));
- }
- }
-
- Ok(body.aligned(aligns))
- }
-}
-
-impl LayoutBlock for AlignNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- // The child only needs to expand along an axis if there's no alignment.
- let mut pod = regions.clone();
- pod.expand &= self.aligns.as_ref().map(Option::is_none);
-
- // Align paragraphs inside the child.
- let mut passed = StyleMap::new();
- if let Some(align) = self.aligns.x {
- passed.set(ParNode::ALIGN, HorizontalAlign(align));
- }
-
- // Layout the child.
- let mut frames = self.child.layout_block(world, &pod, passed.chain(&styles))?;
- for (region, frame) in regions.iter().zip(&mut frames) {
- // Align in the target size. The target size depends on whether we
- // should expand.
- let target = regions.expand.select(region, frame.size());
- let aligns = self
- .aligns
- .map(|align| align.resolve(styles))
- .unwrap_or(Axes::new(Align::Left, Align::Top));
-
- frame.resize(target, aligns);
- }
-
- Ok(frames)
- }
-}
diff --git a/src/library/layout/columns.rs b/src/library/layout/columns.rs
deleted file mode 100644
index df259eab..00000000
--- a/src/library/layout/columns.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-use crate::library::prelude::*;
-use crate::library::text::TextNode;
-
-/// Separate a region into multiple equally sized columns.
-#[derive(Debug, Hash)]
-pub struct ColumnsNode {
- /// How many columns there should be.
- pub columns: NonZeroUsize,
- /// The child to be layouted into the columns. Most likely, this should be a
- /// flow or stack node.
- pub child: Content,
-}
-
-#[node(LayoutBlock)]
-impl ColumnsNode {
- /// The size of the gutter space between each column.
- #[property(resolve)]
- pub const GUTTER: Rel<Length> = Ratio::new(0.04).into();
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self {
- columns: args.expect("column count")?,
- child: args.expect("body")?,
- }
- .pack())
- }
-}
-
-impl LayoutBlock for ColumnsNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- // Separating the infinite space into infinite columns does not make
- // much sense.
- if !regions.first.x.is_finite() {
- return self.child.layout_block(world, regions, styles);
- }
-
- // Determine the width of the gutter and each column.
- let columns = self.columns.get();
- let gutter = styles.get(Self::GUTTER).relative_to(regions.base.x);
- let width = (regions.first.x - gutter * (columns - 1) as f64) / columns as f64;
-
- // Create the pod regions.
- let pod = Regions {
- first: Size::new(width, regions.first.y),
- base: Size::new(width, regions.base.y),
- backlog: std::iter::once(&regions.first.y)
- .chain(regions.backlog.as_slice())
- .flat_map(|&height| std::iter::repeat(height).take(columns))
- .skip(1)
- .collect(),
- last: regions.last,
- expand: Axes::new(true, regions.expand.y),
- };
-
- // Layout the children.
- let mut frames = self.child.layout_block(world, &pod, styles)?.into_iter();
- let mut finished = vec![];
-
- let dir = styles.get(TextNode::DIR);
- let total_regions = (frames.len() as f32 / columns as f32).ceil() as usize;
-
- // Stitch together the columns for each region.
- for region in regions.iter().take(total_regions) {
- // The height should be the parent height if we should expand.
- // Otherwise its the maximum column height for the frame. In that
- // case, the frame is first created with zero height and then
- // resized.
- let height = if regions.expand.y { region.y } else { Abs::zero() };
- let mut output = Frame::new(Size::new(regions.first.x, height));
- let mut cursor = Abs::zero();
-
- for _ in 0 .. columns {
- let frame = match frames.next() {
- Some(frame) => frame,
- None => break,
- };
-
- if !regions.expand.y {
- output.size_mut().y.set_max(frame.height());
- }
-
- let width = frame.width();
- let x = if dir.is_positive() {
- cursor
- } else {
- regions.first.x - cursor - width
- };
-
- output.push_frame(Point::with_x(x), frame);
- cursor += width + gutter;
- }
-
- finished.push(output);
- }
-
- Ok(finished)
- }
-}
-
-/// A column break.
-#[derive(Debug, Clone, Hash)]
-pub struct ColbreakNode {
- pub weak: bool,
-}
-
-#[node]
-impl ColbreakNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let weak = args.named("weak")?.unwrap_or(false);
- Ok(Self { weak }.pack())
- }
-}
diff --git a/src/library/layout/container.rs b/src/library/layout/container.rs
deleted file mode 100644
index 023809d0..00000000
--- a/src/library/layout/container.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use crate::library::prelude::*;
-
-/// An inline-level container that sizes content.
-#[derive(Debug, Clone, Hash)]
-pub struct BoxNode {
- /// How to size the content horizontally and vertically.
- pub sizing: Axes<Option<Rel<Length>>>,
- /// The content to be sized.
- pub child: Content,
-}
-
-#[node(LayoutInline)]
-impl BoxNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let width = args.named("width")?;
- let height = args.named("height")?;
- let body = args.eat::<Content>()?.unwrap_or_default();
- Ok(body.boxed(Axes::new(width, height)))
- }
-}
-
-impl LayoutInline for BoxNode {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- // The "pod" is the region into which the child will be layouted.
- let pod = {
- // Resolve the sizing to a concrete size.
- let size = self
- .sizing
- .resolve(styles)
- .zip(regions.base)
- .map(|(s, b)| s.map(|v| v.relative_to(b)))
- .unwrap_or(regions.first);
-
- // Select the appropriate base and expansion for the child depending
- // on whether it is automatically or relatively sized.
- let is_auto = self.sizing.as_ref().map(Option::is_none);
- let base = is_auto.select(regions.base, size);
- let expand = regions.expand | !is_auto;
-
- Regions::one(size, base, expand)
- };
-
- // Layout the child.
- let mut frames = self.child.layout_inline(world, &pod, styles)?;
-
- // Ensure frame size matches regions size if expansion is on.
- let frame = &mut frames[0];
- let target = regions.expand.select(regions.first, frame.size());
- frame.resize(target, Align::LEFT_TOP);
-
- Ok(frames)
- }
-}
-
-/// A block-level container that places content into a separate flow.
-#[derive(Debug, Clone, Hash)]
-pub struct BlockNode(pub Content);
-
-#[node(LayoutBlock)]
-impl BlockNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.eat()?.unwrap_or_default()).pack())
- }
-}
-
-impl LayoutBlock for BlockNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- self.0.layout_block(world, regions, styles)
- }
-}
diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs
deleted file mode 100644
index f4d18699..00000000
--- a/src/library/layout/flow.rs
+++ /dev/null
@@ -1,267 +0,0 @@
-use std::cmp::Ordering;
-
-use super::{AlignNode, PlaceNode, Spacing};
-use crate::library::prelude::*;
-use crate::library::text::ParNode;
-
-/// Arrange spacing, paragraphs and block-level nodes into a flow.
-///
-/// This node is reponsible for layouting both the top-level content flow and
-/// the contents of boxes.
-#[derive(Hash)]
-pub struct FlowNode(pub StyleVec<FlowChild>);
-
-/// A child of a flow node.
-#[derive(Hash, PartialEq)]
-pub enum FlowChild {
- /// Vertical spacing between other children.
- Spacing(Spacing),
- /// Arbitrary block-level content.
- Block(Content),
- /// A column / region break.
- Colbreak,
-}
-
-#[node(LayoutBlock)]
-impl FlowNode {}
-
-impl LayoutBlock for FlowNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut layouter = FlowLayouter::new(regions);
-
- for (child, map) in self.0.iter() {
- let styles = map.chain(&styles);
- match child {
- FlowChild::Spacing(kind) => {
- layouter.layout_spacing(*kind, styles);
- }
- FlowChild::Block(block) => {
- layouter.layout_block(world, block, styles)?;
- }
- FlowChild::Colbreak => {
- layouter.finish_region();
- }
- }
- }
-
- Ok(layouter.finish())
- }
-}
-
-impl Debug for FlowNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_str("Flow ")?;
- self.0.fmt(f)
- }
-}
-
-impl Debug for FlowChild {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Spacing(kind) => write!(f, "{:?}", kind),
- Self::Block(block) => block.fmt(f),
- Self::Colbreak => f.pad("Colbreak"),
- }
- }
-}
-
-impl PartialOrd for FlowChild {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- match (self, other) {
- (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b),
- _ => None,
- }
- }
-}
-
-/// Performs flow layout.
-pub struct FlowLayouter {
- /// The regions to layout children into.
- regions: Regions,
- /// Whether the flow should expand to fill the region.
- expand: Axes<bool>,
- /// The full size of `regions.size` that was available before we started
- /// subtracting.
- full: Size,
- /// The size used by the frames for the current region.
- used: Size,
- /// The sum of fractions in the current region.
- fr: Fr,
- /// Spacing and layouted blocks.
- items: Vec<FlowItem>,
- /// Finished frames for previous regions.
- finished: Vec<Frame>,
-}
-
-/// A prepared item in a flow layout.
-enum FlowItem {
- /// Absolute spacing between other items.
- Absolute(Abs),
- /// Fractional spacing between other items.
- Fractional(Fr),
- /// A frame for a layouted block and how to align it.
- Frame(Frame, Axes<Align>),
- /// An absolutely placed frame.
- Placed(Frame),
-}
-
-impl FlowLayouter {
- /// Create a new flow layouter.
- pub fn new(regions: &Regions) -> Self {
- let expand = regions.expand;
- let full = regions.first;
-
- // Disable vertical expansion for children.
- let mut regions = regions.clone();
- regions.expand.y = false;
-
- Self {
- regions,
- expand,
- full,
- used: Size::zero(),
- fr: Fr::zero(),
- items: vec![],
- finished: vec![],
- }
- }
-
- /// Layout spacing.
- pub fn layout_spacing(&mut self, spacing: Spacing, styles: StyleChain) {
- match spacing {
- Spacing::Relative(v) => {
- // Resolve the spacing and limit it to the remaining space.
- let resolved = v.resolve(styles).relative_to(self.full.y);
- let limited = resolved.min(self.regions.first.y);
- self.regions.first.y -= limited;
- self.used.y += limited;
- self.items.push(FlowItem::Absolute(resolved));
- }
- Spacing::Fractional(v) => {
- self.items.push(FlowItem::Fractional(v));
- self.fr += v;
- }
- }
- }
-
- /// Layout a block.
- pub fn layout_block(
- &mut self,
- world: Tracked<dyn World>,
- block: &Content,
- styles: StyleChain,
- ) -> SourceResult<()> {
- // Don't even try layouting into a full region.
- if self.regions.is_full() {
- self.finish_region();
- }
-
- // Placed nodes that are out of flow produce placed items which aren't
- // aligned later.
- if let Some(placed) = block.downcast::<PlaceNode>() {
- if placed.out_of_flow() {
- let frame = block.layout_block(world, &self.regions, styles)?.remove(0);
- self.items.push(FlowItem::Placed(frame));
- return Ok(());
- }
- }
-
- // How to align the block.
- let aligns = Axes::new(
- // For non-expanding paragraphs it is crucial that we align the
- // whole paragraph as it is itself aligned.
- styles.get(ParNode::ALIGN),
- // Vertical align node alignment is respected by the flow.
- block
- .downcast::<AlignNode>()
- .and_then(|aligned| aligned.aligns.y)
- .map(|align| align.resolve(styles))
- .unwrap_or(Align::Top),
- );
-
- let frames = block.layout_block(world, &self.regions, styles)?;
- let len = frames.len();
- for (i, mut frame) in frames.into_iter().enumerate() {
- // Set the generic block role.
- frame.apply_role(Role::GenericBlock);
-
- // Grow our size, shrink the region and save the frame for later.
- let size = frame.size();
- self.used.y += size.y;
- self.used.x.set_max(size.x);
- self.regions.first.y -= size.y;
- self.items.push(FlowItem::Frame(frame, aligns));
-
- if i + 1 < len {
- self.finish_region();
- }
- }
-
- Ok(())
- }
-
- /// Finish the frame for one region.
- pub fn finish_region(&mut self) {
- // Determine the size of the flow in this region dependening on whether
- // the region expands.
- let mut size = self.expand.select(self.full, self.used);
-
- // Account for fractional spacing in the size calculation.
- let remaining = self.full.y - self.used.y;
- if self.fr.get() > 0.0 && self.full.y.is_finite() {
- self.used.y = self.full.y;
- size.y = self.full.y;
- }
-
- let mut output = Frame::new(size);
- let mut offset = Abs::zero();
- let mut ruler = Align::Top;
-
- // Place all frames.
- for item in self.items.drain(..) {
- match item {
- FlowItem::Absolute(v) => {
- offset += v;
- }
- FlowItem::Fractional(v) => {
- offset += v.share(self.fr, remaining);
- }
- FlowItem::Frame(frame, aligns) => {
- ruler = ruler.max(aligns.y);
- let x = aligns.x.position(size.x - frame.width());
- let y = offset + ruler.position(size.y - self.used.y);
- let pos = Point::new(x, y);
- offset += frame.height();
- output.push_frame(pos, frame);
- }
- FlowItem::Placed(frame) => {
- output.push_frame(Point::zero(), frame);
- }
- }
- }
-
- // Advance to the next region.
- self.regions.next();
- self.full = self.regions.first;
- self.used = Size::zero();
- self.fr = Fr::zero();
- self.finished.push(output);
- }
-
- /// Finish layouting and return the resulting frames.
- pub fn finish(mut self) -> Vec<Frame> {
- if self.expand.y {
- while self.regions.backlog.len() > 0 {
- self.finish_region();
- }
- }
-
- self.finish_region();
- self.finished
- }
-}
diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs
deleted file mode 100644
index 1bb67691..00000000
--- a/src/library/layout/grid.rs
+++ /dev/null
@@ -1,593 +0,0 @@
-use crate::library::prelude::*;
-
-/// Arrange content in a grid.
-#[derive(Debug, Hash)]
-pub struct GridNode {
- /// Defines sizing for content rows and columns.
- pub tracks: Axes<Vec<TrackSizing>>,
- /// Defines sizing of gutter rows and columns between content.
- pub gutter: Axes<Vec<TrackSizing>>,
- /// The content to be arranged in a grid.
- pub cells: Vec<Content>,
-}
-
-#[node(LayoutBlock)]
-impl GridNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let columns = args.named("columns")?.unwrap_or_default();
- let rows = args.named("rows")?.unwrap_or_default();
- let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default();
- let column_gutter = args.named("column-gutter")?;
- let row_gutter = args.named("row-gutter")?;
- Ok(Self {
- tracks: Axes::new(columns, rows),
- gutter: Axes::new(
- column_gutter.unwrap_or_else(|| base_gutter.clone()),
- row_gutter.unwrap_or(base_gutter),
- ),
- cells: args.all()?,
- }
- .pack())
- }
-}
-
-impl LayoutBlock for GridNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- // Prepare grid layout by unifying content and gutter tracks.
- let layouter = GridLayouter::new(
- world,
- self.tracks.as_deref(),
- self.gutter.as_deref(),
- &self.cells,
- regions,
- styles,
- );
-
- // Measure the columns and layout the grid row-by-row.
- layouter.layout()
- }
-}
-
-/// Defines how to size a grid cell along an axis.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum TrackSizing {
- /// A track that fits its cell's contents.
- Auto,
- /// A track size specified in absolute terms and relative to the parent's
- /// size.
- Relative(Rel<Length>),
- /// A track size specified as a fraction of the remaining free space in the
- /// parent.
- Fractional(Fr),
-}
-
-castable! {
- Vec<TrackSizing>,
- Expected: "integer, auto, relative length, fraction, or array of the latter three",
- Value::Auto => vec![TrackSizing::Auto],
- Value::Length(v) => vec![TrackSizing::Relative(v.into())],
- Value::Ratio(v) => vec![TrackSizing::Relative(v.into())],
- Value::Relative(v) => vec![TrackSizing::Relative(v)],
- Value::Fraction(v) => vec![TrackSizing::Fractional(v)],
- Value::Int(v) => vec![TrackSizing::Auto; Value::Int(v).cast::<NonZeroUsize>()?.get()],
- Value::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .collect(),
-}
-
-castable! {
- TrackSizing,
- Expected: "auto, relative length, or fraction",
- Value::Auto => Self::Auto,
- Value::Length(v) => Self::Relative(v.into()),
- Value::Ratio(v) => Self::Relative(v.into()),
- Value::Relative(v) => Self::Relative(v),
- Value::Fraction(v) => Self::Fractional(v),
-}
-
-/// Performs grid layout.
-pub struct GridLayouter<'a> {
- /// The core context.
- world: Tracked<'a, dyn World>,
- /// The grid cells.
- cells: &'a [Content],
- /// The column tracks including gutter tracks.
- cols: Vec<TrackSizing>,
- /// The row tracks including gutter tracks.
- rows: Vec<TrackSizing>,
- /// The regions to layout children into.
- regions: Regions,
- /// The inherited styles.
- styles: StyleChain<'a>,
- /// Resolved column sizes.
- rcols: Vec<Abs>,
- /// Rows in the current region.
- lrows: Vec<Row>,
- /// The full height of the current region.
- full: Abs,
- /// The used-up size of the current region. The horizontal size is
- /// determined once after columns are resolved and not touched again.
- used: Size,
- /// The sum of fractions in the current region.
- fr: Fr,
- /// Frames for finished regions.
- finished: Vec<Frame>,
-}
-
-/// Produced by initial row layout, auto and relative rows are already finished,
-/// fractional rows not yet.
-enum Row {
- /// Finished row frame of auto or relative row.
- Frame(Frame),
- /// Fractional row with y index.
- Fr(Fr, usize),
-}
-
-impl<'a> GridLayouter<'a> {
- /// Create a new grid layouter.
- ///
- /// This prepares grid layout by unifying content and gutter tracks.
- pub fn new(
- world: Tracked<'a, dyn World>,
- tracks: Axes<&[TrackSizing]>,
- gutter: Axes<&[TrackSizing]>,
- cells: &'a [Content],
- regions: &Regions,
- styles: StyleChain<'a>,
- ) -> Self {
- let mut cols = vec![];
- let mut rows = vec![];
-
- // Number of content columns: Always at least one.
- let c = tracks.x.len().max(1);
-
- // Number of content rows: At least as many as given, but also at least
- // as many as needed to place each item.
- let r = {
- let len = cells.len();
- let given = tracks.y.len();
- let needed = len / c + (len % c).clamp(0, 1);
- given.max(needed)
- };
-
- let auto = TrackSizing::Auto;
- let zero = TrackSizing::Relative(Rel::zero());
- let get_or = |tracks: &[_], idx, default| {
- tracks.get(idx).or(tracks.last()).copied().unwrap_or(default)
- };
-
- // Collect content and gutter columns.
- for x in 0 .. c {
- cols.push(get_or(tracks.x, x, auto));
- cols.push(get_or(gutter.x, x, zero));
- }
-
- // Collect content and gutter rows.
- for y in 0 .. r {
- rows.push(get_or(tracks.y, y, auto));
- rows.push(get_or(gutter.y, y, zero));
- }
-
- // Remove superfluous gutter tracks.
- cols.pop();
- rows.pop();
-
- let full = regions.first.y;
- let rcols = vec![Abs::zero(); cols.len()];
- let lrows = vec![];
-
- // We use the regions for auto row measurement. Since at that moment,
- // columns are already sized, we can enable horizontal expansion.
- let mut regions = regions.clone();
- regions.expand = Axes::new(true, false);
-
- Self {
- world,
- cells,
- cols,
- rows,
- regions,
- styles,
- rcols,
- lrows,
- full,
- used: Size::zero(),
- fr: Fr::zero(),
- finished: vec![],
- }
- }
-
- /// Determines the columns sizes and then layouts the grid row-by-row.
- pub fn layout(mut self) -> SourceResult<Vec<Frame>> {
- self.measure_columns()?;
-
- for y in 0 .. self.rows.len() {
- // Skip to next region if current one is full, but only for content
- // rows, not for gutter rows.
- if y % 2 == 0 && self.regions.is_full() {
- self.finish_region()?;
- }
-
- match self.rows[y] {
- TrackSizing::Auto => self.layout_auto_row(y)?,
- TrackSizing::Relative(v) => self.layout_relative_row(v, y)?,
- TrackSizing::Fractional(v) => {
- self.lrows.push(Row::Fr(v, y));
- self.fr += v;
- }
- }
- }
-
- self.finish_region()?;
- Ok(self.finished)
- }
-
- /// Determine all column sizes.
- fn measure_columns(&mut self) -> SourceResult<()> {
- // Sum of sizes of resolved relative tracks.
- let mut rel = Abs::zero();
-
- // Sum of fractions of all fractional tracks.
- let mut fr = Fr::zero();
-
- // Resolve the size of all relative columns and compute the sum of all
- // fractional tracks.
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- match col {
- TrackSizing::Auto => {}
- TrackSizing::Relative(v) => {
- let resolved =
- v.resolve(self.styles).relative_to(self.regions.base.x);
- *rcol = resolved;
- rel += resolved;
- }
- TrackSizing::Fractional(v) => fr += v,
- }
- }
-
- // Size that is not used by fixed-size columns.
- let available = self.regions.first.x - rel;
- if available >= Abs::zero() {
- // Determine size of auto columns.
- let (auto, count) = self.measure_auto_columns(available)?;
-
- // If there is remaining space, distribute it to fractional columns,
- // otherwise shrink auto columns.
- let remaining = available - auto;
- if remaining >= Abs::zero() {
- if !fr.is_zero() {
- self.grow_fractional_columns(remaining, fr);
- }
- } else {
- self.shrink_auto_columns(available, count);
- }
- }
-
- // Sum up the resolved column sizes once here.
- self.used.x = self.rcols.iter().sum();
-
- Ok(())
- }
-
- /// Measure the size that is available to auto columns.
- fn measure_auto_columns(&mut self, available: Abs) -> SourceResult<(Abs, usize)> {
- let mut auto = Abs::zero();
- let mut count = 0;
-
- // Determine size of auto columns by laying out all cells in those
- // columns, measuring them and finding the largest one.
- for (x, &col) in self.cols.iter().enumerate() {
- if col != TrackSizing::Auto {
- continue;
- }
-
- let mut resolved = Abs::zero();
- for y in 0 .. self.rows.len() {
- if let Some(cell) = self.cell(x, y) {
- let size = Size::new(available, self.regions.base.y);
- let mut pod =
- Regions::one(size, self.regions.base, Axes::splat(false));
-
- // For relative rows, we can already resolve the correct
- // base, for auto it's already correct and for fr we could
- // only guess anyway.
- if let TrackSizing::Relative(v) = self.rows[y] {
- pod.base.y =
- v.resolve(self.styles).relative_to(self.regions.base.y);
- }
-
- let frame =
- cell.layout_block(self.world, &pod, self.styles)?.remove(0);
- resolved.set_max(frame.width());
- }
- }
-
- self.rcols[x] = resolved;
- auto += resolved;
- count += 1;
- }
-
- Ok((auto, count))
- }
-
- /// Distribute remaining space to fractional columns.
- fn grow_fractional_columns(&mut self, remaining: Abs, fr: Fr) {
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- if let TrackSizing::Fractional(v) = col {
- *rcol = v.share(fr, remaining);
- }
- }
- }
-
- /// Redistribute space to auto columns so that each gets a fair share.
- fn shrink_auto_columns(&mut self, available: Abs, count: usize) {
- // The fair share each auto column may have.
- let fair = available / count as f64;
-
- // The number of overlarge auto columns and the space that will be
- // equally redistributed to them.
- let mut overlarge: usize = 0;
- let mut redistribute = available;
-
- // Find out the number of and space used by overlarge auto columns.
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- if col == TrackSizing::Auto {
- if *rcol > fair {
- overlarge += 1;
- } else {
- redistribute -= *rcol;
- }
- }
- }
-
- // Redistribute the space equally.
- let share = redistribute / overlarge as f64;
- for (&col, rcol) in self.cols.iter().zip(&mut self.rcols) {
- if col == TrackSizing::Auto && *rcol > fair {
- *rcol = share;
- }
- }
- }
-
- /// Layout a row with automatic height. Such a row may break across multiple
- /// regions.
- fn layout_auto_row(&mut self, y: usize) -> SourceResult<()> {
- let mut resolved: Vec<Abs> = vec![];
-
- // Determine the size for each region of the row.
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- let mut pod = self.regions.clone();
- pod.first.x = rcol;
- pod.base.x = rcol;
-
- // All widths should be `rcol` except the base for auto columns.
- if self.cols[x] == TrackSizing::Auto {
- pod.base.x = self.regions.base.x;
- }
-
- let mut sizes = cell
- .layout_block(self.world, &pod, self.styles)?
- .into_iter()
- .map(|frame| frame.height());
-
- // For each region, we want to know the maximum height any
- // column requires.
- for (target, size) in resolved.iter_mut().zip(&mut sizes) {
- target.set_max(size);
- }
-
- // New heights are maximal by virtue of being new. Note that
- // this extend only uses the rest of the sizes iterator.
- resolved.extend(sizes);
- }
- }
-
- // Nothing to layout.
- if resolved.is_empty() {
- return Ok(());
- }
-
- // Layout into a single region.
- if let &[first] = resolved.as_slice() {
- let frame = self.layout_single_row(first, y)?;
- self.push_row(frame);
- return Ok(());
- }
-
- // Expand all but the last region if the space is not
- // eaten up by any fr rows.
- if self.fr.is_zero() {
- let len = resolved.len();
- for (region, target) in self.regions.iter().zip(&mut resolved[.. len - 1]) {
- target.set_max(region.y);
- }
- }
-
- // Layout into multiple regions.
- let frames = self.layout_multi_row(&resolved, y)?;
- let len = frames.len();
- for (i, frame) in frames.into_iter().enumerate() {
- self.push_row(frame);
- if i + 1 < len {
- self.finish_region()?;
- }
- }
-
- Ok(())
- }
-
- /// Layout a row with relative height. Such a row cannot break across
- /// multiple regions, but it may force a region break.
- fn layout_relative_row(&mut self, v: Rel<Length>, y: usize) -> SourceResult<()> {
- let resolved = v.resolve(self.styles).relative_to(self.regions.base.y);
- let frame = self.layout_single_row(resolved, y)?;
-
- // Skip to fitting region.
- let height = frame.height();
- while !self.regions.first.y.fits(height) && !self.regions.in_last() {
- self.finish_region()?;
-
- // Don't skip multiple regions for gutter and don't push a row.
- if y % 2 == 1 {
- return Ok(());
- }
- }
-
- self.push_row(frame);
-
- Ok(())
- }
-
- /// Layout a row with fixed height and return its frame.
- fn layout_single_row(&mut self, height: Abs, y: usize) -> SourceResult<Frame> {
- let mut output = Frame::new(Size::new(self.used.x, height));
-
- let mut pos = Point::zero();
-
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- let size = Size::new(rcol, height);
-
- // Set the base to the region's base for auto rows and to the
- // size for relative and fractional rows.
- let base = Axes::new(self.cols[x], self.rows[y])
- .map(|s| s == TrackSizing::Auto)
- .select(self.regions.base, size);
-
- let pod = Regions::one(size, base, Axes::splat(true));
- let frame = cell.layout_block(self.world, &pod, self.styles)?.remove(0);
- match frame.role() {
- Some(Role::ListLabel | Role::ListItemBody) => {
- output.apply_role(Role::ListItem)
- }
- Some(Role::TableCell) => output.apply_role(Role::TableRow),
- _ => {}
- }
-
- output.push_frame(pos, frame);
- }
-
- pos.x += rcol;
- }
-
- Ok(output)
- }
-
- /// Layout a row spanning multiple regions.
- fn layout_multi_row(
- &mut self,
- heights: &[Abs],
- y: usize,
- ) -> SourceResult<Vec<Frame>> {
- // Prepare frames.
- let mut outputs: Vec<_> = heights
- .iter()
- .map(|&h| Frame::new(Size::new(self.used.x, h)))
- .collect();
-
- // Prepare regions.
- let size = Size::new(self.used.x, heights[0]);
- let mut pod = Regions::one(size, self.regions.base, Axes::splat(true));
- pod.backlog = heights[1 ..].to_vec();
-
- // Layout the row.
- let mut pos = Point::zero();
- for (x, &rcol) in self.rcols.iter().enumerate() {
- if let Some(cell) = self.cell(x, y) {
- pod.first.x = rcol;
- pod.base.x = rcol;
-
- // All widths should be `rcol` except the base for auto columns.
- if self.cols[x] == TrackSizing::Auto {
- pod.base.x = self.regions.base.x;
- }
-
- // Push the layouted frames into the individual output frames.
- let frames = cell.layout_block(self.world, &pod, self.styles)?;
- for (output, frame) in outputs.iter_mut().zip(frames) {
- match frame.role() {
- Some(Role::ListLabel | Role::ListItemBody) => {
- output.apply_role(Role::ListItem)
- }
- Some(Role::TableCell) => output.apply_role(Role::TableRow),
- _ => {}
- }
- output.push_frame(pos, frame);
- }
- }
-
- pos.x += rcol;
- }
-
- Ok(outputs)
- }
-
- /// Push a row frame into the current region.
- fn push_row(&mut self, frame: Frame) {
- self.regions.first.y -= frame.height();
- self.used.y += frame.height();
- self.lrows.push(Row::Frame(frame));
- }
-
- /// Finish rows for one region.
- fn finish_region(&mut self) -> SourceResult<()> {
- // Determine the size of the grid in this region, expanding fully if
- // there are fr rows.
- let mut size = self.used;
- if self.fr.get() > 0.0 && self.full.is_finite() {
- size.y = self.full;
- }
-
- // The frame for the region.
- let mut output = Frame::new(size);
- let mut pos = Point::zero();
-
- // Place finished rows and layout fractional rows.
- for row in std::mem::take(&mut self.lrows) {
- let frame = match row {
- Row::Frame(frame) => frame,
- Row::Fr(v, y) => {
- let remaining = self.full - self.used.y;
- let height = v.share(self.fr, remaining);
- self.layout_single_row(height, y)?
- }
- };
-
- let height = frame.height();
- output.push_frame(pos, frame);
- pos.y += height;
- }
-
- self.finished.push(output);
- self.regions.next();
- self.full = self.regions.first.y;
- self.used.y = Abs::zero();
- self.fr = Fr::zero();
-
- Ok(())
- }
-
- /// Get the content of the cell in column `x` and row `y`.
- ///
- /// Returns `None` if it's a gutter cell.
- #[track_caller]
- fn cell(&self, x: usize, y: usize) -> Option<&'a Content> {
- assert!(x < self.cols.len());
- assert!(y < self.rows.len());
-
- // Even columns and rows are children, odd ones are gutter.
- if x % 2 == 0 && y % 2 == 0 {
- let c = 1 + self.cols.len() / 2;
- self.cells.get((y / 2) * c + x / 2)
- } else {
- None
- }
- }
-}
diff --git a/src/library/layout/mod.rs b/src/library/layout/mod.rs
deleted file mode 100644
index 000cb212..00000000
--- a/src/library/layout/mod.rs
+++ /dev/null
@@ -1,814 +0,0 @@
-//! Composable layouts.
-
-mod align;
-mod columns;
-mod container;
-mod flow;
-mod grid;
-mod pad;
-mod page;
-mod place;
-mod spacing;
-mod stack;
-mod transform;
-
-pub use align::*;
-pub use columns::*;
-pub use container::*;
-pub use flow::*;
-pub use grid::*;
-pub use pad::*;
-pub use page::*;
-pub use place::*;
-pub use spacing::*;
-pub use stack::*;
-pub use transform::*;
-
-use std::mem;
-
-use comemo::Tracked;
-use typed_arena::Arena;
-
-use crate::diag::SourceResult;
-use crate::frame::Frame;
-use crate::geom::*;
-use crate::library::structure::{DocNode, ListItem, ListNode, DESC, ENUM, LIST};
-use crate::library::text::{
- LinebreakNode, ParChild, ParNode, ParbreakNode, SmartQuoteNode, SpaceNode, TextNode,
-};
-use crate::model::{
- capability, Barrier, Content, Interruption, Node, SequenceNode, Show, StyleChain,
- StyleEntry, StyleMap, StyleVec, StyleVecBuilder, StyledNode, Target,
-};
-use crate::World;
-
-/// The root-level layout.
-#[capability]
-pub trait Layout: 'static + Sync + Send {
- /// Layout into one frame per page.
- fn layout(&self, world: Tracked<dyn World>) -> SourceResult<Vec<Frame>>;
-}
-
-impl Layout for Content {
- #[comemo::memoize]
- fn layout(&self, world: Tracked<dyn World>) -> SourceResult<Vec<Frame>> {
- let styles = StyleChain::with_root(&world.config().styles);
- let scratch = Scratch::default();
-
- let mut builder = Builder::new(world, &scratch, true);
- builder.accept(self, styles)?;
-
- let (doc, shared) = builder.into_doc(styles)?;
- doc.layout(world, shared)
- }
-}
-
-/// Block-level layout.
-#[capability]
-pub trait LayoutBlock: 'static + Sync + Send {
- /// Layout into one frame per region.
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>>;
-}
-
-impl LayoutBlock for Content {
- #[comemo::memoize]
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- if let Some(node) = self.to::<dyn LayoutBlock>() {
- let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
- let styles = barrier.chain(&styles);
- return node.layout_block(world, regions, styles);
- }
-
- let scratch = Scratch::default();
- let mut builder = Builder::new(world, &scratch, false);
- builder.accept(self, styles)?;
- let (flow, shared) = builder.into_flow(styles)?;
- flow.layout_block(world, regions, shared)
- }
-}
-
-/// Inline-level layout.
-#[capability]
-pub trait LayoutInline: 'static + Sync + Send {
- /// Layout into a single frame.
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>>;
-}
-
-impl LayoutInline for Content {
- #[comemo::memoize]
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- if let Some(node) = self.to::<dyn LayoutInline>() {
- let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
- let styles = barrier.chain(&styles);
- return node.layout_inline(world, regions, styles);
- }
-
- if let Some(node) = self.to::<dyn LayoutBlock>() {
- let barrier = StyleEntry::Barrier(Barrier::new(self.id()));
- let styles = barrier.chain(&styles);
- return node.layout_block(world, regions, styles);
- }
-
- let scratch = Scratch::default();
- let mut builder = Builder::new(world, &scratch, false);
- builder.accept(self, styles)?;
- let (flow, shared) = builder.into_flow(styles)?;
- flow.layout_block(world, regions, shared)
- }
-}
-
-/// A sequence of regions to layout into.
-#[derive(Debug, Clone, Hash)]
-pub struct Regions {
- /// The (remaining) size of the first region.
- pub first: Size,
- /// The base size for relative sizing.
- pub base: Size,
- /// The height of followup regions. The width is the same for all regions.
- pub backlog: Vec<Abs>,
- /// The height of the final region that is repeated once the backlog is
- /// drained. The width is the same for all regions.
- pub last: Option<Abs>,
- /// Whether nodes should expand to fill the regions instead of shrinking to
- /// fit the content.
- pub expand: Axes<bool>,
-}
-
-impl Regions {
- /// Create a new region sequence with exactly one region.
- pub fn one(size: Size, base: Size, expand: Axes<bool>) -> Self {
- Self {
- first: size,
- base,
- backlog: vec![],
- last: None,
- expand,
- }
- }
-
- /// Create a new sequence of same-size regions that repeats indefinitely.
- pub fn repeat(size: Size, base: Size, expand: Axes<bool>) -> Self {
- Self {
- first: size,
- base,
- backlog: vec![],
- last: Some(size.y),
- expand,
- }
- }
-
- /// Create new regions where all sizes are mapped with `f`.
- ///
- /// Note that since all regions must have the same width, the width returned
- /// by `f` is ignored for the backlog and the final region.
- pub fn map<F>(&self, mut f: F) -> Self
- where
- F: FnMut(Size) -> Size,
- {
- let x = self.first.x;
- Self {
- first: f(self.first),
- base: f(self.base),
- backlog: self.backlog.iter().map(|&y| f(Size::new(x, y)).y).collect(),
- last: self.last.map(|y| f(Size::new(x, y)).y),
- expand: self.expand,
- }
- }
-
- /// Whether the first region is full and a region break is called for.
- pub fn is_full(&self) -> bool {
- Abs::zero().fits(self.first.y) && !self.in_last()
- }
-
- /// Whether the first region is the last usable region.
- ///
- /// If this is true, calling `next()` will have no effect.
- pub fn in_last(&self) -> bool {
- self.backlog.len() == 0 && self.last.map_or(true, |height| self.first.y == height)
- }
-
- /// Advance to the next region if there is any.
- pub fn next(&mut self) {
- if let Some(height) = (!self.backlog.is_empty())
- .then(|| self.backlog.remove(0))
- .or(self.last)
- {
- self.first.y = height;
- self.base.y = height;
- }
- }
-
- /// An iterator that returns the sizes of the first and all following
- /// regions, equivalently to what would be produced by calling
- /// [`next()`](Self::next) repeatedly until all regions are exhausted.
- /// This iterater may be infinite.
- pub fn iter(&self) -> impl Iterator<Item = Size> + '_ {
- let first = std::iter::once(self.first);
- let backlog = self.backlog.iter();
- let last = self.last.iter().cycle();
- first.chain(backlog.chain(last).map(|&h| Size::new(self.first.x, h)))
- }
-}
-
-/// Builds a document or a flow node from content.
-struct Builder<'a> {
- /// The core context.
- world: Tracked<'a, dyn World>,
- /// Scratch arenas for building.
- scratch: &'a Scratch<'a>,
- /// The current document building state.
- doc: Option<DocBuilder<'a>>,
- /// The current flow building state.
- flow: FlowBuilder<'a>,
- /// The current paragraph building state.
- par: ParBuilder<'a>,
- /// The current list building state.
- list: ListBuilder<'a>,
-}
-
-/// Temporary storage arenas for building.
-#[derive(Default)]
-struct Scratch<'a> {
- /// An arena where intermediate style chains are stored.
- styles: Arena<StyleChain<'a>>,
- /// An arena where intermediate content resulting from show rules is stored.
- templates: Arena<Content>,
-}
-
-impl<'a> Builder<'a> {
- pub fn new(
- world: Tracked<'a, dyn World>,
- scratch: &'a Scratch<'a>,
- top: bool,
- ) -> Self {
- Self {
- world,
- scratch,
- doc: top.then(|| DocBuilder::default()),
- flow: FlowBuilder::default(),
- par: ParBuilder::default(),
- list: ListBuilder::default(),
- }
- }
-
- pub fn into_doc(
- mut self,
- styles: StyleChain<'a>,
- ) -> SourceResult<(DocNode, StyleChain<'a>)> {
- self.interrupt(Interruption::Page, styles, true)?;
- let (pages, shared) = self.doc.unwrap().pages.finish();
- Ok((DocNode(pages), shared))
- }
-
- pub fn into_flow(
- mut self,
- styles: StyleChain<'a>,
- ) -> SourceResult<(FlowNode, StyleChain<'a>)> {
- self.interrupt(Interruption::Par, styles, false)?;
- let (children, shared) = self.flow.0.finish();
- Ok((FlowNode(children), shared))
- }
-
- pub fn accept(
- &mut self,
- content: &'a Content,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- if let Some(text) = content.downcast::<TextNode>() {
- if let Some(realized) = styles.apply(self.world, Target::Text(&text.0))? {
- let stored = self.scratch.templates.alloc(realized);
- return self.accept(stored, styles);
- }
- } else if let Some(styled) = content.downcast::<StyledNode>() {
- return self.styled(styled, styles);
- } else if let Some(seq) = content.downcast::<SequenceNode>() {
- return self.sequence(seq, styles);
- } else if content.has::<dyn Show>() {
- if self.show(&content, styles)? {
- return Ok(());
- }
- }
-
- if self.list.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt(Interruption::List, styles, false)?;
-
- if content.is::<ListItem>() {
- self.list.accept(content, styles);
- return Ok(());
- }
-
- if self.par.accept(content, styles) {
- return Ok(());
- }
-
- self.interrupt(Interruption::Par, styles, false)?;
-
- if self.flow.accept(content, styles) {
- return Ok(());
- }
-
- let keep = content
- .downcast::<PagebreakNode>()
- .map_or(false, |pagebreak| !pagebreak.weak);
- self.interrupt(Interruption::Page, styles, keep)?;
-
- if let Some(doc) = &mut self.doc {
- doc.accept(content, styles);
- }
-
- // We might want to issue a warning or error for content that wasn't
- // handled (e.g. a pagebreak in a flow building process). However, we
- // don't have the spans here at the moment.
- Ok(())
- }
-
- fn show(
- &mut self,
- content: &'a Content,
- styles: StyleChain<'a>,
- ) -> SourceResult<bool> {
- if let Some(mut realized) = styles.apply(self.world, Target::Node(content))? {
- let mut map = StyleMap::new();
- let barrier = Barrier::new(content.id());
- map.push(StyleEntry::Barrier(barrier));
- map.push(StyleEntry::Barrier(barrier));
- realized = realized.styled_with_map(map);
- let stored = self.scratch.templates.alloc(realized);
- self.accept(stored, styles)?;
- Ok(true)
- } else {
- Ok(false)
- }
- }
-
- fn styled(
- &mut self,
- styled: &'a StyledNode,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- let stored = self.scratch.styles.alloc(styles);
- let styles = styled.map.chain(stored);
- let intr = styled.map.interruption();
-
- if let Some(intr) = intr {
- self.interrupt(intr, styles, false)?;
- }
-
- self.accept(&styled.sub, styles)?;
-
- if let Some(intr) = intr {
- self.interrupt(intr, styles, true)?;
- }
-
- Ok(())
- }
-
- fn interrupt(
- &mut self,
- intr: Interruption,
- styles: StyleChain<'a>,
- keep: bool,
- ) -> SourceResult<()> {
- if intr >= Interruption::List && !self.list.is_empty() {
- mem::take(&mut self.list).finish(self)?;
- }
-
- if intr >= Interruption::Par {
- if !self.par.is_empty() {
- mem::take(&mut self.par).finish(self);
- }
- }
-
- if intr >= Interruption::Page {
- if let Some(doc) = &mut self.doc {
- if !self.flow.is_empty() || (doc.keep_next && keep) {
- mem::take(&mut self.flow).finish(doc, styles);
- }
- doc.keep_next = !keep;
- }
- }
-
- Ok(())
- }
-
- fn sequence(
- &mut self,
- seq: &'a SequenceNode,
- styles: StyleChain<'a>,
- ) -> SourceResult<()> {
- for content in &seq.0 {
- self.accept(content, styles)?;
- }
- Ok(())
- }
-}
-
-/// Accepts pagebreaks and pages.
-struct DocBuilder<'a> {
- /// The page runs built so far.
- pages: StyleVecBuilder<'a, PageNode>,
- /// Whether to keep a following page even if it is empty.
- keep_next: bool,
-}
-
-impl<'a> DocBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) {
- if let Some(pagebreak) = content.downcast::<PagebreakNode>() {
- self.keep_next = !pagebreak.weak;
- }
-
- if let Some(page) = content.downcast::<PageNode>() {
- self.pages.push(page.clone(), styles);
- self.keep_next = false;
- }
- }
-}
-
-impl Default for DocBuilder<'_> {
- fn default() -> Self {
- Self {
- pages: StyleVecBuilder::new(),
- keep_next: true,
- }
- }
-}
-
-/// Accepts flow content.
-#[derive(Default)]
-struct FlowBuilder<'a>(CollapsingBuilder<'a, FlowChild>);
-
-impl<'a> FlowBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- // Weak flow elements:
- // Weakness | Element
- // 0 | weak colbreak
- // 1 | weak fractional spacing
- // 2 | weak spacing
- // 3 | generated weak spacing
- // 4 | generated weak fractional spacing
- // 5 | par spacing
-
- if let Some(_) = content.downcast::<ParbreakNode>() {
- /* Nothing to do */
- } else if let Some(colbreak) = content.downcast::<ColbreakNode>() {
- if colbreak.weak {
- self.0.weak(FlowChild::Colbreak, styles, 0);
- } else {
- self.0.destructive(FlowChild::Colbreak, styles);
- }
- } else if let Some(vertical) = content.downcast::<VNode>() {
- let child = FlowChild::Spacing(vertical.amount);
- let frac = vertical.amount.is_fractional();
- if vertical.weak {
- let weakness = 1 + u8::from(frac) + 2 * u8::from(vertical.generated);
- self.0.weak(child, styles, weakness);
- } else if frac {
- self.0.destructive(child, styles);
- } else {
- self.0.ignorant(child, styles);
- }
- } else if content.has::<dyn LayoutBlock>() {
- let child = FlowChild::Block(content.clone());
- if content.is::<PlaceNode>() {
- self.0.ignorant(child, styles);
- } else {
- self.0.supportive(child, styles);
- }
- } else {
- return false;
- }
-
- true
- }
-
- fn par(&mut self, par: ParNode, styles: StyleChain<'a>, indent: bool) {
- let amount = if indent && !styles.get(ParNode::SPACING_AND_INDENT) {
- styles.get(ParNode::LEADING).into()
- } else {
- styles.get(ParNode::SPACING).into()
- };
-
- self.0.weak(FlowChild::Spacing(amount), styles, 5);
- self.0.supportive(FlowChild::Block(par.pack()), styles);
- self.0.weak(FlowChild::Spacing(amount), styles, 5);
- }
-
- fn finish(self, doc: &mut DocBuilder<'a>, styles: StyleChain<'a>) {
- let (flow, shared) = self.0.finish();
- let styles = if flow.is_empty() { styles } else { shared };
- let node = PageNode(FlowNode(flow).pack());
- doc.pages.push(node, styles);
- }
-
- fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-}
-
-/// Accepts paragraph content.
-#[derive(Default)]
-struct ParBuilder<'a>(CollapsingBuilder<'a, ParChild>);
-
-impl<'a> ParBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- // Weak par elements:
- // Weakness | Element
- // 0 | weak fractional spacing
- // 1 | weak spacing
- // 2 | space
-
- if content.is::<SpaceNode>() {
- self.0.weak(ParChild::Text(' '.into()), styles, 2);
- } else if let Some(linebreak) = content.downcast::<LinebreakNode>() {
- let c = if linebreak.justify { '\u{2028}' } else { '\n' };
- self.0.destructive(ParChild::Text(c.into()), styles);
- } else if let Some(horizontal) = content.downcast::<HNode>() {
- let child = ParChild::Spacing(horizontal.amount);
- let frac = horizontal.amount.is_fractional();
- if horizontal.weak {
- let weakness = u8::from(!frac);
- self.0.weak(child, styles, weakness);
- } else if frac {
- self.0.destructive(child, styles);
- } else {
- self.0.ignorant(child, styles);
- }
- } else if let Some(quote) = content.downcast::<SmartQuoteNode>() {
- self.0.supportive(ParChild::Quote { double: quote.double }, styles);
- } else if let Some(text) = content.downcast::<TextNode>() {
- self.0.supportive(ParChild::Text(text.0.clone()), styles);
- } else if content.has::<dyn LayoutInline>() {
- self.0.supportive(ParChild::Inline(content.clone()), styles);
- } else {
- return false;
- }
-
- true
- }
-
- fn finish(self, parent: &mut Builder<'a>) {
- let (mut children, shared) = self.0.finish();
- if children.is_empty() {
- return;
- }
-
- // Paragraph indent should only apply if the paragraph starts with
- // text and follows directly after another paragraph.
- let indent = shared.get(ParNode::INDENT);
- if !indent.is_zero()
- && children
- .items()
- .find_map(|child| match child {
- ParChild::Spacing(_) => None,
- ParChild::Text(_) | ParChild::Quote { .. } => Some(true),
- ParChild::Inline(_) => Some(false),
- })
- .unwrap_or_default()
- && parent
- .flow
- .0
- .items()
- .rev()
- .find_map(|child| match child {
- FlowChild::Spacing(_) => None,
- FlowChild::Block(content) => Some(content.is::<ParNode>()),
- FlowChild::Colbreak => Some(false),
- })
- .unwrap_or_default()
- {
- children.push_front(ParChild::Spacing(indent.into()));
- }
-
- parent.flow.par(ParNode(children), shared, !indent.is_zero());
- }
-
- fn is_empty(&self) -> bool {
- self.0.is_empty()
- }
-}
-
-/// Accepts list / enum items, spaces, paragraph breaks.
-struct ListBuilder<'a> {
- /// The list items collected so far.
- items: StyleVecBuilder<'a, ListItem>,
- /// Whether the list contains no paragraph breaks.
- tight: bool,
- /// Whether the list can be attached.
- attachable: bool,
- /// Trailing content for which it is unclear whether it is part of the list.
- staged: Vec<(&'a Content, StyleChain<'a>)>,
-}
-
-impl<'a> ListBuilder<'a> {
- fn accept(&mut self, content: &'a Content, styles: StyleChain<'a>) -> bool {
- if self.items.is_empty() {
- if content.is::<ParbreakNode>() {
- self.attachable = false;
- } else if !content.is::<SpaceNode>() && !content.is::<ListItem>() {
- self.attachable = true;
- }
- }
-
- if let Some(item) = content.downcast::<ListItem>() {
- if self
- .items
- .items()
- .next()
- .map_or(true, |first| item.kind() == first.kind())
- {
- self.items.push(item.clone(), styles);
- self.tight &= self.staged.drain(..).all(|(t, _)| !t.is::<ParbreakNode>());
- } else {
- return false;
- }
- } else if !self.items.is_empty()
- && (content.is::<SpaceNode>() || content.is::<ParbreakNode>())
- {
- self.staged.push((content, styles));
- } else {
- return false;
- }
-
- true
- }
-
- fn finish(self, parent: &mut Builder<'a>) -> SourceResult<()> {
- let (items, shared) = self.items.finish();
- let kind = match items.items().next() {
- Some(item) => item.kind(),
- None => return Ok(()),
- };
-
- let tight = self.tight;
- let attached = tight && self.attachable;
- let content = match kind {
- LIST => ListNode::<LIST> { tight, attached, items }.pack(),
- ENUM => ListNode::<ENUM> { tight, attached, items }.pack(),
- DESC | _ => ListNode::<DESC> { tight, attached, items }.pack(),
- };
-
- let stored = parent.scratch.templates.alloc(content);
- parent.accept(stored, shared)?;
-
- for (content, styles) in self.staged {
- parent.accept(content, styles)?;
- }
-
- parent.list.attachable = true;
-
- Ok(())
- }
-
- fn is_empty(&self) -> bool {
- self.items.is_empty()
- }
-}
-
-impl Default for ListBuilder<'_> {
- fn default() -> Self {
- Self {
- items: StyleVecBuilder::default(),
- tight: true,
- attachable: true,
- staged: vec![],
- }
- }
-}
-
-/// A wrapper around a [`StyleVecBuilder`] that allows to collapse items.
-struct CollapsingBuilder<'a, T> {
- /// The internal builder.
- builder: StyleVecBuilder<'a, T>,
- /// Staged weak and ignorant items that we can't yet commit to the builder.
- /// The option is `Some(_)` for weak items and `None` for ignorant items.
- staged: Vec<(T, StyleChain<'a>, Option<u8>)>,
- /// What the last non-ignorant item was.
- last: Last,
-}
-
-/// What the last non-ignorant item was.
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-enum Last {
- Weak,
- Destructive,
- Supportive,
-}
-
-impl<'a, T> CollapsingBuilder<'a, T> {
- /// Create a new style-vec builder.
- pub fn new() -> Self {
- Self {
- builder: StyleVecBuilder::new(),
- staged: vec![],
- last: Last::Destructive,
- }
- }
-
- /// Whether the builder is empty.
- pub fn is_empty(&self) -> bool {
- self.builder.is_empty() && self.staged.is_empty()
- }
-
- /// Can only exist when there is at least one supportive item to its left
- /// and to its right, with no destructive items in between. There may be
- /// ignorant items in between in both directions.
- ///
- /// Between weak items, there may be at least one per layer and among the
- /// candidates the strongest one (smallest `weakness`) wins. When tied,
- /// the one that compares larger through `PartialOrd` wins.
- pub fn weak(&mut self, item: T, styles: StyleChain<'a>, weakness: u8)
- where
- T: PartialOrd,
- {
- if self.last == Last::Destructive {
- return;
- }
-
- if self.last == Last::Weak {
- if let Some(i) =
- self.staged.iter().position(|(prev_item, _, prev_weakness)| {
- prev_weakness.map_or(false, |prev_weakness| {
- weakness < prev_weakness
- || (weakness == prev_weakness && item > *prev_item)
- })
- })
- {
- self.staged.remove(i);
- } else {
- return;
- }
- }
-
- self.staged.push((item, styles, Some(weakness)));
- self.last = Last::Weak;
- }
-
- /// Forces nearby weak items to collapse.
- pub fn destructive(&mut self, item: T, styles: StyleChain<'a>) {
- self.flush(false);
- self.builder.push(item, styles);
- self.last = Last::Destructive;
- }
-
- /// Allows nearby weak items to exist.
- pub fn supportive(&mut self, item: T, styles: StyleChain<'a>) {
- self.flush(true);
- self.builder.push(item, styles);
- self.last = Last::Supportive;
- }
-
- /// Has no influence on other items.
- pub fn ignorant(&mut self, item: T, styles: StyleChain<'a>) {
- self.staged.push((item, styles, None));
- }
-
- /// Iterate over the contained items.
- pub fn items(&self) -> impl DoubleEndedIterator<Item = &T> {
- self.builder.items().chain(self.staged.iter().map(|(item, ..)| item))
- }
-
- /// Return the finish style vec and the common prefix chain.
- pub fn finish(mut self) -> (StyleVec<T>, StyleChain<'a>) {
- self.flush(false);
- self.builder.finish()
- }
-
- /// Push the staged items, filtering out weak items if `supportive` is
- /// false.
- fn flush(&mut self, supportive: bool) {
- for (item, styles, meta) in self.staged.drain(..) {
- if supportive || meta.is_none() {
- self.builder.push(item, styles);
- }
- }
- }
-}
-
-impl<'a, T> Default for CollapsingBuilder<'a, T> {
- fn default() -> Self {
- Self::new()
- }
-}
diff --git a/src/library/layout/pad.rs b/src/library/layout/pad.rs
deleted file mode 100644
index 920660d6..00000000
--- a/src/library/layout/pad.rs
+++ /dev/null
@@ -1,83 +0,0 @@
-use crate::library::prelude::*;
-
-/// Pad content at the sides.
-#[derive(Debug, Hash)]
-pub struct PadNode {
- /// The amount of padding.
- pub padding: Sides<Rel<Length>>,
- /// The content whose sides to pad.
- pub child: Content,
-}
-
-#[node(LayoutBlock)]
-impl PadNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let all = args.named("rest")?.or(args.find()?);
- let x = args.named("x")?;
- let y = args.named("y")?;
- let left = args.named("left")?.or(x).or(all).unwrap_or_default();
- let top = args.named("top")?.or(y).or(all).unwrap_or_default();
- let right = args.named("right")?.or(x).or(all).unwrap_or_default();
- let bottom = args.named("bottom")?.or(y).or(all).unwrap_or_default();
- let body = args.expect::<Content>("body")?;
- let padding = Sides::new(left, top, right, bottom);
- Ok(body.padded(padding))
- }
-}
-
-impl LayoutBlock for PadNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- // Layout child into padded regions.
- let padding = self.padding.resolve(styles);
- let pod = regions.map(|size| shrink(size, padding));
- let mut frames = self.child.layout_block(world, &pod, styles)?;
-
- for frame in &mut frames {
- // Apply the padding inversely such that the grown size padded
- // yields the frame's size.
- let padded = grow(frame.size(), padding);
- let padding = padding.relative_to(padded);
- let offset = Point::new(padding.left, padding.top);
-
- // Grow the frame and translate everything in the frame inwards.
- frame.set_size(padded);
- frame.translate(offset);
- }
-
- Ok(frames)
- }
-}
-
-/// Shrink a size by padding relative to the size itself.
-fn shrink(size: Size, padding: Sides<Rel<Abs>>) -> Size {
- size - padding.relative_to(size).sum_by_axis()
-}
-
-/// Grow a size by padding relative to the grown size.
-/// This is the inverse operation to `shrink()`.
-///
-/// For the horizontal axis the derivation looks as follows.
-/// (Vertical axis is analogous.)
-///
-/// Let w be the grown target width,
-/// s be the given width,
-/// l be the left padding,
-/// r be the right padding,
-/// p = l + r.
-///
-/// We want that: w - l.resolve(w) - r.resolve(w) = s
-///
-/// Thus: w - l.resolve(w) - r.resolve(w) = s
-/// <=> w - p.resolve(w) = s
-/// <=> w - p.rel * w - p.abs = s
-/// <=> (1 - p.rel) * w = s + p.abs
-/// <=> w = (s + p.abs) / (1 - p.rel)
-fn grow(size: Size, padding: Sides<Rel<Abs>>) -> Size {
- size.zip(padding.sum_by_axis())
- .map(|(s, p)| (s + p.abs).safe_div(1.0 - p.rel.get()))
-}
diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs
deleted file mode 100644
index 8d081749..00000000
--- a/src/library/layout/page.rs
+++ /dev/null
@@ -1,421 +0,0 @@
-use std::str::FromStr;
-
-use super::ColumnsNode;
-use crate::library::prelude::*;
-
-/// Layouts its child onto one or multiple pages.
-#[derive(PartialEq, Clone, Hash)]
-pub struct PageNode(pub Content);
-
-#[node]
-impl PageNode {
- /// The unflipped width of the page.
- #[property(resolve)]
- pub const WIDTH: Smart<Length> = Smart::Custom(Paper::A4.width().into());
- /// The unflipped height of the page.
- #[property(resolve)]
- pub const HEIGHT: Smart<Length> = Smart::Custom(Paper::A4.height().into());
- /// Whether the page is flipped into landscape orientation.
- pub const FLIPPED: bool = false;
-
- /// The page's margins.
- #[property(fold)]
- pub const MARGINS: Sides<Option<Smart<Rel<Length>>>> = Sides::splat(Smart::Auto);
-
- /// 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;
- /// The page's footer.
- #[property(referenced)]
- pub const FOOTER: Marginal = Marginal::None;
- /// Content in the page's background.
- #[property(referenced)]
- pub const BACKGROUND: Marginal = Marginal::None;
- /// Content in the page's foreground.
- #[property(referenced)]
- pub const FOREGROUND: Marginal = Marginal::None;
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-
- fn set(...) {
- if let Some(paper) = args.named_or_find::<Paper>("paper")? {
- styles.set(Self::WIDTH, Smart::Custom(paper.width().into()));
- styles.set(Self::HEIGHT, Smart::Custom(paper.height().into()));
- }
- }
-}
-
-impl PageNode {
- /// Layout the page run into a sequence of frames, one per page.
- pub fn layout(
- &self,
- world: Tracked<dyn World>,
- mut page: usize,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- // When one of the lengths is infinite the page fits its content along
- // that axis.
- let width = styles.get(Self::WIDTH).unwrap_or(Abs::inf());
- let height = styles.get(Self::HEIGHT).unwrap_or(Abs::inf());
- let mut size = Size::new(width, height);
- if styles.get(Self::FLIPPED) {
- std::mem::swap(&mut size.x, &mut size.y);
- }
-
- let mut min = width.min(height);
- if !min.is_finite() {
- min = Paper::A4.width();
- }
-
- // Determine the margins.
- let default = Rel::from(0.1190 * min);
- let padding = styles.get(Self::MARGINS).map(|side| side.unwrap_or(default));
-
- let mut child = self.0.clone();
-
- // Realize columns.
- let columns = styles.get(Self::COLUMNS);
- if columns.get() > 1 {
- child = ColumnsNode { columns, child: self.0.clone() }.pack();
- }
-
- // Realize margins.
- child = child.padded(padding);
-
- // Realize background fill.
- if let Some(fill) = styles.get(Self::FILL) {
- child = child.filled(fill);
- }
-
- // Layout the child.
- let regions = Regions::repeat(size, size, size.map(Abs::is_finite));
- let mut frames = child.layout_block(world, &regions, styles)?;
-
- let header = styles.get(Self::HEADER);
- let footer = styles.get(Self::FOOTER);
- let foreground = styles.get(Self::FOREGROUND);
- let background = styles.get(Self::BACKGROUND);
-
- // Realize overlays.
- for frame in &mut frames {
- let size = frame.size();
- let pad = padding.resolve(styles).relative_to(size);
- let pw = size.x - pad.left - pad.right;
- let py = size.y - pad.bottom;
- for (role, marginal, pos, area) in [
- (
- Role::Header,
- header,
- Point::with_x(pad.left),
- Size::new(pw, pad.top),
- ),
- (
- Role::Footer,
- footer,
- Point::new(pad.left, py),
- Size::new(pw, pad.bottom),
- ),
- (Role::Foreground, foreground, Point::zero(), size),
- (Role::Background, background, Point::zero(), size),
- ] {
- if let Some(content) = marginal.resolve(world, page)? {
- let pod = Regions::one(area, area, Axes::splat(true));
- let mut sub = content.layout_block(world, &pod, styles)?.remove(0);
- sub.apply_role(role);
-
- if role == Role::Background {
- frame.prepend_frame(pos, sub);
- } else {
- frame.push_frame(pos, sub);
- }
- }
- }
-
- page += 1;
- }
-
- Ok(frames)
- }
-}
-
-impl Debug for PageNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_str("Page(")?;
- self.0.fmt(f)?;
- f.write_str(")")
- }
-}
-
-/// A page break.
-#[derive(Debug, Copy, Clone, Hash)]
-pub struct PagebreakNode {
- pub weak: bool,
-}
-
-#[node]
-impl PagebreakNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let weak = args.named("weak")?.unwrap_or(false);
- Ok(Self { weak }.pack())
- }
-}
-
-/// A header, footer, foreground or background definition.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Marginal {
- /// Nothing,
- None,
- /// Bare content.
- Content(Content),
- /// A closure mapping from a page number to content.
- Func(Func, Span),
-}
-
-impl Marginal {
- /// Resolve the marginal based on the page number.
- pub fn resolve(
- &self,
- world: Tracked<dyn World>,
- page: usize,
- ) -> SourceResult<Option<Content>> {
- Ok(match self {
- Self::None => None,
- Self::Content(content) => Some(content.clone()),
- Self::Func(func, span) => {
- let args = Args::new(*span, [Value::Int(page as i64)]);
- Some(func.call_detached(world, args)?.display(world))
- }
- })
- }
-}
-
-impl Cast<Spanned<Value>> for Marginal {
- fn is(value: &Spanned<Value>) -> bool {
- matches!(&value.v, Value::Content(_) | Value::Func(_))
- }
-
- fn cast(value: Spanned<Value>) -> StrResult<Self> {
- match value.v {
- Value::None => Ok(Self::None),
- Value::Str(v) => Ok(Self::Content(TextNode(v.into()).pack())),
- Value::Content(v) => Ok(Self::Content(v)),
- Value::Func(v) => Ok(Self::Func(v, value.span)),
- v => Err(format!(
- "expected none, content or function, found {}",
- v.type_name(),
- )),
- }
- }
-}
-
-/// Specification of a paper.
-#[derive(Debug, Copy, Clone)]
-pub struct Paper {
- /// The width of the paper in millimeters.
- width: f64,
- /// The height of the paper in millimeters.
- height: f64,
-}
-
-impl Paper {
- /// The width of the paper.
- pub fn width(self) -> Abs {
- Abs::mm(self.width)
- }
-
- /// The height of the paper.
- pub fn height(self) -> Abs {
- Abs::mm(self.height)
- }
-}
-
-/// Defines paper constants and a paper parsing implementation.
-macro_rules! papers {
- ($(($var:ident: $width:expr, $height: expr, $($pats:tt)*))*) => {
- /// Predefined papers.
- ///
- /// Each paper is parsable from its name in kebab-case.
- impl Paper {
- $(pub const $var: Self = Self { width: $width, height: $height };)*
- }
-
- impl FromStr for Paper {
- type Err = &'static str;
-
- fn from_str(name: &str) -> Result<Self, Self::Err> {
- match name.to_lowercase().as_str() {
- $($($pats)* => Ok(Self::$var),)*
- _ => Err("invalid paper name"),
- }
- }
- }
- };
-}
-
-castable! {
- Paper,
- Expected: "string",
- Value::Str(string) => Self::from_str(&string)?,
-}
-
-// All paper sizes in mm.
-//
-// Resources:
-// - https://papersizes.io/
-// - https://en.wikipedia.org/wiki/Paper_size
-// - https://www.theedkins.co.uk/jo/units/oldunits/print.htm
-// - https://vintagepaper.co/blogs/news/traditional-paper-sizes
-papers! {
- // ---------------------------------------------------------------------- //
- // ISO 216 A Series
- (A0: 841.0, 1189.0, "a0")
- (A1: 594.0, 841.0, "a1")
- (A2: 420.0, 594.0, "a2")
- (A3: 297.0, 420.0, "a3")
- (A4: 210.0, 297.0, "a4")
- (A5: 148.0, 210.0, "a5")
- (A6: 105.0, 148.0, "a6")
- (A7: 74.0, 105.0, "a7")
- (A8: 52.0, 74.0, "a8")
- (A9: 37.0, 52.0, "a9")
- (A10: 26.0, 37.0, "a10")
- (A11: 18.0, 26.0, "a11")
-
- // ISO 216 B Series
- (ISO_B1: 707.0, 1000.0, "iso-b1")
- (ISO_B2: 500.0, 707.0, "iso-b2")
- (ISO_B3: 353.0, 500.0, "iso-b3")
- (ISO_B4: 250.0, 353.0, "iso-b4")
- (ISO_B5: 176.0, 250.0, "iso-b5")
- (ISO_B6: 125.0, 176.0, "iso-b6")
- (ISO_B7: 88.0, 125.0, "iso-b7")
- (ISO_B8: 62.0, 88.0, "iso-b8")
-
- // ISO 216 C Series
- (ISO_C3: 324.0, 458.0, "iso-c3")
- (ISO_C4: 229.0, 324.0, "iso-c4")
- (ISO_C5: 162.0, 229.0, "iso-c5")
- (ISO_C6: 114.0, 162.0, "iso-c6")
- (ISO_C7: 81.0, 114.0, "iso-c7")
- (ISO_C8: 57.0, 81.0, "iso-c8")
-
- // DIN D Series (extension to ISO)
- (DIN_D3: 272.0, 385.0, "din-d3")
- (DIN_D4: 192.0, 272.0, "din-d4")
- (DIN_D5: 136.0, 192.0, "din-d5")
- (DIN_D6: 96.0, 136.0, "din-d6")
- (DIN_D7: 68.0, 96.0, "din-d7")
- (DIN_D8: 48.0, 68.0, "din-d8")
-
- // SIS (used in academia)
- (SIS_G5: 169.0, 239.0, "sis-g5")
- (SIS_E5: 115.0, 220.0, "sis-e5")
-
- // ANSI Extensions
- (ANSI_A: 216.0, 279.0, "ansi-a")
- (ANSI_B: 279.0, 432.0, "ansi-b")
- (ANSI_C: 432.0, 559.0, "ansi-c")
- (ANSI_D: 559.0, 864.0, "ansi-d")
- (ANSI_E: 864.0, 1118.0, "ansi-e")
-
- // ANSI Architectural Paper
- (ARCH_A: 229.0, 305.0, "arch-a")
- (ARCH_B: 305.0, 457.0, "arch-b")
- (ARCH_C: 457.0, 610.0, "arch-c")
- (ARCH_D: 610.0, 914.0, "arch-d")
- (ARCH_E1: 762.0, 1067.0, "arch-e1")
- (ARCH_E: 914.0, 1219.0, "arch-e")
-
- // JIS B Series
- (JIS_B0: 1030.0, 1456.0, "jis-b0")
- (JIS_B1: 728.0, 1030.0, "jis-b1")
- (JIS_B2: 515.0, 728.0, "jis-b2")
- (JIS_B3: 364.0, 515.0, "jis-b3")
- (JIS_B4: 257.0, 364.0, "jis-b4")
- (JIS_B5: 182.0, 257.0, "jis-b5")
- (JIS_B6: 128.0, 182.0, "jis-b6")
- (JIS_B7: 91.0, 128.0, "jis-b7")
- (JIS_B8: 64.0, 91.0, "jis-b8")
- (JIS_B9: 45.0, 64.0, "jis-b9")
- (JIS_B10: 32.0, 45.0, "jis-b10")
- (JIS_B11: 22.0, 32.0, "jis-b11")
-
- // SAC D Series
- (SAC_D0: 764.0, 1064.0, "sac-d0")
- (SAC_D1: 532.0, 760.0, "sac-d1")
- (SAC_D2: 380.0, 528.0, "sac-d2")
- (SAC_D3: 264.0, 376.0, "sac-d3")
- (SAC_D4: 188.0, 260.0, "sac-d4")
- (SAC_D5: 130.0, 184.0, "sac-d5")
- (SAC_D6: 92.0, 126.0, "sac-d6")
-
- // ISO 7810 ID
- (ISO_ID_1: 85.6, 53.98, "iso-id-1")
- (ISO_ID_2: 74.0, 105.0, "iso-id-2")
- (ISO_ID_3: 88.0, 125.0, "iso-id-3")
-
- // ---------------------------------------------------------------------- //
- // Asia
- (ASIA_F4: 210.0, 330.0, "asia-f4")
-
- // Japan
- (JP_SHIROKU_BAN_4: 264.0, 379.0, "jp-shiroku-ban-4")
- (JP_SHIROKU_BAN_5: 189.0, 262.0, "jp-shiroku-ban-5")
- (JP_SHIROKU_BAN_6: 127.0, 188.0, "jp-shiroku-ban-6")
- (JP_KIKU_4: 227.0, 306.0, "jp-kiku-4")
- (JP_KIKU_5: 151.0, 227.0, "jp-kiku-5")
- (JP_BUSINESS_CARD: 91.0, 55.0, "jp-business-card")
-
- // China
- (CN_BUSINESS_CARD: 90.0, 54.0, "cn-business-card")
-
- // Europe
- (EU_BUSINESS_CARD: 85.0, 55.0, "eu-business-card")
-
- // French Traditional (AFNOR)
- (FR_TELLIERE: 340.0, 440.0, "fr-tellière")
- (FR_COURONNE_ECRITURE: 360.0, 460.0, "fr-couronne-écriture")
- (FR_COURONNE_EDITION: 370.0, 470.0, "fr-couronne-édition")
- (FR_RAISIN: 500.0, 650.0, "fr-raisin")
- (FR_CARRE: 450.0, 560.0, "fr-carré")
- (FR_JESUS: 560.0, 760.0, "fr-jésus")
-
- // United Kingdom Imperial
- (UK_BRIEF: 406.4, 342.9, "uk-brief")
- (UK_DRAFT: 254.0, 406.4, "uk-draft")
- (UK_FOOLSCAP: 203.2, 330.2, "uk-foolscap")
- (UK_QUARTO: 203.2, 254.0, "uk-quarto")
- (UK_CROWN: 508.0, 381.0, "uk-crown")
- (UK_BOOK_A: 111.0, 178.0, "uk-book-a")
- (UK_BOOK_B: 129.0, 198.0, "uk-book-b")
-
- // Unites States
- (US_LETTER: 215.9, 279.4, "us-letter")
- (US_LEGAL: 215.9, 355.6, "us-legal")
- (US_TABLOID: 279.4, 431.8, "us-tabloid")
- (US_EXECUTIVE: 84.15, 266.7, "us-executive")
- (US_FOOLSCAP_FOLIO: 215.9, 342.9, "us-foolscap-folio")
- (US_STATEMENT: 139.7, 215.9, "us-statement")
- (US_LEDGER: 431.8, 279.4, "us-ledger")
- (US_OFICIO: 215.9, 340.36, "us-oficio")
- (US_GOV_LETTER: 203.2, 266.7, "us-gov-letter")
- (US_GOV_LEGAL: 215.9, 330.2, "us-gov-legal")
- (US_BUSINESS_CARD: 88.9, 50.8, "us-business-card")
- (US_DIGEST: 139.7, 215.9, "us-digest")
- (US_TRADE: 152.4, 228.6, "us-trade")
-
- // ---------------------------------------------------------------------- //
- // Other
- (NEWSPAPER_COMPACT: 280.0, 430.0, "newspaper-compact")
- (NEWSPAPER_BERLINER: 315.0, 470.0, "newspaper-berliner")
- (NEWSPAPER_BROADSHEET: 381.0, 578.0, "newspaper-broadsheet")
- (PRESENTATION_16_9: 297.0, 167.0625, "presentation-16-9")
- (PRESENTATION_4_3: 280.0, 210.0, "presentation-4-3")
-}
diff --git a/src/library/layout/place.rs b/src/library/layout/place.rs
deleted file mode 100644
index ee38ebe6..00000000
--- a/src/library/layout/place.rs
+++ /dev/null
@@ -1,56 +0,0 @@
-use super::AlignNode;
-use crate::library::prelude::*;
-
-/// Place content at an absolute position.
-#[derive(Debug, Hash)]
-pub struct PlaceNode(pub Content);
-
-#[node(LayoutBlock)]
-impl PlaceNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let aligns = args.find()?.unwrap_or(Axes::with_x(Some(RawAlign::Start)));
- let dx = args.named("dx")?.unwrap_or_default();
- let dy = args.named("dy")?.unwrap_or_default();
- let body = args.expect::<Content>("body")?;
- Ok(Self(body.moved(Axes::new(dx, dy)).aligned(aligns)).pack())
- }
-}
-
-impl LayoutBlock for PlaceNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let out_of_flow = self.out_of_flow();
-
- // The pod is the base area of the region because for absolute
- // placement we don't really care about the already used area.
- let pod = {
- let finite = regions.base.map(Abs::is_finite);
- let expand = finite & (regions.expand | out_of_flow);
- Regions::one(regions.base, regions.base, expand)
- };
-
- let mut frames = self.0.layout_block(world, &pod, styles)?;
-
- // If expansion is off, zero all sizes so that we don't take up any
- // space in our parent. Otherwise, respect the expand settings.
- let target = regions.expand.select(regions.first, Size::zero());
- frames[0].resize(target, Align::LEFT_TOP);
-
- Ok(frames)
- }
-}
-
-impl PlaceNode {
- /// Whether this node wants to be placed relative to its its parent's base
- /// origin. Instead of relative to the parent's current flow/cursor
- /// position.
- pub fn out_of_flow(&self) -> bool {
- self.0
- .downcast::<AlignNode>()
- .map_or(false, |node| node.aligns.y.is_some())
- }
-}
diff --git a/src/library/layout/spacing.rs b/src/library/layout/spacing.rs
deleted file mode 100644
index c410eee7..00000000
--- a/src/library/layout/spacing.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use std::cmp::Ordering;
-
-use crate::library::prelude::*;
-use crate::library::text::ParNode;
-
-/// Horizontal spacing.
-#[derive(Debug, Clone, Hash)]
-pub struct HNode {
- pub amount: Spacing,
- pub weak: bool,
-}
-
-#[node]
-impl HNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let amount = args.expect("spacing")?;
- let weak = args.named("weak")?.unwrap_or(false);
- Ok(Self { amount, weak }.pack())
- }
-}
-
-/// Vertical spacing.
-#[derive(Debug, Clone, Hash)]
-pub struct VNode {
- pub amount: Spacing,
- pub weak: bool,
- pub generated: bool,
-}
-
-#[node]
-impl VNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let amount = args.expect("spacing")?;
- let weak = args.named("weak")?.unwrap_or(false);
- Ok(Self { amount, weak, generated: false }.pack())
- }
-}
-
-/// Kinds of spacing.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Spacing {
- /// Spacing specified in absolute terms and relative to the parent's size.
- Relative(Rel<Length>),
- /// Spacing specified as a fraction of the remaining free space in the
- /// parent.
- Fractional(Fr),
-}
-
-impl Spacing {
- /// Whether this is fractional spacing.
- pub fn is_fractional(self) -> bool {
- matches!(self, Self::Fractional(_))
- }
-}
-
-impl From<Abs> for Spacing {
- fn from(abs: Abs) -> Self {
- Self::Relative(abs.into())
- }
-}
-
-impl PartialOrd for Spacing {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- match (self, other) {
- (Self::Relative(a), Self::Relative(b)) => a.partial_cmp(b),
- (Self::Fractional(a), Self::Fractional(b)) => a.partial_cmp(b),
- _ => None,
- }
- }
-}
-
-castable! {
- Spacing,
- Expected: "relative length or fraction",
- Value::Length(v) => Self::Relative(v.into()),
- Value::Ratio(v) => Self::Relative(v.into()),
- Value::Relative(v) => Self::Relative(v),
- Value::Fraction(v) => Self::Fractional(v),
-}
-
-/// Spacing around and between blocks, relative to paragraph spacing.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct BlockSpacing(Rel<Length>);
-
-castable!(BlockSpacing: Rel<Length>);
-
-impl Resolve for BlockSpacing {
- type Output = Abs;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- let whole = styles.get(ParNode::SPACING);
- self.0.resolve(styles).relative_to(whole)
- }
-}
-
-impl From<Ratio> for BlockSpacing {
- fn from(ratio: Ratio) -> Self {
- Self(ratio.into())
- }
-}
diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs
deleted file mode 100644
index e1e70de9..00000000
--- a/src/library/layout/stack.rs
+++ /dev/null
@@ -1,321 +0,0 @@
-use super::{AlignNode, Spacing};
-use crate::library::prelude::*;
-use crate::library::text::ParNode;
-use crate::model::StyledNode;
-
-/// Arrange content and spacing along an axis.
-#[derive(Debug, Hash)]
-pub struct StackNode {
- /// The stacking direction.
- pub dir: Dir,
- /// The spacing between non-spacing children.
- pub spacing: Option<Spacing>,
- /// The children to be stacked.
- pub children: Vec<StackChild>,
-}
-
-#[node(LayoutBlock)]
-impl StackNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self {
- dir: args.named("dir")?.unwrap_or(Dir::TTB),
- spacing: args.named("spacing")?,
- children: args.all()?,
- }
- .pack())
- }
-}
-
-impl LayoutBlock for StackNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut layouter = StackLayouter::new(self.dir, regions, styles);
-
- // Spacing to insert before the next block.
- let mut deferred = None;
-
- for child in &self.children {
- match child {
- StackChild::Spacing(kind) => {
- layouter.layout_spacing(*kind);
- deferred = None;
- }
- StackChild::Block(block) => {
- if let Some(kind) = deferred {
- layouter.layout_spacing(kind);
- }
-
- layouter.layout_block(world, block, styles)?;
- deferred = self.spacing;
- }
- }
- }
-
- Ok(layouter.finish())
- }
-}
-
-/// A child of a stack node.
-#[derive(Hash)]
-pub enum StackChild {
- /// Spacing between other children.
- Spacing(Spacing),
- /// Arbitrary block-level content.
- Block(Content),
-}
-
-impl Debug for StackChild {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Spacing(kind) => kind.fmt(f),
- Self::Block(block) => block.fmt(f),
- }
- }
-}
-
-castable! {
- StackChild,
- Expected: "relative length, fraction, or content",
- Value::Length(v) => Self::Spacing(Spacing::Relative(v.into())),
- Value::Ratio(v) => Self::Spacing(Spacing::Relative(v.into())),
- Value::Relative(v) => Self::Spacing(Spacing::Relative(v)),
- Value::Fraction(v) => Self::Spacing(Spacing::Fractional(v)),
- Value::Content(v) => Self::Block(v),
-}
-
-/// Performs stack layout.
-pub struct StackLayouter<'a> {
- /// The stacking direction.
- dir: Dir,
- /// The axis of the stacking direction.
- axis: Axis,
- /// The regions to layout children into.
- regions: Regions,
- /// The inherited styles.
- styles: StyleChain<'a>,
- /// Whether the stack itself should expand to fill the region.
- expand: Axes<bool>,
- /// The full size of the current region that was available at the start.
- full: Size,
- /// The generic size used by the frames for the current region.
- used: Gen<Abs>,
- /// The sum of fractions in the current region.
- fr: Fr,
- /// Already layouted items whose exact positions are not yet known due to
- /// fractional spacing.
- items: Vec<StackItem>,
- /// Finished frames for previous regions.
- finished: Vec<Frame>,
-}
-
-/// A prepared item in a stack layout.
-enum StackItem {
- /// Absolute spacing between other items.
- Absolute(Abs),
- /// Fractional spacing between other items.
- Fractional(Fr),
- /// A frame for a layouted block.
- Frame(Frame, Align),
-}
-
-impl<'a> StackLayouter<'a> {
- /// Create a new stack layouter.
- pub fn new(dir: Dir, regions: &Regions, styles: StyleChain<'a>) -> Self {
- let axis = dir.axis();
- let expand = regions.expand;
- let full = regions.first;
-
- // Disable expansion along the block axis for children.
- let mut regions = regions.clone();
- regions.expand.set(axis, false);
-
- Self {
- dir,
- axis,
- regions,
- styles,
- expand,
- full,
- used: Gen::zero(),
- fr: Fr::zero(),
- items: vec![],
- finished: vec![],
- }
- }
-
- /// Add spacing along the spacing direction.
- pub fn layout_spacing(&mut self, spacing: Spacing) {
- match spacing {
- Spacing::Relative(v) => {
- // Resolve the spacing and limit it to the remaining space.
- let resolved =
- v.resolve(self.styles).relative_to(self.regions.base.get(self.axis));
- let remaining = self.regions.first.get_mut(self.axis);
- let limited = resolved.min(*remaining);
- *remaining -= limited;
- self.used.main += limited;
- self.items.push(StackItem::Absolute(resolved));
- }
- Spacing::Fractional(v) => {
- self.fr += v;
- self.items.push(StackItem::Fractional(v));
- }
- }
- }
-
- /// Layout an arbitrary block.
- pub fn layout_block(
- &mut self,
- world: Tracked<dyn World>,
- block: &Content,
- styles: StyleChain,
- ) -> SourceResult<()> {
- if self.regions.is_full() {
- self.finish_region();
- }
-
- // Block-axis alignment of the `AlignNode` is respected
- // by the stack node.
- let align = block
- .downcast::<AlignNode>()
- .and_then(|node| node.aligns.get(self.axis))
- .map(|align| align.resolve(styles))
- .unwrap_or_else(|| {
- if let Some(styled) = block.downcast::<StyledNode>() {
- let map = &styled.map;
- if map.contains(ParNode::ALIGN) {
- return StyleChain::with_root(map).get(ParNode::ALIGN);
- }
- }
-
- self.dir.start().into()
- });
-
- let frames = block.layout_block(world, &self.regions, styles)?;
- let len = frames.len();
- for (i, mut frame) in frames.into_iter().enumerate() {
- // Set the generic block role.
- frame.apply_role(Role::GenericBlock);
-
- // Grow our size, shrink the region and save the frame for later.
- let size = frame.size();
- let size = match self.axis {
- Axis::X => Gen::new(size.y, size.x),
- Axis::Y => Gen::new(size.x, size.y),
- };
-
- self.used.main += size.main;
- self.used.cross.set_max(size.cross);
- *self.regions.first.get_mut(self.axis) -= size.main;
- self.items.push(StackItem::Frame(frame, align));
-
- if i + 1 < len {
- self.finish_region();
- }
- }
-
- Ok(())
- }
-
- /// Advance to the next region.
- pub fn finish_region(&mut self) {
- // Determine the size of the stack in this region dependening on whether
- // the region expands.
- let used = self.used.to_axes(self.axis);
- let mut size = self.expand.select(self.full, used);
-
- // Expand fully if there are fr spacings.
- let full = self.full.get(self.axis);
- let remaining = full - self.used.main;
- if self.fr.get() > 0.0 && full.is_finite() {
- self.used.main = full;
- size.set(self.axis, full);
- }
-
- let mut output = Frame::new(size);
- let mut cursor = Abs::zero();
- let mut ruler: Align = self.dir.start().into();
-
- // Place all frames.
- for item in self.items.drain(..) {
- match item {
- StackItem::Absolute(v) => cursor += v,
- StackItem::Fractional(v) => cursor += v.share(self.fr, remaining),
- StackItem::Frame(frame, align) => {
- if self.dir.is_positive() {
- ruler = ruler.max(align);
- } else {
- ruler = ruler.min(align);
- }
-
- // Align along the block axis.
- let parent = size.get(self.axis);
- let child = frame.size().get(self.axis);
- let block = ruler.position(parent - self.used.main)
- + if self.dir.is_positive() {
- cursor
- } else {
- self.used.main - child - cursor
- };
-
- let pos = Gen::new(Abs::zero(), block).to_point(self.axis);
- cursor += child;
- output.push_frame(pos, frame);
- }
- }
- }
-
- // Advance to the next region.
- self.regions.next();
- self.full = self.regions.first;
- self.used = Gen::zero();
- self.fr = Fr::zero();
- self.finished.push(output);
- }
-
- /// Finish layouting and return the resulting frames.
- pub fn finish(mut self) -> Vec<Frame> {
- self.finish_region();
- self.finished
- }
-}
-
-/// A container with a main and cross component.
-#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Gen<T> {
- /// The main component.
- pub cross: T,
- /// The cross component.
- pub main: T,
-}
-
-impl<T> Gen<T> {
- /// Create a new instance from the two components.
- pub const fn new(cross: T, main: T) -> Self {
- Self { cross, main }
- }
-
- /// Convert to the specific representation, given the current main axis.
- pub fn to_axes(self, main: Axis) -> Axes<T> {
- match main {
- Axis::X => Axes::new(self.main, self.cross),
- Axis::Y => Axes::new(self.cross, self.main),
- }
- }
-}
-
-impl Gen<Abs> {
- /// The zero value.
- pub fn zero() -> Self {
- Self { cross: Abs::zero(), main: Abs::zero() }
- }
-
- /// Convert to a point.
- pub fn to_point(self, main: Axis) -> Point {
- self.to_axes(main).to_point()
- }
-}
diff --git a/src/library/layout/transform.rs b/src/library/layout/transform.rs
deleted file mode 100644
index a73a1827..00000000
--- a/src/library/layout/transform.rs
+++ /dev/null
@@ -1,116 +0,0 @@
-use crate::geom::Transform;
-use crate::library::prelude::*;
-
-/// Move content without affecting layout.
-#[derive(Debug, Hash)]
-pub struct MoveNode {
- /// The offset by which to move the content.
- pub delta: Axes<Rel<Length>>,
- /// The content that should be moved.
- pub child: Content,
-}
-
-#[node(LayoutInline)]
-impl MoveNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let dx = args.named("dx")?.unwrap_or_default();
- let dy = args.named("dy")?.unwrap_or_default();
- Ok(Self {
- delta: Axes::new(dx, dy),
- child: args.expect("body")?,
- }
- .pack())
- }
-}
-
-impl LayoutInline for MoveNode {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames = self.child.layout_inline(world, regions, styles)?;
-
- let delta = self.delta.resolve(styles);
- for frame in &mut frames {
- let delta = delta.zip(frame.size()).map(|(d, s)| d.relative_to(s));
- frame.translate(delta.to_point());
- }
-
- Ok(frames)
- }
-}
-
-/// Transform content without affecting layout.
-#[derive(Debug, Hash)]
-pub struct TransformNode<const T: TransformKind> {
- /// Transformation to apply to the content.
- pub transform: Transform,
- /// The content that should be transformed.
- pub child: Content,
-}
-
-/// Rotate content without affecting layout.
-pub type RotateNode = TransformNode<ROTATE>;
-
-/// Scale content without affecting layout.
-pub type ScaleNode = TransformNode<SCALE>;
-
-#[node(LayoutInline)]
-impl<const T: TransformKind> TransformNode<T> {
- /// The origin of the transformation.
- #[property(resolve)]
- pub const ORIGIN: Axes<Option<RawAlign>> = Axes::default();
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let transform = match T {
- ROTATE => {
- let angle = args.named_or_find("angle")?.unwrap_or_default();
- Transform::rotate(angle)
- }
- SCALE | _ => {
- let all = args.find()?;
- let sx = args.named("x")?.or(all).unwrap_or(Ratio::one());
- let sy = args.named("y")?.or(all).unwrap_or(Ratio::one());
- Transform::scale(sx, sy)
- }
- };
-
- Ok(Self { transform, child: args.expect("body")? }.pack())
- }
-}
-
-impl<const T: TransformKind> LayoutInline for TransformNode<T> {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let origin = styles.get(Self::ORIGIN).unwrap_or(Align::CENTER_HORIZON);
- let mut frames = self.child.layout_inline(world, regions, styles)?;
-
- for frame in &mut frames {
- let Axes { x, y } = origin.zip(frame.size()).map(|(o, s)| o.position(s));
- let transform = Transform::translate(x, y)
- .pre_concat(self.transform)
- .pre_concat(Transform::translate(-x, -y));
-
- frame.transform(transform);
- }
-
- Ok(frames)
- }
-}
-
-/// Kinds of transformations.
-///
-/// The move transformation is handled separately.
-pub type TransformKind = usize;
-
-/// A rotational transformation.
-const ROTATE: TransformKind = 1;
-
-/// A scale transformation.
-const SCALE: TransformKind = 2;
diff --git a/src/library/math/frac.rs b/src/library/math/frac.rs
deleted file mode 100644
index 791fd19a..00000000
--- a/src/library/math/frac.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-use super::*;
-use crate::library::prelude::*;
-
-/// A fraction in a mathematical formula.
-#[derive(Debug, Hash)]
-pub struct FracNode {
- /// The numerator.
- pub num: MathNode,
- /// The denominator.
- pub denom: MathNode,
-}
-
-impl Texify for FracNode {
- fn texify(&self) -> EcoString {
- format_eco!("\\frac{{{}}}{{{}}}", self.num.texify(), self.denom.texify())
- }
-}
diff --git a/src/library/math/mod.rs b/src/library/math/mod.rs
deleted file mode 100644
index 5bb5054d..00000000
--- a/src/library/math/mod.rs
+++ /dev/null
@@ -1,295 +0,0 @@
-//! Mathematical formulas.
-
-mod frac;
-mod script;
-
-pub use frac::*;
-pub use script::*;
-
-use rex::error::{Error, LayoutError};
-use rex::font::FontContext;
-use rex::layout::{LayoutSettings, Style};
-use rex::parser::color::RGBA;
-use rex::render::{Backend, Cursor, Renderer};
-
-use crate::font::Font;
-use crate::library::layout::BlockSpacing;
-use crate::library::prelude::*;
-use crate::library::text::{variant, FontFamily, TextNode};
-
-/// A piece of a mathematical formula.
-#[derive(Debug, Clone, Hash)]
-pub enum MathNode {
- /// Whitespace.
- Space,
- /// A forced line break.
- Linebreak,
- /// An atom in a math formula: `x`, `+`, `12`.
- Atom(EcoString),
- /// A base with optional sub and superscripts: `a_1^2`.
- Script(Arc<ScriptNode>),
- /// A fraction: `x/2`.
- Frac(Arc<FracNode>),
- /// A numbered math alignment indicator: `&`, `&&`.
- Align(usize),
- /// A row of mathematical material.
- Row(Arc<Vec<MathNode>>, Span),
-}
-
-#[node(Show, LayoutInline)]
-impl MathNode {
- /// The math font family.
- #[property(referenced)]
- pub const FAMILY: FontFamily = FontFamily::new("NewComputerModernMath");
- /// The spacing above display math.
- #[property(resolve, shorthand(around))]
- pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
- /// The spacing below display math.
- #[property(resolve, shorthand(around))]
- pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
-
- fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
- todo!()
- }
-}
-
-impl MathNode {
- /// Strip parentheses from the node.
- pub fn unparen(self) -> Self {
- if let Self::Row(row, span) = &self {
- if let [MathNode::Atom(l), .., MathNode::Atom(r)] = row.as_slice() {
- if l == "(" && r == ")" {
- let inner = row[1 .. row.len() - 1].to_vec();
- return Self::Row(Arc::new(inner), *span);
- }
- }
- }
-
- self
- }
-
- /// Whether the formula is display level.
- pub fn display(&self) -> bool {
- if let Self::Row(row, _) = self {
- matches!(row.as_slice(), [MathNode::Space, .., MathNode::Space])
- } else {
- false
- }
- }
-}
-
-impl Show for MathNode {
- fn unguard_parts(&self, _: Selector) -> Content {
- self.clone().pack()
- }
-
- fn field(&self, _: &str) -> Option<Value> {
- None
- }
-
- fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
- Ok(if self.display() {
- self.clone().pack().aligned(Axes::with_x(Some(Align::Center.into())))
- } else {
- self.clone().pack()
- })
- }
-
- fn finalize(
- &self,
- _: Tracked<dyn World>,
- styles: StyleChain,
- realized: Content,
- ) -> SourceResult<Content> {
- Ok(if self.display() {
- realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW))
- } else {
- realized
- })
- }
-}
-
-impl LayoutInline for MathNode {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- _: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let style = if self.display() { Style::Display } else { Style::Text };
- let span = match self {
- &Self::Row(_, span) => span,
- _ => Span::detached(),
- };
-
- Ok(vec![layout_tex(world, self, span, style, styles)?])
- }
-}
-
-/// Layout a TeX formula into a frame.
-fn layout_tex(
- world: Tracked<dyn World>,
- node: &dyn Texify,
- span: Span,
- style: Style,
- styles: StyleChain,
-) -> SourceResult<Frame> {
- let tex = node.texify();
-
- // Load the font.
- let font = world
- .book()
- .select(styles.get(MathNode::FAMILY).as_str(), variant(styles))
- .and_then(|id| world.font(id))
- .ok_or("failed to find math font")
- .at(span)?;
-
- // Prepare the font context.
- let ctx = font
- .math()
- .map(|math| FontContext::new(font.ttf(), math))
- .ok_or("font is not suitable for math")
- .at(span)?;
-
- // Layout the formula.
- let em = styles.get(TextNode::SIZE);
- let settings = LayoutSettings::new(&ctx, em.to_pt(), style);
- let renderer = Renderer::new();
- let layout = renderer
- .layout(&tex, settings)
- .map_err(|err| match err {
- Error::Parse(err) => err.to_string(),
- Error::Layout(LayoutError::Font(err)) => err.to_string(),
- })
- .at(span)?;
-
- // Determine the metrics.
- let (x0, y0, x1, y1) = renderer.size(&layout);
- let width = Abs::pt(x1 - x0);
- let mut top = Abs::pt(y1);
- let mut bottom = Abs::pt(-y0);
- if style != Style::Display {
- let metrics = font.metrics();
- top = styles.get(TextNode::TOP_EDGE).resolve(styles, metrics);
- bottom = -styles.get(TextNode::BOTTOM_EDGE).resolve(styles, metrics);
- };
-
- // Prepare a frame rendering backend.
- let size = Size::new(width, top + bottom);
- let mut backend = FrameBackend {
- frame: {
- let mut frame = Frame::new(size);
- frame.set_baseline(top);
- frame.apply_role(Role::Formula);
- frame
- },
- baseline: top,
- font: font.clone(),
- fill: styles.get(TextNode::FILL),
- lang: styles.get(TextNode::LANG),
- colors: vec![],
- };
-
- // Render into the frame.
- renderer.render(&layout, &mut backend);
- Ok(backend.frame)
-}
-
-/// A ReX rendering backend that renders into a frame.
-struct FrameBackend {
- frame: Frame,
- baseline: Abs,
- font: Font,
- fill: Paint,
- lang: Lang,
- colors: Vec<RGBA>,
-}
-
-impl FrameBackend {
- /// The currently active fill paint.
- fn fill(&self) -> Paint {
- self.colors
- .last()
- .map(|&RGBA(r, g, b, a)| RgbaColor::new(r, g, b, a).into())
- .unwrap_or(self.fill)
- }
-
- /// Convert a cursor to a point.
- fn transform(&self, cursor: Cursor) -> Point {
- Point::new(Abs::pt(cursor.x), self.baseline + Abs::pt(cursor.y))
- }
-}
-
-impl Backend for FrameBackend {
- fn symbol(&mut self, pos: Cursor, gid: u16, scale: f64) {
- self.frame.push(
- self.transform(pos),
- Element::Text(Text {
- font: self.font.clone(),
- size: Abs::pt(scale),
- fill: self.fill(),
- lang: self.lang,
- glyphs: vec![Glyph {
- id: gid,
- x_advance: Em::new(0.0),
- x_offset: Em::new(0.0),
- c: ' ',
- }],
- }),
- );
- }
-
- fn rule(&mut self, pos: Cursor, width: f64, height: f64) {
- self.frame.push(
- self.transform(pos),
- Element::Shape(Shape {
- geometry: Geometry::Rect(Size::new(Abs::pt(width), Abs::pt(height))),
- fill: Some(self.fill()),
- stroke: None,
- }),
- );
- }
-
- fn begin_color(&mut self, color: RGBA) {
- self.colors.push(color);
- }
-
- fn end_color(&mut self) {
- self.colors.pop();
- }
-}
-
-/// Turn a math node into TeX math code.
-trait Texify {
- /// Perform the conversion.
- fn texify(&self) -> EcoString;
-}
-
-impl Texify for MathNode {
- fn texify(&self) -> EcoString {
- match self {
- Self::Space => "".into(),
- Self::Linebreak => r"\\".into(),
- Self::Atom(atom) => atom.chars().map(escape_char).collect(),
- Self::Script(script) => script.texify(),
- Self::Frac(frac) => frac.texify(),
- Self::Align(_) => "".into(),
- Self::Row(row, _) => row.iter().map(Texify::texify).collect(),
- }
- }
-}
-
-#[rustfmt::skip]
-fn escape_char(c: char) -> EcoString {
- match c {
- '{' | '}' | '%' | '&' | '$' | '#' => format_eco!(" \\{c} "),
- 'a' ..= 'z' | 'A' ..= 'Z' | '0' ..= '9' | 'Α' ..= 'Ω' | 'α' ..= 'ω' |
- '*' | '+' | '-' | '[' | '(' | ']' | ')' | '?' | '!' | '=' | '<' | '>' |
- ':' | ',' | ';' | '|' | '/' | '@' | '.' | '"' => c.into(),
- c => unicode_math::SYMBOLS
- .iter()
- .find(|sym| sym.codepoint == c)
- .map(|sym| format_eco!("\\{} ", sym.name))
- .unwrap_or_default(),
- }
-}
diff --git a/src/library/math/script.rs b/src/library/math/script.rs
deleted file mode 100644
index 09f52164..00000000
--- a/src/library/math/script.rs
+++ /dev/null
@@ -1,31 +0,0 @@
-use std::fmt::Write;
-
-use super::*;
-use crate::library::prelude::*;
-
-/// A sub- and/or superscript in a mathematical formula.
-#[derive(Debug, Hash)]
-pub struct ScriptNode {
- /// The base.
- pub base: MathNode,
- /// The subscript.
- pub sub: Option<MathNode>,
- /// The superscript.
- pub sup: Option<MathNode>,
-}
-
-impl Texify for ScriptNode {
- fn texify(&self) -> EcoString {
- let mut tex = self.base.texify();
-
- if let Some(sub) = &self.sub {
- write!(tex, "_{{{}}}", sub.texify()).unwrap();
- }
-
- if let Some(sup) = &self.sup {
- write!(tex, "^{{{}}}", sup.texify()).unwrap();
- }
-
- tex
- }
-}
diff --git a/src/library/mod.rs b/src/library/mod.rs
deleted file mode 100644
index 184c515e..00000000
--- a/src/library/mod.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-//! The standard library.
-//!
-//! Call [`scope`] to obtain a [`Scope`] containing all standard library
-//! definitions.
-
-pub mod graphics;
-pub mod layout;
-pub mod math;
-pub mod prelude;
-pub mod structure;
-pub mod text;
-pub mod utility;
-
-mod ext;
-mod raw;
-
-pub use raw::*;
-
-use crate::geom::{Align, Color, Dir};
-use crate::model::{Node, Scope};
-use crate::LangItems;
-
-/// Construct a scope containing all standard library definitions.
-pub fn scope() -> Scope {
- let mut std = Scope::new();
-
- // Text.
- std.def_node::<text::SpaceNode>("space");
- std.def_node::<text::LinebreakNode>("linebreak");
- std.def_node::<text::SmartQuoteNode>("smartquote");
- std.def_node::<text::TextNode>("text");
- std.def_node::<text::ParNode>("par");
- std.def_node::<text::ParbreakNode>("parbreak");
- std.def_node::<text::StrongNode>("strong");
- std.def_node::<text::EmphNode>("emph");
- std.def_node::<text::RawNode>("raw");
- std.def_node::<text::UnderlineNode>("underline");
- std.def_node::<text::StrikethroughNode>("strike");
- std.def_node::<text::OverlineNode>("overline");
- std.def_node::<text::SuperNode>("super");
- std.def_node::<text::SubNode>("sub");
- std.def_node::<text::LinkNode>("link");
- std.def_node::<text::RepeatNode>("repeat");
- std.def_fn("lower", text::lower);
- std.def_fn("upper", text::upper);
- std.def_fn("smallcaps", text::smallcaps);
-
- // Structure.
- std.def_node::<structure::RefNode>("ref");
- std.def_node::<structure::HeadingNode>("heading");
- std.def_node::<structure::ListNode>("list");
- std.def_node::<structure::EnumNode>("enum");
- std.def_node::<structure::DescNode>("desc");
- std.def_node::<structure::TableNode>("table");
-
- // Layout.
- std.def_node::<layout::PageNode>("page");
- std.def_node::<layout::PagebreakNode>("pagebreak");
- std.def_node::<layout::HNode>("h");
- std.def_node::<layout::VNode>("v");
- std.def_node::<layout::BoxNode>("box");
- std.def_node::<layout::BlockNode>("block");
- std.def_node::<layout::AlignNode>("align");
- std.def_node::<layout::PadNode>("pad");
- std.def_node::<layout::StackNode>("stack");
- std.def_node::<layout::GridNode>("grid");
- std.def_node::<layout::ColumnsNode>("columns");
- std.def_node::<layout::ColbreakNode>("colbreak");
- std.def_node::<layout::PlaceNode>("place");
- std.def_node::<layout::MoveNode>("move");
- std.def_node::<layout::ScaleNode>("scale");
- std.def_node::<layout::RotateNode>("rotate");
-
- // Graphics.
- std.def_node::<graphics::ImageNode>("image");
- std.def_node::<graphics::LineNode>("line");
- std.def_node::<graphics::RectNode>("rect");
- std.def_node::<graphics::SquareNode>("square");
- std.def_node::<graphics::EllipseNode>("ellipse");
- std.def_node::<graphics::CircleNode>("circle");
- std.def_node::<graphics::HideNode>("hide");
-
- // Math.
- std.def_node::<math::MathNode>("math");
- std.define("sum", "∑");
- std.define("in", "∈");
- std.define("arrow", "→");
- std.define("NN", "ℕ");
- std.define("RR", "ℝ");
-
- // Utility.
- std.def_fn("type", utility::type_);
- std.def_fn("assert", utility::assert);
- std.def_fn("eval", utility::eval);
- std.def_fn("int", utility::int);
- std.def_fn("float", utility::float);
- std.def_fn("abs", utility::abs);
- std.def_fn("min", utility::min);
- std.def_fn("max", utility::max);
- std.def_fn("even", utility::even);
- std.def_fn("odd", utility::odd);
- std.def_fn("mod", utility::mod_);
- std.def_fn("range", utility::range);
- std.def_fn("luma", utility::luma);
- std.def_fn("rgb", utility::rgb);
- std.def_fn("cmyk", utility::cmyk);
- std.def_fn("repr", utility::repr);
- std.def_fn("str", utility::str);
- std.def_fn("regex", utility::regex);
- std.def_fn("letter", utility::letter);
- std.def_fn("roman", utility::roman);
- std.def_fn("symbol", utility::symbol);
- std.def_fn("lorem", utility::lorem);
- std.def_fn("csv", utility::csv);
- std.def_fn("json", utility::json);
- std.def_fn("xml", utility::xml);
-
- // Predefined colors.
- std.define("black", Color::BLACK);
- std.define("gray", Color::GRAY);
- std.define("silver", Color::SILVER);
- std.define("white", Color::WHITE);
- std.define("navy", Color::NAVY);
- std.define("blue", Color::BLUE);
- std.define("aqua", Color::AQUA);
- std.define("teal", Color::TEAL);
- std.define("eastern", Color::EASTERN);
- std.define("purple", Color::PURPLE);
- std.define("fuchsia", Color::FUCHSIA);
- std.define("maroon", Color::MAROON);
- std.define("red", Color::RED);
- std.define("orange", Color::ORANGE);
- std.define("yellow", Color::YELLOW);
- std.define("olive", Color::OLIVE);
- std.define("green", Color::GREEN);
- std.define("lime", Color::LIME);
-
- // Other constants.
- std.define("ltr", Dir::LTR);
- std.define("rtl", Dir::RTL);
- std.define("ttb", Dir::TTB);
- std.define("btt", Dir::BTT);
- std.define("start", RawAlign::Start);
- std.define("end", RawAlign::End);
- std.define("left", RawAlign::Specific(Align::Left));
- std.define("center", RawAlign::Specific(Align::Center));
- std.define("right", RawAlign::Specific(Align::Right));
- std.define("top", RawAlign::Specific(Align::Top));
- std.define("horizon", RawAlign::Specific(Align::Horizon));
- std.define("bottom", RawAlign::Specific(Align::Bottom));
-
- std
-}
-
-/// Construct the language map.
-pub fn items() -> LangItems {
- LangItems {
- space: || text::SpaceNode.pack(),
- linebreak: |justify| text::LinebreakNode { justify }.pack(),
- text: |text| text::TextNode(text).pack(),
- smart_quote: |double| text::SmartQuoteNode { double }.pack(),
- parbreak: || text::ParbreakNode.pack(),
- strong: |body| text::StrongNode(body).pack(),
- emph: |body| text::EmphNode(body).pack(),
- raw: |text, lang, block| {
- let content = text::RawNode { text, block }.pack();
- match lang {
- Some(_) => content.styled(text::RawNode::LANG, lang),
- None => content,
- }
- },
- link: |url| text::LinkNode::from_url(url).pack(),
- ref_: |target| structure::RefNode(target).pack(),
- heading: |level, body| structure::HeadingNode { level, body }.pack(),
- list_item: |body| structure::ListItem::List(Box::new(body)).pack(),
- enum_item: |number, body| {
- structure::ListItem::Enum(number, Box::new(body)).pack()
- },
- desc_item: |term, body| {
- structure::ListItem::Desc(Box::new(structure::DescItem { term, body })).pack()
- },
- }
-}
diff --git a/src/library/prelude.rs b/src/library/prelude.rs
deleted file mode 100644
index 66e35e68..00000000
--- a/src/library/prelude.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-//! Helpful imports for creating library functionality.
-
-pub use std::fmt::{self, Debug, Formatter};
-pub use std::hash::Hash;
-pub use std::io;
-pub use std::num::NonZeroUsize;
-pub use std::sync::Arc;
-
-pub use comemo::Tracked;
-
-pub use super::ext::{ContentExt, StyleMapExt};
-pub use super::layout::{Layout, LayoutBlock, LayoutInline, Regions};
-pub use super::text::TextNode;
-pub use super::{RawAlign, RawStroke};
-pub use crate::diag::{
- with_alternative, At, FileError, FileResult, SourceError, SourceResult, StrResult,
-};
-pub use crate::frame::*;
-pub use crate::geom::*;
-pub use crate::model::{
- capability, node, Arg, Args, Array, Capability, Cast, Content, Dict, Dynamic, Fold,
- Func, Key, Node, Resolve, Scope, Selector, Show, Smart, Str, StyleChain, StyleMap,
- StyleVec, Value, Vm,
-};
-pub use crate::syntax::{Span, Spanned};
-pub use crate::util::EcoString;
-pub use crate::{LangItems, World};
diff --git a/src/library/raw.rs b/src/library/raw.rs
deleted file mode 100644
index 67aa651d..00000000
--- a/src/library/raw.rs
+++ /dev/null
@@ -1,149 +0,0 @@
-use std::fmt::{self, Debug, Formatter};
-
-use crate::geom::{Abs, Align, Axes, Axis, Get, Length, Paint, Stroke};
-use crate::library::text::TextNode;
-use crate::model::{Fold, Resolve, Smart, StyleChain, Value};
-
-/// The unresolved alignment representation.
-#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub enum RawAlign {
- /// Align at the start side of the text direction.
- Start,
- /// Align at the end side of the text direction.
- End,
- /// Align at a specific alignment.
- Specific(Align),
-}
-
-impl Resolve for RawAlign {
- type Output = Align;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- let dir = styles.get(TextNode::DIR);
- match self {
- Self::Start => dir.start().into(),
- Self::End => dir.end().into(),
- Self::Specific(align) => align,
- }
- }
-}
-
-impl RawAlign {
- /// The axis this alignment belongs to.
- pub const fn axis(self) -> Axis {
- match self {
- Self::Start | Self::End => Axis::X,
- Self::Specific(align) => align.axis(),
- }
- }
-}
-
-impl From<Align> for RawAlign {
- fn from(align: Align) -> Self {
- Self::Specific(align)
- }
-}
-
-impl Debug for RawAlign {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Start => f.pad("start"),
- Self::End => f.pad("end"),
- Self::Specific(align) => align.fmt(f),
- }
- }
-}
-
-dynamic! {
- RawAlign: "alignment",
-}
-
-dynamic! {
- Axes<RawAlign>: "2d alignment",
-}
-
-castable! {
- Axes<Option<RawAlign>>,
- Expected: "1d or 2d alignment",
- @align: RawAlign => {
- let mut aligns = Axes::default();
- aligns.set(align.axis(), Some(*align));
- aligns
- },
- @aligns: Axes<RawAlign> => aligns.map(Some),
-}
-
-/// The unresolved stroke representation.
-///
-/// In this representation, both fields are optional so that you can pass either
-/// just a paint (`red`), just a thickness (`0.1em`) or both (`2pt + red`) where
-/// this is expected.
-#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct RawStroke<T = Length> {
- /// The stroke's paint.
- pub paint: Smart<Paint>,
- /// The stroke's thickness.
- pub thickness: Smart<T>,
-}
-
-impl RawStroke<Abs> {
- /// Unpack the stroke, filling missing fields from the `default`.
- pub fn unwrap_or(self, default: Stroke) -> Stroke {
- Stroke {
- paint: self.paint.unwrap_or(default.paint),
- thickness: self.thickness.unwrap_or(default.thickness),
- }
- }
-
- /// Unpack the stroke, filling missing fields with the default values.
- pub fn unwrap_or_default(self) -> Stroke {
- self.unwrap_or(Stroke::default())
- }
-}
-
-impl Resolve for RawStroke {
- type Output = RawStroke<Abs>;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- RawStroke {
- paint: self.paint,
- thickness: self.thickness.resolve(styles),
- }
- }
-}
-
-impl Fold for RawStroke<Abs> {
- type Output = Self;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- Self {
- paint: self.paint.or(outer.paint),
- thickness: self.thickness.or(outer.thickness),
- }
- }
-}
-
-impl<T: Debug> Debug for RawStroke<T> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match (self.paint, &self.thickness) {
- (Smart::Custom(paint), Smart::Custom(thickness)) => {
- write!(f, "{thickness:?} + {paint:?}")
- }
- (Smart::Custom(paint), Smart::Auto) => paint.fmt(f),
- (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f),
- (Smart::Auto, Smart::Auto) => f.pad("<stroke>"),
- }
- }
-}
-
-dynamic! {
- RawStroke: "stroke",
- Value::Length(thickness) => Self {
- paint: Smart::Auto,
- thickness: Smart::Custom(thickness),
- },
- Value::Color(color) => Self {
- paint: Smart::Custom(color.into()),
- thickness: Smart::Auto,
- },
-}
diff --git a/src/library/structure/doc.rs b/src/library/structure/doc.rs
deleted file mode 100644
index c3af3f1c..00000000
--- a/src/library/structure/doc.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use crate::library::layout::PageNode;
-use crate::library::prelude::*;
-
-/// A sequence of page runs.
-#[derive(Hash)]
-pub struct DocNode(pub StyleVec<PageNode>);
-
-impl DocNode {
- /// Layout the document into a sequence of frames, one per page.
- pub fn layout(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- let mut frames = vec![];
- for (page, map) in self.0.iter() {
- let number = 1 + frames.len();
- frames.extend(page.layout(world, number, map.chain(&styles))?);
- }
- Ok(frames)
- }
-}
-
-impl Debug for DocNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_str("Doc ")?;
- self.0.fmt(f)
- }
-}
diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs
deleted file mode 100644
index 5b056c30..00000000
--- a/src/library/structure/heading.rs
+++ /dev/null
@@ -1,176 +0,0 @@
-use crate::library::layout::{BlockNode, BlockSpacing};
-use crate::library::prelude::*;
-use crate::library::text::{FontFamily, TextNode, TextSize};
-
-/// A section heading.
-#[derive(Debug, Hash)]
-pub struct HeadingNode {
- /// The logical nesting depth of the section, starting from one. In the
- /// default style, this controls the text size of the heading.
- pub level: NonZeroUsize,
- /// The heading's contents.
- pub body: Content,
-}
-
-#[node(Show)]
-impl HeadingNode {
- /// The heading's font family. Just the normal text family if `auto`.
- #[property(referenced)]
- pub const FAMILY: Leveled<Smart<FontFamily>> = Leveled::Value(Smart::Auto);
- /// The color of text in the heading. Just the normal text color if `auto`.
- #[property(referenced)]
- pub const FILL: Leveled<Smart<Paint>> = Leveled::Value(Smart::Auto);
- /// The size of text in the heading.
- #[property(referenced)]
- pub const SIZE: Leveled<TextSize> = Leveled::Mapping(|level| {
- let size = match level.get() {
- 1 => 1.4,
- 2 => 1.2,
- _ => 1.0,
- };
- TextSize(Em::new(size).into())
- });
-
- /// Whether text in the heading is strengthend.
- #[property(referenced)]
- pub const STRONG: Leveled<bool> = Leveled::Value(true);
- /// Whether text in the heading is emphasized.
- #[property(referenced)]
- pub const EMPH: Leveled<bool> = Leveled::Value(false);
- /// Whether the heading is underlined.
- #[property(referenced)]
- pub const UNDERLINE: Leveled<bool> = Leveled::Value(false);
-
- /// The spacing above the heading.
- #[property(referenced, shorthand(around))]
- pub const ABOVE: Leveled<Option<BlockSpacing>> = Leveled::Mapping(|level| {
- let ratio = match level.get() {
- 1 => 1.5,
- _ => 1.2,
- };
- Some(Ratio::new(ratio).into())
- });
- /// The spacing below the heading.
- #[property(referenced, shorthand(around))]
- pub const BELOW: Leveled<Option<BlockSpacing>> =
- Leveled::Value(Some(Ratio::new(0.55).into()));
-
- /// Whether the heading appears in the outline.
- pub const OUTLINED: bool = true;
- /// Whether the heading is numbered.
- pub const NUMBERED: bool = true;
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self {
- body: args.expect("body")?,
- level: args.named("level")?.unwrap_or(NonZeroUsize::new(1).unwrap()),
- }
- .pack())
- }
-}
-
-impl Show for HeadingNode {
- fn unguard_parts(&self, sel: Selector) -> Content {
- Self { body: self.body.unguard(sel), ..*self }.pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "level" => Some(Value::Int(self.level.get() as i64)),
- "body" => Some(Value::Content(self.body.clone())),
- _ => None,
- }
- }
-
- fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
- Ok(BlockNode(self.body.clone()).pack())
- }
-
- fn finalize(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- mut realized: Content,
- ) -> SourceResult<Content> {
- macro_rules! resolve {
- ($key:expr) => {
- styles.get($key).resolve(world, self.level)?
- };
- }
-
- let mut map = StyleMap::new();
- map.set(TextNode::SIZE, resolve!(Self::SIZE));
-
- if let Smart::Custom(family) = resolve!(Self::FAMILY) {
- map.set_family(family, styles);
- }
-
- if let Smart::Custom(fill) = resolve!(Self::FILL) {
- map.set(TextNode::FILL, fill);
- }
-
- if resolve!(Self::STRONG) {
- realized = realized.strong();
- }
-
- if resolve!(Self::EMPH) {
- realized = realized.emph();
- }
-
- if resolve!(Self::UNDERLINE) {
- realized = realized.underlined();
- }
-
- realized = realized.styled_with_map(map);
- realized = realized.spaced(
- resolve!(Self::ABOVE).resolve(styles),
- resolve!(Self::BELOW).resolve(styles),
- );
-
- Ok(realized)
- }
-}
-
-/// Either the value or a closure mapping to the value.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Leveled<T> {
- /// A bare value.
- Value(T),
- /// A simple mapping from a heading level to a value.
- Mapping(fn(NonZeroUsize) -> T),
- /// A closure mapping from a heading level to a value.
- Func(Func, Span),
-}
-
-impl<T: Cast + Clone> Leveled<T> {
- /// Resolve the value based on the level.
- pub fn resolve(
- &self,
- world: Tracked<dyn World>,
- level: NonZeroUsize,
- ) -> SourceResult<T> {
- Ok(match self {
- Self::Value(value) => value.clone(),
- Self::Mapping(mapping) => mapping(level),
- Self::Func(func, span) => {
- let args = Args::new(*span, [Value::Int(level.get() as i64)]);
- func.call_detached(world, args)?.cast().at(*span)?
- }
- })
- }
-}
-
-impl<T: Cast> Cast<Spanned<Value>> for Leveled<T> {
- fn is(value: &Spanned<Value>) -> bool {
- matches!(&value.v, Value::Func(_)) || T::is(&value.v)
- }
-
- fn cast(value: Spanned<Value>) -> StrResult<Self> {
- match value.v {
- Value::Func(v) => Ok(Self::Func(v, value.span)),
- v => T::cast(v)
- .map(Self::Value)
- .map_err(|msg| with_alternative(msg, "function")),
- }
- }
-}
diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs
deleted file mode 100644
index f061c5f8..00000000
--- a/src/library/structure/list.rs
+++ /dev/null
@@ -1,371 +0,0 @@
-use unscanny::Scanner;
-
-use crate::library::layout::{BlockSpacing, GridNode, HNode, TrackSizing};
-use crate::library::prelude::*;
-use crate::library::text::{ParNode, SpaceNode};
-use crate::library::utility::Numbering;
-
-/// An unordered (bulleted) or ordered (numbered) list.
-#[derive(Debug, Hash)]
-pub struct ListNode<const L: ListKind = LIST> {
- /// If true, the items are separated by leading instead of list spacing.
- pub tight: bool,
- /// If true, the spacing above the list is leading instead of above spacing.
- pub attached: bool,
- /// The individual bulleted or numbered items.
- pub items: StyleVec<ListItem>,
-}
-
-/// An ordered list.
-pub type EnumNode = ListNode<ENUM>;
-
-/// A description list.
-pub type DescNode = ListNode<DESC>;
-
-#[node(Show)]
-impl<const L: ListKind> ListNode<L> {
- /// How the list is labelled.
- #[property(referenced)]
- pub const LABEL: Label = Label::Default;
- /// The indentation of each item's label.
- #[property(resolve)]
- pub const INDENT: Length = Length::zero();
- /// The space between the label and the body of each item.
- #[property(resolve)]
- pub const BODY_INDENT: Length = Em::new(match L {
- LIST | ENUM => 0.5,
- DESC | _ => 1.0,
- })
- .into();
-
- /// The spacing above the list.
- #[property(resolve, shorthand(around))]
- pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
- /// The spacing below the list.
- #[property(resolve, shorthand(around))]
- pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
- /// The spacing between the items of a wide (non-tight) list.
- #[property(resolve)]
- pub const SPACING: BlockSpacing = Ratio::one().into();
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let items = match L {
- LIST => args
- .all()?
- .into_iter()
- .map(|body| ListItem::List(Box::new(body)))
- .collect(),
- ENUM => {
- let mut number: usize = args.named("start")?.unwrap_or(1);
- args.all()?
- .into_iter()
- .map(|body| {
- let item = ListItem::Enum(Some(number), Box::new(body));
- number += 1;
- item
- })
- .collect()
- }
- DESC | _ => args
- .all()?
- .into_iter()
- .map(|item| ListItem::Desc(Box::new(item)))
- .collect(),
- };
-
- Ok(Self {
- tight: args.named("tight")?.unwrap_or(true),
- attached: args.named("attached")?.unwrap_or(false),
- items,
- }
- .pack())
- }
-}
-
-impl<const L: ListKind> Show for ListNode<L> {
- fn unguard_parts(&self, sel: Selector) -> Content {
- Self {
- items: self.items.map(|item| item.unguard(sel)),
- ..*self
- }
- .pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "tight" => Some(Value::Bool(self.tight)),
- "attached" => Some(Value::Bool(self.attached)),
- "items" => Some(Value::Array(
- self.items.items().map(|item| item.encode()).collect(),
- )),
- _ => None,
- }
- }
-
- fn realize(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- ) -> SourceResult<Content> {
- let mut cells = vec![];
- let mut number = 1;
-
- let label = styles.get(Self::LABEL);
- let indent = styles.get(Self::INDENT);
- let body_indent = styles.get(Self::BODY_INDENT);
- let gutter = if self.tight {
- styles.get(ParNode::LEADING)
- } else {
- styles.get(Self::SPACING)
- };
-
- for (item, map) in self.items.iter() {
- if let &ListItem::Enum(Some(n), _) = item {
- number = n;
- }
-
- cells.push(Content::empty());
-
- let label = if L == LIST || L == ENUM {
- label.resolve(world, L, number)?.styled_with_map(map.clone())
- } else {
- Content::empty()
- };
-
- cells.push(label);
- cells.push(Content::empty());
-
- let body = match &item {
- ListItem::List(body) => body.as_ref().clone(),
- ListItem::Enum(_, body) => body.as_ref().clone(),
- ListItem::Desc(item) => Content::sequence(vec![
- HNode {
- amount: (-body_indent).into(),
- weak: false,
- }
- .pack(),
- (item.term.clone() + TextNode(':'.into()).pack()).strong(),
- SpaceNode.pack(),
- item.body.clone(),
- ]),
- };
-
- cells.push(body.styled_with_map(map.clone()));
- number += 1;
- }
-
- Ok(GridNode {
- tracks: Axes::with_x(vec![
- TrackSizing::Relative(indent.into()),
- TrackSizing::Auto,
- TrackSizing::Relative(body_indent.into()),
- TrackSizing::Auto,
- ]),
- gutter: Axes::with_y(vec![TrackSizing::Relative(gutter.into())]),
- cells,
- }
- .pack())
- }
-
- fn finalize(
- &self,
- _: Tracked<dyn World>,
- styles: StyleChain,
- realized: Content,
- ) -> SourceResult<Content> {
- let mut above = styles.get(Self::ABOVE);
- let mut below = styles.get(Self::BELOW);
-
- if self.attached {
- if above.is_some() {
- above = Some(styles.get(ParNode::LEADING));
- }
- if below.is_some() {
- below = Some(styles.get(ParNode::SPACING));
- }
- }
-
- Ok(realized.spaced(above, below))
- }
-}
-
-/// An item in a list.
-#[derive(Clone, PartialEq, Hash)]
-pub enum ListItem {
- /// An item of an unordered list.
- List(Box<Content>),
- /// An item of an ordered list.
- Enum(Option<usize>, Box<Content>),
- /// An item of a description list.
- Desc(Box<DescItem>),
-}
-
-impl ListItem {
- /// What kind of item this is.
- pub fn kind(&self) -> ListKind {
- match self {
- Self::List(_) => LIST,
- Self::Enum { .. } => ENUM,
- Self::Desc { .. } => DESC,
- }
- }
-
- fn unguard(&self, sel: Selector) -> Self {
- match self {
- Self::List(body) => Self::List(Box::new(body.unguard(sel))),
- Self::Enum(number, body) => Self::Enum(*number, Box::new(body.unguard(sel))),
- Self::Desc(item) => Self::Desc(Box::new(DescItem {
- term: item.term.unguard(sel),
- body: item.body.unguard(sel),
- })),
- }
- }
-
- /// Encode the item into a value.
- fn encode(&self) -> Value {
- match self {
- Self::List(body) => Value::Content(body.as_ref().clone()),
- Self::Enum(number, body) => Value::Dict(dict! {
- "number" => match *number {
- Some(n) => Value::Int(n as i64),
- None => Value::None,
- },
- "body" => Value::Content(body.as_ref().clone()),
- }),
- Self::Desc(item) => Value::Dict(dict! {
- "term" => Value::Content(item.term.clone()),
- "body" => Value::Content(item.body.clone()),
- }),
- }
- }
-}
-
-impl Debug for ListItem {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::List(body) => write!(f, "- {body:?}"),
- Self::Enum(number, body) => match number {
- Some(n) => write!(f, "{n}. {body:?}"),
- None => write!(f, "+ {body:?}"),
- },
- Self::Desc(item) => item.fmt(f),
- }
- }
-}
-
-#[node]
-impl ListItem {}
-
-/// A description list item.
-#[derive(Clone, PartialEq, Hash)]
-pub struct DescItem {
- /// The term described by the list item.
- pub term: Content,
- /// The description of the term.
- pub body: Content,
-}
-
-castable! {
- DescItem,
- Expected: "dictionary with `term` and `body` keys",
- Value::Dict(dict) => {
- let term: Content = dict.get("term")?.clone().cast()?;
- let body: Content = dict.get("body")?.clone().cast()?;
- Self { term, body }
- },
-}
-
-impl Debug for DescItem {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "/ {:?}: {:?}", self.term, self.body)
- }
-}
-
-/// How to label a list.
-pub type ListKind = usize;
-
-/// An unordered list.
-pub const LIST: ListKind = 0;
-
-/// An ordered list.
-pub const ENUM: ListKind = 1;
-
-/// A description list.
-pub const DESC: ListKind = 2;
-
-/// How to label a list or enumeration.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Label {
- /// The default labelling.
- Default,
- /// A pattern with prefix, numbering, lower / upper case and suffix.
- Pattern(EcoString, Numbering, bool, EcoString),
- /// Bare content.
- Content(Content),
- /// A closure mapping from an item number to a value.
- Func(Func, Span),
-}
-
-impl Label {
- /// Resolve the value based on the level.
- pub fn resolve(
- &self,
- world: Tracked<dyn World>,
- kind: ListKind,
- number: usize,
- ) -> SourceResult<Content> {
- Ok(match self {
- Self::Default => match kind {
- LIST => TextNode('•'.into()).pack(),
- ENUM => TextNode(format_eco!("{}.", number)).pack(),
- DESC | _ => panic!("description lists don't have a label"),
- },
- Self::Pattern(prefix, numbering, upper, suffix) => {
- let fmt = numbering.apply(number);
- let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() };
- TextNode(format_eco!("{}{}{}", prefix, mid, suffix)).pack()
- }
- Self::Content(content) => content.clone(),
- Self::Func(func, span) => {
- let args = Args::new(*span, [Value::Int(number as i64)]);
- func.call_detached(world, args)?.display(world)
- }
- })
- }
-}
-
-impl Cast<Spanned<Value>> for Label {
- fn is(value: &Spanned<Value>) -> bool {
- matches!(&value.v, Value::Content(_) | Value::Func(_))
- }
-
- fn cast(value: Spanned<Value>) -> StrResult<Self> {
- match value.v {
- Value::None => Ok(Self::Content(Content::empty())),
- Value::Str(pattern) => {
- let mut s = Scanner::new(&pattern);
- let mut prefix;
- let numbering = loop {
- prefix = s.before();
- match s.eat().map(|c| c.to_ascii_lowercase()) {
- Some('1') => break Numbering::Arabic,
- Some('a') => break Numbering::Letter,
- Some('i') => break Numbering::Roman,
- Some('*') => break Numbering::Symbol,
- Some(_) => {}
- None => Err("invalid pattern")?,
- }
- };
- let upper = s.scout(-1).map_or(false, char::is_uppercase);
- let suffix = s.after().into();
- Ok(Self::Pattern(prefix.into(), numbering, upper, suffix))
- }
- Value::Content(v) => Ok(Self::Content(v)),
- Value::Func(v) => Ok(Self::Func(v, value.span)),
- v => Err(format!(
- "expected string, content or function, found {}",
- v.type_name(),
- )),
- }
- }
-}
diff --git a/src/library/structure/mod.rs b/src/library/structure/mod.rs
deleted file mode 100644
index 088d1e6c..00000000
--- a/src/library/structure/mod.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-//! Document structuring.
-
-mod doc;
-mod heading;
-mod list;
-mod reference;
-mod table;
-
-pub use doc::*;
-pub use heading::*;
-pub use list::*;
-pub use reference::*;
-pub use table::*;
diff --git a/src/library/structure/reference.rs b/src/library/structure/reference.rs
deleted file mode 100644
index b4e8b047..00000000
--- a/src/library/structure/reference.rs
+++ /dev/null
@@ -1,29 +0,0 @@
-use crate::library::prelude::*;
-
-/// A reference to a label.
-#[derive(Debug, Hash)]
-pub struct RefNode(pub EcoString);
-
-#[node(Show)]
-impl RefNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("label")?).pack())
- }
-}
-
-impl Show for RefNode {
- fn unguard_parts(&self, _: Selector) -> Content {
- Self(self.0.clone()).pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "label" => Some(Value::Str(self.0.clone().into())),
- _ => None,
- }
- }
-
- fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
- Ok(TextNode(format_eco!("@{}", self.0)).pack())
- }
-}
diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs
deleted file mode 100644
index 8a4eb302..00000000
--- a/src/library/structure/table.rs
+++ /dev/null
@@ -1,160 +0,0 @@
-use crate::library::layout::{BlockSpacing, GridNode, TrackSizing};
-use crate::library::prelude::*;
-
-/// A table of items.
-#[derive(Debug, Hash)]
-pub struct TableNode {
- /// Defines sizing for content rows and columns.
- pub tracks: Axes<Vec<TrackSizing>>,
- /// Defines sizing of gutter rows and columns between content.
- pub gutter: Axes<Vec<TrackSizing>>,
- /// The content to be arranged in the table.
- pub cells: Vec<Content>,
-}
-
-#[node(Show)]
-impl TableNode {
- /// How to fill the cells.
- #[property(referenced)]
- pub const FILL: Celled<Option<Paint>> = Celled::Value(None);
- /// 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: Rel<Length> = Abs::pt(5.0).into();
-
- /// The spacing above the table.
- #[property(resolve, shorthand(around))]
- pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
- /// The spacing below the table.
- #[property(resolve, shorthand(around))]
- pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let columns = args.named("columns")?.unwrap_or_default();
- let rows = args.named("rows")?.unwrap_or_default();
- let base_gutter: Vec<TrackSizing> = args.named("gutter")?.unwrap_or_default();
- let column_gutter = args.named("column-gutter")?;
- let row_gutter = args.named("row-gutter")?;
- Ok(Self {
- tracks: Axes::new(columns, rows),
- gutter: Axes::new(
- column_gutter.unwrap_or_else(|| base_gutter.clone()),
- row_gutter.unwrap_or(base_gutter),
- ),
- cells: args.all()?,
- }
- .pack())
- }
-}
-
-impl Show for TableNode {
- fn unguard_parts(&self, sel: Selector) -> Content {
- Self {
- tracks: self.tracks.clone(),
- gutter: self.gutter.clone(),
- cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(),
- }
- .pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "cells" => Some(Value::Array(
- self.cells.iter().cloned().map(Value::Content).collect(),
- )),
- _ => None,
- }
- }
-
- fn realize(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- ) -> SourceResult<Content> {
- let fill = styles.get(Self::FILL);
- let stroke = styles.get(Self::STROKE).map(RawStroke::unwrap_or_default);
- let padding = styles.get(Self::PADDING);
-
- let cols = self.tracks.x.len().max(1);
- let cells = self
- .cells
- .iter()
- .cloned()
- .enumerate()
- .map(|(i, child)| {
- let mut child = child.padded(Sides::splat(padding));
-
- if let Some(stroke) = stroke {
- child = child.stroked(stroke);
- }
-
- let x = i % cols;
- let y = i / cols;
- if let Some(fill) = fill.resolve(world, x, y)? {
- child = child.filled(fill);
- }
-
- Ok(child)
- })
- .collect::<SourceResult<_>>()?;
-
- Ok(GridNode {
- tracks: self.tracks.clone(),
- gutter: self.gutter.clone(),
- cells,
- }
- .pack())
- }
-
- fn finalize(
- &self,
- _: Tracked<dyn World>,
- styles: StyleChain,
- realized: Content,
- ) -> SourceResult<Content> {
- Ok(realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)))
- }
-}
-
-/// A value that can be configured per cell.
-#[derive(Debug, Clone, PartialEq, Hash)]
-pub enum Celled<T> {
- /// A bare value, the same for all cells.
- Value(T),
- /// A closure mapping from cell coordinates to a value.
- Func(Func, Span),
-}
-
-impl<T: Cast + Clone> Celled<T> {
- /// Resolve the value based on the cell position.
- pub fn resolve(
- &self,
- world: Tracked<dyn World>,
- x: usize,
- y: usize,
- ) -> SourceResult<T> {
- Ok(match self {
- Self::Value(value) => value.clone(),
- Self::Func(func, span) => {
- let args = Args::new(*span, [Value::Int(x as i64), Value::Int(y as i64)]);
- func.call_detached(world, args)?.cast().at(*span)?
- }
- })
- }
-}
-
-impl<T: Cast> Cast<Spanned<Value>> for Celled<T> {
- fn is(value: &Spanned<Value>) -> bool {
- matches!(&value.v, Value::Func(_)) || T::is(&value.v)
- }
-
- fn cast(value: Spanned<Value>) -> StrResult<Self> {
- match value.v {
- Value::Func(v) => Ok(Self::Func(v, value.span)),
- v => T::cast(v)
- .map(Self::Value)
- .map_err(|msg| with_alternative(msg, "function")),
- }
- }
-}
diff --git a/src/library/text/deco.rs b/src/library/text/deco.rs
deleted file mode 100644
index 158647f2..00000000
--- a/src/library/text/deco.rs
+++ /dev/null
@@ -1,249 +0,0 @@
-use kurbo::{BezPath, Line, ParamCurve};
-use ttf_parser::{GlyphId, OutlineBuilder};
-
-use super::TextNode;
-use crate::library::prelude::*;
-
-/// Typeset underline, stricken-through or overlined text.
-#[derive(Debug, Hash)]
-pub struct DecoNode<const L: DecoLine>(pub Content);
-
-/// Typeset underlined text.
-pub type UnderlineNode = DecoNode<UNDERLINE>;
-
-/// Typeset stricken-through text.
-pub type StrikethroughNode = DecoNode<STRIKETHROUGH>;
-
-/// Typeset overlined text.
-pub type OverlineNode = DecoNode<OVERLINE>;
-
-#[node(Show)]
-impl<const L: DecoLine> DecoNode<L> {
- /// How to stroke the line. The text color and thickness are read from the
- /// font 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)]
- pub const OFFSET: Smart<Length> = Smart::Auto;
- /// Amount that the line will be longer or shorter than its associated text.
- #[property(resolve)]
- pub const EXTENT: Length = Length::zero();
- /// Whether the line skips sections in which it would collide
- /// with the glyphs. Does not apply to strikethrough.
- pub const EVADE: bool = true;
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl<const L: DecoLine> Show for DecoNode<L> {
- fn unguard_parts(&self, sel: Selector) -> Content {
- Self(self.0.unguard(sel)).pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "body" => Some(Value::Content(self.0.clone())),
- _ => None,
- }
- }
-
- fn realize(
- &self,
- _: Tracked<dyn World>,
- styles: StyleChain,
- ) -> SourceResult<Content> {
- Ok(self.0.clone().styled(TextNode::DECO, Decoration {
- line: L,
- stroke: styles.get(Self::STROKE).unwrap_or_default(),
- offset: styles.get(Self::OFFSET),
- extent: styles.get(Self::EXTENT),
- evade: styles.get(Self::EVADE),
- }))
- }
-}
-
-/// Defines a line that is positioned over, under or on top of text.
-///
-/// For more details, see [`DecoNode`].
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct Decoration {
- pub line: DecoLine,
- pub stroke: RawStroke<Abs>,
- pub offset: Smart<Abs>,
- pub extent: Abs,
- pub evade: bool,
-}
-
-/// A kind of decorative line.
-pub type DecoLine = usize;
-
-/// A line under text.
-pub const UNDERLINE: DecoLine = 0;
-
-/// A line through text.
-pub const STRIKETHROUGH: DecoLine = 1;
-
-/// A line over text.
-pub const OVERLINE: DecoLine = 2;
-
-/// Add line decorations to a single run of shaped text.
-pub fn decorate(
- frame: &mut Frame,
- deco: &Decoration,
- text: &Text,
- shift: Abs,
- pos: Point,
- width: Abs,
-) {
- let font_metrics = text.font.metrics();
- let metrics = match deco.line {
- STRIKETHROUGH => font_metrics.strikethrough,
- OVERLINE => font_metrics.overline,
- UNDERLINE | _ => font_metrics.underline,
- };
-
- let evade = deco.evade && deco.line != STRIKETHROUGH;
- let offset = deco.offset.unwrap_or(-metrics.position.at(text.size)) - shift;
- let stroke = deco.stroke.unwrap_or(Stroke {
- paint: text.fill,
- thickness: metrics.thickness.at(text.size),
- });
-
- let gap_padding = 0.08 * text.size;
- let min_width = 0.162 * text.size;
-
- let mut start = pos.x - deco.extent;
- let end = pos.x + (width + 2.0 * deco.extent);
-
- let mut push_segment = |from: Abs, to: Abs| {
- let origin = Point::new(from, pos.y + offset);
- let target = Point::new(to - from, Abs::zero());
-
- if target.x >= min_width || !evade {
- let shape = Geometry::Line(target).stroked(stroke);
- frame.push(origin, Element::Shape(shape));
- }
- };
-
- if !evade {
- push_segment(start, end);
- return;
- }
-
- let line = Line::new(
- kurbo::Point::new(pos.x.to_raw(), offset.to_raw()),
- kurbo::Point::new((pos.x + width).to_raw(), offset.to_raw()),
- );
-
- let mut x = pos.x;
- let mut intersections = vec![];
-
- for glyph in text.glyphs.iter() {
- let dx = glyph.x_offset.at(text.size) + x;
- let mut builder =
- BezPathBuilder::new(font_metrics.units_per_em, text.size, dx.to_raw());
-
- let bbox = text.font.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
- let path = builder.finish();
-
- x += glyph.x_advance.at(text.size);
-
- // Only do the costly segments intersection test if the line
- // intersects the bounding box.
- if bbox.map_or(false, |bbox| {
- let y_min = -text.font.to_em(bbox.y_max).at(text.size);
- let y_max = -text.font.to_em(bbox.y_min).at(text.size);
-
- offset >= y_min && offset <= y_max
- }) {
- // Find all intersections of segments with the line.
- intersections.extend(
- path.segments()
- .flat_map(|seg| seg.intersect_line(line))
- .map(|is| Abs::raw(line.eval(is.line_t).x)),
- );
- }
- }
-
- // When emitting the decorative line segments, we move from left to
- // right. The intersections are not necessarily in this order, yet.
- intersections.sort();
-
- for gap in intersections.chunks_exact(2) {
- let l = gap[0] - gap_padding;
- let r = gap[1] + gap_padding;
-
- if start >= end {
- break;
- }
-
- if start >= l {
- start = r;
- continue;
- }
-
- push_segment(start, l);
- start = r;
- }
-
- if start < end {
- push_segment(start, end);
- }
-}
-
-/// Builds a kurbo [`BezPath`] for a glyph.
-struct BezPathBuilder {
- path: BezPath,
- units_per_em: f64,
- font_size: Abs,
- x_offset: f64,
-}
-
-impl BezPathBuilder {
- fn new(units_per_em: f64, font_size: Abs, x_offset: f64) -> Self {
- Self {
- path: BezPath::new(),
- units_per_em,
- font_size,
- x_offset,
- }
- }
-
- fn finish(self) -> BezPath {
- self.path
- }
-
- fn p(&self, x: f32, y: f32) -> kurbo::Point {
- kurbo::Point::new(self.s(x) + self.x_offset, -self.s(y))
- }
-
- fn s(&self, v: f32) -> f64 {
- Em::from_units(v, self.units_per_em).at(self.font_size).to_raw()
- }
-}
-
-impl OutlineBuilder for BezPathBuilder {
- fn move_to(&mut self, x: f32, y: f32) {
- self.path.move_to(self.p(x, y));
- }
-
- fn line_to(&mut self, x: f32, y: f32) {
- self.path.line_to(self.p(x, y));
- }
-
- fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
- self.path.quad_to(self.p(x1, y1), self.p(x, y));
- }
-
- fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
- self.path.curve_to(self.p(x1, y1), self.p(x2, y2), self.p(x, y));
- }
-
- fn close(&mut self) {
- self.path.close_path();
- }
-}
diff --git a/src/library/text/link.rs b/src/library/text/link.rs
deleted file mode 100644
index 1e9adc3e..00000000
--- a/src/library/text/link.rs
+++ /dev/null
@@ -1,114 +0,0 @@
-use super::TextNode;
-use crate::library::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: Option<Content>,
-}
-
-impl LinkNode {
- /// Create a link node from a URL with its bare text.
- pub fn from_url(url: EcoString) -> Self {
- Self { dest: Destination::Url(url), body: None }
- }
-}
-
-#[node(Show)]
-impl LinkNode {
- /// The fill color of text in the link. Just the surrounding text color
- /// if `auto`.
- pub const FILL: Smart<Paint> = Smart::Auto;
- /// Whether to underline the link.
- pub const UNDERLINE: Smart<bool> = Smart::Auto;
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let dest = args.expect::<Destination>("destination")?;
- let body = match dest {
- Destination::Url(_) => args.eat()?,
- Destination::Internal(_) => Some(args.expect("body")?),
- };
- Ok(Self { dest, body }.pack())
- }
-}
-
-castable! {
- Destination,
- Expected: "string or dictionary with `page`, `x`, and `y` keys",
- Value::Str(string) => Self::Url(string.into()),
- Value::Dict(dict) => {
- let page = dict.get("page")?.clone().cast()?;
- let x: Length = dict.get("x")?.clone().cast()?;
- let y: Length = dict.get("y")?.clone().cast()?;
- Self::Internal(Location { page, pos: Point::new(x.abs, y.abs) })
- },
-}
-
-impl Show for LinkNode {
- fn unguard_parts(&self, sel: Selector) -> Content {
- Self {
- dest: self.dest.clone(),
- body: self.body.as_ref().map(|body| body.unguard(sel)),
- }
- .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(match &self.body {
- Some(body) => Value::Content(body.clone()),
- None => Value::None,
- }),
- _ => None,
- }
- }
-
- fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
- Ok(self
- .body
- .clone()
- .unwrap_or_else(|| match &self.dest {
- Destination::Url(url) => {
- let mut text = url.as_str();
- for prefix in ["mailto:", "tel:"] {
- text = text.trim_start_matches(prefix);
- }
- let shorter = text.len() < url.len();
- TextNode(if shorter { text.into() } else { url.clone() }).pack()
- }
- Destination::Internal(_) => Content::empty(),
- })
- .styled(TextNode::LINK, Some(self.dest.clone())))
- }
-
- fn finalize(
- &self,
- _: Tracked<dyn World>,
- styles: StyleChain,
- mut realized: Content,
- ) -> SourceResult<Content> {
- let mut map = StyleMap::new();
- if let Smart::Custom(fill) = styles.get(Self::FILL) {
- map.set(TextNode::FILL, fill);
- }
-
- if match styles.get(Self::UNDERLINE) {
- Smart::Auto => match &self.dest {
- Destination::Url(_) => true,
- Destination::Internal(_) => false,
- },
- Smart::Custom(underline) => underline,
- } {
- realized = realized.underlined();
- }
-
- Ok(realized.styled_with_map(map))
- }
-}
diff --git a/src/library/text/mod.rs b/src/library/text/mod.rs
deleted file mode 100644
index 18e747d0..00000000
--- a/src/library/text/mod.rs
+++ /dev/null
@@ -1,593 +0,0 @@
-//! Text handling and paragraph layout.
-
-mod deco;
-mod link;
-mod par;
-mod quotes;
-mod raw;
-mod shaping;
-mod shift;
-
-pub use deco::*;
-pub use link::*;
-pub use par::*;
-pub use quotes::*;
-pub use raw::*;
-pub use shaping::*;
-pub use shift::*;
-
-use std::borrow::Cow;
-
-use rustybuzz::Tag;
-
-use crate::font::{FontMetrics, FontStretch, FontStyle, FontWeight, VerticalFontMetric};
-use crate::library::prelude::*;
-use crate::util::EcoString;
-
-/// A single run of text with the same style.
-#[derive(Debug, Clone, Hash)]
-pub struct TextNode(pub EcoString);
-
-#[node]
-impl TextNode {
- /// A prioritized sequence of font families.
- #[property(skip, referenced)]
- pub const FAMILY: Vec<FontFamily> = vec![FontFamily::new("IBM Plex Sans")];
- /// Whether to allow font fallback when the primary font list contains no
- /// match.
- pub const FALLBACK: bool = true;
-
- /// How the font is styled.
- pub const STYLE: FontStyle = FontStyle::Normal;
- /// The boldness / thickness of the font's glyphs.
- pub const WEIGHT: FontWeight = FontWeight::REGULAR;
- /// The width of the glyphs.
- pub const STRETCH: FontStretch = FontStretch::NORMAL;
-
- /// The size of the glyphs.
- #[property(shorthand, fold)]
- pub const SIZE: TextSize = Abs::pt(11.0);
- /// The glyph fill color.
- #[property(shorthand)]
- pub const FILL: Paint = Color::BLACK.into();
- /// The amount of space that should be added between characters.
- #[property(resolve)]
- pub const TRACKING: Length = Length::zero();
- /// The width of spaces relative to the font's space width.
- #[property(resolve)]
- pub const SPACING: Rel<Length> = Rel::one();
- /// The offset of the baseline.
- #[property(resolve)]
- pub const BASELINE: Length = Length::zero();
- /// Whether certain glyphs can hang over into the margin.
- pub const OVERHANG: bool = true;
- /// The top end of the text bounding box.
- pub const TOP_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::CapHeight);
- /// The bottom end of the text bounding box.
- pub const BOTTOM_EDGE: TextEdge = TextEdge::Metric(VerticalFontMetric::Baseline);
-
- /// An ISO 639-1/2/3 language code.
- pub const LANG: Lang = Lang::ENGLISH;
- /// An ISO 3166-1 alpha-2 region code.
- pub const REGION: Option<Region> = 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 smart quotes.
- pub const SMART_QUOTES: bool = true;
-
- /// Whether to apply kerning ("kern").
- pub const KERNING: bool = true;
- /// Whether to apply stylistic alternates. ("salt")
- pub const ALTERNATES: bool = false;
- /// Which stylistic set to apply. ("ss01" - "ss20")
- pub const STYLISTIC_SET: Option<StylisticSet> = None;
- /// Whether standard ligatures are active. ("liga", "clig")
- pub const LIGATURES: bool = true;
- /// Whether ligatures that should be used sparingly are active. ("dlig")
- pub const DISCRETIONARY_LIGATURES: bool = false;
- /// Whether historical ligatures are active. ("hlig")
- pub const HISTORICAL_LIGATURES: bool = false;
- /// Which kind of numbers / figures to select.
- pub const NUMBER_TYPE: Smart<NumberType> = Smart::Auto;
- /// The width of numbers / figures.
- pub const NUMBER_WIDTH: Smart<NumberWidth> = Smart::Auto;
- /// Whether to have a slash through the zero glyph. ("zero")
- pub const SLASHED_ZERO: bool = false;
- /// Whether to convert fractions. ("frac")
- pub const FRACTIONS: bool = false;
- /// Raw OpenType features to apply.
- #[property(fold)]
- pub const FEATURES: Vec<(Tag, u32)> = vec![];
-
- /// Whether the font weight should be increased by 300.
- #[property(skip, fold)]
- pub const BOLD: Toggle = false;
- /// Whether the font style should be inverted.
- #[property(skip, fold)]
- pub const ITALIC: Toggle = false;
- /// A case transformation that should be applied to the text.
- #[property(skip)]
- pub const CASE: Option<Case> = None;
- /// Whether small capital glyphs should be used. ("smcp")
- #[property(skip)]
- pub const SMALLCAPS: bool = false;
- /// A destination the text should be linked to.
- #[property(skip, referenced)]
- pub const LINK: Option<Destination> = None;
- /// Decorative lines.
- #[property(skip, fold)]
- pub const DECO: Decoration = vec![];
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- // The text constructor is special: It doesn't create a text node.
- // Instead, it leaves the passed argument structurally unchanged, but
- // styles all text in it.
- args.expect("body")
- }
-
- fn set(...) {
- if let Some(family) = args.named("family")? {
- styles.set(Self::FAMILY, family);
- } else {
- let mut count = 0;
- let mut content = false;
- for item in args.items.iter().filter(|item| item.name.is_none()) {
- if EcoString::is(&item.value) {
- count += 1;
- } else if <Content as Cast<Spanned<Value>>>::is(&item.value) {
- content = true;
- }
- }
-
- // Skip the final string if it's needed as the body.
- if constructor && !content && count > 0 {
- count -= 1;
- }
-
- if count > 0 {
- let mut list = Vec::with_capacity(count);
- for _ in 0 .. count {
- list.push(args.find()?.unwrap());
- }
-
- styles.set(Self::FAMILY, list);
- }
- }
- }
-}
-
-/// A lowercased font family like "arial".
-#[derive(Clone, Eq, PartialEq, Hash)]
-pub struct FontFamily(EcoString);
-
-impl FontFamily {
- /// Create a named font family variant.
- pub fn new(string: &str) -> Self {
- Self(string.to_lowercase().into())
- }
-
- /// The lowercased family name.
- pub fn as_str(&self) -> &str {
- &self.0
- }
-}
-
-impl Debug for FontFamily {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.0.fmt(f)
- }
-}
-
-castable! {
- FontFamily,
- Expected: "string",
- Value::Str(string) => Self::new(&string),
-}
-
-castable! {
- Vec<FontFamily>,
- Expected: "string or array of strings",
- Value::Str(string) => vec![FontFamily::new(&string)],
- Value::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .map(|string: EcoString| FontFamily::new(&string))
- .collect(),
-}
-
-castable! {
- FontStyle,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "normal" => Self::Normal,
- "italic" => Self::Italic,
- "oblique" => Self::Oblique,
- _ => Err(r#"expected "normal", "italic" or "oblique""#)?,
- },
-}
-
-castable! {
- FontWeight,
- Expected: "integer or string",
- Value::Int(v) => Value::Int(v)
- .cast::<usize>()?
- .try_into()
- .map_or(Self::BLACK, Self::from_number),
- Value::Str(string) => match string.as_str() {
- "thin" => Self::THIN,
- "extralight" => Self::EXTRALIGHT,
- "light" => Self::LIGHT,
- "regular" => Self::REGULAR,
- "medium" => Self::MEDIUM,
- "semibold" => Self::SEMIBOLD,
- "bold" => Self::BOLD,
- "extrabold" => Self::EXTRABOLD,
- "black" => Self::BLACK,
- _ => Err("unknown font weight")?,
- },
-}
-
-castable! {
- FontStretch,
- Expected: "ratio",
- Value::Ratio(v) => Self::from_ratio(v.get() as f32),
-}
-
-/// The size of text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct TextSize(pub Length);
-
-impl Fold for TextSize {
- type Output = Abs;
-
- fn fold(self, outer: Self::Output) -> Self::Output {
- self.0.em.at(outer) + self.0.abs
- }
-}
-
-castable!(TextSize: Length);
-
-/// Specifies the bottom or top edge of text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum TextEdge {
- /// An edge specified using one of the well-known font metrics.
- Metric(VerticalFontMetric),
- /// An edge specified as a length.
- Length(Length),
-}
-
-impl TextEdge {
- /// Resolve the value of the text edge given a font's metrics.
- pub fn resolve(self, styles: StyleChain, metrics: &FontMetrics) -> Abs {
- match self {
- Self::Metric(metric) => metrics.vertical(metric).resolve(styles),
- Self::Length(length) => length.resolve(styles),
- }
- }
-}
-
-castable! {
- TextEdge,
- Expected: "string or length",
- Value::Length(v) => Self::Length(v),
- Value::Str(string) => Self::Metric(match string.as_str() {
- "ascender" => VerticalFontMetric::Ascender,
- "cap-height" => VerticalFontMetric::CapHeight,
- "x-height" => VerticalFontMetric::XHeight,
- "baseline" => VerticalFontMetric::Baseline,
- "descender" => VerticalFontMetric::Descender,
- _ => Err("unknown font metric")?,
- }),
-}
-
-castable! {
- Lang,
- Expected: "string",
- Value::Str(string) => Self::from_str(&string)
- .ok_or("expected two or three letter language code (ISO 639-1/2/3)")?,
-}
-
-castable! {
- Region,
- Expected: "string",
- Value::Str(string) => Self::from_str(&string)
- .ok_or("expected two letter region code (ISO 3166-1 alpha-2)")?,
-}
-
-/// 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() {
- Axis::X => Self(*dir),
- Axis::Y => Err("must be horizontal")?,
- },
-}
-
-impl Resolve for Smart<HorizontalDir> {
- type Output = Dir;
-
- fn resolve(self, styles: StyleChain) -> Self::Output {
- match self {
- Smart::Auto => styles.get(TextNode::LANG).dir(),
- Smart::Custom(dir) => dir.0,
- }
- }
-}
-
-/// Whether to hyphenate text.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct Hyphenate(pub bool);
-
-castable!(Hyphenate: bool);
-
-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.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct StylisticSet(u8);
-
-impl StylisticSet {
- /// Create a new set, clamping to 1-20.
- pub fn new(index: u8) -> Self {
- Self(index.clamp(1, 20))
- }
-
- /// Get the value, guaranteed to be 1-20.
- pub fn get(self) -> u8 {
- self.0
- }
-}
-
-castable! {
- StylisticSet,
- Expected: "integer",
- Value::Int(v) => match v {
- 1 ..= 20 => Self::new(v as u8),
- _ => Err("must be between 1 and 20")?,
- },
-}
-
-/// Which kind of numbers / figures to select.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum NumberType {
- /// Numbers that fit well with capital text. ("lnum")
- Lining,
- /// Numbers that fit well into a flow of upper- and lowercase text. ("onum")
- OldStyle,
-}
-
-castable! {
- NumberType,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "lining" => Self::Lining,
- "old-style" => Self::OldStyle,
- _ => Err(r#"expected "lining" or "old-style""#)?,
- },
-}
-
-/// The width of numbers / figures.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum NumberWidth {
- /// Number widths are glyph specific. ("pnum")
- Proportional,
- /// All numbers are of equal width / monospaced. ("tnum")
- Tabular,
-}
-
-castable! {
- NumberWidth,
- Expected: "string",
- Value::Str(string) => match string.as_str() {
- "proportional" => Self::Proportional,
- "tabular" => Self::Tabular,
- _ => Err(r#"expected "proportional" or "tabular""#)?,
- },
-}
-
-castable! {
- Vec<(Tag, u32)>,
- Expected: "array of strings or dictionary mapping tags to integers",
- Value::Array(values) => values
- .into_iter()
- .filter_map(|v| v.cast().ok())
- .map(|string: EcoString| (Tag::from_bytes_lossy(string.as_bytes()), 1))
- .collect(),
- Value::Dict(values) => values
- .into_iter()
- .filter_map(|(k, v)| {
- let tag = Tag::from_bytes_lossy(k.as_bytes());
- let num = v.cast::<i64>().ok()?.try_into().ok()?;
- Some((tag, num))
- })
- .collect(),
-}
-
-impl Fold for Vec<(Tag, u32)> {
- type Output = Self;
-
- fn fold(mut self, outer: Self::Output) -> Self::Output {
- self.extend(outer);
- self
- }
-}
-
-/// A text space.
-#[derive(Debug, Clone, Hash)]
-pub struct SpaceNode;
-
-#[node]
-impl SpaceNode {
- fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
- Ok(Self.pack())
- }
-}
-
-/// A line break.
-#[derive(Debug, Clone, Hash)]
-pub struct LinebreakNode {
- pub justify: bool,
-}
-
-#[node]
-impl LinebreakNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- let justify = args.named("justify")?.unwrap_or(false);
- Ok(Self { justify }.pack())
- }
-}
-
-/// A smart quote.
-#[derive(Debug, Clone, Hash)]
-pub struct SmartQuoteNode {
- pub double: bool,
-}
-
-#[node]
-impl SmartQuoteNode {
- fn construct(_: &mut 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(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- case(Case::Lower, args)
-}
-
-/// Convert a string or content to uppercase.
-pub fn upper(_: &mut 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(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let body: Content = args.expect("content")?;
- Ok(Value::Content(body.styled(TextNode::SMALLCAPS, true)))
-}
-
-/// Strong text, rendered in boldface by default.
-#[derive(Debug, Hash)]
-pub struct StrongNode(pub Content);
-
-#[node(Show)]
-impl StrongNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl Show for StrongNode {
- fn unguard_parts(&self, sel: Selector) -> Content {
- Self(self.0.unguard(sel)).pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "body" => Some(Value::Content(self.0.clone())),
- _ => None,
- }
- }
-
- fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
- Ok(self.0.clone().styled(TextNode::BOLD, Toggle))
- }
-}
-
-/// Emphasized text, rendered with an italic font by default.
-#[derive(Debug, Hash)]
-pub struct EmphNode(pub Content);
-
-#[node(Show)]
-impl EmphNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl Show for EmphNode {
- fn unguard_parts(&self, sel: Selector) -> Content {
- Self(self.0.unguard(sel)).pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "body" => Some(Value::Content(self.0.clone())),
- _ => None,
- }
- }
-
- fn realize(&self, _: Tracked<dyn World>, _: StyleChain) -> SourceResult<Content> {
- Ok(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
- }
-}
-
-impl Fold for Decoration {
- type Output = Vec<Self>;
-
- fn fold(self, mut outer: Self::Output) -> Self::Output {
- outer.insert(0, self);
- outer
- }
-}
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
deleted file mode 100644
index 50089b20..00000000
--- a/src/library/text/par.rs
+++ /dev/null
@@ -1,1266 +0,0 @@
-use std::cmp::Ordering;
-
-use unicode_bidi::{BidiInfo, Level as BidiLevel};
-use unicode_script::{Script, UnicodeScript};
-use xi_unicode::LineBreakIterator;
-
-use super::{shape, Lang, Quoter, Quotes, ShapedText, TextNode};
-use crate::library::layout::Spacing;
-use crate::library::prelude::*;
-use crate::util::EcoString;
-
-/// Arrange text, spacing and inline-level nodes into a paragraph.
-#[derive(Hash)]
-pub struct ParNode(pub StyleVec<ParChild>);
-
-/// A uniformly styled atomic piece of a paragraph.
-#[derive(Hash, PartialEq)]
-pub enum ParChild {
- /// A chunk of text.
- Text(EcoString),
- /// A single or double smart quote.
- Quote { double: bool },
- /// Horizontal spacing between other children.
- Spacing(Spacing),
- /// Arbitrary inline-level content.
- Inline(Content),
-}
-
-#[node(LayoutBlock)]
-impl ParNode {
- /// The spacing between lines.
- #[property(resolve)]
- pub const LEADING: Length = Em::new(0.65).into();
- /// The extra spacing between paragraphs.
- #[property(resolve)]
- pub const SPACING: Length = Em::new(1.2).into();
- /// The indent the first line of a consecutive paragraph should have.
- #[property(resolve)]
- pub const INDENT: Length = Length::zero();
- /// Whether to allow paragraph spacing when there is paragraph indent.
- pub const SPACING_AND_INDENT: bool = false;
-
- /// 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 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 is in a
- // separate paragraph and styles it.
- Ok(Content::sequence(vec![
- ParbreakNode.pack(),
- args.expect("body")?,
- ParbreakNode.pack(),
- ]))
- }
-}
-
-impl LayoutBlock for ParNode {
- fn layout_block(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- // Collect all text into one string for BiDi analysis.
- let (text, segments) = collect(self, &styles);
-
- // 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, world, regions.first.x);
-
- // Stack the lines into one frame per region.
- stack(&p, world, &lines, regions)
- }
-}
-
-impl Debug for ParNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.write_str("Par ")?;
- self.0.fmt(f)
- }
-}
-
-impl Debug for ParChild {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Text(text) => write!(f, "Text({:?})", text),
- Self::Quote { double } => write!(f, "Quote({double})"),
- Self::Spacing(kind) => write!(f, "{:?}", kind),
- Self::Inline(inline) => inline.fmt(f),
- }
- }
-}
-
-impl PartialOrd for ParChild {
- fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
- match (self, other) {
- (Self::Spacing(a), Self::Spacing(b)) => a.partial_cmp(b),
- _ => None,
- }
- }
-}
-
-/// A horizontal alignment.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub struct HorizontalAlign(pub RawAlign);
-
-castable! {
- HorizontalAlign,
- Expected: "alignment",
- @align: RawAlign => 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""#)?,
- },
-}
-
-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.
-#[derive(Debug, Clone, Hash)]
-pub struct ParbreakNode;
-
-#[node]
-impl ParbreakNode {
- fn construct(_: &mut Vm, _: &mut Args) -> SourceResult<Content> {
- Ok(Self.pack())
- }
-}
-
-/// Repeats content to fill a line.
-#[derive(Debug, Hash)]
-pub struct RepeatNode(pub Content);
-
-#[node(LayoutInline)]
-impl RepeatNode {
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl LayoutInline for RepeatNode {
- fn layout_inline(
- &self,
- world: Tracked<dyn World>,
- regions: &Regions,
- styles: StyleChain,
- ) -> SourceResult<Vec<Frame>> {
- self.0.layout_inline(world, regions, styles)
- }
-}
-
-/// 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> {
- /// 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 resolved leading between lines.
- leading: Abs,
- /// 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>,
-) -> (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();
-
- while let Some((child, map)) = iter.next() {
- let styles = map.chain(&styles);
- let segment = match child {
- ParChild::Text(text) => {
- let prev = full.len();
- if let Some(case) = styles.get(TextNode::CASE) {
- full.push_str(&case.apply(text));
- } else {
- full.push_str(text);
- }
- Segment::Text(full.len() - prev)
- }
- &ParChild::Quote { double } => {
- 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, _)| match child {
- ParChild::Text(text) => text.chars().next(),
- ParChild::Quote { .. } => Some('"'),
- ParChild::Spacing(_) => Some(SPACING_REPLACE),
- ParChild::Inline(_) => Some(NODE_REPLACE),
- });
-
- full.push_str(quoter.quote(&quotes, double, peeked));
- } else {
- full.push(if double { '"' } else { '\'' });
- }
- Segment::Text(full.len() - prev)
- }
- &ParChild::Spacing(spacing) => {
- full.push(SPACING_REPLACE);
- Segment::Spacing(spacing)
- }
- ParChild::Inline(inline) => {
- full.push(NODE_REPLACE);
- Segment::Inline(inline)
- }
- };
-
- 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<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.downcast::<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_inline(world, &pod, styles)?.remove(0);
- frame.translate(Point::with_y(styles.get(TextNode::BASELINE)));
- frame.apply_role(Role::GenericInline);
- items.push(Item::Frame(frame));
- }
- }
- }
-
- cursor = end;
- }
-
- Ok(Preparation {
- bidi,
- items,
- styles,
- hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE),
- lang: shared_get(styles, &par.0, TextNode::LANG),
- leading: styles.get(ParNode::LEADING),
- 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<'a>>(
- styles: StyleChain<'a>,
- children: &StyleVec<ParChild>,
- key: K,
-) -> Option<K::Output> {
- children
- .styles()
- .all(|map| !map.contains(key))
- .then(|| styles.get(key))
-}
-
-/// Find suitable linebreaks.
-fn linebreak<'a>(
- p: &'a Preparation<'a>,
- world: Tracked<dyn World>,
- width: Abs,
-) -> Vec<Line<'a>> {
- match p.styles.get(ParNode::LINEBREAKS) {
- Linebreaks::Simple => linebreak_simple(p, world, width),
- Linebreaks::Optimized => linebreak_optimized(p, world, 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>,
- world: Tracked<dyn World>,
- 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, world, 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, world, 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>,
- world: Tracked<dyn World>,
- 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, world, 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, world, 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,
- world: Tracked<dyn World>,
- 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(world, shifted);
- if hyphen || shy {
- reshaped.push_hyphen(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(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 stack(
- p: &Preparation,
- world: Tracked<dyn World>,
- lines: &[Line],
- regions: &Regions,
-) -> SourceResult<Vec<Frame>> {
- // 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();
- }
-
- // State for final frame building.
- let mut regions = regions.clone();
- let mut finished = vec![];
- let mut first = true;
- let mut output = Frame::new(Size::with_x(width));
- output.apply_role(Role::Paragraph);
-
- // Stack the lines into one frame per region.
- for line in lines {
- let frame = commit(p, world, line, &regions, width)?;
- let height = frame.size().y;
-
- while !regions.first.y.fits(height) && !regions.in_last() {
- finished.push(output);
- output = Frame::new(Size::with_x(width));
- output.apply_role(Role::Paragraph);
- regions.next();
- first = true;
- }
-
- if !first {
- output.size_mut().y += p.leading;
- }
-
- let pos = Point::with_y(output.height());
- output.size_mut().y += height;
- output.push_frame(pos, frame);
-
- regions.first.y -= height + p.leading;
- first = false;
- }
-
- finished.push(output);
- Ok(finished)
-}
-
-/// Commit to a line and build its frame.
-fn commit(
- p: &Preparation,
- world: Tracked<dyn World>,
- line: &Line,
- regions: &Regions,
- 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(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, regions.base.y);
- let pod = Regions::one(size, regions.base, Axes::new(false, false));
- let frame = repeat.layout_inline(world, &pod, *styles)?.remove(0);
- 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/src/library/text/quotes.rs b/src/library/text/quotes.rs
deleted file mode 100644
index 0a22646a..00000000
--- a/src/library/text/quotes.rs
+++ /dev/null
@@ -1,148 +0,0 @@
-use super::{Lang, Region};
-use crate::syntax::is_newline;
-
-/// State machine for smart quote subtitution.
-#[derive(Debug, Clone)]
-pub struct Quoter {
- /// How many quotes have been opened.
- quote_depth: usize,
- /// Whether an opening quote might follow.
- expect_opening: bool,
- /// Whether the last character was numeric.
- last_num: bool,
-}
-
-impl Quoter {
- /// Start quoting.
- pub fn new() -> Self {
- Self {
- quote_depth: 0,
- expect_opening: true,
- last_num: false,
- }
- }
-
- /// Process the last seen character.
- pub fn last(&mut self, c: char) {
- self.expect_opening = is_ignorable(c) || is_opening_bracket(c);
- self.last_num = c.is_numeric();
- }
-
- /// Process and substitute a quote.
- pub fn quote<'a>(
- &mut self,
- quotes: &Quotes<'a>,
- double: bool,
- peeked: Option<char>,
- ) -> &'a str {
- let peeked = peeked.unwrap_or(' ');
- if self.expect_opening {
- self.quote_depth += 1;
- quotes.open(double)
- } else if self.quote_depth > 0
- && (peeked.is_ascii_punctuation() || is_ignorable(peeked))
- {
- self.quote_depth -= 1;
- quotes.close(double)
- } else if self.last_num {
- quotes.prime(double)
- } else {
- quotes.fallback(double)
- }
- }
-}
-
-impl Default for Quoter {
- fn default() -> Self {
- Self::new()
- }
-}
-
-fn is_ignorable(c: char) -> bool {
- c.is_whitespace() || is_newline(c)
-}
-
-fn is_opening_bracket(c: char) -> bool {
- matches!(c, '(' | '{' | '[')
-}
-
-/// Decides which quotes to subtitute smart quotes with.
-pub struct Quotes<'s> {
- /// The opening single quote.
- pub single_open: &'s str,
- /// The closing single quote.
- pub single_close: &'s str,
- /// The opening double quote.
- pub double_open: &'s str,
- /// The closing double quote.
- pub double_close: &'s str,
-}
-
-impl<'s> Quotes<'s> {
- /// Create a new `Quotes` struct with the defaults for a language and
- /// region.
- ///
- /// The language should be specified as an all-lowercase ISO 639-1 code, the
- /// region as an all-uppercase ISO 3166-alpha2 code.
- ///
- /// Currently, the supported languages are: English, Czech, Danish, German,
- /// Swiss / Liechtensteinian German, Estonian, Icelandic, Lithuanian,
- /// Latvian, Slovak, Slovenian, Bosnian, Finnish, Swedish, French,
- /// Hungarian, Polish, Romanian, Japanese, Traditional Chinese, Russian, and
- /// Norwegian.
- ///
- /// For unknown languages, the English quotes are used.
- pub fn from_lang(lang: Lang, region: Option<Region>) -> Self {
- let region = region.as_ref().map(Region::as_str);
- let (single_open, single_close, double_open, double_close) = match lang.as_str() {
- "de" if matches!(region, Some("CH" | "LI")) => ("‹", "›", "«", "»"),
- "cs" | "da" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => {
- ("‚", "‘", "„", "“")
- }
- "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"),
- "bs" | "fi" | "sv" => ("’", "’", "”", "”"),
- "hu" | "pl" | "ro" => ("’", "’", "„", "”"),
- "ru" | "no" | "nn" => ("’", "’", "«", "»"),
- _ => return Self::default(),
- };
-
- Self {
- single_open,
- single_close,
- double_open,
- double_close,
- }
- }
-
- /// The opening quote.
- fn open(&self, double: bool) -> &'s str {
- if double { self.double_open } else { self.single_open }
- }
-
- /// The closing quote.
- fn close(&self, double: bool) -> &'s str {
- if double { self.double_close } else { self.single_close }
- }
-
- /// Which character should be used as a prime.
- fn prime(&self, double: bool) -> &'static str {
- if double { "″" } else { "′" }
- }
-
- /// Which character should be used as a fallback quote.
- fn fallback(&self, double: bool) -> &'static str {
- if double { "\"" } else { "’" }
- }
-}
-
-impl Default for Quotes<'_> {
- /// Returns the english quotes as default.
- fn default() -> Self {
- Self {
- single_open: "‘",
- single_close: "’",
- double_open: "“",
- double_close: "”",
- }
- }
-}
diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs
deleted file mode 100644
index 0c769636..00000000
--- a/src/library/text/raw.rs
+++ /dev/null
@@ -1,205 +0,0 @@
-use once_cell::sync::Lazy;
-use syntect::easy::HighlightLines;
-use syntect::highlighting::{
- Color, FontStyle, Style, StyleModifier, Theme, ThemeItem, ThemeSettings,
-};
-use syntect::parsing::SyntaxSet;
-
-use super::{FontFamily, Hyphenate, LinebreakNode, TextNode};
-use crate::library::layout::{BlockNode, BlockSpacing};
-use crate::library::prelude::*;
-
-/// Monospaced text with optional syntax highlighting.
-#[derive(Debug, Hash)]
-pub struct RawNode {
- /// The raw text.
- pub text: EcoString,
- /// Whether the node is block-level.
- pub block: bool,
-}
-
-#[node(Show)]
-impl RawNode {
- /// The language to syntax-highlight in.
- #[property(referenced)]
- pub const LANG: Option<EcoString> = None;
- /// The raw text's font family.
- #[property(referenced)]
- pub const FAMILY: FontFamily = FontFamily::new("IBM Plex Mono");
- /// The spacing above block-level raw.
- #[property(resolve, shorthand(around))]
- pub const ABOVE: Option<BlockSpacing> = Some(Ratio::one().into());
- /// The spacing below block-level raw.
- #[property(resolve, shorthand(around))]
- pub const BELOW: Option<BlockSpacing> = Some(Ratio::one().into());
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self {
- text: args.expect("text")?,
- block: args.named("block")?.unwrap_or(false),
- }
- .pack())
- }
-}
-
-impl Show for RawNode {
- fn unguard_parts(&self, _: Selector) -> Content {
- Self { text: self.text.clone(), ..*self }.pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "text" => Some(Value::Str(self.text.clone().into())),
- "block" => Some(Value::Bool(self.block)),
- _ => None,
- }
- }
-
- fn realize(
- &self,
- _: Tracked<dyn World>,
- styles: StyleChain,
- ) -> SourceResult<Content> {
- let lang = styles.get(Self::LANG).as_ref().map(|s| s.to_lowercase());
- let foreground = THEME
- .settings
- .foreground
- .map(Color::from)
- .unwrap_or(Color::BLACK)
- .into();
-
- let mut realized = if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) {
- let root = match lang.as_deref() {
- Some("typc") => crate::syntax::parse_code(&self.text),
- _ => crate::syntax::parse(&self.text),
- };
-
- let mut seq = vec![];
- crate::syntax::highlight::highlight_themed(&root, &THEME, |range, style| {
- seq.push(styled(&self.text[range], foreground, style));
- });
-
- Content::sequence(seq)
- } else if let Some(syntax) =
- lang.and_then(|token| SYNTAXES.find_syntax_by_token(&token))
- {
- let mut seq = vec![];
- let mut highlighter = HighlightLines::new(syntax, &THEME);
- for (i, line) in self.text.lines().enumerate() {
- if i != 0 {
- seq.push(LinebreakNode { justify: false }.pack());
- }
-
- for (style, piece) in
- highlighter.highlight_line(line, &SYNTAXES).into_iter().flatten()
- {
- seq.push(styled(piece, foreground, style));
- }
- }
-
- Content::sequence(seq)
- } else {
- TextNode(self.text.clone()).pack()
- };
-
- if self.block {
- realized = BlockNode(realized).pack();
- }
-
- let mut map = StyleMap::new();
- map.set(TextNode::OVERHANG, false);
- map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false)));
- map.set(TextNode::SMART_QUOTES, false);
-
- Ok(realized.styled_with_map(map))
- }
-
- fn finalize(
- &self,
- _: Tracked<dyn World>,
- styles: StyleChain,
- mut realized: Content,
- ) -> SourceResult<Content> {
- let mut map = StyleMap::new();
- map.set_family(styles.get(Self::FAMILY).clone(), styles);
-
- if self.block {
- realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW));
- }
-
- Ok(realized.styled_with_map(map))
- }
-}
-
-/// Style a piece of text with a syntect style.
-fn styled(piece: &str, foreground: Paint, style: Style) -> Content {
- let mut body = TextNode(piece.into()).pack();
-
- let paint = style.foreground.into();
- if paint != foreground {
- body = body.styled(TextNode::FILL, paint);
- }
-
- if style.font_style.contains(FontStyle::BOLD) {
- body = body.strong();
- }
-
- if style.font_style.contains(FontStyle::ITALIC) {
- body = body.emph();
- }
-
- if style.font_style.contains(FontStyle::UNDERLINE) {
- body = body.underlined();
- }
-
- body
-}
-
-/// The syntect syntax definitions.
-static SYNTAXES: Lazy<SyntaxSet> = Lazy::new(|| SyntaxSet::load_defaults_newlines());
-
-/// The default theme used for syntax highlighting.
-#[rustfmt::skip]
-pub static THEME: Lazy<Theme> = Lazy::new(|| Theme {
- name: Some("Typst Light".into()),
- author: Some("The Typst Project Developers".into()),
- settings: ThemeSettings::default(),
- scopes: vec![
- item("comment", Some("#8a8a8a"), None),
- item("constant.character.escape", Some("#1d6c76"), None),
- item("constant.character.shortcut", Some("#1d6c76"), None),
- item("markup.bold", None, Some(FontStyle::BOLD)),
- item("markup.italic", None, Some(FontStyle::ITALIC)),
- item("markup.underline", None, Some(FontStyle::UNDERLINE)),
- item("markup.raw", Some("#818181"), None),
- item("string.other.math.typst", None, None),
- item("punctuation.definition.math", Some("#298e0d"), None),
- item("keyword.operator.math", Some("#1d6c76"), None),
- item("markup.heading, entity.name.section", None, Some(FontStyle::BOLD)),
- item("markup.heading.typst", None, Some(FontStyle::BOLD | FontStyle::UNDERLINE)),
- item("punctuation.definition.list", Some("#8b41b1"), None),
- item("markup.list.term", None, Some(FontStyle::BOLD)),
- item("entity.name.label, markup.other.reference", Some("#1d6c76"), None),
- item("keyword, constant.language, variable.language", Some("#d73a49"), None),
- item("storage.type, storage.modifier", Some("#d73a49"), None),
- item("constant", Some("#b60157"), None),
- item("string", Some("#298e0d"), None),
- item("entity.name, variable.function, support", Some("#4b69c6"), None),
- item("support.macro", Some("#16718d"), None),
- item("meta.annotation", Some("#301414"), None),
- item("entity.other, meta.interpolation", Some("#8b41b1"), None),
- item("invalid", Some("#ff0000"), None),
- ],
-});
-
-/// Create a syntect theme item.
-fn item(scope: &str, color: Option<&str>, font_style: Option<FontStyle>) -> ThemeItem {
- ThemeItem {
- scope: scope.parse().unwrap(),
- style: StyleModifier {
- foreground: color.map(|s| s.parse::<RgbaColor>().unwrap().into()),
- background: None,
- font_style,
- },
- }
-}
diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs
deleted file mode 100644
index 9ac3db1c..00000000
--- a/src/library/text/shaping.rs
+++ /dev/null
@@ -1,654 +0,0 @@
-use std::ops::Range;
-use std::str::FromStr;
-
-use rustybuzz::{Feature, Tag, UnicodeBuffer};
-
-use super::*;
-use crate::font::{Font, FontVariant};
-use crate::library::prelude::*;
-use crate::util::SliceExt;
-
-/// The result of shaping text.
-///
-/// 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 struct ShapedText<'a> {
- /// The text that was shaped.
- pub text: &'a str,
- /// The text direction.
- pub dir: Dir,
- /// The text's style properties.
- pub styles: StyleChain<'a>,
- /// The font variant.
- pub variant: FontVariant,
- /// The font size.
- pub size: Abs,
- /// The width of the text's bounding box.
- pub width: Abs,
- /// The shaped glyphs.
- pub glyphs: Cow<'a, [ShapedGlyph]>,
-}
-
-/// A single glyph resulting from shaping.
-#[derive(Debug, Clone)]
-pub struct ShapedGlyph {
- /// The font the glyph is contained in.
- pub font: Font,
- /// The glyph's index in the font.
- pub glyph_id: u16,
- /// The advance width of the glyph.
- pub x_advance: Em,
- /// The horizontal offset of the glyph.
- pub x_offset: Em,
- /// The vertical offset of the glyph.
- pub y_offset: Em,
- /// The byte index in the source text where this glyph's cluster starts. A
- /// cluster is a sequence of one or multiple glyphs that cannot be
- /// separated and must always be treated as a union.
- pub cluster: usize,
- /// Whether splitting the shaping result before this glyph would yield the
- /// same results as shaping the parts to both sides of `text_index`
- /// separately.
- pub safe_to_break: bool,
- /// The first char in this glyph's cluster.
- pub c: char,
-}
-
-impl ShapedGlyph {
- /// Whether the glyph is a space.
- pub fn is_space(&self) -> bool {
- matches!(self.c, ' ' | '\u{00A0}' | ' ')
- }
-
- /// Whether the glyph is justifiable.
- pub fn is_justifiable(&self) -> bool {
- self.is_space() || matches!(self.c, ',' | '。' | '、')
- }
-}
-
-/// A side you can go toward.
-enum Side {
- /// To the left-hand side.
- Left,
- /// To the right-hand side.
- Right,
-}
-
-impl<'a> ShapedText<'a> {
- /// Build the shaped text's frame.
- ///
- /// The `justification` defines how much extra advance width each
- /// [justifiable glyph](ShapedGlyph::is_justifiable) will get.
- pub fn build(&self, world: Tracked<dyn World>, justification: Abs) -> Frame {
- let (top, bottom) = self.measure(world);
- let size = Size::new(self.width, top + bottom);
-
- let mut offset = Abs::zero();
- let mut frame = Frame::new(size);
- frame.set_baseline(top);
-
- let shift = self.styles.get(TextNode::BASELINE);
- let lang = self.styles.get(TextNode::LANG);
- let decos = self.styles.get(TextNode::DECO);
- let fill = self.styles.get(TextNode::FILL);
- let link = self.styles.get(TextNode::LINK);
-
- for ((font, y_offset), group) in
- self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset))
- {
- let pos = Point::new(offset, top + shift + y_offset.at(self.size));
-
- let glyphs = group
- .iter()
- .map(|glyph| Glyph {
- id: glyph.glyph_id,
- x_advance: glyph.x_advance
- + if glyph.is_justifiable() {
- frame.size_mut().x += justification;
- Em::from_length(justification, self.size)
- } else {
- Em::zero()
- },
- x_offset: glyph.x_offset,
- c: glyph.c,
- })
- .collect();
-
- let text = Text {
- font,
- size: self.size,
- lang,
- fill,
- glyphs,
- };
-
- let text_layer = frame.layer();
- let width = text.width();
-
- // Apply line decorations.
- for deco in &decos {
- decorate(&mut frame, &deco, &text, shift, pos, width);
- }
-
- frame.insert(text_layer, pos, Element::Text(text));
- offset += width;
- }
-
- // Apply link if it exists.
- if let Some(dest) = link {
- frame.link(dest.clone());
- }
-
- frame
- }
-
- /// Measure the top and bottom extent of this text.
- fn measure(&self, world: Tracked<dyn World>) -> (Abs, Abs) {
- let mut top = Abs::zero();
- let mut bottom = Abs::zero();
-
- let top_edge = self.styles.get(TextNode::TOP_EDGE);
- let bottom_edge = self.styles.get(TextNode::BOTTOM_EDGE);
-
- // Expand top and bottom by reading the font's vertical metrics.
- let mut expand = |font: &Font| {
- let metrics = font.metrics();
- top.set_max(top_edge.resolve(self.styles, metrics));
- bottom.set_max(-bottom_edge.resolve(self.styles, metrics));
- };
-
- if self.glyphs.is_empty() {
- // When there are no glyphs, we just use the vertical metrics of the
- // first available font.
- for family in families(self.styles) {
- if let Some(font) = world
- .book()
- .select(family, self.variant)
- .and_then(|id| world.font(id))
- {
- expand(&font);
- break;
- }
- }
- } else {
- for g in self.glyphs.iter() {
- expand(&g.font);
- }
- }
-
- (top, bottom)
- }
-
- /// How many justifiable glyphs the text contains.
- pub fn justifiables(&self) -> usize {
- self.glyphs.iter().filter(|g| g.is_justifiable()).count()
- }
-
- /// The width of the spaces in the text.
- pub fn stretch(&self) -> Abs {
- self.glyphs
- .iter()
- .filter(|g| g.is_justifiable())
- .map(|g| g.x_advance)
- .sum::<Em>()
- .at(self.size)
- }
-
- /// Reshape a range of the shaped text, reusing information from this
- /// shaping process if possible.
- pub fn reshape(
- &'a self,
- world: Tracked<dyn World>,
- text_range: Range<usize>,
- ) -> ShapedText<'a> {
- if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
- Self {
- text: &self.text[text_range],
- dir: self.dir,
- styles: self.styles,
- size: self.size,
- variant: self.variant,
- width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
- glyphs: Cow::Borrowed(glyphs),
- }
- } else {
- shape(world, &self.text[text_range], self.styles, self.dir)
- }
- }
-
- /// Push a hyphen to end of the text.
- pub fn push_hyphen(&mut self, world: Tracked<dyn World>) {
- families(self.styles).find_map(|family| {
- let font = world
- .book()
- .select(family, self.variant)
- .and_then(|id| world.font(id))?;
- let ttf = font.ttf();
- let glyph_id = ttf.glyph_index('-')?;
- let x_advance = font.to_em(ttf.glyph_hor_advance(glyph_id)?);
- let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default();
- self.width += x_advance.at(self.size);
- self.glyphs.to_mut().push(ShapedGlyph {
- font,
- glyph_id: glyph_id.0,
- x_advance,
- x_offset: Em::zero(),
- y_offset: Em::zero(),
- cluster,
- safe_to_break: true,
- c: '-',
- });
- Some(())
- });
- }
-
- /// Find the subslice of glyphs that represent the given text range if both
- /// sides are safe to break.
- fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> {
- let Range { mut start, mut end } = text_range;
- if !self.dir.is_positive() {
- std::mem::swap(&mut start, &mut end);
- }
-
- let left = self.find_safe_to_break(start, Side::Left)?;
- let right = self.find_safe_to_break(end, Side::Right)?;
- Some(&self.glyphs[left .. right])
- }
-
- /// Find the glyph offset matching the text index that is most towards the
- /// given side and safe-to-break.
- fn find_safe_to_break(&self, text_index: usize, towards: Side) -> Option<usize> {
- let ltr = self.dir.is_positive();
-
- // Handle edge cases.
- let len = self.glyphs.len();
- if text_index == 0 {
- return Some(if ltr { 0 } else { len });
- } else if text_index == self.text.len() {
- return Some(if ltr { len } else { 0 });
- }
-
- // Find any glyph with the text index.
- let mut idx = self
- .glyphs
- .binary_search_by(|g| {
- let ordering = g.cluster.cmp(&text_index);
- if ltr { ordering } else { ordering.reverse() }
- })
- .ok()?;
-
- let next = match towards {
- Side::Left => usize::checked_sub,
- Side::Right => usize::checked_add,
- };
-
- // Search for the outermost glyph with the text index.
- while let Some(next) = next(idx, 1) {
- if self.glyphs.get(next).map_or(true, |g| g.cluster != text_index) {
- break;
- }
- idx = next;
- }
-
- // RTL needs offset one because the left side of the range should be
- // exclusive and the right side inclusive, contrary to the normal
- // behaviour of ranges.
- self.glyphs[idx].safe_to_break.then(|| idx + (!ltr) as usize)
- }
-}
-
-impl Debug for ShapedText<'_> {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- self.text.fmt(f)
- }
-}
-
-/// Holds shaping results and metadata common to all shaped segments.
-struct ShapingContext<'a> {
- world: Tracked<'a, dyn World>,
- glyphs: Vec<ShapedGlyph>,
- used: Vec<Font>,
- styles: StyleChain<'a>,
- size: Abs,
- variant: FontVariant,
- tags: Vec<rustybuzz::Feature>,
- fallback: bool,
- dir: Dir,
-}
-
-/// Shape text into [`ShapedText`].
-pub fn shape<'a>(
- world: Tracked<dyn World>,
- text: &'a str,
- styles: StyleChain<'a>,
- dir: Dir,
-) -> ShapedText<'a> {
- let size = styles.get(TextNode::SIZE);
-
- let mut ctx = ShapingContext {
- world,
- size,
- glyphs: vec![],
- used: vec![],
- styles,
- variant: variant(styles),
- tags: tags(styles),
- fallback: styles.get(TextNode::FALLBACK),
- dir,
- };
-
- if !text.is_empty() {
- shape_segment(&mut ctx, 0, &text, families(styles));
- }
-
- track_and_space(&mut ctx);
-
- ShapedText {
- text,
- dir,
- styles,
- variant: ctx.variant,
- size,
- width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
- glyphs: Cow::Owned(ctx.glyphs),
- }
-}
-
-/// Shape text with font fallback using the `families` iterator.
-fn shape_segment<'a>(
- ctx: &mut ShapingContext,
- base: usize,
- text: &str,
- mut families: impl Iterator<Item = &'a str> + Clone,
-) {
- // Fonts dont have newlines and tabs.
- if text.chars().all(|c| c == '\n' || c == '\t') {
- return;
- }
-
- // Find the next available family.
- let book = ctx.world.book();
- let mut selection = families.find_map(|family| {
- book.select(family, ctx.variant)
- .and_then(|id| ctx.world.font(id))
- .filter(|font| !ctx.used.contains(font))
- });
-
- // Do font fallback if the families are exhausted and fallback is enabled.
- if selection.is_none() && ctx.fallback {
- let first = ctx.used.first().map(Font::info);
- selection = book
- .select_fallback(first, ctx.variant, text)
- .and_then(|id| ctx.world.font(id))
- .filter(|font| !ctx.used.contains(font));
- }
-
- // Extract the font id or shape notdef glyphs if we couldn't find any font.
- let font = if let Some(font) = selection {
- font
- } else {
- if let Some(font) = ctx.used.first().cloned() {
- shape_tofus(ctx, base, text, font);
- }
- return;
- };
-
- ctx.used.push(font.clone());
-
- // Fill the buffer with our text.
- let mut buffer = UnicodeBuffer::new();
- buffer.push_str(text);
- buffer.set_language(language(ctx.styles));
- buffer.set_direction(match ctx.dir {
- Dir::LTR => rustybuzz::Direction::LeftToRight,
- Dir::RTL => rustybuzz::Direction::RightToLeft,
- _ => unimplemented!("vertical text layout"),
- });
-
- // Shape!
- let buffer = rustybuzz::shape(font.rusty(), &ctx.tags, buffer);
- let infos = buffer.glyph_infos();
- let pos = buffer.glyph_positions();
-
- // Collect the shaped glyphs, doing fallback and shaping parts again with
- // the next font if necessary.
- let mut i = 0;
- while i < infos.len() {
- let info = &infos[i];
- let cluster = info.cluster as usize;
-
- if info.glyph_id != 0 {
- // Add the glyph to the shaped output.
- // TODO: Don't ignore y_advance.
- ctx.glyphs.push(ShapedGlyph {
- font: font.clone(),
- glyph_id: info.glyph_id as u16,
- x_advance: font.to_em(pos[i].x_advance),
- x_offset: font.to_em(pos[i].x_offset),
- y_offset: font.to_em(pos[i].y_offset),
- cluster: base + cluster,
- safe_to_break: !info.unsafe_to_break(),
- c: text[cluster ..].chars().next().unwrap(),
- });
- } else {
- // Determine the source text range for the tofu sequence.
- let range = {
- // First, search for the end of the tofu sequence.
- let k = i;
- while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) {
- i += 1;
- }
-
- // Then, determine the start and end text index.
- //
- // Examples:
- // Everything is shown in visual order. Tofus are written as "_".
- // We want to find out that the tofus span the text `2..6`.
- // Note that the clusters are longer than 1 char.
- //
- // Left-to-right:
- // Text: h a l i h a l l o
- // Glyphs: A _ _ C E
- // Clusters: 0 2 4 6 8
- // k=1 i=2
- //
- // Right-to-left:
- // Text: O L L A H I L A H
- // Glyphs: E C _ _ A
- // Clusters: 8 6 4 2 0
- // k=2 i=3
- let ltr = ctx.dir.is_positive();
- let first = if ltr { k } else { i };
- let start = infos[first].cluster as usize;
- let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) };
- let end = last
- .and_then(|last| infos.get(last))
- .map_or(text.len(), |info| info.cluster as usize);
-
- start .. end
- };
-
- // Trim half-baked cluster.
- let remove = base + range.start .. base + range.end;
- while ctx.glyphs.last().map_or(false, |g| remove.contains(&g.cluster)) {
- ctx.glyphs.pop();
- }
-
- // Recursively shape the tofu sequence with the next family.
- shape_segment(ctx, base + range.start, &text[range], families.clone());
- }
-
- i += 1;
- }
-
- ctx.used.pop();
-}
-
-/// Shape the text with tofus from the given font.
-fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) {
- let x_advance = font.advance(0).unwrap_or_default();
- for (cluster, c) in text.char_indices() {
- ctx.glyphs.push(ShapedGlyph {
- font: font.clone(),
- glyph_id: 0,
- x_advance,
- x_offset: Em::zero(),
- y_offset: Em::zero(),
- cluster: base + cluster,
- safe_to_break: true,
- c,
- });
- }
-}
-
-/// Apply tracking and spacing to the shaped glyphs.
-fn track_and_space(ctx: &mut ShapingContext) {
- let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), ctx.size);
- let spacing = ctx
- .styles
- .get(TextNode::SPACING)
- .map(|abs| Em::from_length(abs, ctx.size));
-
- let mut glyphs = ctx.glyphs.iter_mut().peekable();
- while let Some(glyph) = glyphs.next() {
- // Make non-breaking space same width as normal space.
- if glyph.c == '\u{00A0}' {
- glyph.x_advance -= nbsp_delta(&glyph.font).unwrap_or_default();
- }
-
- if glyph.is_space() {
- glyph.x_advance = spacing.relative_to(glyph.x_advance);
- }
-
- if glyphs.peek().map_or(false, |next| glyph.cluster != next.cluster) {
- glyph.x_advance += tracking;
- }
- }
-}
-
-/// Difference between non-breaking and normal space.
-fn nbsp_delta(font: &Font) -> Option<Em> {
- let space = font.ttf().glyph_index(' ')?.0;
- let nbsp = font.ttf().glyph_index('\u{00A0}')?.0;
- Some(font.advance(nbsp)? - font.advance(space)?)
-}
-
-/// Resolve the font variant with `BOLD` and `ITALIC` factored in.
-pub fn variant(styles: StyleChain) -> FontVariant {
- let mut variant = FontVariant::new(
- styles.get(TextNode::STYLE),
- styles.get(TextNode::WEIGHT),
- styles.get(TextNode::STRETCH),
- );
-
- if styles.get(TextNode::BOLD) {
- variant.weight = variant.weight.thicken(300);
- }
-
- if styles.get(TextNode::ITALIC) {
- variant.style = match variant.style {
- FontStyle::Normal => FontStyle::Italic,
- FontStyle::Italic => FontStyle::Normal,
- FontStyle::Oblique => FontStyle::Normal,
- }
- }
-
- variant
-}
-
-/// Resolve a prioritized iterator over the font families.
-fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
- const FALLBACKS: &[&str] = &[
- "ibm plex sans",
- "twitter color emoji",
- "noto color emoji",
- "apple color emoji",
- "segoe ui emoji",
- ];
-
- let tail = if styles.get(TextNode::FALLBACK) { FALLBACKS } else { &[] };
- styles
- .get(TextNode::FAMILY)
- .iter()
- .map(|family| family.as_str())
- .chain(tail.iter().copied())
-}
-
-/// Collect the tags of the OpenType features to apply.
-fn tags(styles: StyleChain) -> Vec<Feature> {
- let mut tags = vec![];
- let mut feat = |tag, value| {
- tags.push(Feature::new(Tag::from_bytes(tag), value, ..));
- };
-
- // Features that are on by default in Harfbuzz are only added if disabled.
- if !styles.get(TextNode::KERNING) {
- feat(b"kern", 0);
- }
-
- // Features that are off by default in Harfbuzz are only added if enabled.
- if styles.get(TextNode::SMALLCAPS) {
- feat(b"smcp", 1);
- }
-
- if styles.get(TextNode::ALTERNATES) {
- feat(b"salt", 1);
- }
-
- let storage;
- if let Some(set) = styles.get(TextNode::STYLISTIC_SET) {
- storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10];
- feat(&storage, 1);
- }
-
- if !styles.get(TextNode::LIGATURES) {
- feat(b"liga", 0);
- feat(b"clig", 0);
- }
-
- if styles.get(TextNode::DISCRETIONARY_LIGATURES) {
- feat(b"dlig", 1);
- }
-
- if styles.get(TextNode::HISTORICAL_LIGATURES) {
- feat(b"hilg", 1);
- }
-
- match styles.get(TextNode::NUMBER_TYPE) {
- Smart::Auto => {}
- Smart::Custom(NumberType::Lining) => feat(b"lnum", 1),
- Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1),
- }
-
- match styles.get(TextNode::NUMBER_WIDTH) {
- Smart::Auto => {}
- Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1),
- Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1),
- }
-
- if styles.get(TextNode::SLASHED_ZERO) {
- feat(b"zero", 1);
- }
-
- if styles.get(TextNode::FRACTIONS) {
- feat(b"frac", 1);
- }
-
- for (tag, value) in styles.get(TextNode::FEATURES) {
- tags.push(Feature::new(tag, value, ..))
- }
-
- tags
-}
-
-/// Process the language and and region of a style chain into a
-/// rustybuzz-compatible BCP 47 language.
-fn language(styles: StyleChain) -> rustybuzz::Language {
- let mut bcp: EcoString = styles.get(TextNode::LANG).as_str().into();
- if let Some(region) = styles.get(TextNode::REGION) {
- bcp.push('-');
- bcp.push_str(region.as_str());
- }
- rustybuzz::Language::from_str(&bcp).unwrap()
-}
diff --git a/src/library/text/shift.rs b/src/library/text/shift.rs
deleted file mode 100644
index c3cf8b03..00000000
--- a/src/library/text/shift.rs
+++ /dev/null
@@ -1,187 +0,0 @@
-use super::{variant, SpaceNode, TextNode, TextSize};
-use crate::library::prelude::*;
-use crate::model::SequenceNode;
-use crate::util::EcoString;
-
-/// Sub or superscript text.
-///
-/// The text is rendered smaller and its baseline is raised. To provide the best
-/// typography possible, we first try to transform the text to superscript
-/// codepoints. If that fails, we fall back to rendering shrunk normal letters
-/// in a raised way.
-#[derive(Debug, Hash)]
-pub struct ShiftNode<const S: ScriptKind>(pub Content);
-
-/// Shift the text into superscript.
-pub type SuperNode = ShiftNode<SUPERSCRIPT>;
-
-/// Shift the text into subscript.
-pub type SubNode = ShiftNode<SUBSCRIPT>;
-
-#[node(Show)]
-impl<const S: ScriptKind> ShiftNode<S> {
- /// Whether to prefer the dedicated sub- and superscript characters of the
- /// font.
- pub const TYPOGRAPHIC: bool = true;
- /// The baseline shift for synthetic sub- and superscripts.
- pub const BASELINE: Length =
- Em::new(if S == SUPERSCRIPT { -0.5 } else { 0.2 }).into();
- /// The font size for synthetic sub- and superscripts.
- pub const SIZE: TextSize = TextSize(Em::new(0.6).into());
-
- fn construct(_: &mut Vm, args: &mut Args) -> SourceResult<Content> {
- Ok(Self(args.expect("body")?).pack())
- }
-}
-
-impl<const S: ScriptKind> Show for ShiftNode<S> {
- fn unguard_parts(&self, _: Selector) -> Content {
- Self(self.0.clone()).pack()
- }
-
- fn field(&self, name: &str) -> Option<Value> {
- match name {
- "body" => Some(Value::Content(self.0.clone())),
- _ => None,
- }
- }
-
- fn realize(
- &self,
- world: Tracked<dyn World>,
- styles: StyleChain,
- ) -> SourceResult<Content> {
- let mut transformed = None;
- if styles.get(Self::TYPOGRAPHIC) {
- if let Some(text) = search_text(&self.0, S) {
- if is_shapable(world, &text, styles) {
- transformed = Some(TextNode(text).pack());
- }
- }
- };
-
- Ok(transformed.unwrap_or_else(|| {
- let mut map = StyleMap::new();
- map.set(TextNode::BASELINE, styles.get(Self::BASELINE));
- map.set(TextNode::SIZE, styles.get(Self::SIZE));
- self.0.clone().styled_with_map(map)
- }))
- }
-}
-
-/// Find and transform the text contained in `content` to the given script kind
-/// if and only if it only consists of `Text`, `Space`, and `Empty` leaf nodes.
-fn search_text(content: &Content, mode: ScriptKind) -> Option<EcoString> {
- if content.is_empty() {
- Some(EcoString::new())
- } else if content.is::<SpaceNode>() {
- Some(' '.into())
- } else if let Some(text) = content.downcast::<TextNode>() {
- if let Some(sup) = convert_script(&text.0, mode) {
- return Some(sup);
- }
- None
- } else if let Some(seq) = content.downcast::<SequenceNode>() {
- let mut full = EcoString::new();
- for item in seq.0.iter() {
- match search_text(item, mode) {
- Some(text) => full.push_str(&text),
- None => return None,
- }
- }
- Some(full)
- } else {
- None
- }
-}
-
-/// Checks whether the first retrievable family contains all code points of the
-/// given string.
-fn is_shapable(world: Tracked<dyn World>, text: &str, styles: StyleChain) -> bool {
- for family in styles.get(TextNode::FAMILY).iter() {
- if let Some(font) = world
- .book()
- .select(family.as_str(), variant(styles))
- .and_then(|id| world.font(id))
- {
- return text.chars().all(|c| font.ttf().glyph_index(c).is_some());
- }
- }
-
- false
-}
-
-/// Convert a string to sub- or superscript codepoints if all characters
-/// can be mapped to such a codepoint.
-fn convert_script(text: &str, mode: ScriptKind) -> Option<EcoString> {
- let mut result = EcoString::with_capacity(text.len());
- let converter = match mode {
- SUPERSCRIPT => to_superscript_codepoint,
- SUBSCRIPT | _ => to_subscript_codepoint,
- };
-
- for c in text.chars() {
- match converter(c) {
- Some(c) => result.push(c),
- None => return None,
- }
- }
-
- Some(result)
-}
-
-/// Convert a character to its corresponding Unicode superscript.
-fn to_superscript_codepoint(c: char) -> Option<char> {
- char::from_u32(match c {
- '0' => 0x2070,
- '1' => 0x00B9,
- '2' => 0x00B2,
- '3' => 0x00B3,
- '4' ..= '9' => 0x2070 + (c as u32 + 4 - '4' as u32),
- '+' => 0x207A,
- '-' => 0x207B,
- '=' => 0x207C,
- '(' => 0x207D,
- ')' => 0x207E,
- 'n' => 0x207F,
- 'i' => 0x2071,
- ' ' => 0x0020,
- _ => return None,
- })
-}
-
-/// Convert a character to its corresponding Unicode subscript.
-fn to_subscript_codepoint(c: char) -> Option<char> {
- char::from_u32(match c {
- '0' => 0x2080,
- '1' ..= '9' => 0x2080 + (c as u32 - '0' as u32),
- '+' => 0x208A,
- '-' => 0x208B,
- '=' => 0x208C,
- '(' => 0x208D,
- ')' => 0x208E,
- 'a' => 0x2090,
- 'e' => 0x2091,
- 'o' => 0x2092,
- 'x' => 0x2093,
- 'h' => 0x2095,
- 'k' => 0x2096,
- 'l' => 0x2097,
- 'm' => 0x2098,
- 'n' => 0x2099,
- 'p' => 0x209A,
- 's' => 0x209B,
- 't' => 0x209C,
- ' ' => 0x0020,
- _ => return None,
- })
-}
-
-/// A category of script.
-pub type ScriptKind = usize;
-
-/// Text that is rendered smaller and raised, also known as superior.
-const SUPERSCRIPT: ScriptKind = 0;
-
-/// Text that is rendered smaller and lowered, also known as inferior.
-const SUBSCRIPT: ScriptKind = 1;
diff --git a/src/library/utility/color.rs b/src/library/utility/color.rs
deleted file mode 100644
index a5a5704d..00000000
--- a/src/library/utility/color.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-use std::str::FromStr;
-
-use crate::library::prelude::*;
-
-/// Create a grayscale color.
-pub fn luma(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Component(luma) = args.expect("gray component")?;
- Ok(Value::Color(LumaColor::new(luma).into()))
-}
-
-/// Create an RGB(A) color.
-pub fn rgb(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- Ok(Value::Color(
- if let Some(string) = args.find::<Spanned<EcoString>>()? {
- match RgbaColor::from_str(&string.v) {
- Ok(color) => color.into(),
- Err(msg) => bail!(string.span, msg),
- }
- } else {
- let Component(r) = args.expect("red component")?;
- let Component(g) = args.expect("green component")?;
- let Component(b) = args.expect("blue component")?;
- let Component(a) = args.eat()?.unwrap_or(Component(255));
- RgbaColor::new(r, g, b, a).into()
- },
- ))
-}
-
-/// Create a CMYK color.
-pub fn cmyk(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let RatioComponent(c) = args.expect("cyan component")?;
- let RatioComponent(m) = args.expect("magenta component")?;
- let RatioComponent(y) = args.expect("yellow component")?;
- let RatioComponent(k) = args.expect("key component")?;
- Ok(Value::Color(CmykColor::new(c, m, y, k).into()))
-}
-
-/// An integer or ratio component.
-struct Component(u8);
-
-castable! {
- Component,
- Expected: "integer or ratio",
- Value::Int(v) => match v {
- 0 ..= 255 => Self(v as u8),
- _ => Err("must be between 0 and 255")?,
- },
- Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) {
- Self((v.get() * 255.0).round() as u8)
- } else {
- Err("must be between 0% and 100%")?
- },
-}
-
-/// A component that must be a ratio.
-struct RatioComponent(u8);
-
-castable! {
- RatioComponent,
- Expected: "ratio",
- Value::Ratio(v) => if (0.0 ..= 1.0).contains(&v.get()) {
- Self((v.get() * 255.0).round() as u8)
- } else {
- Err("must be between 0% and 100%")?
- },
-}
diff --git a/src/library/utility/data.rs b/src/library/utility/data.rs
deleted file mode 100644
index 0cff42c1..00000000
--- a/src/library/utility/data.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-use std::fmt::Write;
-
-use crate::diag::format_xml_like_error;
-use crate::library::prelude::*;
-
-/// Read structured data from a CSV file.
-pub fn csv(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v: path, span } =
- args.expect::<Spanned<EcoString>>("path to csv file")?;
-
- let path = vm.locate(&path).at(span)?;
- let data = vm.world.file(&path).at(span)?;
-
- let mut builder = csv::ReaderBuilder::new();
- builder.has_headers(false);
-
- let mut reader = builder.from_reader(data.as_slice());
- let mut vec = vec![];
-
- for result in reader.records() {
- let row = result.map_err(format_csv_error).at(span)?;
- let array = row.iter().map(|field| Value::Str(field.into())).collect();
- vec.push(Value::Array(array))
- }
-
- Ok(Value::Array(Array::from_vec(vec)))
-}
-
-/// Format the user-facing CSV error message.
-fn format_csv_error(error: csv::Error) -> String {
- match error.kind() {
- csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(),
- csv::ErrorKind::UnequalLengths { pos, expected_len, len } => {
- let mut msg = format!(
- "failed to parse csv file: found {len} instead of {expected_len} fields"
- );
- if let Some(pos) = pos {
- write!(msg, " in line {}", pos.line()).unwrap();
- }
- msg
- }
- _ => "failed to parse csv file".into(),
- }
-}
-
-/// Read structured data from a JSON file.
-pub fn json(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v: path, span } =
- args.expect::<Spanned<EcoString>>("path to json file")?;
-
- let path = vm.locate(&path).at(span)?;
- let data = vm.world.file(&path).at(span)?;
- let value: serde_json::Value =
- serde_json::from_slice(&data).map_err(format_json_error).at(span)?;
-
- Ok(convert_json(value))
-}
-
-/// Convert a JSON value to a Typst value.
-fn convert_json(value: serde_json::Value) -> Value {
- match value {
- serde_json::Value::Null => Value::None,
- serde_json::Value::Bool(v) => Value::Bool(v),
- serde_json::Value::Number(v) => match v.as_i64() {
- Some(int) => Value::Int(int),
- None => Value::Float(v.as_f64().unwrap_or(f64::NAN)),
- },
- serde_json::Value::String(v) => Value::Str(v.into()),
- serde_json::Value::Array(v) => {
- Value::Array(v.into_iter().map(convert_json).collect())
- }
- serde_json::Value::Object(v) => Value::Dict(
- v.into_iter()
- .map(|(key, value)| (key.into(), convert_json(value)))
- .collect(),
- ),
- }
-}
-
-/// Format the user-facing JSON error message.
-fn format_json_error(error: serde_json::Error) -> String {
- assert!(error.is_syntax() || error.is_eof());
- format!(
- "failed to parse json file: syntax error in line {}",
- error.line()
- )
-}
-
-/// Read structured data from an XML file.
-pub fn xml(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v: path, span } =
- args.expect::<Spanned<EcoString>>("path to xml file")?;
-
- let path = vm.locate(&path).at(span)?;
- let data = vm.world.file(&path).at(span)?;
- let text = std::str::from_utf8(&data).map_err(FileError::from).at(span)?;
-
- let document = roxmltree::Document::parse(text).map_err(format_xml_error).at(span)?;
-
- Ok(convert_xml(document.root()))
-}
-
-/// Convert an XML node to a Typst value.
-fn convert_xml(node: roxmltree::Node) -> Value {
- if node.is_text() {
- return Value::Str(node.text().unwrap_or_default().into());
- }
-
- let children: Array = node.children().map(convert_xml).collect();
- if node.is_root() {
- return Value::Array(children);
- }
-
- let tag: Str = node.tag_name().name().into();
- let attrs: Dict = node
- .attributes()
- .iter()
- .map(|attr| (attr.name().into(), attr.value().into()))
- .collect();
-
- Value::Dict(dict! {
- "tag" => tag,
- "attrs" => attrs,
- "children" => children,
- })
-}
-
-/// Format the user-facing XML error message.
-fn format_xml_error(error: roxmltree::Error) -> String {
- format_xml_like_error("xml file", error)
-}
diff --git a/src/library/utility/math.rs b/src/library/utility/math.rs
deleted file mode 100644
index 7c3af490..00000000
--- a/src/library/utility/math.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-use std::cmp::Ordering;
-
-use crate::library::prelude::*;
-
-/// Convert a value to an integer.
-pub fn int(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v, span } = args.expect("value")?;
- Ok(Value::Int(match v {
- Value::Bool(v) => v as i64,
- Value::Int(v) => v,
- Value::Float(v) => v as i64,
- Value::Str(v) => match v.parse() {
- Ok(v) => v,
- Err(_) => bail!(span, "invalid integer"),
- },
- v => bail!(span, "cannot convert {} to integer", v.type_name()),
- }))
-}
-
-/// Convert a value to a float.
-pub fn float(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v, span } = args.expect("value")?;
- Ok(Value::Float(match v {
- Value::Int(v) => v as f64,
- Value::Float(v) => v,
- Value::Str(v) => match v.parse() {
- Ok(v) => v,
- Err(_) => bail!(span, "invalid float"),
- },
- v => bail!(span, "cannot convert {} to float", v.type_name()),
- }))
-}
-
-/// The absolute value of a numeric value.
-pub fn abs(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v, span } = args.expect("numeric value")?;
- Ok(match v {
- Value::Int(v) => Value::Int(v.abs()),
- Value::Float(v) => Value::Float(v.abs()),
- Value::Angle(v) => Value::Angle(v.abs()),
- Value::Ratio(v) => Value::Ratio(v.abs()),
- Value::Fraction(v) => Value::Fraction(v.abs()),
- Value::Length(_) | Value::Relative(_) => {
- bail!(span, "cannot take absolute value of a length")
- }
- v => bail!(span, "expected numeric value, found {}", v.type_name()),
- })
-}
-
-/// The minimum of a sequence of values.
-pub fn min(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- minmax(args, Ordering::Less)
-}
-
-/// The maximum of a sequence of values.
-pub fn max(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- minmax(args, Ordering::Greater)
-}
-
-/// Find the minimum or maximum of a sequence of values.
-fn minmax(args: &mut Args, goal: Ordering) -> SourceResult<Value> {
- let mut extremum = args.expect::<Value>("value")?;
- for Spanned { v, span } in args.all::<Spanned<Value>>()? {
- match v.partial_cmp(&extremum) {
- Some(ordering) => {
- if ordering == goal {
- extremum = v;
- }
- }
- None => bail!(
- span,
- "cannot compare {} and {}",
- extremum.type_name(),
- v.type_name(),
- ),
- }
- }
- Ok(extremum)
-}
-
-/// Whether an integer is even.
-pub fn even(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- Ok(Value::Bool(args.expect::<i64>("integer")? % 2 == 0))
-}
-
-/// Whether an integer is odd.
-pub fn odd(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- Ok(Value::Bool(args.expect::<i64>("integer")? % 2 != 0))
-}
-
-/// The modulo of two numbers.
-pub fn mod_(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v: v1, span: span1 } = args.expect("integer or float")?;
- let Spanned { v: v2, span: span2 } = args.expect("integer or float")?;
-
- let (a, b) = match (v1, v2) {
- (Value::Int(a), Value::Int(b)) => match a.checked_rem(b) {
- Some(res) => return Ok(Value::Int(res)),
- None => bail!(span2, "divisor must not be zero"),
- },
- (Value::Int(a), Value::Float(b)) => (a as f64, b),
- (Value::Float(a), Value::Int(b)) => (a, b as f64),
- (Value::Float(a), Value::Float(b)) => (a, b),
- (Value::Int(_), b) | (Value::Float(_), b) => bail!(
- span2,
- format!("expected integer or float, found {}", b.type_name())
- ),
- (a, _) => bail!(
- span1,
- format!("expected integer or float, found {}", a.type_name())
- ),
- };
-
- if b == 0.0 {
- bail!(span2, "divisor must not be zero");
- }
-
- Ok(Value::Float(a % b))
-}
-
-/// Create a sequence of numbers.
-pub fn range(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let first = args.expect::<i64>("end")?;
- let (start, end) = match args.eat::<i64>()? {
- Some(second) => (first, second),
- None => (0, first),
- };
-
- let step: i64 = match args.named("step")? {
- Some(Spanned { v: 0, span }) => bail!(span, "step must not be zero"),
- Some(Spanned { v, .. }) => v,
- None => 1,
- };
-
- let mut x = start;
- let mut seq = vec![];
-
- while x.cmp(&end) == 0.cmp(&step) {
- seq.push(Value::Int(x));
- x += step;
- }
-
- Ok(Value::Array(Array::from_vec(seq)))
-}
diff --git a/src/library/utility/mod.rs b/src/library/utility/mod.rs
deleted file mode 100644
index 2d637d29..00000000
--- a/src/library/utility/mod.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-//! Computational utility functions.
-
-mod color;
-mod data;
-mod math;
-mod string;
-
-pub use color::*;
-pub use data::*;
-pub use math::*;
-pub use string::*;
-
-use comemo::Track;
-
-use crate::library::prelude::*;
-use crate::model::{Eval, Route, Scopes, Vm};
-use crate::syntax::Source;
-
-/// The name of a value's type.
-pub fn type_(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- Ok(args.expect::<Value>("value")?.type_name().into())
-}
-
-/// Ensure that a condition is fulfilled.
-pub fn assert(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v, span } = args.expect::<Spanned<bool>>("condition")?;
- if !v {
- bail!(span, "assertion failed");
- }
- Ok(Value::None)
-}
-
-/// Evaluate a string as Typst markup.
-pub fn eval(vm: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v: text, span } = args.expect::<Spanned<String>>("source")?;
-
- // Parse the source and set a synthetic span for all nodes.
- let source = Source::synthesized(text, span);
- let ast = source.ast()?;
-
- // Evaluate the source.
- let std = &vm.world.config().std;
- let scopes = Scopes::new(Some(std));
- let route = Route::default();
- let mut sub = Vm::new(vm.world, route.track(), None, scopes);
- let result = ast.eval(&mut sub);
-
- // Handle control flow.
- if let Some(flow) = sub.flow {
- bail!(flow.forbidden());
- }
-
- Ok(Value::Content(result?))
-}
diff --git a/src/library/utility/string.rs b/src/library/utility/string.rs
deleted file mode 100644
index 66f127d1..00000000
--- a/src/library/utility/string.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-use crate::library::prelude::*;
-use crate::model::Regex;
-
-/// The string representation of a value.
-pub fn repr(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- Ok(args.expect::<Value>("value")?.repr().into())
-}
-
-/// Convert a value to a string.
-pub fn str(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v, span } = args.expect("value")?;
- Ok(Value::Str(match v {
- Value::Int(v) => format_str!("{}", v),
- Value::Float(v) => format_str!("{}", v),
- Value::Str(v) => v,
- v => bail!(span, "cannot convert {} to string", v.type_name()),
- }))
-}
-
-/// Create blind text.
-pub fn lorem(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let words: usize = args.expect("number of words")?;
- Ok(Value::Str(lipsum::lipsum(words).into()))
-}
-
-/// Create a regular expression.
-pub fn regex(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- let Spanned { v, span } = args.expect::<Spanned<EcoString>>("regular expression")?;
- Ok(Regex::new(&v).at(span)?.into())
-}
-
-/// Converts an integer into one or multiple letters.
-pub fn letter(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- numbered(Numbering::Letter, args)
-}
-
-/// Converts an integer into a roman numeral.
-pub fn roman(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- numbered(Numbering::Roman, args)
-}
-
-/// Convert a number into a symbol.
-pub fn symbol(_: &mut Vm, args: &mut Args) -> SourceResult<Value> {
- numbered(Numbering::Symbol, args)
-}
-
-fn numbered(numbering: Numbering, args: &mut Args) -> SourceResult<Value> {
- let n = args.expect::<usize>("non-negative integer")?;
- Ok(Value::Str(numbering.apply(n).into()))
-}
-
-/// Allows to convert a number into letters, roman numerals and symbols.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum Numbering {
- Arabic,
- Letter,
- Roman,
- Symbol,
-}
-
-impl Numbering {
- /// Apply the numbering to the given number.
- pub fn apply(self, mut n: usize) -> EcoString {
- match self {
- Self::Arabic => {
- format_eco!("{}", n)
- }
- Self::Letter => {
- if n == 0 {
- return '-'.into();
- }
-
- n -= 1;
-
- let mut letters = vec![];
- loop {
- letters.push(b'a' + (n % 26) as u8);
- n /= 26;
- if n == 0 {
- break;
- }
- }
-
- letters.reverse();
- String::from_utf8(letters).unwrap().into()
- }
- Self::Roman => {
- if n == 0 {
- return 'N'.into();
- }
-
- // Adapted from Yann Villessuzanne's roman.rs under the Unlicense, at
- // https://github.com/linfir/roman.rs/
- let mut fmt = EcoString::new();
- for &(name, value) in ROMANS {
- while n >= value {
- n -= value;
- fmt.push_str(name);
- }
- }
-
- fmt
- }
- Self::Symbol => {
- if n == 0 {
- return '-'.into();
- }
-
- let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
- let amount = ((n - 1) / SYMBOLS.len()) + 1;
- std::iter::repeat(symbol).take(amount).collect()
- }
- }
- }
-}
-
-const ROMANS: &[(&str, usize)] = &[
- ("M̅", 1000000),
- ("D̅", 500000),
- ("C̅", 100000),
- ("L̅", 50000),
- ("X̅", 10000),
- ("V̅", 5000),
- ("I̅V̅", 4000),
- ("M", 1000),
- ("CM", 900),
- ("D", 500),
- ("CD", 400),
- ("C", 100),
- ("XC", 90),
- ("L", 50),
- ("XL", 40),
- ("X", 10),
- ("IX", 9),
- ("V", 5),
- ("IV", 4),
- ("I", 1),
-];
-
-const SYMBOLS: &[char] = &['*', '†', '‡', '§', '‖', '¶'];