summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml3
-rw-r--r--src/export/pdf.rs14
-rw-r--r--src/layout/flex.rs154
-rw-r--r--src/layout/mod.rs18
-rw-r--r--src/layout/stacked.rs135
-rw-r--r--src/layout/tree.rs53
-rw-r--r--src/macros.rs2
-rw-r--r--src/size.rs14
-rw-r--r--tests/layouting.rs114
-rw-r--r--tests/layouts/pagebreaks.typ2
-rw-r--r--tests/layouts/shakespeare-right.tps88
-rw-r--r--tests/layouts/shakespeare.tpl (renamed from tests/layouts/shakespeare.tps)0
-rw-r--r--tests/layouts/shakespeare.typ8
-rw-r--r--tests/layouts/styles.typ (renamed from tests/layouts/styles.tps)5
-rw-r--r--tests/render.py73
16 files changed, 416 insertions, 269 deletions
diff --git a/.gitignore b/.gitignore
index 83240860..773446c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,4 @@
**/*.rs.bk
Cargo.lock
things
-test-cache
+tests/cache
diff --git a/Cargo.toml b/Cargo.toml
index ee945338..c2430db0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,9 @@ byteorder = "1"
smallvec = "0.6.10"
unicode-xid = "0.1.0"
+[dev-dependencies]
+regex = "1"
+
[[bin]]
name = "typstc"
path = "src/bin/main.rs"
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index 3c718c2e..f029a37f 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -80,7 +80,7 @@ impl<'d, W: Write> ExportProcess<'d, W> {
) -> PdfResult<ExportProcess<'d, W>>
{
let (fonts, font_remap) = Self::subset_fonts(layouts, font_loader)?;
- let offsets = Self::calculate_offset(layouts.count(), fonts.len());
+ let offsets = Self::calculate_offsets(layouts.count(), fonts.len());
Ok(ExportProcess {
writer: PdfWriter::new(target),
@@ -155,7 +155,7 @@ impl<'d, W: Write> ExportProcess<'d, W> {
/// We need to know in advance which IDs to use for which objects to cross-reference them.
/// Therefore, we calculate them in the beginning.
- fn calculate_offset(layout_count: usize, font_count: usize) -> Offsets {
+ fn calculate_offsets(layout_count: usize, font_count: usize) -> Offsets {
let catalog = 1;
let page_tree = catalog + 1;
let pages = (page_tree + 1, page_tree + layout_count as Ref);
@@ -203,7 +203,11 @@ impl<'d, W: Write> ExportProcess<'d, W> {
)?;
// The page objects (non-root nodes in the page tree).
- for (id, page) in ids(self.offsets.pages).zip(self.layouts) {
+ let iter = ids(self.offsets.pages)
+ .zip(ids(self.offsets.contents))
+ .zip(self.layouts);
+
+ for ((page_id, content_id), page) in iter {
let rect = Rect::new(
0.0,
0.0,
@@ -212,10 +216,10 @@ impl<'d, W: Write> ExportProcess<'d, W> {
);
self.writer.write_obj(
- id,
+ page_id,
Page::new(self.offsets.page_tree)
.media_box(rect)
- .contents(ids(self.offsets.contents)),
+ .content(content_id),
)?;
}
diff --git a/src/layout/flex.rs b/src/layout/flex.rs
index a4b3ed6d..80cc2074 100644
--- a/src/layout/flex.rs
+++ b/src/layout/flex.rs
@@ -21,22 +21,19 @@ pub struct FlexLayouter {
ctx: FlexContext,
units: Vec<FlexUnit>,
- actions: LayoutActionList,
- usable: Size2D,
- dimensions: Size2D,
- cursor: Size2D,
-
+ stack: StackLayouter,
+ usable_width: Size,
run: FlexRun,
- next_glue: Option<Layout>,
+ cached_glue: Option<Layout>,
}
/// The context for flex layouting.
#[derive(Debug, Copy, Clone)]
pub struct FlexContext {
- /// The space to layout the boxes in.
pub space: LayoutSpace,
/// The spacing between two lines of boxes.
pub flex_spacing: Size,
+ pub extra_space: Option<LayoutSpace>,
}
enum FlexUnit {
@@ -49,7 +46,7 @@ enum FlexUnit {
}
struct FlexRun {
- content: Vec<(Size2D, Layout)>,
+ content: Vec<(Size, Layout)>,
size: Size2D,
}
@@ -60,17 +57,17 @@ impl FlexLayouter {
ctx,
units: vec![],
- actions: LayoutActionList::new(),
- usable: ctx.space.usable(),
- dimensions: match ctx.space.alignment {
- Alignment::Left => Size2D::zero(),
- Alignment::Right => Size2D::with_x(ctx.space.usable().x),
- },
-
- cursor: Size2D::new(ctx.space.padding.left, ctx.space.padding.top),
+ stack: StackLayouter::new(StackContext {
+ space: ctx.space,
+ extra_space: ctx.extra_space,
+ }),
- run: FlexRun::new(),
- next_glue: None,
+ usable_width: ctx.space.usable().x,
+ run: FlexRun {
+ content: vec![],
+ size: Size2D::zero()
+ },
+ cached_glue: None,
}
}
@@ -90,12 +87,14 @@ impl FlexLayouter {
}
/// Compute the justified layout.
- pub fn finish(mut self) -> LayoutResult<Layout> {
+ ///
+ /// The layouter is not consumed by this to prevent ownership problems
+ /// with borrowed layouters. The state of the layouter is not reset.
+ /// Therefore, it should not be further used after calling `finish`.
+ pub fn finish(&mut self) -> LayoutResult<MultiLayout> {
// Move the units out of the layout because otherwise, we run into
// ownership problems.
- let units = self.units;
- self.units = Vec::new();
-
+ let units = std::mem::replace(&mut self.units, vec![]);
for unit in units {
match unit {
FlexUnit::Boxed(boxed) => self.layout_box(boxed)?,
@@ -104,109 +103,96 @@ impl FlexLayouter {
}
// Finish the last flex run.
- self.finish_flex_run();
+ self.finish_run()?;
- Ok(Layout {
- dimensions: if self.ctx.space.shrink_to_fit {
- self.dimensions.padded(self.ctx.space.padding)
- } else {
- self.ctx.space.dimensions
- },
- actions: self.actions.into_vec(),
- debug_render: true,
- })
- }
-
- /// Whether this layouter contains any items.
- pub fn is_empty(&self) -> bool {
- self.units.is_empty()
+ self.stack.finish()
}
+ /// Layout a content box into the current flex run or start a new run if
+ /// it does not fit.
fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> {
- let next_glue_width = self
- .next_glue
+ let glue_width = self
+ .cached_glue
.as_ref()
- .map(|g| g.dimensions.x)
+ .map(|layout| layout.dimensions.x)
.unwrap_or(Size::zero());
- let new_line_width = self.run.size.x + next_glue_width + boxed.dimensions.x;
+ let new_line_width = self.run.size.x + glue_width + boxed.dimensions.x;
+
+ if self.overflows_line(new_line_width) {
+ self.cached_glue = None;
- if self.overflows(new_line_width) {
- // If the box does not even fit on its own line, then
- // we can't do anything.
- if self.overflows(boxed.dimensions.x) {
- return Err(LayoutError::NotEnoughSpace);
+ // If the box does not even fit on its own line, then we try
+ // it in the next space, or we have to give up if there is none.
+ if self.overflows_line(boxed.dimensions.x) {
+ if self.ctx.extra_space.is_some() {
+ self.stack.finish_layout(true)?;
+ return self.layout_box(boxed);
+ } else {
+ return Err(LayoutError::NotEnoughSpace("cannot fit box into flex run"));
+ }
}
- self.next_glue = None;
- self.finish_flex_run();
+ self.finish_run()?;
} else {
// Only add the glue if we did not move to a new line.
self.flush_glue();
}
- self.add_to_flex_run(boxed);
+ self.add_to_run(boxed);
Ok(())
}
fn layout_glue(&mut self, glue: Layout) {
self.flush_glue();
- self.next_glue = Some(glue);
+ self.cached_glue = Some(glue);
}
fn flush_glue(&mut self) {
- if let Some(glue) = self.next_glue.take() {
- self.add_to_flex_run(glue);
+ if let Some(glue) = self.cached_glue.take() {
+ let new_line_width = self.run.size.x + glue.dimensions.x;
+ if !self.overflows_line(new_line_width) {
+ self.add_to_run(glue);
+ }
}
}
- fn add_to_flex_run(&mut self, layout: Layout) {
- let position = self.cursor;
+ fn add_to_run(&mut self, layout: Layout) {
+ let x = self.run.size.x;
- self.cursor.x += layout.dimensions.x;
self.run.size.x += layout.dimensions.x;
self.run.size.y = crate::size::max(self.run.size.y, layout.dimensions.y);
- self.run.content.push((position, layout));
+ self.run.content.push((x, layout));
}
- fn finish_flex_run(&mut self) {
- // Add all layouts from the current flex run at the correct positions.
- match self.ctx.space.alignment {
- Alignment::Left => {
- for (position, layout) in self.run.content.drain(..) {
- self.actions.add_layout(position, layout);
- }
- }
+ fn finish_run(&mut self) -> LayoutResult<()> {
+ self.run.size.y += self.ctx.flex_spacing;
- Alignment::Right => {
- let extra_space = Size2D::with_x(self.usable.x - self.run.size.x);
- for (position, layout) in self.run.content.drain(..) {
- self.actions.add_layout(position + extra_space, layout);
- }
- }
+ let mut actions = LayoutActionList::new();
+ for (x, layout) in self.run.content.drain(..) {
+ let position = Size2D::with_x(x);
+ actions.add_layout(position, layout);
}
- self.dimensions.x = crate::size::max(self.dimensions.x, self.run.size.x);
- self.dimensions.y += self.ctx.flex_spacing;
- self.dimensions.y += self.run.size.y;
+ self.stack.add(Layout {
+ dimensions: self.run.size,
+ actions: actions.into_vec(),
+ debug_render: false,
+ })?;
- self.cursor.x = self.ctx.space.padding.left;
- self.cursor.y += self.run.size.y + self.ctx.flex_spacing;
self.run.size = Size2D::zero();
+
+ Ok(())
}
- fn overflows(&self, line: Size) -> bool {
- line > self.usable.x
+ /// Whether this layouter contains any items.
+ pub fn is_empty(&self) -> bool {
+ self.units.is_empty()
}
-}
-impl FlexRun {
- fn new() -> FlexRun {
- FlexRun {
- content: vec![],
- size: Size2D::zero()
- }
+ fn overflows_line(&self, line: Size) -> bool {
+ line > self.usable_width
}
}
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 031226b9..b0fba4f2 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -48,6 +48,7 @@ impl Layout {
self.dimensions.x.to_pt(),
self.dimensions.y.to_pt()
)?;
+ writeln!(f, "{}", self.actions.len())?;
for action in &self.actions {
action.serialize(f)?;
writeln!(f)?;
@@ -93,6 +94,17 @@ impl MultiLayout {
}
}
+impl MultiLayout {
+ /// Serialize this collection of layouts into an output buffer.
+ pub fn serialize<W: Write>(&self, f: &mut W) -> io::Result<()> {
+ writeln!(f, "{}", self.count())?;
+ for layout in self {
+ layout.serialize(f)?;
+ }
+ Ok(())
+ }
+}
+
impl IntoIterator for MultiLayout {
type Item = Layout;
type IntoIter = std::vec::IntoIter<Layout>;
@@ -112,7 +124,7 @@ impl<'a> IntoIterator for &'a MultiLayout {
}
/// The general context for layouting.
-#[derive(Copy, Clone)]
+#[derive(Debug, Copy, Clone)]
pub struct LayoutContext<'a, 'p> {
pub loader: &'a SharedFontLoader<'p>,
pub style: &'a TextStyle,
@@ -154,7 +166,7 @@ pub enum Alignment {
/// The error type for layouting.
pub enum LayoutError {
/// There is not enough space to add an item.
- NotEnoughSpace,
+ NotEnoughSpace(&'static str),
/// There was no suitable font for the given character.
NoSuitableFont(char),
/// An error occured while gathering font data.
@@ -167,7 +179,7 @@ pub type LayoutResult<T> = Result<T, LayoutError>;
error_type! {
err: LayoutError,
show: f => match err {
- LayoutError::NotEnoughSpace => write!(f, "not enough space"),
+ LayoutError::NotEnoughSpace(desc) => write!(f, "not enough space: {}", desc),
LayoutError::NoSuitableFont(c) => write!(f, "no suitable font for '{}'", c),
LayoutError::Font(err) => write!(f, "font error: {}", err),
},
diff --git a/src/layout/stacked.rs b/src/layout/stacked.rs
index bfca4e3e..367a03d7 100644
--- a/src/layout/stacked.rs
+++ b/src/layout/stacked.rs
@@ -5,44 +5,38 @@ use super::*;
/// The boxes are arranged vertically, each layout gettings it's own "line".
pub struct StackLayouter {
ctx: StackContext,
+ layouts: MultiLayout,
actions: LayoutActionList,
+
+ space: LayoutSpace,
usable: Size2D,
dimensions: Size2D,
cursor: Size2D,
+ in_extra_space: bool,
+ started: bool,
}
/// The context for stack layouting.
#[derive(Debug, Copy, Clone)]
pub struct StackContext {
- /// The space to layout the boxes in.
pub space: LayoutSpace,
+ pub extra_space: Option<LayoutSpace>,
}
impl StackLayouter {
/// Create a new stack layouter.
pub fn new(ctx: StackContext) -> StackLayouter {
- let space = ctx.space;
-
StackLayouter {
ctx,
+ layouts: MultiLayout::new(),
actions: LayoutActionList::new(),
+ space: ctx.space,
usable: ctx.space.usable(),
- dimensions: match ctx.space.alignment {
- Alignment::Left => Size2D::zero(),
- Alignment::Right => Size2D::with_x(space.usable().x),
- },
-
- cursor: Size2D::new(
- // If left-align, the cursor points to the top-left corner of
- // each box. If we right-align, it points to the top-right
- // corner.
- match ctx.space.alignment {
- Alignment::Left => space.padding.left,
- Alignment::Right => space.dimensions.x - space.padding.right,
- },
- space.padding.top,
- ),
+ dimensions: start_dimensions(ctx.space),
+ cursor: start_cursor(ctx.space),
+ in_extra_space: false,
+ started: true,
}
}
@@ -53,19 +47,30 @@ impl StackLayouter {
/// Add a sublayout to the bottom.
pub fn add(&mut self, layout: Layout) -> LayoutResult<()> {
+ if !self.started {
+ self.start_new_space()?;
+ }
+
let new_dimensions = Size2D {
x: crate::size::max(self.dimensions.x, layout.dimensions.x),
y: self.dimensions.y + layout.dimensions.y,
};
if self.overflows(new_dimensions) {
- return Err(LayoutError::NotEnoughSpace);
+ if self.ctx.extra_space.is_some() &&
+ !(self.in_extra_space && self.overflows(layout.dimensions))
+ {
+ self.finish_layout(true)?;
+ return self.add(layout);
+ } else {
+ return Err(LayoutError::NotEnoughSpace("cannot fit box into stack"));
+ }
}
// Determine where to put the box. When we right-align it, we want the
// cursor to point to the top-right corner of the box. Therefore, the
// position has to be moved to the left by the width of the box.
- let position = match self.ctx.space.alignment {
+ let position = match self.space.alignment {
Alignment::Left => self.cursor,
Alignment::Right => self.cursor - Size2D::with_x(layout.dimensions.x),
};
@@ -88,26 +93,74 @@ impl StackLayouter {
/// Add vertical space after the last layout.
pub fn add_space(&mut self, space: Size) -> LayoutResult<()> {
- if self.overflows(self.dimensions + Size2D::with_y(space)) {
- return Err(LayoutError::NotEnoughSpace);
+ if !self.started {
+ self.start_new_space()?;
}
- self.cursor.y += space;
- self.dimensions.y += space;
+ let new_dimensions = self.dimensions + Size2D::with_y(space);
+
+ if self.overflows(new_dimensions) {
+ if self.ctx.extra_space.is_some() {
+ self.finish_layout(false)?;
+ } else {
+ return Err(LayoutError::NotEnoughSpace("cannot fit space into stack"));
+ }
+ } else {
+ self.cursor.y += space;
+ self.dimensions.y += space;
+ }
Ok(())
}
/// Finish the layouting.
- pub fn finish(self) -> Layout {
- Layout {
- dimensions: if self.ctx.space.shrink_to_fit {
- self.dimensions.padded(self.ctx.space.padding)
+ ///
+ /// The layouter is not consumed by this to prevent ownership problems.
+ /// It should not be used further.
+ pub fn finish(&mut self) -> LayoutResult<MultiLayout> {
+ if self.started {
+ self.finish_layout(false)?;
+ }
+ Ok(std::mem::replace(&mut self.layouts, MultiLayout::new()))
+ }
+
+ /// Finish the current layout and start a new one in an extra space
+ /// (if there is an extra space).
+ ///
+ /// If `start_new_empty` is true, a new empty layout will be started. Otherwise,
+ /// the new layout only emerges when new content is added.
+ pub fn finish_layout(&mut self, start_new_empty: bool) -> LayoutResult<()> {
+ let actions = std::mem::replace(&mut self.actions, LayoutActionList::new());
+ self.layouts.add(Layout {
+ dimensions: if self.space.shrink_to_fit {
+ self.dimensions.padded(self.space.padding)
} else {
- self.ctx.space.dimensions
+ self.space.dimensions
},
- actions: self.actions.into_vec(),
+ actions: actions.into_vec(),
debug_render: true,
+ });
+
+ self.started = false;
+
+ if start_new_empty {
+ self.start_new_space()?;
+ }
+
+ Ok(())
+ }
+
+ pub fn start_new_space(&mut self) -> LayoutResult<()> {
+ if let Some(space) = self.ctx.extra_space {
+ self.started = true;
+ self.space = space;
+ self.usable = space.usable();
+ self.dimensions = start_dimensions(space);
+ self.cursor = start_cursor(space);
+ self.in_extra_space = true;
+ Ok(())
+ } else {
+ Err(LayoutError::NotEnoughSpace("no extra space to start"))
}
}
@@ -121,10 +174,30 @@ impl StackLayouter {
/// Whether this layouter contains any items.
pub fn is_empty(&self) -> bool {
- self.actions.is_empty()
+ self.layouts.is_empty() && self.actions.is_empty()
}
fn overflows(&self, dimensions: Size2D) -> bool {
!self.usable.fits(dimensions)
}
}
+
+fn start_dimensions(space: LayoutSpace) -> Size2D {
+ match space.alignment {
+ Alignment::Left => Size2D::zero(),
+ Alignment::Right => Size2D::with_x(space.usable().x),
+ }
+}
+
+fn start_cursor(space: LayoutSpace) -> Size2D {
+ Size2D {
+ // If left-align, the cursor points to the top-left corner of
+ // each box. If we right-align, it points to the top-right
+ // corner.
+ x: match space.alignment {
+ Alignment::Left => space.padding.left,
+ Alignment::Right => space.dimensions.x - space.padding.right,
+ },
+ y: space.padding.top,
+ }
+}
diff --git a/src/layout/tree.rs b/src/layout/tree.rs
index 50616883..c8695e83 100644
--- a/src/layout/tree.rs
+++ b/src/layout/tree.rs
@@ -19,14 +19,13 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> {
TreeLayouter {
ctx,
- stack: StackLayouter::new(StackContext { space: ctx.space }),
+ stack: StackLayouter::new(StackContext {
+ space: ctx.space,
+ extra_space: ctx.extra_space
+ }),
flex: FlexLayouter::new(FlexContext {
- space: LayoutSpace {
- dimensions: ctx.space.usable(),
- padding: SizeBox::zero(),
- alignment: ctx.space.alignment,
- shrink_to_fit: true,
- },
+ space: flex_space(ctx.space),
+ extra_space: ctx.extra_space.map(|s| flex_space(s)),
flex_spacing: flex_spacing(&ctx.style),
}),
style: Cow::Borrowed(ctx.style),
@@ -48,10 +47,8 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
// Finish the current flex layouting process.
Node::Newline => {
- self.layout_flex()?;
-
let space = paragraph_spacing(&self.style);
- self.stack.add_space(space)?;
+ self.layout_flex(space)?;
}
// Toggle the text styles.
@@ -70,12 +67,10 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
fn finish(mut self) -> LayoutResult<MultiLayout> {
// If there are remainings, add them to the layout.
if !self.flex.is_empty() {
- self.layout_flex()?;
+ self.layout_flex(Size::zero())?;
}
- Ok(MultiLayout {
- layouts: vec![self.stack.finish()],
- })
+ self.stack.finish()
}
/// Add text to the flex layout. If `glue` is true, the text will be a glue
@@ -98,29 +93,38 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
}
/// Finish the current flex layout and add it the stack.
- fn layout_flex(&mut self) -> LayoutResult<()> {
+ fn layout_flex(&mut self, after_space: Size) -> LayoutResult<()> {
if self.flex.is_empty() {
return Ok(());
}
+ let layouts = self.flex.finish()?;
+ self.stack.add_many(layouts)?;
+ self.stack.add_space(after_space)?;
+
let mut ctx = self.flex.ctx();
ctx.space.dimensions = self.stack.remaining();
ctx.flex_spacing = flex_spacing(&self.style);
- let next = FlexLayouter::new(ctx);
- let flex = std::mem::replace(&mut self.flex, next);
- let boxed = flex.finish()?;
+ self.flex = FlexLayouter::new(ctx);
- self.stack.add(boxed)
+ Ok(())
}
/// Layout a function.
fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> {
let mut ctx = self.ctx;
ctx.style = &self.style;
+
ctx.space.dimensions = self.stack.remaining();
ctx.space.padding = SizeBox::zero();
- ctx.space.shrink_to_fit = true;
+ ctx.space.shrink_to_fit = false;
+
+ if let Some(space) = ctx.extra_space.as_mut() {
+ space.dimensions = space.dimensions.unpadded(space.padding);
+ space.padding = SizeBox::zero();
+ space.shrink_to_fit = false;
+ }
let commands = func.body.layout(ctx)?;
@@ -137,6 +141,15 @@ impl<'a, 'p> TreeLayouter<'a, 'p> {
}
}
+fn flex_space(space: LayoutSpace) -> LayoutSpace {
+ LayoutSpace {
+ dimensions: space.usable(),
+ padding: SizeBox::zero(),
+ alignment: space.alignment,
+ shrink_to_fit: true,
+ }
+}
+
fn flex_spacing(style: &TextStyle) -> Size {
(style.line_spacing - 1.0) * Size::pt(style.font_size)
}
diff --git a/src/macros.rs b/src/macros.rs
index 831a37c8..a1c182fb 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -12,6 +12,7 @@ macro_rules! error_type {
impl std::fmt::Display for $err {
fn fmt(&self, $f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ #[allow(unused)]
let $var = self;
$show
}
@@ -22,6 +23,7 @@ macro_rules! error_type {
impl std::error::Error for $err {
// The source method is only generated if an implementation was given.
$(fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ #[allow(unused)]
let $var = self;
$source
})*
diff --git a/src/size.rs b/src/size.rs
index c7439439..414fd855 100644
--- a/src/size.rs
+++ b/src/size.rs
@@ -131,6 +131,15 @@ impl Size2D {
}
}
+ /// Return a [`Size2D`] reduced by the paddings of the given box.
+ #[inline]
+ pub fn unpadded(&self, padding: SizeBox) -> Size2D {
+ Size2D {
+ x: self.x - padding.left - padding.right,
+ y: self.y - padding.top - padding.bottom,
+ }
+ }
+
/// Whether the given [`Size2D`] fits into this one, that is,
/// both coordinate values are smaller.
#[inline]
@@ -189,6 +198,11 @@ debug_display!(Size);
/// An error which can be returned when parsing a size.
pub struct ParseSizeError;
+error_type! {
+ err: ParseSizeError,
+ show: f => write!(f, "failed to parse size"),
+}
+
impl FromStr for Size {
type Err = ParseSizeError;
diff --git a/tests/layouting.rs b/tests/layouting.rs
index 75aaa66b..f77c6e85 100644
--- a/tests/layouting.rs
+++ b/tests/layouting.rs
@@ -3,12 +3,16 @@ use std::io::{BufWriter, Read, Write};
use std::process::Command;
use std::time::Instant;
+use regex::{Regex, Captures};
+
use typst::export::pdf::PdfExporter;
use typst::layout::LayoutAction;
use typst::toddle::query::FileSystemFontProvider;
+use typst::size::{Size, Size2D, SizeBox};
+use typst::style::PageStyle;
use typst::Typesetter;
-const CACHE_DIR: &str = "test-cache";
+const CACHE_DIR: &str = "tests/cache";
fn main() {
let mut perfect_match = false;
@@ -31,6 +35,10 @@ fn main() {
for entry in fs::read_dir("tests/layouts/").unwrap() {
let path = entry.unwrap().path();
+ if path.extension() != Some(std::ffi::OsStr::new("typ")) {
+ continue;
+ }
+
let name = path.file_stem().unwrap().to_str().unwrap();
let matches = if perfect_match {
@@ -51,36 +59,47 @@ fn main() {
/// Create a _PDF_ with a name from the source code.
fn test(name: &str, src: &str) {
- print!("Testing: {}", name);
+ println!("Testing: {}", name);
+
+ let (src, size) = preprocess(src);
let mut typesetter = Typesetter::new();
let provider = FileSystemFontProvider::from_listing("fonts/fonts.toml").unwrap();
typesetter.add_font_provider(provider.clone());
+ if let Some(dimensions) = size {
+ typesetter.set_page_style(PageStyle {
+ dimensions,
+ margins: SizeBox::zero()
+ });
+ }
+
let start = Instant::now();
// Layout into box layout.
- let tree = typesetter.parse(src).unwrap();
- let layout = typesetter.layout(&tree).unwrap();
+ let tree = typesetter.parse(&src).unwrap();
+ let layouts = typesetter.layout(&tree).unwrap();
let end = Instant::now();
let duration = end - start;
- println!(" [{:?}]", duration);
+ println!(" => {:?}", duration);
+ println!();
// Write the serialed layout file.
- let path = format!("{}/serialized/{}.box", CACHE_DIR, name);
+ let path = format!("{}/serialized/{}.lay", CACHE_DIR, name);
let mut file = File::create(path).unwrap();
// Find all used fonts and their filenames.
let mut map = Vec::new();
let mut loader = typesetter.loader().borrow_mut();
- let single = &layout.layouts[0];
- for action in &single.actions {
- if let LayoutAction::SetFont(index, _) = action {
- if map.iter().find(|(i, _)| i == index).is_none() {
- let (_, provider_index) = loader.get_provider_and_index(*index);
- let filename = provider.get_path(provider_index).to_str().unwrap();
- map.push((*index, filename));
+ for layout in &layouts {
+ for action in &layout.actions {
+ if let LayoutAction::SetFont(index, _) = action {
+ if map.iter().find(|(i, _)| i == index).is_none() {
+ let (_, provider_index) = loader.get_provider_and_index(*index);
+ let filename = provider.get_path(provider_index).to_str().unwrap();
+ map.push((*index, filename));
+ }
}
}
}
@@ -91,7 +110,8 @@ fn test(name: &str, src: &str) {
for (index, path) in map {
writeln!(file, "{} {}", index, path).unwrap();
}
- single.serialize(&mut file).unwrap();
+
+ layouts.serialize(&mut file).unwrap();
// Render the layout into a PNG.
Command::new("python")
@@ -104,5 +124,69 @@ fn test(name: &str, src: &str) {
let path = format!("{}/pdf/{}.pdf", CACHE_DIR, name);
let file = BufWriter::new(File::create(path).unwrap());
let exporter = PdfExporter::new();
- exporter.export(&layout, typesetter.loader(), file).unwrap();
+ exporter.export(&layouts, typesetter.loader(), file).unwrap();
+}
+
+fn preprocess<'a>(src: &'a str) -> (String, Option<Size2D>) {
+ let include_regex = Regex::new(r"\{include:((.|\.|\-)*)\}").unwrap();
+ let lorem_regex = Regex::new(r"\{lorem:(\d*)\}").unwrap();
+ let size_regex = Regex::new(r"\{(size:(([\d\w]*)\*([\d\w]*)))\}").unwrap();
+
+ let mut size = None;
+
+ let mut preprocessed = size_regex.replace_all(&src, |cap: &Captures| {
+ let width_str = cap.get(3).unwrap().as_str();
+ let height_str = cap.get(4).unwrap().as_str();
+
+ let width = width_str.parse::<Size>().unwrap();
+ let height = height_str.parse::<Size>().unwrap();
+
+ size = Some(Size2D::new(width, height));
+
+ "".to_string()
+ }).to_string();
+
+ let mut changed = true;
+ while changed {
+ changed = false;
+ preprocessed = include_regex.replace_all(&preprocessed, |cap: &Captures| {
+ changed = true;
+ let filename = cap.get(1).unwrap().as_str();
+
+ let path = format!("tests/layouts/{}", filename);
+ let mut file = File::open(path).unwrap();
+ let mut buf = String::new();
+ file.read_to_string(&mut buf).unwrap();
+ buf
+ }).to_string();
+ }
+
+ preprocessed= lorem_regex.replace_all(&preprocessed, |cap: &Captures| {
+ let num_str = cap.get(1).unwrap().as_str();
+ let num_words = num_str.parse::<usize>().unwrap();
+
+ generate_lorem(num_words)
+ }).to_string();
+
+ (preprocessed, size)
+}
+
+fn generate_lorem(num_words: usize) -> String {
+ const LOREM: [&str; 69] = [
+ "Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit.", "Etiam",
+ "suscipit", "porta", "pretium.", "Donec", "eu", "lorem", "hendrerit,", "scelerisque",
+ "lectus", "at,", "consequat", "ligula.", "Nulla", "elementum", "massa", "et", "viverra",
+ "consectetur.", "Donec", "blandit", "metus", "ut", "ipsum", "commodo", "congue.", "Nullam",
+ "auctor,", "mi", "vel", "tristique", "venenatis,", "nisl", "nunc", "tristique", "diam,",
+ "aliquam", "pellentesque", "lorem", "massa", "vel", "neque.", "Sed", "malesuada", "ante",
+ "nisi,", "sit", "amet", "auctor", "risus", "fermentum", "in.", "Sed", "blandit", "mollis",
+ "mi,", "non", "tristique", "nisi", "fringilla", "at."
+ ];
+
+ let mut buf = String::new();
+ for i in 0 .. num_words {
+ buf.push_str(LOREM[i % LOREM.len()]);
+ buf.push(' ');
+ }
+ buf
}
diff --git a/tests/layouts/pagebreaks.typ b/tests/layouts/pagebreaks.typ
new file mode 100644
index 00000000..6887613f
--- /dev/null
+++ b/tests/layouts/pagebreaks.typ
@@ -0,0 +1,2 @@
+{size:200pt*200pt}
+{lorem:400}
diff --git a/tests/layouts/shakespeare-right.tps b/tests/layouts/shakespeare-right.tps
deleted file mode 100644
index db670fdf..00000000
--- a/tests/layouts/shakespeare-right.tps
+++ /dev/null
@@ -1,88 +0,0 @@
-[align: right][
- [bold][Scene 5: _The Tower of London_]
-
- [italic][Enter Mortimer, brought in a chair, and Gaolers.]
-
- *Mortimer.* Kind keepers of my weak decaying age,
- Let dying Mortimer here rest himself.
- Even like a man new haled from the rack,
- So fare my limbs with long imprisonment;
- And these grey locks, the pursuivants of death,
- Nestor-like aged in an age of care,
- Argue the end of Edmund Mortimer.
- These eyes, like lamps whose wasting oil is spent,
- Wax dim, as drawing to their exigent;
- Weak shoulders, overborne with burdening grief,
- And pithless arms, like to a withered vine
- That droops his sapless branches to the ground.
- Yet are these feet, whose strengthless stay is numb,
- Unable to support this lump of clay,
- Swift-winged with desire to get a grave,
- As witting I no other comfort have.
- But tell me, keeper, will my nephew come?
-
- *First Keeper.* Richard Plantagenet, my lord, will come.
- We sent unto the Temple, unto his chamber;
- And answer was return'd that he will come.
-
- *Mortimer.* Enough; my soul shall then be satisfied.
- Poor gentleman! his wrong doth equal mine.
- Since Henry Monmouth first began to reign,
- Before whose glory I was great in arms,
- This loathsome sequestration have I had;
- And even since then hath Richard been obscur'd,
- Depriv'd of honour and inheritance.
- But now the arbitrator of despairs,
- Just Death, kind umpire of men's miseries,
- With sweet enlargement doth dismiss me hence.
- I would his troubles likewise were expir'd,
- That so he might recover what was lost.
-
-
- [italic][Enter Richard Plantagenet]
-
- *First Keeper.* My lord, your loving nephew now is come.
-
- *Mortimer.* Richard Plantagenet, my friend, is he come?
-
- *Plantagenet.* Ay, noble uncle, thus ignobly us'd,
- Your nephew, late despised Richard, comes.
-
- *Mortimer.* Direct mine arms I may embrace his neck
- And in his bosom spend my latter gasp.
- O, tell me when my lips do touch his cheeks,
- That I may kindly give one fainting kiss.
- And now declare, sweet stem from York's great stock,
- Why didst thou say of late thou wert despis'd?
-
- *Plantagenet.* First, lean thine aged back against mine arm;
- And, in that ease, I'll tell thee my disease.
- This day, in argument upon a case,
- Some words there grew 'twixt Somerset and me;
- Among which terms he us'd his lavish tongue
- And did upbraid me with my father's death;
- Which obloquy set bars before my tongue,
- Else with the like I had requited him.
- Therefore, good uncle, for my father's sake,
- In honour of a true Plantagenet,
- And for alliance sake, declare the cause
- My father, Earl of Cambridge, lost his head.
-
- *Mortimer.* That cause, fair nephew, that imprison'd me
- And hath detain'd me all my flow'ring youth
- Within a loathsome dungeon, there to pine,
- Was cursed instrument of his decease.
-
- *Plantagenet.* Discover more at large what cause that was,
- For I am ignorant and cannot guess.
-
- *Mortimer.* I will, if that my fading breath permit
- And death approach not ere my tale be done.
- Henry the Fourth, grandfather to this king,
- Depos'd his nephew Richard, Edward's son,
- The first-begotten and the lawful heir
- Of Edward king, the third of that descent;
- During whose reign the Percies of the north,
- Finding his usurpation most unjust,
- Endeavour'd my advancement to the throne ...
-]
diff --git a/tests/layouts/shakespeare.tps b/tests/layouts/shakespeare.tpl
index e0839302..e0839302 100644
--- a/tests/layouts/shakespeare.tps
+++ b/tests/layouts/shakespeare.tpl
diff --git a/tests/layouts/shakespeare.typ b/tests/layouts/shakespeare.typ
new file mode 100644
index 00000000..09edd361
--- /dev/null
+++ b/tests/layouts/shakespeare.typ
@@ -0,0 +1,8 @@
+// Basic unboxed
+{include:shakespeare.tpl}
+
+// Boxed, but still left-aligned
+[align: left][{include:shakespeare.tpl}]
+
+// Boxed, and right-aligned
+[align: right][{include:shakespeare.tpl}]
diff --git a/tests/layouts/styles.tps b/tests/layouts/styles.typ
index ef5d4e3b..767a0b73 100644
--- a/tests/layouts/styles.tps
+++ b/tests/layouts/styles.typ
@@ -1,8 +1,5 @@
_Multiline:_
-Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
-eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
-voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet
-clita kasd gubergren, no sea takimata sanctus est.
+{lorem:45}
_Emoji:_ Hello World! 🌍
diff --git a/tests/render.py b/tests/render.py
index e52054c1..4cb1e000 100644
--- a/tests/render.py
+++ b/tests/render.py
@@ -5,36 +5,73 @@ from PIL import Image, ImageDraw, ImageFont
BASE = os.path.dirname(__file__)
-CACHE_DIR = os.path.join(BASE, "../test-cache/");
+CACHE_DIR = os.path.join(BASE, "cache/");
def main():
assert len(sys.argv) == 2, "usage: python render.py <name>"
name = sys.argv[1]
- filename = os.path.join(CACHE_DIR, f"serialized/{name}.box")
+ filename = os.path.join(CACHE_DIR, f"serialized/{name}.lay")
with open(filename, encoding="utf-8") as file:
lines = [line[:-1] for line in file.readlines()]
- fonts = {}
- font_count = int(lines[0])
- for i in range(font_count):
- parts = lines[1 + i].split(' ', 1)
- index = int(parts[0])
- path = parts[1]
- fonts[index] = os.path.join(BASE, "../fonts", path)
+ renderer = MultiboxRenderer(lines)
+ renderer.render()
+ image = renderer.export()
- width, height = (float(s) for s in lines[font_count + 1].split())
+ pathlib.Path(os.path.join(CACHE_DIR, "rendered")).mkdir(parents=True, exist_ok=True)
+ image.save(CACHE_DIR + "rendered/" + name + ".png")
- renderer = Renderer(fonts, width, height)
- for command in lines[font_count + 2:]:
- renderer.execute(command)
- pathlib.Path(os.path.join(CACHE_DIR, "rendered")).mkdir(parents=True, exist_ok=True)
- renderer.export(name)
+class MultiboxRenderer:
+ def __init__(self, lines):
+ self.combined = None
+
+ self.fonts = {}
+ font_count = int(lines[0])
+ for i in range(font_count):
+ parts = lines[i + 1].split(' ', 1)
+ index = int(parts[0])
+ path = parts[1]
+ self.fonts[index] = os.path.join(BASE, "../fonts", path)
+
+ self.content = lines[font_count + 1:]
+
+ def render(self):
+ images = []
+
+ layout_count = int(self.content[0])
+ start = 1
+
+ for _ in range(layout_count):
+ width, height = (float(s) for s in self.content[start].split())
+ action_count = int(self.content[start + 1])
+ start += 2
+
+ renderer = BoxRenderer(self.fonts, width, height)
+ for i in range(action_count):
+ command = self.content[start + i]
+ renderer.execute(command)
+
+ images.append(renderer.export())
+ start += action_count
+
+ width = max(image.width for image in images) + 20
+ height = sum(image.height for image in images) + 10 * (len(images) + 1)
+
+ self.combined = Image.new('RGBA', (width, height))
+
+ cursor = 10
+ for image in images:
+ self.combined.paste(image, (10, cursor))
+ cursor += 10 + image.height
+
+ def export(self):
+ return self.combined
-class Renderer:
+class BoxRenderer:
def __init__(self, fonts, width, height):
self.fonts = fonts
self.size = (pix(width), pix(height))
@@ -102,8 +139,8 @@ class Renderer:
else:
raise Exception("invalid command")
- def export(self, name):
- self.img.save(CACHE_DIR + "rendered/" + name + ".png")
+ def export(self):
+ return self.img
def pix(points):