use rustyfi_backend::{
FontKey, FontMetrics, GraphicsElem, HorzBox, Length, Page, PageGeometry, PlacedLine,
PureHorzBox,
};
use rustyfi_lang::ast::Ast;
use rustyfi_lang::eval;
use rustyfi_lang::primitives;
use rustyfi_lang::value::{DocumentValue, Value};
use rustyfi_syntax::Span;
use std::rc::Rc;
struct Mono;
impl FontMetrics for Mono {
fn advance(&self, _f: FontKey, _c: char, size: Length) -> Option<Length> {
Some(size * 0.5)
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size * 0.75
}
fn descender(&self, _f: FontKey, size: Length) -> Length {
size * 0.25
}
}
fn var(name: &str) -> Ast {
Ast::Var(name.to_string(), Span::default())
}
fn apply_all(name: &str, args: Vec<Ast>) -> Ast {
args.into_iter()
.fold(var(name), |f, a| Ast::Apply(Box::new(f), Box::new(a)))
}
fn len(pt: f64) -> Ast {
Ast::Length(Length::pt(pt))
}
fn anchor_callback(key: &str) -> Ast {
Ast::Lambda(
"p".to_string(),
Rc::new(Ast::Sequential(
Box::new(apply_all(
"register-destination",
vec![Ast::Str(key.to_string()), var("p")],
)),
Box::new(Ast::List(Vec::new())),
)),
)
}
fn anchor_box_ast(key: &str) -> Ast {
Ast::Apply(
Box::new(apply_all(
"inline-graphics",
vec![len(0.0), len(0.0), len(0.0)],
)),
Box::new(anchor_callback(key)),
)
}
fn eval_ast(interp: &mut eval::Interp, ast: &Ast) -> Result<Value, eval::EvalError> {
let env = primitives::base_env();
interp.eval(&env, ast)
}
fn geometry() -> PageGeometry {
PageGeometry {
paper_width: Length::pt(400.0),
paper_height: Length::pt(300.0),
text_origin: (Length::pt(0.0), Length::pt(0.0)),
text_width: Length::pt(400.0),
text_height: Length::pt(300.0),
}
}
fn doc_with_pages(pages: Vec<Page>) -> DocumentValue {
DocumentValue {
geometry: geometry(),
pages,
images: Vec::new(),
extras: Default::default(),
reflow_source: None,
reflow_links: Vec::new(),
reflow_dests: Vec::new(),
reflow_frame_decos: Vec::new(),
}
}
fn page_with(bx: PureHorzBox, x: f64, baseline_y: f64) -> Page {
Page {
body_lines: usize::MAX,
lines: vec![PlacedLine {
x: Length::pt(x),
baseline_y: Length::pt(baseline_y),
contents: vec![(Length::ZERO, bx)],
}],
}
}
fn marker_box(key: &str, pt: (f64, f64), origin_independent: bool) -> PureHorzBox {
PureHorzBox::Graphics {
width: Length::ZERO,
height: Length::ZERO,
depth: Length::ZERO,
elems: vec![GraphicsElem::Destination {
key: key.to_string(),
pt: (Length::pt(pt.0), Length::pt(pt.1)),
}],
origin_independent,
}
}
#[test]
fn register_destination_inside_an_inline_graphics_callback_does_not_error() {
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
let v = eval_ast(&mut interp, &anchor_box_ast("eq:pythagoras"))
.expect("an anchor callback must not hit the during-page-break gate");
let boxes = match v {
Value::InlineBoxes(bs) => bs,
other => panic!("expected inline-boxes, got {other:?}"),
};
let elems = match &boxes[..] {
[HorzBox::Pure(PureHorzBox::Graphics { elems, .. })] => elems,
other => panic!("expected one Graphics box, got {other:?}"),
};
assert_eq!(
elems,
&vec![GraphicsElem::Destination {
key: "eq:pythagoras".to_string(),
pt: (Length::ZERO, Length::ZERO),
}],
"the deferred registration must ride in the box as a marker at the \
box-local point the callback named"
);
assert!(
interp.destinations.is_empty(),
"an eager callback must DEFER, not register"
);
}
#[test]
fn the_origin_independence_probe_does_not_duplicate_the_registration() {
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
let v = eval_ast(&mut interp, &anchor_box_ast("k")).expect("must evaluate");
let elems = match &v {
Value::InlineBoxes(bs) => match &bs[..] {
[HorzBox::Pure(PureHorzBox::Graphics { elems, .. })] => elems,
other => panic!("expected one Graphics box, got {other:?}"),
},
other => panic!("expected inline-boxes, got {other:?}"),
};
assert_eq!(elems.len(), 1, "one call, one marker: {elems:?}");
}
#[test]
fn an_anchor_only_callback_is_not_classified_page_absolute() {
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
let v = eval_ast(&mut interp, &anchor_box_ast("k")).expect("must evaluate");
match &v {
Value::InlineBoxes(bs) => match &bs[..] {
[HorzBox::Pure(PureHorzBox::Graphics {
origin_independent, ..
})] => assert!(
!origin_independent,
"a callback whose registration point tracks its argument is \
position-RELATIVE, however empty its ink"
),
other => panic!("expected one Graphics box, got {other:?}"),
},
other => panic!("expected inline-boxes, got {other:?}"),
}
}
#[test]
fn fire_hooks_resolves_a_marker_to_the_boxs_placed_page_and_point() {
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
let blank = Page {
body_lines: usize::MAX,
lines: Vec::new(),
};
let doc = doc_with_pages(vec![
blank,
page_with(marker_box("eq:1", (3.0, 7.0), false), 50.0, 100.0),
]);
rustyfi_lang::fire_hooks(&mut interp, &doc).expect("fire_hooks must succeed");
assert_eq!(interp.destinations.len(), 1);
let d = &interp.destinations[0];
assert_eq!(d.page, 1, "the page the box was PLACED on");
assert_eq!(d.name, "nameddest0", "minted through the shared dest_names");
assert_eq!(d.x, Length::pt(53.0), "line x (50) + box-local x (3)");
assert_eq!(d.y, Length::pt(207.0));
}
#[test]
fn a_page_absolute_boxs_marker_is_taken_as_already_final() {
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
let doc = doc_with_pages(vec![page_with(
marker_box("fixed", (3.0, 7.0), true),
50.0,
100.0,
)]);
rustyfi_lang::fire_hooks(&mut interp, &doc).expect("fire_hooks must succeed");
assert_eq!(interp.destinations.len(), 1);
let d = &interp.destinations[0];
assert_eq!((d.x, d.y), (Length::pt(3.0), Length::pt(7.0)));
}
#[test]
fn an_evaluated_anchor_box_becomes_a_named_destination_once_placed() {
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
let v = eval_ast(&mut interp, &anchor_box_ast("eq:euler")).expect("must evaluate");
let bx = match v {
Value::InlineBoxes(mut bs) => {
let HorzBox::Pure(p) = bs.remove(0);
p
}
other => panic!("expected inline-boxes, got {other:?}"),
};
let doc = doc_with_pages(vec![page_with(bx, 120.0, 60.0)]);
rustyfi_lang::fire_hooks(&mut interp, &doc).expect("fire_hooks must succeed");
assert_eq!(interp.destinations.len(), 1);
let d = &interp.destinations[0];
assert_eq!(d.page, 0);
assert_eq!(d.name, "nameddest0");
assert_eq!((d.x, d.y), (Length::pt(120.0), Length::pt(240.0)));
}
#[test]
fn the_during_page_break_gate_still_refuses_a_bare_call() {
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
eval_ast(&mut interp, &anchor_box_ast("k")).expect("must evaluate");
let bare = apply_all(
"register-destination",
vec![
Ast::Str("chapter1".to_string()),
Ast::Tuple(vec![len(10.0), len(20.0)]),
],
);
let err = eval_ast(&mut interp, &bare).expect_err("the gate must still refuse this");
assert!(
err.msg.contains("page breaking"),
"error should still name the during-page-break gate: {}",
err.msg
);
}
#[test]
fn a_callback_running_during_a_page_break_registers_directly() {
let mono = Mono;
let mut interp = eval::Interp::new(&mono);
interp.current_page = Some(0);
let v = eval_ast(&mut interp, &anchor_box_ast("k")).expect("must evaluate");
match &v {
Value::InlineBoxes(bs) => match &bs[..] {
[HorzBox::Pure(PureHorzBox::Graphics { elems, .. })] => {
assert!(elems.is_empty(), "no deferral inside the window: {elems:?}");
}
other => panic!("expected one Graphics box, got {other:?}"),
},
other => panic!("expected inline-boxes, got {other:?}"),
}
assert!(!interp.destinations.is_empty());
assert!(interp.destinations.iter().all(|d| d.page == 0));
}
#[test]
fn shift_graphics_moves_a_destination_marker_with_the_ink() {
let m = GraphicsElem::Destination {
key: "eq:1".to_string(),
pt: (Length::pt(3.0), Length::pt(7.0)),
};
let shifted = rustyfi_backend::shift_graphics((Length::pt(10.0), Length::pt(20.0)), &m);
assert_eq!(
shifted,
GraphicsElem::Destination {
key: "eq:1".to_string(),
pt: (Length::pt(13.0), Length::pt(27.0)),
}
);
}
#[test]
fn a_destination_marker_has_no_bounding_box() {
let m = GraphicsElem::Destination {
key: "eq:1".to_string(),
pt: (Length::pt(3.0), Length::pt(7.0)),
};
assert!(rustyfi_backend::graphics_bbox(&m).is_none());
}