Test [page] function 📕

- Make page break behaviour more consistent
- Allow skipping reference image testing for single tests with `// compare-ref: false` (useful for tests which only check error messages)
This commit is contained in:
Laurenz 2020-12-17 00:20:27 +01:00
parent 2336aeb4c3
commit 81e80ecfba
22 changed files with 137 additions and 57 deletions

View File

@ -5,10 +5,10 @@ authors = ["The Typst Project Developers"]
edition = "2018"
publish = false
[dependencies]
[dev-dependencies]
criterion = "0.3"
fontdock = { path = "../../fontdock" }
typst = { path = ".." }
criterion = "0.3"
[[bench]]
name = "typst"

View File

@ -25,7 +25,7 @@ use crate::diag::{Deco, Feedback, Pass};
use crate::env::SharedEnv;
use crate::geom::{BoxAlign, Dir, Flow, Gen, Length, Linear, Relative, Sides, Size};
use crate::layout::{
Document, Expansion, LayoutNode, Pad, Pages, Par, Softness, Spacing, Stack, Text,
Document, Expansion, LayoutNode, Pad, Pages, Par, Spacing, Stack, Text,
};
use crate::syntax::*;
@ -35,9 +35,9 @@ use crate::syntax::*;
/// evaluation.
pub fn eval(tree: &SynTree, env: SharedEnv, state: State) -> Pass<Document> {
let mut ctx = EvalContext::new(env, state);
ctx.start_page_group(false);
ctx.start_page_group(Softness::Hard);
tree.eval(&mut ctx);
ctx.end_page_group(true);
ctx.end_page_group(|s| s == Softness::Hard);
ctx.finish()
}
@ -117,28 +117,35 @@ impl EvalContext {
/// Start a page group based on the active page state.
///
/// If both this `hard` and the one in the matching call to `end_page_group`
/// are false, empty page runs will be omitted from the output.
/// The `softness` is a hint on whether empty pages should be kept in the
/// output.
///
/// This also starts an inner paragraph.
pub fn start_page_group(&mut self, hard: bool) {
pub fn start_page_group(&mut self, softness: Softness) {
self.start_group(PageGroup {
size: self.state.page.size,
padding: self.state.page.margins(),
flow: self.state.flow,
align: self.state.align,
hard,
softness,
});
self.start_par_group();
}
/// End a page group and push it to the finished page runs.
/// End a page group, returning its [`Softness`].
///
/// Whether the page is kept when it's empty is decided by `keep_empty`
/// based on its softness. If kept, the page is pushed to the finished page
/// runs.
///
/// This also ends an inner paragraph.
pub fn end_page_group(&mut self, hard: bool) {
pub fn end_page_group(
&mut self,
keep_empty: impl FnOnce(Softness) -> bool,
) -> Softness {
self.end_par_group();
let (group, children) = self.end_group::<PageGroup>();
if hard || group.hard || !children.is_empty() {
if !children.is_empty() || keep_empty(group.softness) {
self.runs.push(Pages {
size: group.size,
child: LayoutNode::dynamic(Pad {
@ -152,6 +159,7 @@ impl EvalContext {
}),
})
}
group.softness
}
/// Start a content group.
@ -273,7 +281,7 @@ struct PageGroup {
padding: Sides<Linear>,
flow: Flow,
align: BoxAlign,
hard: bool,
softness: Softness,
}
/// A group for generic content.
@ -286,6 +294,15 @@ struct ParGroup {
line_spacing: Length,
}
/// Defines how items interact with surrounding items.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Softness {
/// Soft items can be skipped in some circumstances.
Soft,
/// Hard items are always retained.
Hard,
}
/// Evaluate an item.
///
/// _Note_: Evaluation is not necessarily pure, it may change the active state.

View File

@ -1,14 +1,21 @@
use std::fmt::{self, Debug, Formatter};
use super::*;
use crate::eval::Softness;
/// A spacing node.
#[derive(Copy, Clone, PartialEq)]
pub struct Spacing {
/// The amount of spacing to insert.
pub amount: Length,
/// Spacing interaction, see [`Softness`]'s documentation for more
/// information.
/// Defines how spacing interacts with surrounding spacing.
///
/// Hard spacing assures that a fixed amount of spacing will always be
/// inserted. Soft spacing will be consumed by previous soft spacing or
/// neighbouring hard spacing and can be used to insert overridable spacing,
/// e.g. between words or paragraphs.
///
/// This field is only used in evaluation, not in layouting.
pub softness: Softness,
}
@ -32,18 +39,3 @@ impl From<Spacing> for LayoutNode {
Self::Spacing(spacing)
}
}
/// Defines how spacing interacts with surrounding spacing.
///
/// Hard spacing assures that a fixed amount of spacing will always be inserted.
/// Soft spacing will be consumed by previous soft spacing or neighbouring hard
/// spacing and can be used to insert overridable spacing, e.g. between words or
/// paragraphs.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum Softness {
/// Soft spacing is not laid out if it directly follows other soft spacing
/// or if it touches hard spacing.
Soft,
/// Hard spacing is always laid out and consumes surrounding soft spacing.
Hard,
}

View File

@ -1,7 +1,8 @@
use std::fmt::{self, Display, Formatter};
use crate::eval::Softness;
use crate::geom::{Length, Linear};
use crate::layout::{Expansion, Fixed, Softness, Spacing, Stack};
use crate::layout::{Expansion, Fixed, Spacing, Stack};
use crate::paper::{Paper, PaperClass};
use crate::prelude::*;
@ -184,8 +185,8 @@ pub fn boxed(mut args: Args, ctx: &mut EvalContext) -> Value {
let body = args.find::<SynTree>().unwrap_or_default();
let width = args.get::<_, Linear>(ctx, "width");
let height = args.get::<_, Linear>(ctx, "height");
let main = args.get::<_, Spanned<Dir>>(ctx, "main");
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross");
let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
ctx.set_flow(Gen::new(main, cross));
args.done(ctx);
@ -269,7 +270,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
let snapshot = ctx.state.clone();
let body = args.find::<SynTree>();
if let Some(paper) = args.find::<Paper>() {
if let Some(paper) = args.get::<_, Paper>(ctx, 0) {
ctx.state.page.class = paper.class;
ctx.state.page.size = paper.size();
}
@ -309,21 +310,24 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
std::mem::swap(&mut size.width, &mut size.height);
}
let main = args.get::<_, Spanned<Dir>>(ctx, "main");
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross");
let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
ctx.set_flow(Gen::new(main, cross));
args.done(ctx);
let mut softness = ctx.end_page_group(|_| false);
if let Some(body) = body {
ctx.end_page_group(false);
ctx.start_page_group(true);
// TODO: Restrict body to a single page?
ctx.start_page_group(Softness::Hard);
body.eval(ctx);
ctx.end_page_group(|s| s == Softness::Hard);
ctx.state = snapshot;
softness = Softness::Soft;
}
ctx.end_page_group(false);
ctx.start_page_group(false);
ctx.start_page_group(softness);
Value::None
}
@ -331,7 +335,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
/// `pagebreak`: Start a new page.
pub fn pagebreak(args: Args, ctx: &mut EvalContext) -> Value {
args.done(ctx);
ctx.end_page_group(false);
ctx.start_page_group(true);
ctx.end_page_group(|_| true);
ctx.start_page_group(Softness::Hard);
Value::None
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 B

BIN
tests/ref/image-formats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

BIN
tests/ref/page-body.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
tests/ref/page-dirs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
tests/ref/page-metrics.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,4 +1,4 @@
// Small integration test of syntax, page setup, box layout and alignment.
// Test integration of syntax, page setup, box layout and alignment.
[page: width=450pt, height=300pt, margins=1cm]

View File

@ -1,5 +1,8 @@
// error: 5:9-5:30 failed to load image
// Test error cases of the `image` function.
// compare-ref: false
// error: 8:9-8:30 failed to load image
// error: 11:9-11:30 failed to load image
// File does not exist.
[image: "path/does/not/exist"]

View File

@ -1,3 +1,5 @@
// Test configuring the size and fitting behaviour of images.
// Fit to width of page.
[image: "res/rhino.png"]

View File

@ -0,0 +1,8 @@
// Test loading different image formats.
// Load an RGBA PNG image.
[image: "res/rhino.png"]
[pagebreak]
// Load an RGB JPEG image.
[image: "res/tiger.jpg"]

View File

@ -1,2 +0,0 @@
// Load an RGB JPEG image.
[image: "res/tiger.jpg"]

View File

@ -1,2 +0,0 @@
// Load an RGBA PNG image.
[image: "res/rhino.png"]

11
tests/typ/page-body.typ Normal file
View File

@ -0,0 +1,11 @@
// Test a combination of pages with bodies and normal content.
[page: height=50pt]
[page][First]
[page][Second]
[pagebreak]
Fourth
[page][Fifth]
Sixth
[page][Seventh and last]

5
tests/typ/page-dirs.typ Normal file
View File

@ -0,0 +1,5 @@
// Test changing the layouting directions of pages.
[page: main-dir=btt, cross-dir=rtl]
Right to left!

11
tests/typ/page-error.typ Normal file
View File

@ -0,0 +1,11 @@
// Test error cases of the `page` function.
// compare-ref: false
// error: 8:8-8:19 invalid paper
// error: 11:17-11:20 aligned axis
// Invalid paper.
[page: nonexistant]
// Aligned axes.
[page: main-dir=ltr]

View File

@ -0,0 +1,25 @@
// Test configuring page sizes and margins.
// Set width.
[page: width=50pt][High]
// Set height.
[page: height=50pt][Wide]
// Set all margins at once.
[page: margins=40pt][
[align: top, left][TL]
[align: bottom, right][BR]
]
// Set individual margins.
[page: left=0pt >> align: left][Left]
[page: right=0pt >> align: right][Right]
[page: top=0pt >> align: top][Top]
[page: bottom=0pt >> align: bottom][Bottom]
// Ensure that specific margins override general margins.
[page: margins=0pt, left=40pt][Overriden]
// Flip the page.
[page: a10, flip=true][Flipped]

View File

@ -127,7 +127,7 @@ fn test(
let src = fs::read_to_string(src_path).unwrap();
let map = LineMap::new(&src);
let ref_diags = parse_diags(&src, &map);
let (ref_diags, compare_ref) = parse_metadata(&src, &map);
let mut state = State::default();
state.page.size = Size::uniform(Length::pt(120.0));
@ -167,14 +167,16 @@ fn test(
}
}
if let Ok(ref_pixmap) = Pixmap::load_png(&ref_path) {
if canvas.pixmap != ref_pixmap {
println!(" Does not match reference image. ❌");
if compare_ref {
if let Ok(ref_pixmap) = Pixmap::load_png(&ref_path) {
if canvas.pixmap != ref_pixmap {
println!(" Does not match reference image. ❌");
ok = false;
}
} else {
println!(" Failed to open reference image. ❌");
ok = false;
}
} else {
println!(" Failed to open reference image. ❌");
ok = false;
}
if ok {
@ -184,10 +186,13 @@ fn test(
ok
}
fn parse_diags(src: &str, map: &LineMap) -> SpanVec<Diag> {
fn parse_metadata(src: &str, map: &LineMap) -> (SpanVec<Diag>, bool) {
let mut diags = vec![];
let mut compare_ref = true;
for line in src.lines() {
compare_ref &= !line.starts_with("// compare-ref: false");
let (level, rest) = if let Some(rest) = line.strip_prefix("// error: ") {
(Level::Error, rest)
} else if let Some(rest) = line.strip_prefix("// warning: ") {
@ -211,7 +216,8 @@ fn parse_diags(src: &str, map: &LineMap) -> SpanVec<Diag> {
}
diags.sort();
diags
(diags, compare_ref)
}
fn print_diag(diag: &Spanned<Diag>, map: &LineMap) {