///|
// Structural invariants of the page-model IR that the types themselves
// cannot enforce (the IR structs are pub(all) so frontends build them as
// literals). `PageModel::validate` is the seed of the reference-free
// invariant layer from the design record: layout tests assert it returns
// no violations for every model they produce.
///|
fn check_color(violations : Array[String], where_ : String, c : Color) -> Unit {
if c.r < 0 || c.r > 255 || c.g < 0 || c.g > 255 || c.b < 0 || c.b > 255 {
violations.push("\{where_}: color channel out of 0..=255: \{c.to_hex()}")
}
}
///|
/// Check every structural invariant; return one human-readable message per
/// violation (empty means the model is well-formed):
///
/// - every `GlyphRun` has exactly one advance per UTF-16 code unit of its
/// text, a non-negative size, and a font index into `fonts`;
/// - every `RectItem` has non-negative extents, and its `stroke` and
/// `stroke_w_pt` agree (both present or both absent);
/// - every color channel is within 0..=255;
/// - every page has positive extents.
pub fn PageModel::validate(self : PageModel) -> Array[String] {
let violations : Array[String] = []
for p, page in self.pages {
if page.width_pt <= 0 || page.height_pt <= 0 {
violations.push("page \{p}: non-positive page size")
}
for i, item in page.items {
let where_ = "page \{p} item \{i}"
match item {
Text(run) => {
if run.advances_pt.length() != run.text.length() {
violations.push(
"\{where_}: \{run.advances_pt.length()} advances for \{run.text.length()} UTF-16 code units",
)
}
if run.font < 0 || run.font >= self.fonts.length() {
violations.push(
"\{where_}: font index \{run.font} outside fonts[0..\{self.fonts.length()}]",
)
}
if run.size_pt <= 0 {
violations.push("\{where_}: non-positive font size")
}
check_color(violations, where_, run.color)
}
Rect(rect) => {
if rect.w_pt < 0 || rect.h_pt < 0 {
violations.push("\{where_}: negative rect extent")
}
match rect.stroke {
Some(stroke_color) => {
if rect.stroke_w_pt <= 0 {
violations.push(
"\{where_}: stroke color with non-positive stroke width",
)
}
check_color(violations, where_, stroke_color)
}
None =>
if rect.stroke_w_pt != 0 {
violations.push("\{where_}: stroke width without stroke color")
}
}
match rect.fill {
Some(fill_color) => check_color(violations, where_, fill_color)
None => ()
}
if rect.fill is None && rect.stroke is None {
violations.push("\{where_}: rect paints nothing")
}
}
Image(image) => {
if image.w_pt <= 0 || image.h_pt <= 0 {
violations.push("\{where_}: non-positive image extent")
}
if image.data.length() == 0 {
violations.push("\{where_}: empty image data")
}
}
Link(link) =>
if link.w_pt < 0 || link.h_pt < 0 {
violations.push("\{where_}: negative link extent")
}
}
}
}
violations
}