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"
|
edition = "2018"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dev-dependencies]
|
||||||
|
criterion = "0.3"
|
||||||
fontdock = { path = "../../fontdock" }
|
fontdock = { path = "../../fontdock" }
|
||||||
typst = { path = ".." }
|
typst = { path = ".." }
|
||||||
criterion = "0.3"
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "typst"
|
name = "typst"
|
||||||
|
@ -25,7 +25,7 @@ use crate::diag::{Deco, Feedback, Pass};
|
|||||||
use crate::env::SharedEnv;
|
use crate::env::SharedEnv;
|
||||||
use crate::geom::{BoxAlign, Dir, Flow, Gen, Length, Linear, Relative, Sides, Size};
|
use crate::geom::{BoxAlign, Dir, Flow, Gen, Length, Linear, Relative, Sides, Size};
|
||||||
use crate::layout::{
|
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::*;
|
use crate::syntax::*;
|
||||||
|
|
||||||
@ -35,9 +35,9 @@ use crate::syntax::*;
|
|||||||
/// evaluation.
|
/// evaluation.
|
||||||
pub fn eval(tree: &SynTree, env: SharedEnv, state: State) -> Pass<Document> {
|
pub fn eval(tree: &SynTree, env: SharedEnv, state: State) -> Pass<Document> {
|
||||||
let mut ctx = EvalContext::new(env, state);
|
let mut ctx = EvalContext::new(env, state);
|
||||||
ctx.start_page_group(false);
|
ctx.start_page_group(Softness::Hard);
|
||||||
tree.eval(&mut ctx);
|
tree.eval(&mut ctx);
|
||||||
ctx.end_page_group(true);
|
ctx.end_page_group(|s| s == Softness::Hard);
|
||||||
ctx.finish()
|
ctx.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,28 +117,35 @@ impl EvalContext {
|
|||||||
|
|
||||||
/// Start a page group based on the active page state.
|
/// 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`
|
/// The `softness` is a hint on whether empty pages should be kept in the
|
||||||
/// are false, empty page runs will be omitted from the output.
|
/// output.
|
||||||
///
|
///
|
||||||
/// This also starts an inner paragraph.
|
/// 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 {
|
self.start_group(PageGroup {
|
||||||
size: self.state.page.size,
|
size: self.state.page.size,
|
||||||
padding: self.state.page.margins(),
|
padding: self.state.page.margins(),
|
||||||
flow: self.state.flow,
|
flow: self.state.flow,
|
||||||
align: self.state.align,
|
align: self.state.align,
|
||||||
hard,
|
softness,
|
||||||
});
|
});
|
||||||
self.start_par_group();
|
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.
|
/// 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();
|
self.end_par_group();
|
||||||
let (group, children) = self.end_group::<PageGroup>();
|
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 {
|
self.runs.push(Pages {
|
||||||
size: group.size,
|
size: group.size,
|
||||||
child: LayoutNode::dynamic(Pad {
|
child: LayoutNode::dynamic(Pad {
|
||||||
@ -152,6 +159,7 @@ impl EvalContext {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
group.softness
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start a content group.
|
/// Start a content group.
|
||||||
@ -273,7 +281,7 @@ struct PageGroup {
|
|||||||
padding: Sides<Linear>,
|
padding: Sides<Linear>,
|
||||||
flow: Flow,
|
flow: Flow,
|
||||||
align: BoxAlign,
|
align: BoxAlign,
|
||||||
hard: bool,
|
softness: Softness,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A group for generic content.
|
/// A group for generic content.
|
||||||
@ -286,6 +294,15 @@ struct ParGroup {
|
|||||||
line_spacing: Length,
|
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.
|
/// Evaluate an item.
|
||||||
///
|
///
|
||||||
/// _Note_: Evaluation is not necessarily pure, it may change the active state.
|
/// _Note_: Evaluation is not necessarily pure, it may change the active state.
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::eval::Softness;
|
||||||
|
|
||||||
/// A spacing node.
|
/// A spacing node.
|
||||||
#[derive(Copy, Clone, PartialEq)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
pub struct Spacing {
|
pub struct Spacing {
|
||||||
/// The amount of spacing to insert.
|
/// The amount of spacing to insert.
|
||||||
pub amount: Length,
|
pub amount: Length,
|
||||||
/// Spacing interaction, see [`Softness`]'s documentation for more
|
/// Defines how spacing interacts with surrounding spacing.
|
||||||
/// information.
|
///
|
||||||
|
/// 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,
|
pub softness: Softness,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,18 +39,3 @@ impl From<Spacing> for LayoutNode {
|
|||||||
Self::Spacing(spacing)
|
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 std::fmt::{self, Display, Formatter};
|
||||||
|
|
||||||
|
use crate::eval::Softness;
|
||||||
use crate::geom::{Length, Linear};
|
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::paper::{Paper, PaperClass};
|
||||||
use crate::prelude::*;
|
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 body = args.find::<SynTree>().unwrap_or_default();
|
||||||
let width = args.get::<_, Linear>(ctx, "width");
|
let width = args.get::<_, Linear>(ctx, "width");
|
||||||
let height = args.get::<_, Linear>(ctx, "height");
|
let height = args.get::<_, Linear>(ctx, "height");
|
||||||
let main = args.get::<_, Spanned<Dir>>(ctx, "main");
|
let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
|
||||||
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross");
|
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
|
||||||
ctx.set_flow(Gen::new(main, cross));
|
ctx.set_flow(Gen::new(main, cross));
|
||||||
args.done(ctx);
|
args.done(ctx);
|
||||||
|
|
||||||
@ -269,7 +270,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
|
|||||||
let snapshot = ctx.state.clone();
|
let snapshot = ctx.state.clone();
|
||||||
let body = args.find::<SynTree>();
|
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.class = paper.class;
|
||||||
ctx.state.page.size = paper.size();
|
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);
|
std::mem::swap(&mut size.width, &mut size.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
let main = args.get::<_, Spanned<Dir>>(ctx, "main");
|
let main = args.get::<_, Spanned<Dir>>(ctx, "main-dir");
|
||||||
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross");
|
let cross = args.get::<_, Spanned<Dir>>(ctx, "cross-dir");
|
||||||
ctx.set_flow(Gen::new(main, cross));
|
ctx.set_flow(Gen::new(main, cross));
|
||||||
|
|
||||||
args.done(ctx);
|
args.done(ctx);
|
||||||
|
|
||||||
|
let mut softness = ctx.end_page_group(|_| false);
|
||||||
|
|
||||||
if let Some(body) = body {
|
if let Some(body) = body {
|
||||||
ctx.end_page_group(false);
|
// TODO: Restrict body to a single page?
|
||||||
ctx.start_page_group(true);
|
ctx.start_page_group(Softness::Hard);
|
||||||
body.eval(ctx);
|
body.eval(ctx);
|
||||||
|
ctx.end_page_group(|s| s == Softness::Hard);
|
||||||
ctx.state = snapshot;
|
ctx.state = snapshot;
|
||||||
|
softness = Softness::Soft;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.end_page_group(false);
|
ctx.start_page_group(softness);
|
||||||
ctx.start_page_group(false);
|
|
||||||
|
|
||||||
Value::None
|
Value::None
|
||||||
}
|
}
|
||||||
@ -331,7 +335,7 @@ pub fn page(mut args: Args, ctx: &mut EvalContext) -> Value {
|
|||||||
/// `pagebreak`: Start a new page.
|
/// `pagebreak`: Start a new page.
|
||||||
pub fn pagebreak(args: Args, ctx: &mut EvalContext) -> Value {
|
pub fn pagebreak(args: Args, ctx: &mut EvalContext) -> Value {
|
||||||
args.done(ctx);
|
args.done(ctx);
|
||||||
ctx.end_page_group(false);
|
ctx.end_page_group(|_| true);
|
||||||
ctx.start_page_group(true);
|
ctx.start_page_group(Softness::Hard);
|
||||||
Value::None
|
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]
|
[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: 8:9-8:30 failed to load image
|
||||||
|
// error: 11:9-11:30 failed to load image
|
||||||
|
|
||||||
// File does not exist.
|
// File does not exist.
|
||||||
[image: "path/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.
|
// Fit to width of page.
|
||||||
[image: "res/rhino.png"]
|
[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 src = fs::read_to_string(src_path).unwrap();
|
||||||
let map = LineMap::new(&src);
|
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();
|
let mut state = State::default();
|
||||||
state.page.size = Size::uniform(Length::pt(120.0));
|
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 compare_ref {
|
||||||
if canvas.pixmap != ref_pixmap {
|
if let Ok(ref_pixmap) = Pixmap::load_png(&ref_path) {
|
||||||
println!(" Does not match reference image. ❌");
|
if canvas.pixmap != ref_pixmap {
|
||||||
|
println!(" Does not match reference image. ❌");
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!(" Failed to open reference image. ❌");
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
println!(" Failed to open reference image. ❌");
|
|
||||||
ok = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
@ -184,10 +186,13 @@ fn test(
|
|||||||
ok
|
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 diags = vec![];
|
||||||
|
let mut compare_ref = true;
|
||||||
|
|
||||||
for line in src.lines() {
|
for line in src.lines() {
|
||||||
|
compare_ref &= !line.starts_with("// compare-ref: false");
|
||||||
|
|
||||||
let (level, rest) = if let Some(rest) = line.strip_prefix("// error: ") {
|
let (level, rest) = if let Some(rest) = line.strip_prefix("// error: ") {
|
||||||
(Level::Error, rest)
|
(Level::Error, rest)
|
||||||
} else if let Some(rest) = line.strip_prefix("// warning: ") {
|
} 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.sort();
|
||||||
diags
|
|
||||||
|
(diags, compare_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_diag(diag: &Spanned<Diag>, map: &LineMap) {
|
fn print_diag(diag: &Spanned<Diag>, map: &LineMap) {
|
||||||
|