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)
@ -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"
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
Before Width: | Height: | Size: 120 B |
BIN
tests/ref/image-formats.png
Normal file
After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 37 KiB |
BIN
tests/ref/page-body.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
tests/ref/page-dirs.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
tests/ref/page-metrics.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
@ -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]
|
||||
|
||||
|
@ -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"]
|
||||
|
@ -1,3 +1,5 @@
|
||||
// Test configuring the size and fitting behaviour of images.
|
||||
|
||||
// Fit to width of page.
|
||||
[image: "res/rhino.png"]
|
||||
|
||||
|
8
tests/typ/image-formats.typ
Normal 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"]
|
@ -1,2 +0,0 @@
|
||||
// Load an RGB JPEG image.
|
||||
[image: "res/tiger.jpg"]
|
@ -1,2 +0,0 @@
|
||||
// Load an RGBA PNG image.
|
||||
[image: "res/rhino.png"]
|
11
tests/typ/page-body.typ
Normal 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
@ -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
@ -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]
|
25
tests/typ/page-metrics.typ
Normal 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]
|
@ -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,6 +167,7 @@ fn test(
|
||||
}
|
||||
}
|
||||
|
||||
if compare_ref {
|
||||
if let Ok(ref_pixmap) = Pixmap::load_png(&ref_path) {
|
||||
if canvas.pixmap != ref_pixmap {
|
||||
println!(" Does not match reference image. ❌");
|
||||
@ -176,6 +177,7 @@ fn test(
|
||||
println!(" Failed to open reference image. ❌");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ok {
|
||||
println!("\x1b[1ATesting {}. ✔", name);
|
||||
@ -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) {
|
||||
|