use crate::eval::{available_fields, eval_error, DecoEntry, EvalError, Interp};
use crate::quoted::{BText, IText, MathElem};
use crate::value::{BaseEnv, DocumentValue, Env, TextInfo, Value};
use rustyfi_backend::char_script;
use rustyfi_backend::{
break_into_lines, break_opportunities, chop_page, default_math_variant_char, fit_cell,
graphics_bbox, linear_transform_graphics, linear_transform_path, measure_block,
natural_metrics, path_bbox, place_block_at, placed_line_extent, shift_graphics, shift_path,
Annot, AnnotAction, BreakKind, Cell, Closing, Color, Context, Dash, DecoId, DocExtras, DocInfo,
FontKey, GraphicsElem, GraphicsFnId, HookId, HorzBox, HorzStringInfo, HyphenLang, ImageId,
ImageResource, InlineMarkKind, Language, Length, ListMarkKind, MathCharClass, MathConstants,
MathCorner, MathGlyph, MathKind, MathScriptLevel, NamedDest, OutlineEntry, Paddings, Page,
PageGeometry, PaperSize, Path, PathSeg, Point, PrePath, PureHorzBox, Script, ScriptFont,
Subpath, TabularBox, VertBox, VertVariantPolicy, FORCED_BREAK_PENALTY, MIN_FIRST_ASCENDER,
NO_BREAK_PENALTY,
};
#[cfg(feature = "pdf-image")]
use rustyfi_backend::{ImportedObjects, ObjRepr, PdfPageResource};
use rustyfi_syntax::RustyfiVersion;
use std::collections::BTreeMap;
#[cfg(feature = "pdf-image")]
use std::collections::BTreeSet;
use std::rc::Rc;
use std::sync::Arc;
use unicode_normalization::UnicodeNormalization;
use unicode_segmentation::UnicodeSegmentation;
const FONT_REGULAR: FontKey = FontKey(0);
const FONT_BOLD: FontKey = FontKey(1);
const FONT_OBLIQUE: FontKey = FontKey(2);
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VersionSpan {
Both,
V0_0Only,
V0_1Only,
}
impl VersionSpan {
pub fn allows(self, version: RustyfiVersion) -> bool {
match (self, version) {
(VersionSpan::Both, _) => true,
(VersionSpan::V0_0Only, RustyfiVersion::V0_0) => true,
(VersionSpan::V0_1Only, RustyfiVersion::V0_1) => true,
_ => false,
}
}
}
pub struct PrimDef {
pub name: &'static str,
pub arity: usize,
pub run: fn(&mut Interp, Vec<Value>) -> Result<Value, EvalError>,
pub version: VersionSpan,
}
impl std::fmt::Debug for PrimDef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"PrimDef({}/{}, {:?})",
self.name, self.arity, self.version
)
}
}
macro_rules! prims {
($($($tag:ident)? $name:literal ($arity:literal) => $f:path;)*) => {
static PRIM_DEFS: &[PrimDef] = &[
$(PrimDef {
name: $name,
arity: $arity,
run: $f,
version: prims!(@span $($tag)?),
},)*
];
};
(@span) => { VersionSpan::Both };
(@span v006) => { VersionSpan::V0_0Only };
(@span v01) => { VersionSpan::V0_1Only };
}
macro_rules! version_forked_prims {
($($v006:ident, $v01:ident => $body:path;)*) => {$(
fn $v006(interp: &mut Interp, args: Vec<Value>) -> Result<Value, EvalError> {
$body(interp, RustyfiVersion::V0_0, args)
}
fn $v01(interp: &mut Interp, args: Vec<Value>) -> Result<Value, EvalError> {
$body(interp, RustyfiVersion::V0_1, args)
}
)*};
}
version_forked_prims! {
prim_inline_graphics_v006, prim_inline_graphics_v01 => prim_inline_graphics;
prim_inline_graphics_outer_v006, prim_inline_graphics_outer_v01
=> prim_inline_graphics_outer;
prim_tabular_v006, prim_tabular_v01 => prim_tabular;
prim_inline_frame_outer_v006, prim_inline_frame_outer_v01 => prim_inline_frame_outer;
prim_inline_frame_inner_v006, prim_inline_frame_inner_v01 => prim_inline_frame_inner;
prim_inline_frame_breakable_v006, prim_inline_frame_breakable_v01
=> prim_inline_frame_breakable;
prim_block_frame_breakable_v006, prim_block_frame_breakable_v01
=> prim_block_frame_breakable;
}
prims! {
"read-inline" (2) => prim_read_inline;
"read-block" (2) => prim_read_block;
"line-break" (4) => prim_line_break;
v006 "page-break" (4) => prim_page_break_v006;
v01 "page-break" (4) => prim_page_break_v01;
v006 "page-break-multicolumn" (7) => prim_page_break_multicolumn_v006;
v01 "page-break-multicolumn" (7) => prim_page_break_multicolumn_v01;
v006 "page-break-two-column" (6) => prim_page_break_two_column_v006;
v01 "page-break-two-column" (6) => prim_page_break_two_column_v01;
"+" (2) => prim_int_add;
"-" (2) => prim_int_sub;
"*" (2) => prim_int_mul;
"/" (2) => prim_int_div;
"mod" (2) => prim_int_mod;
"==" (2) => prim_int_eq;
"<>" (2) => prim_int_ne;
"<" (2) => prim_int_lt;
">" (2) => prim_int_gt;
"<=" (2) => prim_int_le;
">=" (2) => prim_int_ge;
v01 "<<" (2) => prim_bit_shift_left;
v01 ">>" (2) => prim_bit_shift_right;
v01 "band" (2) => prim_band;
v01 "bor" (2) => prim_bor;
v01 "bxor" (2) => prim_bxor;
v01 "bnot" (1) => prim_bnot;
"&&" (2) => prim_bool_and;
"||" (2) => prim_bool_or;
"not" (1) => prim_bool_not;
"+." (2) => prim_float_add;
"-." (2) => prim_float_sub;
"*." (2) => prim_float_mul;
"/." (2) => prim_float_div;
"float" (1) => prim_float_of_int;
"round" (1) => prim_round;
v01 ">." (2) => prim_float_gt;
v01 "<." (2) => prim_float_lt;
v01 ">=." (2) => prim_float_ge;
v01 "<=." (2) => prim_float_le;
"+'" (2) => prim_length_add;
"-'" (2) => prim_length_sub;
"*'" (2) => prim_length_scale;
"/'" (2) => prim_length_div;
"<'" (2) => prim_length_lt;
">'" (2) => prim_length_gt;
"^" (2) => prim_string_concat;
"arabic" (1) => prim_arabic;
"string-same" (2) => prim_string_same;
"::" (2) => prim_list_cons;
"!" (1) => prim_deref;
"string-length" (1) => prim_string_length;
"string-sub" (3) => prim_string_sub;
"string-explode" (1) => prim_string_explode;
"regexp-of-string" (1) => prim_regexp_of_string;
"string-match" (2) => prim_string_match;
"split-on-regexp" (2) => prim_split_on_regexp;
"embed-string" (1) => prim_embed_string;
"set-font-size" (2) => prim_set_font_size;
"get-font-size" (1) => prim_get_font_size;
"set-leading" (2) => prim_set_leading;
"set-paragraph-margin" (3) => prim_set_paragraph_margin;
"get-text-width" (1) => prim_get_text_width;
"get-initial-context" (2) => prim_get_initial_context;
"set-font-key" (2) => prim_set_font_key;
"++" (2) => prim_inline_concat;
"+++" (2) => prim_block_concat;
"inline-skip" (1) => prim_inline_skip;
"inline-glue" (3) => prim_inline_glue;
"block-skip" (1) => prim_block_skip;
"list-mark" (1) => prim_list_mark;
"inline-mark" (1) => prim_inline_mark;
"sin" (1) => prim_sin;
"asin" (1) => prim_asin;
"cos" (1) => prim_cos;
"acos" (1) => prim_acos;
"tan" (1) => prim_tan;
"atan" (1) => prim_atan;
"atan2" (2) => prim_atan2;
"log" (1) => prim_log;
"exp" (1) => prim_exp;
"ceil" (1) => prim_ceil;
"floor" (1) => prim_floor;
"show-float" (1) => prim_show_float;
"string-byte-length" (1) => prim_string_byte_length;
"string-sub-bytes" (3) => prim_string_sub_bytes;
"string-unexplode" (1) => prim_string_unexplode;
v01 "normalize-string-to-nfc" (1) => prim_normalize_string_to_nfc;
v01 "normalize-string-to-nfd" (1) => prim_normalize_string_to_nfd;
v01 "split-grapheme-cluster" (1) => prim_split_grapheme_cluster;
"display-message" (1) => prim_display_message;
"abort-with-message" (1) => prim_abort_with_message;
"load-image" (1) => prim_load_image; "use-image-by-width" (2) => prim_use_image_by_width; "load-pdf-image" (2) => prim_load_pdf_image;
v01 "read-file" (1) => prim_read_file;
v01 "register-document-information" (1) => prim_register_document_information;
"start-path" (1) => prim_start_path;
"line-to" (2) => prim_line_to;
"terminate-path" (1) => prim_terminate_path;
"close-with-line" (1) => prim_close_with_line;
"fill" (2) => prim_fill;
"stroke" (3) => prim_stroke;
v006 "inline-graphics" (4) => prim_inline_graphics_v006;
v01 "inline-graphics" (4) => prim_inline_graphics_v01;
v006 "tabular" (2) => prim_tabular_v006;
v01 "tabular" (2) => prim_tabular_v01;
v006 "inline-graphics-outer" (3) => prim_inline_graphics_outer_v006;
v01 "inline-graphics-outer" (3) => prim_inline_graphics_outer_v01;
"bezier-to" (4) => prim_bezier_to;
"close-with-bezier" (3) => prim_close_with_bezier;
"shift-path" (2) => prim_shift_path;
"linear-transform-path" (5) => prim_linear_transform_path;
"shift-graphics" (2) => prim_shift_graphics;
"linear-transform-graphics" (5) => prim_linear_transform_graphics;
v006 "get-graphics-bbox" (1) => prim_get_graphics_bbox_v006;
v01 "get-graphics-bbox" (1) => prim_get_graphics_bbox_v01;
"get-path-bbox" (1) => prim_get_path_bbox;
"dashed-stroke" (4) => prim_dashed_stroke;
"draw-text" (2) => prim_draw_text;
v01 "unite-graphics" (1) => prim_unite_graphics;
v01 "clip-graphics-by-path" (2) => prim_clip_graphics_by_path;
"get-natural-metrics" (1) => prim_get_natural_metrics;
v006 "inline-frame-outer" (3) => prim_inline_frame_outer_v006;
v01 "inline-frame-outer" (3) => prim_inline_frame_outer_v01;
v006 "inline-frame-inner" (3) => prim_inline_frame_inner_v006;
v01 "inline-frame-inner" (3) => prim_inline_frame_inner_v01;
"set-manual-rising" (2) => prim_set_manual_rising;
"script-guard" (2) => prim_script_guard;
"discretionary" (4) => prim_discretionary;
v006 "get-axis-height" (1) => prim_get_axis_height;
"hook-page-break" (1) => prim_hook_page_break;
"hook-page-break-block" (1) => prim_hook_page_break_block;
"register-cross-reference" (2) => prim_register_cross_reference;
"get-cross-reference" (1) => prim_get_cross_reference;
"probe-cross-reference" (1) => prim_probe_cross_reference;
"get-leftmost-script" (1) => prim_get_leftmost_script;
"get-rightmost-script" (1) => prim_get_rightmost_script;
v006 "inline-frame-breakable" (3) => prim_inline_frame_breakable_v006;
v01 "inline-frame-breakable" (3) => prim_inline_frame_breakable_v01;
"register-destination" (2) => prim_register_destination;
"register-link-to-uri" (6) => prim_register_link_to_uri;
"register-link-to-location" (6) => prim_register_link_to_location;
v006 "math-char" (2) => prim_math_char_v006;
v01 "math-char" (3) => prim_math_char_v01;
v006 "math-big-char" (2) => prim_math_big_char_v006;
v01 "math-big-char" (3) => prim_math_big_char_v01;
v006 "math-char-with-kern" (4) => prim_math_char_with_kern_v006;
v01 "math-char-with-kern" (5) => prim_math_char_with_kern_v01;
v006 "math-big-char-with-kern" (4) => prim_math_big_char_with_kern_v006;
v01 "math-big-char-with-kern" (5) => prim_math_big_char_with_kern_v01;
v006 "math-concat" (2) => prim_math_concat_v006;
v01 "math-concat" (2) => prim_math_concat_v01;
v006 "math-group" (3) => prim_math_group_v006;
v01 "math-group" (3) => prim_math_group_v01;
v006 "math-sup" (2) => prim_math_sup_v006;
v01 "math-sup" (3) => prim_math_sup_v01;
v006 "math-sub" (2) => prim_math_sub_v006;
v01 "math-sub" (3) => prim_math_sub_v01;
v006 "math-frac" (2) => prim_math_frac_v006;
v01 "math-frac" (3) => prim_math_frac_v01;
v006 "math-radical" (2) => prim_math_radical_v006;
v01 "math-radical" (3) => prim_math_radical_v01;
v006 "math-lower" (2) => prim_math_lower_v006;
v01 "math-lower" (3) => prim_math_lower_v01;
v006 "math-upper" (2) => prim_math_upper_v006;
v01 "math-upper" (3) => prim_math_upper_v01;
v006 "math-pull-in-scripts" (3) => prim_math_pull_in_scripts;
v006 "math-color" (2) => prim_math_color;
v006 "math-char-class" (2) => prim_math_char_class;
v006 "math-variant-char" (2) => prim_math_variant_char;
v006 "set-math-variant-char" (4) => prim_set_math_variant_char_v006;
v01 "set-math-variant-char" (3) => prim_set_math_variant_char_v01;
v006 "get-left-math-class" (2) => prim_get_left_math_class_v006;
v01 "get-left-math-class" (1) => prim_get_left_math_class_v01;
v006 "get-right-math-class" (2) => prim_get_right_math_class_v006;
v01 "get-right-math-class" (1) => prim_get_right_math_class_v01;
v006 "math-paren" (3) => prim_math_paren_v006;
v01 "math-paren" (4) => prim_math_paren_v01;
v006 "math-paren-with-middle" (4) => prim_math_paren_with_middle_v006;
v01 "math-paren-with-middle" (5) => prim_math_paren_with_middle_v01;
v006 "text-in-math" (2) => prim_text_in_math;
"convert-string-for-math" (3) => prim_convert_string_for_math;
v006 "embed-math" (2) => prim_embed_math_v006;
v01 "embed-math" (2) => prim_embed_math_v01;
"set-math-command" (2) => prim_set_math_command;
v006 "set-math-font" (2) => prim_set_math_font_v006;
v01 "set-math-font" (2) => prim_set_math_font_v01;
v01 "load-single-font" (1) => prim_load_single_font;
v006 "space-between-maths" (3) => prim_space_between_maths_v006;
v01 "space-between-maths" (3) => prim_space_between_maths_v01;
v01 "read-math" (2) => prim_read_math;
v01 "stringify-math" (2) => prim_stringify_math;
v01 "set-math-char" (4) => prim_set_math_char;
v01 "set-math-char-class" (2) => prim_set_math_char_class;
v01 "get-math-char-class" (1) => prim_get_math_char_class;
v01 "embed-inline-to-math" (2) => prim_embed_inline_to_math;
v01 "get-math-axis-height-ratio" (1) => prim_get_math_axis_height_ratio;
v01 "%math-attach-scripts" (4) => prim_math_attach_scripts;
v01 "load-hyphenation-dictionary" (1) => prim_load_hyphenation_dictionary;
v01 "load-unicode-char-database" (3) => prim_load_unicode_char_database;
v01 "set-hyphenation-dictionary" (2) => prim_set_hyphenation_dictionary;
v01 "set-unicode-char-database" (2) => prim_set_unicode_char_database;
"raise-inline" (2) => prim_raise_inline;
"embed-block-breakable" (2) => prim_embed_block_breakable;
"unite-path" (2) => prim_unite_path;
"set-min-gap-of-lines" (2) => prim_set_min_gap_of_lines;
"set-text-color" (2) => prim_set_text_color;
"get-text-color" (1) => prim_get_text_color;
"set-hyphen-penalty" (2) => prim_set_hyphen_penalty;
"set-hyphen-min" (3) => prim_set_hyphen_min;
"set-space-ratio" (4) => prim_set_space_ratio;
"set-space-ratio-between-scripts" (6) => prim_set_space_ratio_between_scripts;
"split-into-lines" (1) => prim_split_into_lines;
v006 "block-frame-breakable" (4) => prim_block_frame_breakable_v006;
v01 "block-frame-breakable" (4) => prim_block_frame_breakable_v01;
"embed-block-top" (3) => prim_embed_block_top;
v006 "set-font" (3) => prim_set_font_v006;
v01 "set-font" (3) => prim_set_font_v01;
v006 "get-font" (2) => prim_get_font_v006;
v01 "get-font" (2) => prim_get_font_v01;
"set-code-text-command" (2) => prim_set_code_text_command;
"get-natural-length" (1) => prim_get_natural_length;
"set-dominant-wide-script" (2) => prim_set_dominant_wide_script;
"set-dominant-narrow-script" (2) => prim_set_dominant_narrow_script;
"set-language" (3) => prim_set_language;
"get-dominant-wide-script" (1) => prim_get_dominant_wide_script;
"get-dominant-narrow-script" (1) => prim_get_dominant_narrow_script;
"get-language" (2) => prim_get_language;
"set-every-word-break" (3) => prim_set_every_word_break;
"register-outline" (1) => prim_register_outline;
"extract-string" (1) => prim_extract_string;
"embed-block-bottom" (3) => prim_embed_block_bottom;
"line-stack-bottom" (1) => prim_line_stack_bottom;
"line-stack-top" (1) => prim_line_stack_top;
"add-footnote" (1) => prim_add_footnote;
v006 "get-initial-text-info" (1) => prim_get_initial_text_info_v006;
v01 "get-initial-text-info" (2) => prim_get_initial_text_info_v01;
"deepen-indent" (2) => prim_deepen_indent;
"break" (1) => prim_break;
}
pub fn base_env() -> BaseEnv {
base_env_with_version(RustyfiVersion::V0_0)
}
pub fn base_env_with_version(version: RustyfiVersion) -> BaseEnv {
let mut env = BaseEnv::new();
for def in PRIM_DEFS {
if !def.version.allows(version) {
continue;
}
env.define(
def.name,
Value::Prim {
def,
applied: Vec::new(),
},
);
}
env.define(
"inline-fil",
Value::InlineBoxes(vec![HorzBox::Pure(PureHorzBox::OuterFil)]),
);
env.define("inline-nil", Value::InlineBoxes(Vec::new()));
env.define("block-nil", Value::BlockBoxes(Vec::new()));
env.define("omit-skip-after", Value::InlineBoxes(Vec::new()));
env.define("clear-page", Value::BlockBoxes(vec![VertBox::ClearPage]));
if version == RustyfiVersion::V0_1 {
env.define("here", Value::Str(String::new()));
}
env
}
fn as_context(v: Value) -> Result<Context, EvalError> {
match v {
Value::Context(c) => Ok(*c),
other => eval_error(format!("expected a context, got {}", other.type_name())),
}
}
fn as_text_info(v: Value) -> Result<TextInfo, EvalError> {
match v {
Value::TextInfo(t) => Ok(t),
other => eval_error(format!("expected a text-info, got {}", other.type_name())),
}
}
fn as_hyphenation(v: Value) -> Result<HyphenLang, EvalError> {
match v {
Value::Hyphenation(tag) => Ok(tag),
other => eval_error(format!("expected a hyphenation, got {}", other.type_name())),
}
}
fn as_inline_text(v: Value) -> Result<(Rc<Vec<IText>>, Env), EvalError> {
match v {
Value::InlineText { elems, env } => Ok((elems, env)),
other => eval_error(format!("expected inline-text, got {}", other.type_name())),
}
}
fn as_block_text(v: Value) -> Result<(Rc<Vec<BText>>, Env), EvalError> {
match v {
Value::BlockText { elems, env } => Ok((elems, env)),
other => eval_error(format!("expected block-text, got {}", other.type_name())),
}
}
fn as_inline_boxes(v: Value) -> Result<Vec<HorzBox>, EvalError> {
match v {
Value::InlineBoxes(b) => Ok(b),
other => eval_error(format!("expected inline-boxes, got {}", other.type_name())),
}
}
fn as_block_boxes(v: Value) -> Result<Vec<VertBox>, EvalError> {
match v {
Value::BlockBoxes(b) => Ok(b),
other => eval_error(format!("expected block-boxes, got {}", other.type_name())),
}
}
fn as_int(v: Value) -> Result<i64, EvalError> {
match v {
Value::Int(n) => Ok(n),
other => eval_error(format!("expected int, got {}", other.type_name())),
}
}
fn as_float(v: Value) -> Result<f64, EvalError> {
match v {
Value::Float(x) => Ok(x),
other => eval_error(format!("expected float, got {}", other.type_name())),
}
}
fn as_bool(v: Value) -> Result<bool, EvalError> {
match v {
Value::Bool(b) => Ok(b),
other => eval_error(format!("expected bool, got {}", other.type_name())),
}
}
fn as_str(v: Value) -> Result<String, EvalError> {
match v {
Value::Str(s) => Ok(s),
other => eval_error(format!("expected string, got {}", other.type_name())),
}
}
fn prim_regexp_of_string(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
Ok(Value::Str(s))
}
fn prim_string_match(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let input = as_str(args.pop().unwrap())?;
let pattern = as_str(args.pop().unwrap())?;
Ok(Value::Bool(regexp_full_match(&pattern, &input)))
}
fn regexp_full_match(pattern: &str, input: &str) -> bool {
let p: Vec<char> = pattern.chars().collect();
if p.len() >= 2 && p[0] == '[' && p[p.len() - 1] == ']' {
let mut chars = input.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => char_in_class(&p[1..p.len() - 1], c),
_ => false,
}
} else {
input == pattern
}
}
fn prim_split_on_regexp(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let input = as_str(args.pop().unwrap())?;
let pattern = as_str(args.pop().unwrap())?;
let is_delim = single_char_matcher(&pattern);
let mut segments: Vec<Value> = Vec::new();
let mut seg_start = 0usize;
let mut cur = String::new();
for (idx, c) in input.chars().enumerate() {
if is_delim(c) {
segments.push(Value::Tuple(vec![
Value::Int(seg_start as i64),
Value::Str(std::mem::take(&mut cur)),
]));
seg_start = idx + 1;
} else {
cur.push(c);
}
}
segments.push(Value::Tuple(vec![
Value::Int(seg_start as i64),
Value::Str(cur),
]));
Ok(Value::List(segments))
}
fn single_char_matcher(pattern: &str) -> Box<dyn Fn(char) -> bool> {
let p: Vec<char> = pattern.chars().collect();
if p.len() >= 2 && p[0] == '[' && p[p.len() - 1] == ']' {
let cls: Vec<char> = p[1..p.len() - 1].to_vec();
Box::new(move |c| char_in_class(&cls, c))
} else if p.len() == 2 && p[0] == '\\' {
let lit = p[1];
Box::new(move |c| c == lit)
} else if p.len() == 1 {
let lit = p[0];
Box::new(move |c| c == lit)
} else {
Box::new(|_| false)
}
}
fn char_in_class(cls: &[char], c: char) -> bool {
let (neg, cls) = match cls.first() {
Some('^') => (true, &cls[1..]),
_ => (false, cls),
};
let mut i = 0;
let mut found = false;
while i < cls.len() {
if i + 2 < cls.len() && cls[i + 1] == '-' {
if cls[i] <= c && c <= cls[i + 2] {
found = true;
}
i += 3;
} else {
if cls[i] == c {
found = true;
}
i += 1;
}
}
found ^ neg
}
fn as_length(v: Value) -> Result<Length, EvalError> {
match v {
Value::Length(l) => Ok(l),
other => eval_error(format!("expected length, got {}", other.type_name())),
}
}
fn as_list(v: Value) -> Result<Vec<Value>, EvalError> {
match v {
Value::List(items) => Ok(items),
other => eval_error(format!("expected list, got {}", other.type_name())),
}
}
fn as_image(v: Value) -> Result<ImageId, EvalError> {
match v {
Value::Image(id) => Ok(id),
other => eval_error(format!("expected image, got {}", other.type_name())),
}
}
fn as_point(v: Value) -> Result<Point, EvalError> {
match v {
Value::Tuple(vs) if vs.len() == 2 => {
let mut it = vs.into_iter();
let x = as_length(it.next().unwrap())?;
let y = as_length(it.next().unwrap())?;
Ok((x, y))
}
other => eval_error(format!(
"expected a point (length * length), got {}",
other.type_name()
)),
}
}
fn as_color(v: Value) -> Result<Color, EvalError> {
match v {
Value::Ctor(name, payload) => match (name.as_str(), payload.map(|b| *b)) {
("Gray", Some(p)) => Ok(Color::Gray(as_float(p)?)),
("RGB", Some(Value::Tuple(vs))) if vs.len() == 3 => {
let mut it = vs.into_iter();
let r = as_float(it.next().unwrap())?;
let g = as_float(it.next().unwrap())?;
let b = as_float(it.next().unwrap())?;
Ok(Color::Rgb(r, g, b))
}
("CMYK", Some(Value::Tuple(vs))) if vs.len() == 4 => {
let mut it = vs.into_iter();
let c = as_float(it.next().unwrap())?;
let m = as_float(it.next().unwrap())?;
let y = as_float(it.next().unwrap())?;
let k = as_float(it.next().unwrap())?;
Ok(Color::Cmyk(c, m, y, k))
}
(other, _) => eval_error(format!(
"expected a color (Gray/RGB/CMYK), got variant '{other}'"
)),
},
other => eval_error(format!("expected a color, got {}", other.type_name())),
}
}
fn as_script(v: Value) -> Result<Script, EvalError> {
match v {
Value::Ctor(name, None) => match name.as_str() {
"HanIdeographic" => Ok(Script::HanIdeographic),
"Kana" => Ok(Script::Kana),
"Latin" => Ok(Script::Latin),
"OtherScript" => Ok(Script::OtherScript),
other => eval_error(format!("expected a script, got variant '{other}'")),
},
other => eval_error(format!("expected a script, got {}", other.type_name())),
}
}
fn make_script_value(s: Script) -> Value {
let name = match s {
Script::HanIdeographic => "HanIdeographic",
Script::Kana => "Kana",
Script::Latin => "Latin",
Script::OtherScript => "OtherScript",
};
Value::Ctor(name.to_string(), None)
}
fn as_language(v: Value) -> Result<Language, EvalError> {
match v {
Value::Ctor(name, None) => match name.as_str() {
"Japanese" => Ok(Language::Japanese),
"English" => Ok(Language::English),
"NoLanguageSystem" => Ok(Language::NoLanguageSystem),
other => eval_error(format!("expected a language, got variant '{other}'")),
},
other => eval_error(format!("expected a language, got {}", other.type_name())),
}
}
fn make_language_value(l: Language) -> Value {
let name = match l {
Language::Japanese => "Japanese",
Language::English => "English",
Language::NoLanguageSystem => "NoLanguageSystem",
};
Value::Ctor(name.to_string(), None)
}
fn as_page(v: Value) -> Result<PaperSize, EvalError> {
match v {
Value::Ctor(name, payload) => match (name.as_str(), payload.map(|b| *b)) {
("A0Paper", None) => Ok(PaperSize::A0),
("A1Paper", None) => Ok(PaperSize::A1),
("A2Paper", None) => Ok(PaperSize::A2),
("A3Paper", None) => Ok(PaperSize::A3),
("A4Paper", None) => Ok(PaperSize::A4),
("A5Paper", None) => Ok(PaperSize::A5),
("USLetter", None) => Ok(PaperSize::USLetter),
("USLegal", None) => Ok(PaperSize::USLegal),
("UserDefinedPaper", Some(Value::Tuple(vs))) if vs.len() == 2 => {
let mut it = vs.into_iter();
let w = as_length(it.next().unwrap())?;
let h = as_length(it.next().unwrap())?;
Ok(PaperSize::UserDefined(w, h))
}
(other, _) => eval_error(format!(
"expected a page (A4Paper/.../UserDefinedPaper), got variant '{other}'"
)),
},
other => eval_error(format!("expected a page, got {}", other.type_name())),
}
}
fn as_page_v01(v: Value) -> Result<PaperSize, EvalError> {
match v {
Value::Tuple(vs) if vs.len() == 2 => {
let mut it = vs.into_iter();
let w = as_length(it.next().unwrap())?;
let h = as_length(it.next().unwrap())?;
Ok(PaperSize::UserDefined(w, h))
}
other => eval_error(format!(
"expected a page as (length * length), got {}",
other.type_name()
)),
}
}
fn as_paddings(v: Value) -> Result<(Length, Length, Length, Length), EvalError> {
match v {
Value::Tuple(vs) if vs.len() == 4 => {
let mut it = vs.into_iter();
let l = as_length(it.next().unwrap())?;
let r = as_length(it.next().unwrap())?;
let t = as_length(it.next().unwrap())?;
let b = as_length(it.next().unwrap())?;
Ok((l, r, t, b))
}
other => eval_error(format!(
"expected paddings (length * length * length * length), got {}",
other.type_name()
)),
}
}
fn as_cell(v: Value) -> Result<Cell, EvalError> {
match v {
Value::Ctor(name, payload) => match (name.as_str(), payload.map(|b| *b)) {
("NormalCell", Some(Value::Tuple(vs))) if vs.len() == 2 => {
let mut it = vs.into_iter();
let (l, r, t, b) = as_paddings(it.next().unwrap())?;
let ib = as_inline_boxes(it.next().unwrap())?;
Ok(Cell::Normal(Paddings { l, r, t, b }, ib))
}
("EmptyCell", None) => Ok(Cell::Empty),
("MultiCell", Some(Value::Tuple(vs))) if vs.len() == 4 => {
let mut it = vs.into_iter();
let numrow = as_int(it.next().unwrap())?;
let numcol = as_int(it.next().unwrap())?;
let (l, r, t, b) = as_paddings(it.next().unwrap())?;
let ib = as_inline_boxes(it.next().unwrap())?;
Ok(Cell::Multi(
numrow.max(0) as usize,
numcol.max(0) as usize,
Paddings { l, r, t, b },
ib,
))
}
(other, _) => eval_error(format!(
"expected a cell (NormalCell/EmptyCell/MultiCell), got variant '{other}'"
)),
},
other => eval_error(format!("expected a cell, got {}", other.type_name())),
}
}
fn as_cell_grid(v: Value) -> Result<Vec<Vec<Cell>>, EvalError> {
as_list(v)?
.into_iter()
.map(|row| -> Result<Vec<Cell>, EvalError> {
as_list(row)?.into_iter().map(as_cell).collect()
})
.collect()
}
fn as_prepath(v: Value) -> Result<PrePath, EvalError> {
match v {
Value::PrePath(p) => Ok(p),
other => eval_error(format!("expected pre-path, got {}", other.type_name())),
}
}
fn as_path(v: Value) -> Result<Path, EvalError> {
match v {
Value::Path(p) => Ok(p),
other => eval_error(format!("expected path, got {}", other.type_name())),
}
}
fn as_graphics(v: Value) -> Result<GraphicsElem, EvalError> {
match v {
Value::Graphics(g) => Ok(g),
other => eval_error(format!("expected graphics, got {}", other.type_name())),
}
}
fn as_dash(v: Value) -> Result<Dash, EvalError> {
match v {
Value::Tuple(vs) if vs.len() == 3 => {
let mut it = vs.into_iter();
let d1 = as_length(it.next().unwrap())?;
let d2 = as_length(it.next().unwrap())?;
let d0 = as_length(it.next().unwrap())?;
Ok((d1, d2, d0))
}
other => eval_error(format!(
"expected a dash pattern (length * length * length), got {}",
other.type_name()
)),
}
}
fn make_point_value(pt: Point) -> Value {
Value::Tuple(vec![Value::Length(pt.0), Value::Length(pt.1)])
}
fn make_length_list(lens: &[Length]) -> Value {
Value::List(lens.iter().map(|l| Value::Length(*l)).collect())
}
macro_rules! binop_prim {
($name:ident, ($as_a:path, $as_b:path), $ctor:ident, |$a:ident, $b:ident| $body:expr) => {
fn $name(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let $b = $as_b(args.pop().unwrap())?;
let $a = $as_a(args.pop().unwrap())?;
Ok(Value::$ctor($body))
}
};
($name:ident, $as:path, $ctor:ident, |$a:ident, $b:ident| $body:expr) => {
binop_prim!($name, ($as, $as), $ctor, |$a, $b| $body);
};
}
macro_rules! cmp_prim {
($name:ident, $as:path, |$a:ident, $b:ident| $body:expr) => {
binop_prim!($name, ($as, $as), Bool, |$a, $b| $body);
};
}
macro_rules! unop_prim {
($name:ident, $as:path, $ctor:ident, |$a:ident| $body:expr) => {
fn $name(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let $a = $as(args.pop().unwrap())?;
Ok(Value::$ctor($body))
}
};
}
macro_rules! binop_prim_try {
($name:ident, $as:path, |$a:ident, $b:ident| $body:expr) => {
fn $name(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let $b = $as(args.pop().unwrap())?;
let $a = $as(args.pop().unwrap())?;
$body
}
};
}
pub fn read_inline(
interp: &mut Interp,
ctx: &Context,
elems: &[IText],
env: &Env,
) -> Result<Vec<HorzBox>, EvalError> {
let mut out = Vec::new();
for elem in elems {
match elem {
IText::Text(text) => text_to_boxes(interp, ctx, text, &mut out)?,
IText::CodeText(text) => match ctx.code_text_command {
Some(id) => {
let cmd = interp.math_commands[id.0].clone();
let v = interp.apply(cmd, Value::Context(Box::new(ctx.clone())))?;
let v = interp.apply(v, Value::Str(text.clone()))?;
out.extend(as_inline_boxes(v)?);
}
None => text_to_boxes(interp, ctx, text, &mut out)?,
},
IText::Cmd { cmd, args } => {
let cmd = cmd.run(env, interp)?;
let mut v = interp.apply(cmd, Value::Context(Box::new(ctx.clone())))?;
for arg in args {
let mut opt_vals = Vec::with_capacity(arg.opts.len());
for (label, e) in &arg.opts {
opt_vals.push((label.clone(), e.run(env, interp)?));
}
let arg_v = arg.arg.run(env, interp)?;
v = interp.apply_with_opts(v, opt_vals, arg_v)?;
}
out.extend(as_inline_boxes(v)?);
}
IText::Embed { expr, span } => {
let v = expr.run(env, interp)?;
match v {
Value::InlineText {
elems: sub_elems,
env: cap_env,
} => {
out.extend(read_inline(interp, ctx, &sub_elems, &cap_env)?);
}
other => {
return Err(EvalError {
span: Some(*span),
msg: format!(
"expected inline-text in '#…;' embed, got {}",
other.type_name()
),
});
}
}
}
IText::EmbedMath { elems, .. } => {
let installed = ctx
.math_command
.and_then(|id| interp.math_commands.get(id.0).cloned());
match installed {
Some(cmd) => {
let v = interp.apply(cmd, Value::Context(Box::new(ctx.clone())))?;
let v = interp.apply(
v,
Value::MathText {
elems: Rc::clone(elems),
env: env.clone(),
},
)?;
out.extend(as_inline_boxes(v)?);
}
None => {
let mut atoms = Vec::new();
if interp.version.math_is_split() {
for e in elems.iter() {
reflect_math_elem_v01(interp, ctx, e, env, &mut atoms)?;
}
} else {
for e in elems.iter() {
reflect_math_elem(interp, e, env, &mut atoms)?;
}
}
out.push(HorzBox::Pure(layout_math_value(interp, ctx, &atoms)?));
}
}
}
}
}
Ok(insert_box_interscript_glue(out, ctx))
}
fn read_block(
interp: &mut Interp,
ctx: &Context,
elems: &[BText],
env: &Env,
) -> Result<Vec<VertBox>, EvalError> {
let mut out = Vec::new();
for elem in elems {
match elem {
BText::Cmd { cmd, args } => {
let cmd = cmd.run(env, interp)?;
let mut v = interp.apply(cmd, Value::Context(Box::new(ctx.clone())))?;
for arg in args {
let mut opt_vals = Vec::with_capacity(arg.opts.len());
for (label, e) in &arg.opts {
opt_vals.push((label.clone(), e.run(env, interp)?));
}
let arg_v = arg.arg.run(env, interp)?;
v = interp.apply_with_opts(v, opt_vals, arg_v)?;
}
out.extend(as_block_boxes(v)?);
}
BText::Embed { expr, span } => {
let v = expr.run(env, interp)?;
match v {
Value::BlockText {
elems: sub_elems,
env: cap_env,
} => {
out.extend(read_block(interp, ctx, &sub_elems, &cap_env)?);
}
other => {
return Err(EvalError {
span: Some(*span),
msg: format!(
"expected block-text in '#…;' embed, got {}",
other.type_name()
),
});
}
}
}
}
}
Ok(out)
}
fn uax14_boundaries(text: &str) -> Vec<Option<BreakKind>> {
let mut boundary = vec![None; text.len() + 1];
for (offset, kind) in break_opportunities(text) {
if offset < text.len() {
boundary[offset] = Some(kind);
}
}
boundary
}
fn script_font(ctx: &Context, script: Script) -> ScriptFont {
if script == Script::OtherScript && ctx.dominant_narrow_script != Script::OtherScript {
return script_font(ctx, ctx.dominant_narrow_script);
}
if script == Script::Latin {
ScriptFont {
font: ctx.font,
..ctx.font_scheme[Script::Latin as usize]
}
} else {
ctx.font_scheme[script as usize]
}
}
fn measure_run(
interp: &Interp,
font: FontKey,
fallback_font: FontKey,
text: &str,
size: Length,
) -> Result<Length, EvalError> {
let mut width = Length::ZERO;
for c in text.chars() {
let advance = interp
.metrics
.advance(font, c, size)
.or_else(|| interp.metrics.advance(fallback_font, c, size))
.unwrap_or(size * 0.5);
width += advance;
}
Ok(width)
}
fn make_inner_string_pure_box(
interp: &Interp,
ctx: &Context,
sf: ScriptFont,
size: Length,
rising: Length,
text: String,
) -> Result<PureHorzBox, EvalError> {
let width = measure_run(interp, sf.font, ctx.font, &text, size)?;
let (height, depth) = interp.metrics.run_vextent(sf.font, &text, size);
Ok(PureHorzBox::InnerString {
info: HorzStringInfo {
font: sf.font,
size,
rising,
color: ctx.text_color,
},
height,
depth,
text,
width,
})
}
fn is_latin_cjk_boundary(a: Script, b: Script) -> bool {
let is_cjk = |s| matches!(s, Script::HanIdeographic | Script::Kana);
(a == Script::Latin && is_cjk(b)) || (is_cjk(a) && b == Script::Latin)
}
fn is_open_punct(c: char) -> bool {
matches!(
c,
'(' | '['
| '{'
| '"'
| '\''
| '('
| '「'
| '『'
| '【'
| '〔'
| '〈'
| '《'
| '['
| '{'
| '〖'
| '〘'
| '〚'
| '“'
| '‘'
)
}
fn is_close_punct(c: char) -> bool {
matches!(
c,
')' | ']'
| '}'
| '"'
| '\''
| ')'
| '」'
| '』'
| '】'
| '〕'
| '〉'
| '》'
| ']'
| '}'
| '〗'
| '〙'
| '〛'
| '”'
| '’'
| '、'
| '。'
| ','
| '.'
| '・'
| ':'
| ';'
)
}
fn interscript_glue_suppressed(l: char, r: char) -> bool {
is_open_punct(l) || is_close_punct(r)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum JlClass {
Open,
Close,
FullStop,
Comma,
MiddleDot,
}
fn jl_class(c: char) -> Option<JlClass> {
match c {
'(' | '「' | '『' | '【' | '〔' | '〈' | '《' | '[' | '{' | '〖' | '〘' | '〚' => {
Some(JlClass::Open)
}
')' | '」' | '』' | '】' | '〕' | '〉' | '》' | ']' | '}' | '〗' | '〙' | '〛' => {
Some(JlClass::Close)
}
'。' | '.' => Some(JlClass::FullStop),
'、' | ',' => Some(JlClass::Comma),
'・' | ':' | ';' => Some(JlClass::MiddleDot),
_ => None,
}
}
fn cjk_trailing_kern(c: char) -> f64 {
match jl_class(c) {
Some(JlClass::Close) | Some(JlClass::FullStop) | Some(JlClass::Comma) => -0.5,
Some(JlClass::MiddleDot) => -0.25,
_ => 0.0,
}
}
fn cjk_leading_kern(c: char) -> f64 {
match jl_class(c) {
Some(JlClass::Open) => -0.5,
Some(JlClass::MiddleDot) => -0.25,
_ => 0.0,
}
}
fn cjk_pair_space(
a: char,
size_a: Length,
b: char,
size_b: Length,
raw_size: Length,
adjacent_stretch: f64,
) -> (Length, Length, Length) {
use JlClass::*;
let (ca, cb) = (jl_class(a), jl_class(b));
let kern = size_a * cjk_trailing_kern(a) + size_b * cjk_leading_kern(b);
let size_m = Length::max(size_a, size_b);
let hwsoft = |s: Length| (s * 0.5, s * 0.25, s * 0.25);
let hwhard = |s: Length| (s * 0.5, Length::ZERO, s * 0.25);
let cls = match (ca, cb) {
(Some(Close), Some(Open)) | (Some(Comma), Some(Open)) => Some(hwsoft(size_m)),
(Some(FullStop), Some(Open)) => Some(hwhard(size_m)),
(_, Some(Open)) => Some(hwsoft(size_b)),
(Some(Close), Some(Comma)) | (Some(Close), Some(FullStop)) => None,
(Some(Close), _) | (Some(Comma), _) => Some(hwsoft(size_a)),
(Some(FullStop), _) => Some(hwhard(size_a)),
_ => None,
};
match cls {
Some((n, sh, st)) => (kern + n, sh, st),
None => (kern, Length::ZERO, raw_size * adjacent_stretch),
}
}
fn box_leading_char(b: &HorzBox) -> Option<char> {
match b {
HorzBox::Pure(PureHorzBox::InnerString { text, .. }) => text.chars().next(),
HorzBox::Pure(PureHorzBox::Math { .. }) => Some('x'),
_ => None,
}
}
fn box_trailing_char(b: &HorzBox) -> Option<char> {
match b {
HorzBox::Pure(PureHorzBox::InnerString { text, .. }) => text.chars().last(),
HorzBox::Pure(PureHorzBox::Math { .. }) => Some('x'),
_ => None,
}
}
fn interscript_glue(ctx: &Context, left: Script, right: Script) -> PureHorzBox {
PureHorzBox::OuterEmpty {
natural: ctx.font_size * ctx.script_space_map[left as usize][right as usize],
shrinkable: Length::ZERO,
stretchable: Length::ZERO,
}
}
fn insert_box_interscript_glue(boxes: Vec<HorzBox>, ctx: &Context) -> Vec<HorzBox> {
if boxes.len() < 2 {
return boxes;
}
let mut out: Vec<HorzBox> = Vec::with_capacity(boxes.len());
for b in boxes {
if let (Some(pc), Some(cc)) = (out.last().and_then(box_trailing_char), box_leading_char(&b))
{
let (ls, rs) = (char_script(pc), char_script(cc));
if is_latin_cjk_boundary(ls, rs) && !interscript_glue_suppressed(pc, cc) {
out.push(HorzBox::Pure(interscript_glue(ctx, ls, rs)));
}
}
out.push(b);
}
out
}
fn text_to_boxes(
interp: &mut Interp,
ctx: &Context,
text: &str,
out: &mut Vec<HorzBox>,
) -> Result<(), EvalError> {
let space_width = ctx.font_size * ctx.space_natural;
let boundary = uax14_boundaries(text);
let mut word = String::new();
let flush_word =
|word: &mut String, script: Script, out: &mut Vec<HorzBox>| -> Result<(), EvalError> {
if word.is_empty() {
return Ok(());
}
let sf = script_font(ctx, script);
let size = ctx.font_size * sf.ratio;
let rising = ctx.font_size * sf.rising + ctx.manual_rising;
let breaks = match ctx.hyphen_dictionary {
Some(tag) if script == Script::Latin => {
let (clean, shy_breaks) = crate::hyphenation::strip_soft_hyphens(word);
if !shy_breaks.is_empty() {
*word = clean;
shy_breaks
} else {
crate::hyphenation::hyphenate_word(
tag,
word,
ctx.left_hyphen_min.max(0) as usize,
ctx.right_hyphen_min.max(0) as usize,
)
}
}
_ => Vec::new(),
};
if breaks.is_empty() {
out.push(HorzBox::Pure(make_inner_string_pure_box(
interp,
ctx,
sf,
size,
rising,
std::mem::take(word),
)?));
return Ok(());
}
let chars: Vec<char> = word.chars().collect();
let penalty = ctx.hyphen_badness.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
let mut prev = 0usize;
for &b in &breaks {
let fragment: String = chars[prev..b].iter().collect();
out.push(HorzBox::Pure(make_inner_string_pure_box(
interp, ctx, sf, size, rising, fragment,
)?));
let hyphen_box =
make_inner_string_pure_box(interp, ctx, sf, size, rising, "-".to_string())?;
out.push(HorzBox::Pure(PureHorzBox::Discretionary {
penalty,
pre_break: vec![hyphen_box],
post_break: Vec::new(),
no_break: Vec::new(),
}));
prev = b;
}
let tail: String = chars[prev..].iter().collect();
out.push(HorzBox::Pure(make_inner_string_pure_box(
interp, ctx, sf, size, rising, tail,
)?));
word.clear();
Ok(())
};
let mut word_script: Option<Script> = None;
let mut prev_script: Option<Script> = None;
let mut prev_char: Option<char> = None;
for (i, c) in text.char_indices() {
if c == ' ' || c == '\n' {
let is_cjk_script = |s| matches!(s, Script::HanIdeographic | Script::Kana);
let prev_cjk = prev_script.is_some_and(is_cjk_script);
let rest = &text[i + c.len_utf8()..];
let run_continues = rest
.chars()
.next()
.is_some_and(|ch| matches!(ch, ' ' | '\n'));
let next_cjk = rest
.chars()
.find(|ch| !matches!(ch, ' ' | '\n'))
.is_some_and(|ch| is_cjk_script(char_script(ch)));
if prev_cjk || next_cjk {
let keep = c == ' ' && !run_continues && prev_cjk && next_cjk;
if !keep {
continue;
}
}
}
if c == ' ' || c == '\n' {
if let Some(s) = word_script.take() {
flush_word(&mut word, s, out)?;
}
prev_script = None;
prev_char = None;
let rigid_space = ctx.space_shrink == 0.0 && ctx.space_stretch == 0.0;
if rigid_space
|| !matches!(
out.last(),
Some(HorzBox::Pure(PureHorzBox::OuterEmpty { .. }))
)
{
out.push(HorzBox::Pure(PureHorzBox::OuterEmpty {
natural: space_width,
shrinkable: ctx.font_size * ctx.space_shrink,
stretchable: ctx.font_size * ctx.space_stretch,
}));
}
continue;
}
let script = char_script(c);
if let (Some(prev), Some(pc)) = (prev_script, prev_char) {
if is_latin_cjk_boundary(prev, script) && !interscript_glue_suppressed(pc, c) {
if let Some(s) = word_script.take() {
if !word.is_empty() {
flush_word(&mut word, s, out)?;
}
}
out.push(HorzBox::Pure(interscript_glue(ctx, prev, script)));
}
}
if let Some(cur) = word_script {
if cur != script {
flush_word(&mut word, cur, out)?;
}
}
word_script = Some(script);
prev_script = Some(script);
prev_char = Some(c);
word.push(c);
let is_gated_soft_hyphen =
c == '\u{ad}' && script == Script::Latin && ctx.hyphen_dictionary.is_some();
if !is_gated_soft_hyphen {
let after = i + c.len_utf8();
let is_cjk = |s| matches!(s, Script::HanIdeographic | Script::Kana);
let next_char = text[after..].chars().next();
let next_is_cjk = next_char.is_some_and(|nc| is_cjk(char_script(nc)));
let pair = if is_cjk(script) && next_is_cjk {
let nc = next_char.expect("checked");
let size_a = ctx.font_size * script_font(ctx, script).ratio;
let size_b = ctx.font_size * script_font(ctx, char_script(nc)).ratio;
Some(cjk_pair_space(
c,
size_a,
nc,
size_b,
ctx.font_size,
ctx.adjacent_stretch,
))
} else {
None
};
match boundary[after] {
Some(kind) => {
flush_word(&mut word, script, out)?;
word_script = None;
let mut no_break = Vec::new();
if let Some((n, sh, st)) = pair {
if n != Length::ZERO {
no_break.push(PureHorzBox::FixedEmpty { width: n });
}
no_break.push(PureHorzBox::OuterEmpty {
natural: Length::ZERO,
shrinkable: sh,
stretchable: st,
});
}
out.push(HorzBox::Pure(PureHorzBox::Discretionary {
penalty: match kind {
BreakKind::Allowed => 0,
BreakKind::Mandatory => FORCED_BREAK_PENALTY,
},
pre_break: Vec::new(),
post_break: Vec::new(),
no_break,
}));
}
None => {
if let Some((_, sh, st)) = pair {
if sh != Length::ZERO || st != Length::ZERO {
flush_word(&mut word, script, out)?;
word_script = None;
out.push(HorzBox::Pure(PureHorzBox::Discretionary {
penalty: NO_BREAK_PENALTY,
pre_break: Vec::new(),
post_break: Vec::new(),
no_break: vec![PureHorzBox::OuterEmpty {
natural: Length::ZERO,
shrinkable: sh,
stretchable: st,
}],
}));
}
}
}
}
}
}
match word_script {
Some(s) => flush_word(&mut word, s, out),
None => Ok(()),
}
}
const SCRIPT_SCALE: f64 = 0.7;
const SUP_SHIFT: f64 = 0.5;
const SUP_SHIFT_CRAMPED: f64 = SUP_SHIFT;
const SUB_SHIFT: f64 = 0.25;
const FRAC_NUMER_SHIFT_FALLBACK: f64 = 0.33;
const FRAC_DENOM_SHIFT_FALLBACK: f64 = 0.33;
struct MathC {
c: Option<MathConstants>,
cramped: bool,
}
impl MathC {
fn of(interp: &Interp, ctx: &Context) -> Self {
Self {
c: interp.metrics.math_constants(ctx.math_font),
cramped: ctx.math_cramped,
}
}
fn sup_shift(&self, s: Length) -> Length {
match self.c {
None => {
s * if self.cramped {
SUP_SHIFT_CRAMPED
} else {
SUP_SHIFT
}
}
Some(c) => {
s * if self.cramped {
c.superscript_shift_up_cramped
} else {
c.superscript_shift_up
}
}
}
}
fn sub_shift(&self, s: Length) -> Length {
self.c
.map(|c| s * c.subscript_shift_down)
.unwrap_or(s * SUB_SHIFT)
}
fn script_scale(&self) -> f64 {
self.c.map(|c| c.script_scale_down).unwrap_or(SCRIPT_SCALE)
}
fn axis(&self, s: Length) -> Length {
self.c.map(|c| s * c.axis_height).unwrap_or(s * 0.25)
}
fn sup_shift_clamped(&self, s: Length, h_base: Length, d_sup: Length) -> Length {
match self.c {
None => self.sup_shift(s),
Some(c) => {
let shift_up = if self.cramped {
c.superscript_shift_up_cramped
} else {
c.superscript_shift_up
};
let cand1 = s * shift_up;
let cand2 = h_base - s * c.superscript_baseline_drop_max;
let cand3 = s * c.superscript_bottom_min + d_sup;
cand1.max(cand2).max(cand3)
}
}
}
fn sub_shift_clamped(&self, s: Length, d_base: Length, h_sub: Length) -> Length {
match self.c {
None => self.sub_shift(s),
Some(c) => {
let cand1 = s * c.subscript_shift_down;
let cand2 = d_base + s * c.subscript_baseline_drop_min;
let cand3 = h_sub - s * c.subscript_top_max;
cand1.max(cand2).max(cand3)
}
}
}
fn correct_script_gap(
&self,
s: Length,
d_sup: Length,
h_sub: Length,
sup: Length,
sub: Length,
) -> (Length, Length) {
let Some(c) = self.c else {
return (sup, sub);
};
let gap_min = s * c.sub_superscript_gap_min;
let gap = (sup - d_sup) - (h_sub - sub);
if gap < gap_min {
let corr = (gap_min - gap) * 0.5;
(sup + corr, sub + corr)
} else {
(sup, sub)
}
}
fn upper_limit_shift(&self, s: Length, h_base: Length, d_up: Length) -> Length {
match self.c {
None => self.sup_shift(s),
Some(c) => {
let cand1 = h_base + s * c.upper_limit_baseline_rise_min;
let cand2 = h_base + s * c.upper_limit_gap_min + d_up;
cand1.max(cand2)
}
}
}
fn lower_limit_shift(&self, s: Length, d_base: Length, h_low: Length) -> Length {
match self.c {
None => self.sub_shift(s),
Some(c) => {
let cand1 = d_base + s * c.lower_limit_baseline_drop_min;
let cand2 = d_base + s * c.lower_limit_gap_min + h_low;
cand1.max(cand2)
}
}
}
fn frac_rule(&self, s: Length) -> Length {
self.c
.map(|c| s * c.fraction_rule_thickness)
.unwrap_or(s * 0.04)
}
fn frac_numer_shift(&self, s: Length, d_numer: Length) -> Length {
match self.c {
None => s * FRAC_NUMER_SHIFT_FALLBACK,
Some(c) => {
let std = s * c.fraction_numer_shift_up;
let gap =
self.axis(s) + self.frac_rule(s) * 0.5 + s * c.fraction_numer_gap_min + d_numer;
std.max(gap)
}
}
}
fn frac_denom_shift(&self, s: Length, h_denom: Length) -> Length {
match self.c {
None => -(s * FRAC_DENOM_SHIFT_FALLBACK),
Some(c) => {
let std = -(s * c.fraction_denom_shift_down);
let gap =
self.axis(s) - self.frac_rule(s) * 0.5 - s * c.fraction_denom_gap_min - h_denom;
std.min(gap)
}
}
}
fn radical_bar_metrics(&self, s: Length, h_cont: Length) -> (Length, Length, Length) {
match self.c {
Some(c) => (
h_cont + s * c.radical_vertical_gap,
s * c.radical_rule_thickness,
s * c.radical_extra_ascender,
),
None => (h_cont + s * 0.06, s * 0.04, s * 0.06),
}
}
}
fn glyphs_extent(glyphs: &[MathGlyph]) -> (Length, Length) {
let mut height = Length::ZERO;
let mut depth = Length::ZERO;
for g in glyphs {
height = height.max(g.dy + g.height);
depth = depth.max(g.depth - g.dy);
}
(height, depth)
}
fn inner_ink_extent(glyphs: &[MathGlyph], rules: &[GraphicsElem]) -> (Length, Length) {
let (mut height, mut depth) = glyphs_extent(glyphs);
for r in rules {
if let Some(((_, min_y), (_, max_y))) = graphics_bbox(r) {
height = height.max(max_y);
depth = depth.max(-min_y);
}
}
(height, depth)
}
#[allow(clippy::too_many_arguments)]
fn superscript_kern(
interp: &Interp,
ctx: &Context,
size: Length,
script_size: Length,
base_glyphs: &[MathGlyph],
script_glyphs: &[MathGlyph],
sup_shift: Length,
h_base: Length,
d_sup: Length,
) -> Length {
let font = ctx.math_font;
let last_base = base_glyphs.last().and_then(|g| g.text.chars().last());
let first_script = script_glyphs.first().and_then(|g| g.text.chars().next());
let l_italic = last_base
.and_then(|c| interp.metrics.italic_correction(font, c, size))
.unwrap_or(Length::ZERO);
let l_base = sup_shift - d_sup;
let l_sup = h_base - sup_shift;
let l_kernbase = last_base
.and_then(|c| {
interp
.metrics
.math_kern(font, c, size, MathCorner::TopRight, l_base)
})
.unwrap_or(Length::ZERO);
let l_kernsup = first_script
.and_then(|c| {
interp
.metrics
.math_kern(font, c, script_size, MathCorner::BottomLeft, l_sup)
})
.unwrap_or(Length::ZERO);
l_italic + l_kernbase + l_kernsup
}
fn ascii_math_kind(c: char) -> MathKind {
match c {
'+' | '-' | '*' | '/' => MathKind::Bin,
'=' | '<' | '>' => MathKind::Rel,
',' | ';' | ':' | '.' => MathKind::Punct,
_ => MathKind::Ord,
}
}
fn normalize_math_kind(prev: MathKind, next: MathKind, raw: MathKind) -> MathKind {
if raw != MathKind::Bin {
return raw;
}
let unary_left = matches!(
prev,
MathKind::Op
| MathKind::Bin
| MathKind::Rel
| MathKind::Open
| MathKind::Punct
| MathKind::End
);
let unary_right = matches!(next, MathKind::Rel | MathKind::Close | MathKind::Punct);
if unary_left || unary_right {
MathKind::Ord
} else {
MathKind::Bin
}
}
const SPACE_MATH_BIN: f64 = 0.25;
const SPACE_MATH_REL: f64 = 0.375;
const SPACE_MATH_OP: f64 = 0.125;
const SPACE_MATH_PUNCT: f64 = 0.125;
const SPACE_MATH_INNER: f64 = 0.125;
const SPACE_MATH_PREFIX: f64 = 0.125;
fn space_before(prev: MathKind, cur: MathKind, in_script: bool, font_size: Length) -> Length {
use MathKind::*;
let ratio = if in_script {
match (prev, cur) {
(Op, Ord) | (Ord, Op) | (Op, Op) | (Close, Op) | (Inner, Op) => SPACE_MATH_OP,
_ => return Length::ZERO,
}
} else {
match (prev, cur) {
(Punct, _) => SPACE_MATH_PUNCT,
(Inner, Ord) | (Inner, Open) | (Inner, Punct) | (Inner, Inner) | (Ord, Inner)
| (Prefix, Inner) | (Close, Inner) => SPACE_MATH_INNER,
(_, Close) => return Length::ZERO,
(Ord, Open) | (Prefix, Open) => return Length::ZERO,
(Bin, Ord) | (Bin, Prefix) | (Bin, Op) | (Bin, Open) | (Bin, Inner) | (Ord, Bin)
| (Close, Bin) | (Inner, Bin) => SPACE_MATH_BIN,
(Rel, Ord) | (Rel, Op) | (Rel, Inner) | (Rel, Open) | (Rel, Prefix) | (Ord, Rel)
| (Op, Rel) | (Inner, Rel) | (Close, Rel) => SPACE_MATH_REL,
(Op, Ord) | (Op, Op) | (Op, Inner) | (Op, Prefix) | (Ord, Op) | (Close, Op)
| (Inner, Op) => SPACE_MATH_OP,
(Ord, Prefix) | (Inner, Prefix) => SPACE_MATH_PREFIX,
(_, End) | (End, _) => return Length::ZERO,
_ => return Length::ZERO,
}
};
font_size * ratio
}
fn math_in_script(ctx: &Context, size: Length) -> bool {
size != ctx.font_size || ctx.math_script_level != MathScriptLevel::Base
}
fn math_glyph_font(interp: &Interp, ctx: &Context, c: char, size: Length) -> FontKey {
if interp.metrics.advance(ctx.math_font, c, size).is_some() {
ctx.math_font
} else {
ctx.font
}
}
fn math_char_available(interp: &Interp, ctx: &Context, c: char, size: Length) -> bool {
interp.metrics.advance(ctx.math_font, c, size).is_some()
|| interp.metrics.advance(ctx.font, c, size).is_some()
}
fn push_char_glyph(
interp: &mut Interp,
ctx: &Context,
c: char,
size: Length,
out: &mut Vec<MathGlyph>,
x: &mut Length,
) -> Result<(), EvalError> {
let font = math_glyph_font(interp, ctx, c, size);
if math_in_script(ctx, size) {
if let Some(v) = interp.metrics.math_script_variant(font, c, size) {
out.push(MathGlyph {
info: HorzStringInfo {
font,
size,
rising: Length::ZERO,
color: ctx.text_color,
},
text: c.to_string(),
gid: Some(v.gid),
dx: *x,
dy: Length::ZERO,
width: v.advance,
height: v.height,
depth: v.depth,
});
*x += v.advance;
return Ok(());
}
}
let advance = interp.metrics.advance(font, c, size).unwrap_or(size * 0.5);
let (height, depth) = math_glyph_vextent(interp, font, c, size);
out.push(MathGlyph {
info: HorzStringInfo {
font,
size,
rising: Length::ZERO,
color: ctx.text_color,
},
text: c.to_string(),
gid: None,
dx: *x,
dy: Length::ZERO,
width: advance,
height,
depth,
});
*x += advance;
Ok(())
}
fn math_glyph_vextent(interp: &Interp, font: FontKey, c: char, size: Length) -> (Length, Length) {
match interp.metrics.glyph_vextent(font, c, size) {
Some((h, d)) => (h.max(Length::ZERO), d.max(Length::ZERO)),
None => (
interp.metrics.ascender(font, size),
interp.metrics.descender(font, size),
),
}
}
fn push_big_char_glyph(
interp: &mut Interp,
ctx: &Context,
c: char,
size: Length,
out: &mut Vec<MathGlyph>,
x: &mut Length,
) -> Result<(), EvalError> {
let font = math_glyph_font(interp, ctx, c, size);
match interp
.metrics
.math_vertical_variant(font, c, size, VertVariantPolicy::BigOp)
{
Some(v) => {
out.push(MathGlyph {
info: HorzStringInfo {
font,
size,
rising: Length::ZERO,
color: ctx.text_color,
},
text: c.to_string(),
gid: Some(v.gid),
dx: *x,
dy: Length::ZERO,
width: v.advance,
height: v.height,
depth: v.depth,
});
*x += v.advance;
Ok(())
}
None => push_char_glyph(interp, ctx, c, size, out, x),
}
}
fn push_delimiter_glyph(
interp: &mut Interp,
ctx: &Context,
c: char,
size: Length,
target: Length,
axis: Length,
out: &mut Vec<MathGlyph>,
x: &mut Length,
) -> Result<(), EvalError> {
let font = math_glyph_font(interp, ctx, c, size);
let variant =
interp
.metrics
.math_vertical_variant(font, c, size, VertVariantPolicy::AtLeast(target));
let discrete_covers = variant
.map(|v| (v.height + v.depth).0 >= target.0)
.unwrap_or(false);
if !discrete_covers {
if let Some(parts) = interp.metrics.math_vertical_assembly(font, c, size, target) {
if !parts.is_empty() {
let hadv = match variant {
Some(v) => v.advance,
None => interp
.metrics
.advance(font, c, size)
.unwrap_or(Length::ZERO),
};
let total = parts
.last()
.map(|(_, dy, adv)| *dy + *adv)
.unwrap_or(Length::ZERO);
let base_off = axis - total * 0.5;
for (i, (gid, dy_local, adv)) in parts.iter().enumerate() {
out.push(MathGlyph {
info: HorzStringInfo {
font,
size,
rising: Length::ZERO,
color: ctx.text_color,
},
text: c.to_string(),
gid: Some(*gid),
dx: *x,
dy: base_off + *dy_local,
width: if i == 0 { hadv } else { Length::ZERO },
height: *adv,
depth: Length::ZERO,
});
}
*x += hadv;
return Ok(());
}
}
}
match variant {
Some(v) => {
let dy = axis - (v.height - v.depth) * 0.5;
out.push(MathGlyph {
info: HorzStringInfo {
font,
size,
rising: Length::ZERO,
color: ctx.text_color,
},
text: c.to_string(),
gid: Some(v.gid),
dx: *x,
dy,
width: v.advance,
height: v.height,
depth: v.depth,
});
*x += v.advance;
Ok(())
}
None => push_char_glyph(interp, ctx, c, size, out, x),
}
}
fn layout_script(
interp: &mut Interp,
ctx: &Context,
elems: &[MathElem],
size: Length,
) -> Result<(Vec<MathGlyph>, Length), EvalError> {
let mut glyphs = Vec::new();
let mut x = Length::ZERO;
let mut last_kind: Option<MathKind> = None;
for e in elems {
layout_math_elem(interp, ctx, e, size, &mut glyphs, &mut x, &mut last_kind)?;
}
Ok((glyphs, x))
}
fn place_script(
out: &mut Vec<MathGlyph>,
x: &mut Length,
script_glyphs: Vec<MathGlyph>,
script_width: Length,
dy_shift: Length,
) {
let base_x = *x;
for mut g in script_glyphs {
g.dx = base_x + g.dx;
g.dy = g.dy + dy_shift;
out.push(g);
}
*x = base_x + script_width;
}
fn layout_math_elem(
interp: &mut Interp,
ctx: &Context,
elem: &MathElem,
size: Length,
out: &mut Vec<MathGlyph>,
x: &mut Length,
last_kind: &mut Option<MathKind>,
) -> Result<(), EvalError> {
match elem {
MathElem::Chars(s) => {
for c in s.chars() {
let kind = ascii_math_kind(c);
if let Some(prev) = *last_kind {
*x += space_before(prev, kind, math_in_script(ctx, size), size);
}
push_char_glyph(interp, ctx, c, size, out, x)?;
*last_kind = Some(kind);
}
Ok(())
}
MathElem::Group(elems) => {
for e in elems {
layout_math_elem(interp, ctx, e, size, out, x, last_kind)?;
}
Ok(())
}
MathElem::Sup(base, script) => {
let base_start = out.len();
layout_math_elem(interp, ctx, base, size, out, x, last_kind)?;
let mc = MathC::of(interp, ctx);
let script_size = ctx.font_size * mc.script_scale();
let (h_base, _) = glyphs_extent(&out[base_start..]);
let (script_glyphs, script_width) = layout_script(interp, ctx, script, script_size)?;
let (_, d_sup) = glyphs_extent(&script_glyphs);
let sup_shift = mc.sup_shift_clamped(ctx.font_size, h_base, d_sup);
let kern = superscript_kern(
interp,
ctx,
size,
script_size,
&out[base_start..],
&script_glyphs,
sup_shift,
h_base,
d_sup,
);
*x += kern;
place_script(out, x, script_glyphs, script_width, sup_shift);
Ok(())
}
MathElem::Sub(base, script) => {
let base_start = out.len();
layout_math_elem(interp, ctx, base, size, out, x, last_kind)?;
let mc = MathC::of(interp, ctx);
let script_size = ctx.font_size * mc.script_scale();
let (_, d_base) = glyphs_extent(&out[base_start..]);
let (script_glyphs, script_width) = layout_script(interp, ctx, script, script_size)?;
let (h_sub, _) = glyphs_extent(&script_glyphs);
let sub_shift = mc.sub_shift_clamped(ctx.font_size, d_base, h_sub);
place_script(out, x, script_glyphs, script_width, -sub_shift);
Ok(())
}
MathElem::Primes(base, n) => {
let base_start = out.len();
layout_math_elem(interp, ctx, base, size, out, x, last_kind)?;
let mc = MathC::of(interp, ctx);
let script_size = ctx.font_size * mc.script_scale();
let (h_base, _) = glyphs_extent(&out[base_start..]);
let primes = vec![MathElem::Chars("\u{2032}".repeat(*n))];
let (script_glyphs, script_width) = layout_script(interp, ctx, &primes, script_size)?;
let (_, d_sup) = glyphs_extent(&script_glyphs);
let sup_shift = mc.sup_shift_clamped(ctx.font_size, h_base, d_sup);
let kern = superscript_kern(
interp,
ctx,
size,
script_size,
&out[base_start..],
&script_glyphs,
sup_shift,
h_base,
d_sup,
);
*x += kern;
place_script(out, x, script_glyphs, script_width, sup_shift);
Ok(())
}
MathElem::Cmd { name, span, .. } => Err(EvalError {
span: Some(*span),
msg: format!("math command `{name}` needs the math package (phase 7 roadmap A)"),
}),
MathElem::Embed { span, .. } => Err(EvalError {
span: Some(*span),
msg: "embedding a program value in math needs the math package \
(phase 7 roadmap A)"
.into(),
}),
}
}
pub fn read_math(
interp: &mut Interp,
ctx: &Context,
elems: &[MathElem],
) -> Result<PureHorzBox, EvalError> {
let mut glyphs: Vec<MathGlyph> = Vec::new();
let mut x = Length::ZERO;
let mut last_kind: Option<MathKind> = None;
for e in elems {
layout_math_elem(
interp,
ctx,
e,
ctx.font_size,
&mut glyphs,
&mut x,
&mut last_kind,
)?;
}
let width = x;
let mut height = Length::ZERO;
let mut depth = Length::ZERO;
for g in &glyphs {
height = height.max(g.dy + g.height);
depth = depth.max(g.depth - g.dy);
}
Ok(PureHorzBox::Math {
width,
height,
depth,
glyphs,
rules: Vec::new(),
})
}
fn prim_read_inline(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let it = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let (elems, env) = as_inline_text(it)?;
Ok(Value::InlineBoxes(read_inline(interp, &ctx, &elems, &env)?))
}
fn prim_read_block(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let bt = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let (elems, env) = as_block_text(bt)?;
Ok(Value::BlockBoxes(read_block(interp, &ctx, &elems, &env)?))
}
fn prim_line_break(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ib = as_inline_boxes(args.pop().unwrap())?;
let ctx = as_context(args.pop().unwrap())?;
let _is_breakable_bottom = as_bool(args.pop().unwrap())?;
let _is_breakable_top = as_bool(args.pop().unwrap())?;
let lines = break_into_lines(&ctx, ib);
let mut out = Vec::with_capacity(lines.len() + 2);
if !lines.is_empty() {
let first_height = lines
.iter()
.find_map(|vb| match vb {
VertBox::Line { height, .. } => Some(*height),
_ => None,
})
.unwrap_or(Length::ZERO);
let pad = (MIN_FIRST_ASCENDER - first_height).max(Length::ZERO);
out.push(VertBox::ParagTop(ctx.paragraph_top + pad));
out.extend(lines);
out.push(VertBox::Skip(ctx.paragraph_bottom));
}
for vb in &mut out {
if let VertBox::Line { contents, .. } = vb {
resolve_outer_graphics_in_contents(interp, contents)?;
}
}
Ok(Value::BlockBoxes(out))
}
fn record_field(
fields: &BTreeMap<String, Value>,
record_name: &str,
field: &str,
) -> Result<Value, EvalError> {
match fields.get(field) {
Some(v) => Ok(v.clone()),
None => eval_error(format!(
"{record_name} record is missing field '{field}' (available fields: {})",
available_fields(fields)
)),
}
}
fn read_content_scheme(v: Value) -> Result<(Point, Length), EvalError> {
let fields = match v {
Value::Record(m) => m,
other => {
return eval_error(format!(
"a page-content-scheme closure must return a record, got {}",
other.type_name()
))
}
};
let origin = as_point(record_field(&fields, "page-content-scheme", "text-origin")?)?;
let height = as_length(record_field(&fields, "page-content-scheme", "text-height")?)?;
Ok((origin, height))
}
fn read_parts_scheme(v: Value) -> Result<(Point, Vec<VertBox>, Point, Vec<VertBox>), EvalError> {
let fields = match v {
Value::Record(m) => m,
other => {
return eval_error(format!(
"a page-parts closure must return a record, got {}",
other.type_name()
))
}
};
let header_origin = as_point(record_field(&fields, "page-parts", "header-origin")?)?;
let header_content = as_block_boxes(record_field(&fields, "page-parts", "header-content")?)?;
let footer_origin = as_point(record_field(&fields, "page-parts", "footer-origin")?)?;
let footer_content = as_block_boxes(record_field(&fields, "page-parts", "footer-content")?)?;
Ok((header_origin, header_content, footer_origin, footer_content))
}
const PAGE_NUMBER_LIMIT: i64 = 10_000;
fn prim_page_break_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let bb = as_block_boxes(args.pop().unwrap())?;
let pagepartsf = args.pop().unwrap();
let pagecontf = args.pop().unwrap();
let paper = as_page(args.pop().unwrap())?;
page_break_core(
interp,
paper,
vec![Length::ZERO],
None,
None,
pagecontf,
pagepartsf,
bb,
)
}
fn prim_page_break_v01(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let bb = as_block_boxes(args.pop().unwrap())?;
let pagepartsf = args.pop().unwrap();
let pagecontf = args.pop().unwrap();
let paper = as_page_v01(args.pop().unwrap())?;
page_break_core(
interp,
paper,
vec![Length::ZERO],
None,
None,
pagecontf,
pagepartsf,
bb,
)
}
fn prim_page_break_two_column_v006(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let bb = as_block_boxes(args.pop().unwrap())?;
let pagepartsf = args.pop().unwrap();
let pagecontf = args.pop().unwrap();
let columnhookf = args.pop().unwrap();
let origin_shift = as_length(args.pop().unwrap())?;
let paper = as_page(args.pop().unwrap())?;
page_break_core(
interp,
paper,
vec![Length::ZERO, origin_shift],
Some(columnhookf),
None,
pagecontf,
pagepartsf,
bb,
)
}
fn prim_page_break_two_column_v01(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let bb = as_block_boxes(args.pop().unwrap())?;
let pagepartsf = args.pop().unwrap();
let pagecontf = args.pop().unwrap();
let columnhookf = args.pop().unwrap();
let origin_shift = as_length(args.pop().unwrap())?;
let paper = as_page_v01(args.pop().unwrap())?;
page_break_core(
interp,
paper,
vec![Length::ZERO, origin_shift],
Some(columnhookf),
None,
pagecontf,
pagepartsf,
bb,
)
}
fn prim_page_break_multicolumn_v006(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let bb = as_block_boxes(args.pop().unwrap())?;
let pagepartsf = args.pop().unwrap();
let pagecontf = args.pop().unwrap();
let columnendhookf = args.pop().unwrap();
let columnhookf = args.pop().unwrap();
let mut origin_shifts = vec![Length::ZERO];
for v in as_list(args.pop().unwrap())? {
origin_shifts.push(as_length(v)?);
}
let paper = as_page(args.pop().unwrap())?;
page_break_core(
interp,
paper,
origin_shifts,
Some(columnhookf),
Some(columnendhookf),
pagecontf,
pagepartsf,
bb,
)
}
fn prim_page_break_multicolumn_v01(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let bb = as_block_boxes(args.pop().unwrap())?;
let pagepartsf = args.pop().unwrap();
let pagecontf = args.pop().unwrap();
let columnendhookf = args.pop().unwrap();
let columnhookf = args.pop().unwrap();
let mut origin_shifts = vec![Length::ZERO];
for v in as_list(args.pop().unwrap())? {
origin_shifts.push(as_length(v)?);
}
let paper = as_page_v01(args.pop().unwrap())?;
page_break_core(
interp,
paper,
origin_shifts,
Some(columnhookf),
Some(columnendhookf),
pagecontf,
pagepartsf,
bb,
)
}
fn apply_column_hook(
interp: &mut Interp,
hook: &Value,
remaining: &mut Vec<VertBox>,
) -> Result<(), EvalError> {
let inserted = as_block_boxes(interp.apply(hook.clone(), Value::Unit)?)?;
remaining.splice(0..0, inserted);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn page_break_core(
interp: &mut Interp,
paper: PaperSize,
origin_shifts: Vec<Length>,
columnhookf: Option<Value>,
columnendhookf: Option<Value>,
pagecontf: Value,
pagepartsf: Value,
bb: Vec<VertBox>,
) -> Result<Value, EvalError> {
let (paper_w, paper_h) = paper.dims();
let reflow_source = bb.clone();
let mut remaining = bb;
let mut pages: Vec<Page> = Vec::new();
let mut pageno: i64 = 1;
loop {
if pageno > PAGE_NUMBER_LIMIT {
return eval_error(format!(
"page number limit exceeded ({PAGE_NUMBER_LIMIT}); a column hook keeps injecting content"
));
}
let mut pb_fields = BTreeMap::new();
pb_fields.insert("page-number".to_string(), Value::Int(pageno));
let pbinfo = Value::Record(pb_fields);
let sch = interp.apply(pagecontf.clone(), pbinfo.clone())?;
let (origin, height) = read_content_scheme(sch)?;
let (x0, y0) = origin;
let mut lines = Vec::new();
for shift in &origin_shifts {
if let Some(hook) = &columnhookf {
apply_column_hook(interp, hook, &mut remaining)?;
}
lines.extend(chop_page((x0 + *shift, y0), height, &mut remaining));
if remaining.is_empty() {
break; }
}
if let Some(hook) = &columnendhookf {
apply_column_hook(interp, hook, &mut remaining)?;
}
if remaining.is_empty() && !lines.iter().any(|l| placed_line_extent(l).is_some()) {
break;
}
let body_lines = lines.len();
let parts = interp.apply(pagepartsf.clone(), pbinfo)?;
let (header_origin, header_content, footer_origin, footer_content) =
read_parts_scheme(parts)?;
lines.extend(place_block_at(header_origin, header_content));
lines.extend(place_block_at(footer_origin, footer_content));
pages.push(Page { lines, body_lines });
if remaining.is_empty() {
break;
}
pageno += 1;
}
let images = interp.images.clone();
Ok(Value::Document(Rc::new(DocumentValue {
geometry: PageGeometry::for_paper(paper_w, paper_h),
pages,
images,
extras: DocExtras::default(),
reflow_source: Some(reflow_source),
reflow_links: Vec::new(),
reflow_dests: Vec::new(),
})))
}
binop_prim!(prim_int_add, as_int, Int, |a, b| a.wrapping_add(b));
binop_prim!(prim_int_sub, as_int, Int, |a, b| a.wrapping_sub(b));
binop_prim!(prim_int_mul, as_int, Int, |a, b| a.wrapping_mul(b));
binop_prim_try!(prim_int_div, as_int, |a, b| if b == 0 {
eval_error("division by zero")
} else {
Ok(Value::Int(a / b))
});
binop_prim_try!(prim_int_mod, as_int, |a, b| if b == 0 {
eval_error("division by zero")
} else {
Ok(Value::Int(a % b))
});
cmp_prim!(prim_int_eq, as_int, |a, b| a == b);
cmp_prim!(prim_int_ne, as_int, |a, b| a != b);
cmp_prim!(prim_int_lt, as_int, |a, b| a < b);
cmp_prim!(prim_int_gt, as_int, |a, b| a > b);
cmp_prim!(prim_int_le, as_int, |a, b| a <= b);
cmp_prim!(prim_int_ge, as_int, |a, b| a >= b);
binop_prim!(prim_band, as_int, Int, |a, b| a & b);
binop_prim!(prim_bor, as_int, Int, |a, b| a | b);
binop_prim!(prim_bxor, as_int, Int, |a, b| a ^ b);
unop_prim!(prim_bnot, as_int, Int, |a| !a);
binop_prim_try!(
prim_bit_shift_left,
as_int,
|a, b| if !(0..=63).contains(&b) {
eval_error("Bit offset out of bounds for '<<'")
} else {
Ok(Value::Int(((a as u64) << b) as i64))
}
);
binop_prim_try!(
prim_bit_shift_right,
as_int,
|a, b| if !(0..=63).contains(&b) {
eval_error("Bit offset out of bounds for '>>'")
} else {
Ok(Value::Int(((a as u64) >> b) as i64))
}
);
binop_prim!(prim_bool_and, as_bool, Bool, |a, b| a && b);
binop_prim!(prim_bool_or, as_bool, Bool, |a, b| a || b);
unop_prim!(prim_bool_not, as_bool, Bool, |a| !a);
binop_prim!(prim_float_add, as_float, Float, |a, b| a + b);
binop_prim!(prim_float_sub, as_float, Float, |a, b| a - b);
binop_prim!(prim_float_mul, as_float, Float, |a, b| a * b);
binop_prim!(prim_float_div, as_float, Float, |a, b| a / b);
unop_prim!(prim_float_of_int, as_int, Float, |n| n as f64);
unop_prim!(prim_round, as_float, Int, |x| x as i64);
cmp_prim!(prim_float_gt, as_float, |a, b| a > b);
cmp_prim!(prim_float_lt, as_float, |a, b| a < b);
cmp_prim!(prim_float_ge, as_float, |a, b| a >= b);
cmp_prim!(prim_float_le, as_float, |a, b| a <= b);
binop_prim!(prim_length_add, as_length, Length, |a, b| a + b);
binop_prim!(prim_length_sub, as_length, Length, |a, b| a - b);
binop_prim!(prim_length_scale, (as_length, as_float), Length, |a, b| a
* b);
binop_prim!(prim_length_div, as_length, Float, |a, b| a / b);
cmp_prim!(prim_length_lt, as_length, |a, b| a < b);
cmp_prim!(prim_length_gt, as_length, |a, b| b < a);
binop_prim!(prim_string_concat, as_str, Str, |a, b| a + &b);
unop_prim!(prim_arabic, as_int, Str, |n| n.to_string());
cmp_prim!(prim_string_same, as_str, |a, b| a == b);
fn prim_list_cons(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let tail = args.pop().unwrap();
let head = args.pop().unwrap();
let mut list = match tail {
Value::List(v) => v,
other => return eval_error(format!("expected list, got {}", other.type_name())),
};
list.insert(0, head);
Ok(Value::List(list))
}
fn prim_deref(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let v = args.pop().unwrap();
match v {
Value::Ref(cell) => Ok(cell.borrow().clone()),
other => eval_error(format!(
"expected a mutable cell for '!', got {}",
other.type_name()
)),
}
}
unop_prim!(prim_string_length, as_str, Int, |s| s.chars().count()
as i64);
fn prim_string_sub(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let wid = as_int(args.pop().unwrap())?;
let pos = as_int(args.pop().unwrap())?;
let s = as_str(args.pop().unwrap())?;
if wid < 0 || pos < 0 {
return eval_error("illegal index for string-sub");
}
let chars: Vec<char> = s.chars().collect();
let pos = pos as usize;
let wid = wid as usize;
match pos.checked_add(wid) {
Some(end) if end <= chars.len() => Ok(Value::Str(chars[pos..end].iter().collect())),
_ => eval_error("illegal index for string-sub"),
}
}
unop_prim!(prim_string_explode, as_str, List, |s| s
.chars()
.map(|c| Value::Int(c as i64))
.collect());
fn prim_embed_string(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
Ok(Value::InlineText {
elems: Rc::new(vec![IText::Text(s)]),
env: Env::root(),
})
}
fn prim_set_font_size(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let size = as_length(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
font_size: size,
..ctx
})))
}
fn prim_get_font_size(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
Ok(Value::Length(ctx.font_size))
}
fn prim_set_leading(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let leading = as_length(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context { leading, ..ctx })))
}
fn prim_set_paragraph_margin(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let bottom = as_length(args.pop().unwrap())?;
let top = as_length(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
paragraph_top: top,
paragraph_bottom: bottom,
..ctx
})))
}
fn prim_get_text_width(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
Ok(Value::Length(ctx.paragraph_width))
}
fn prim_get_initial_context(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let cmd = args.pop().unwrap();
let width = as_length(args.pop().unwrap())?;
let mut ctx = Context::initial(width);
ctx.math_command = Some(interp.register_math_command(cmd));
for (idx, script) in [
Script::HanIdeographic,
Script::Kana,
Script::Latin,
Script::OtherScript,
]
.into_iter()
.enumerate()
{
if let Some((font, ratio, rising)) = interp.metrics.default_script_font(script) {
ctx.font_scheme[idx] = ScriptFont {
font,
ratio,
rising,
};
if script == Script::Latin {
ctx.font = font;
}
}
}
if let Some(font) = interp.metrics.default_math_font() {
ctx.math_font = font;
}
Ok(Value::Context(Box::new(ctx)))
}
fn prim_set_font_key(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let key = as_int(args.pop().unwrap())?;
if key < 0 || key > i64::from(u16::MAX) {
return eval_error(format!("set-font-key: font key {key} is out of range"));
}
Ok(Value::Context(Box::new(Context {
font: FontKey(key as u16),
..ctx
})))
}
fn prim_inline_concat(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mut b = as_inline_boxes(args.pop().unwrap())?;
let mut a = as_inline_boxes(args.pop().unwrap())?;
a.append(&mut b);
Ok(Value::InlineBoxes(a))
}
fn prim_block_concat(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mut b = as_block_boxes(args.pop().unwrap())?;
let mut a = as_block_boxes(args.pop().unwrap())?;
a.append(&mut b);
Ok(Value::BlockBoxes(a))
}
fn prim_inline_skip(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let width = as_length(args.pop().unwrap())?;
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::FixedEmpty { width },
)]))
}
fn prim_inline_glue(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let stretchable = as_length(args.pop().unwrap())?;
let shrinkable = as_length(args.pop().unwrap())?;
let natural = as_length(args.pop().unwrap())?;
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::OuterEmpty {
natural,
shrinkable,
stretchable,
},
)]))
}
fn prim_block_skip(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let len = as_length(args.pop().unwrap())?;
Ok(Value::BlockBoxes(vec![VertBox::Skip(len)]))
}
fn prim_list_mark(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let tag = as_int(args.pop().unwrap())?;
let kind = match tag {
0 => ListMarkKind::ListStart { ordered: false },
1 => ListMarkKind::ListStart { ordered: true },
2 => ListMarkKind::ListEnd,
3 => ListMarkKind::ItemStart,
4 => ListMarkKind::ItemEnd,
other => return eval_error(format!("list-mark: unknown tag {other}")),
};
Ok(Value::BlockBoxes(vec![VertBox::ListMark(kind)]))
}
fn prim_inline_mark(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let tag = as_int(args.pop().unwrap())?;
let kind = match tag {
0 => InlineMarkKind::EmphStart { strong: false },
1 => InlineMarkKind::EmphStart { strong: true },
2 => InlineMarkKind::EmphEnd,
3 => InlineMarkKind::BulletStart,
4 => InlineMarkKind::BulletEnd,
other => return eval_error(format!("inline-mark: unknown tag {other}")),
};
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::InlineMark(kind),
)]))
}
binop_prim!(prim_atan2, as_float, Float, |a, b| a.atan2(b));
unop_prim!(prim_sin, as_float, Float, |x| x.sin());
unop_prim!(prim_asin, as_float, Float, |x| x.asin());
unop_prim!(prim_cos, as_float, Float, |x| x.cos());
unop_prim!(prim_acos, as_float, Float, |x| x.acos());
unop_prim!(prim_tan, as_float, Float, |x| x.tan());
unop_prim!(prim_atan, as_float, Float, |x| x.atan());
unop_prim!(prim_log, as_float, Float, |x| x.ln());
unop_prim!(prim_exp, as_float, Float, |x| x.exp());
unop_prim!(prim_ceil, as_float, Float, |x| x.ceil());
unop_prim!(prim_floor, as_float, Float, |x| x.floor());
unop_prim!(prim_show_float, as_float, Str, |x| ocaml_show_float(x));
fn ocaml_show_float(x: f64) -> String {
if x.is_nan() {
return "nan".to_string();
}
if x.is_infinite() {
return if x < 0.0 { "-infinity" } else { "infinity" }.to_string();
}
const PREC: i32 = 12;
let sci = format!("{:.*e}", (PREC - 1) as usize, x);
let epos = sci
.find('e')
.expect("scientific formatting always emits 'e'");
let exp: i32 = sci[epos + 1..].parse().expect("well-formed exponent");
let body = if exp < -4 || exp >= PREC {
let mantissa = trim_trailing_fractional_zeros(&sci[..epos]);
format!(
"{mantissa}e{}{:02}",
if exp < 0 { "-" } else { "+" },
exp.abs()
)
} else {
let decimals = (PREC - 1 - exp).max(0) as usize;
trim_trailing_fractional_zeros(&format!("{:.*}", decimals, x)).to_string()
};
if body.bytes().all(|b| b.is_ascii_digit() || b == b'-') {
format!("{body}.")
} else {
body
}
}
fn trim_trailing_fractional_zeros(s: &str) -> &str {
if !s.contains('.') {
return s;
}
s.trim_end_matches('0').trim_end_matches('.')
}
unop_prim!(prim_string_byte_length, as_str, Int, |s| s.len() as i64);
fn prim_string_sub_bytes(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let wid = as_int(args.pop().unwrap())?;
let pos = as_int(args.pop().unwrap())?;
let s = as_str(args.pop().unwrap())?;
if wid < 0 || pos < 0 {
return eval_error("illegal index for string-sub-bytes");
}
let (pos, wid) = (pos as usize, wid as usize);
match pos.checked_add(wid) {
Some(end) if end <= s.len() && s.is_char_boundary(pos) && s.is_char_boundary(end) => {
Ok(Value::Str(s[pos..end].to_string()))
}
_ => eval_error("illegal index for string-sub-bytes"),
}
}
fn prim_string_unexplode(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let items = as_list(args.pop().unwrap())?;
let mut s = String::new();
for v in items {
let n = as_int(v)?;
match u32::try_from(n).ok().and_then(char::from_u32) {
Some(c) => s.push(c),
None => {
return eval_error(format!(
"string-unexplode: {n} is not a valid Unicode scalar value"
))
}
}
}
Ok(Value::Str(s))
}
fn prim_normalize_string_to_nfc(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
Ok(Value::Str(s.nfc().collect()))
}
fn prim_normalize_string_to_nfd(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
Ok(Value::Str(s.nfd().collect()))
}
fn prim_split_grapheme_cluster(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
let clusters: Vec<Value> = s
.graphemes(true)
.map(|g| Value::Str(g.to_string()))
.collect();
Ok(Value::List(clusters))
}
fn prim_display_message(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let msg = as_str(args.pop().unwrap())?;
eprintln!("{msg}");
Ok(Value::Unit)
}
fn prim_abort_with_message(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let msg = as_str(args.pop().unwrap())?;
eval_error(msg)
}
fn prim_load_image(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let path = as_str(args.pop().unwrap())?;
let decoded = image::open(&path).map_err(|e| EvalError {
span: None,
msg: format!("load-image: cannot decode '{path}': {e}"),
})?;
let rgb = decoded.to_rgb8();
let (px_w, px_h) = rgb.dimensions();
let jpeg_dct = std::fs::read(&path)
.ok()
.and_then(ImageResource::sniff_baseline_jpeg_dct);
let id = ImageId(interp.images.len());
interp.images.push(ImageResource {
samples: rgb.into_raw(),
px_w,
px_h,
jpeg_dct,
pdf: None,
});
Ok(Value::Image(id))
}
fn prim_use_image_by_width(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let width = as_length(args.pop().unwrap())?;
let image = as_image(args.pop().unwrap())?;
let resource = interp.images.get(image.0).ok_or_else(|| EvalError {
span: None,
msg: format!("internal error: image id {} out of range", image.0),
})?;
let (iw, ih) = resource.intrinsic_dims_pt();
if iw == 0.0 {
return eval_error("use-image-by-width: image has zero width, cannot scale");
}
let height = width * (ih / iw);
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::Image {
width,
height,
image,
},
)]))
}
#[cfg(feature = "pdf-image")]
fn prim_load_pdf_image(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let pageno = as_int(args.pop().unwrap())?;
let path = as_str(args.pop().unwrap())?;
if pageno < 1 {
return eval_error(format!(
"load-pdf-image: page number must be >= 1 (got {pageno})"
));
}
let doc = lopdf::Document::load(&path).map_err(|e| {
let msg = match &e {
lopdf::Error::IO(io_e) => format!("load-pdf-image: cannot open '{path}': {io_e}"),
other => format!("load-pdf-image: cannot parse PDF '{path}': {other}"),
};
EvalError { span: None, msg }
})?;
if doc.is_encrypted() {
return eval_error(format!(
"load-pdf-image: '{path}' is encrypted; not supported"
));
}
let pages = doc.get_pages();
let page_id = *pages.get(&(pageno as u32)).ok_or_else(|| EvalError {
span: None,
msg: format!("load-pdf-image: '{path}' has no page {pageno}"),
})?;
let page_dict = doc.get_dictionary(page_id).map_err(|e| EvalError {
span: None,
msg: format!("load-pdf-image: cannot parse PDF '{path}': {e}"),
})?;
let media_box = resolve_pdf_media_box(&doc, page_dict).ok_or_else(|| EvalError {
span: None,
msg: format!("load-pdf-image: page {pageno} of '{path}' has no usable MediaBox"),
})?;
let content = doc.get_page_content(page_id).map_err(|e| EvalError {
span: None,
msg: format!("load-pdf-image: cannot parse PDF '{path}': {e}"),
})?;
let resources = import_pdf_resources(&doc, page_dict);
let id = ImageId(interp.images.len());
interp.images.push(ImageResource {
samples: Vec::new(),
px_w: 0,
px_h: 0,
jpeg_dct: None,
pdf: Some(PdfPageResource {
media_box,
content,
resources,
}),
});
Ok(Value::Image(id))
}
#[cfg(not(feature = "pdf-image"))]
fn prim_load_pdf_image(_interp: &mut Interp, _args: Vec<Value>) -> Result<Value, EvalError> {
eval_error(
"load-pdf-image: this build has no PDF reader (the `pdf-image` feature \
is off). It is off for WebAssembly, where the primitive could not work \
regardless: it takes a filesystem path."
.to_string(),
)
}
#[cfg(feature = "pdf-image")]
fn resolve_pdf_media_box(
doc: &lopdf::Document,
page_dict: &lopdf::Dictionary,
) -> Option<(f64, f64, f64, f64)> {
let mut cur = page_dict;
let mut seen: BTreeSet<(u32, u16)> = BTreeSet::new();
loop {
if let Ok(obj) = cur.get(b"MediaBox") {
if let Ok(arr) = obj.as_array() {
if arr.len() == 4 {
let mut v = [0f64; 4];
let mut ok = true;
for (slot, item) in v.iter_mut().zip(arr.iter()) {
match item.as_float() {
Ok(f) => *slot = f as f64,
Err(_) => {
ok = false;
break;
}
}
}
if ok {
return Some((v[0], v[1], v[2], v[3]));
}
}
}
}
match cur.get(b"Parent").and_then(|o| o.as_reference()) {
Ok(parent_id) => {
if !seen.insert(parent_id) {
return None; }
cur = doc.get_dictionary(parent_id).ok()?;
}
Err(_) => return None,
}
}
}
#[cfg(feature = "pdf-image")]
fn import_pdf_resources(doc: &lopdf::Document, page_dict: &lopdf::Dictionary) -> ImportedObjects {
let mut out: Vec<(u32, ObjRepr)> = Vec::new();
let mut seen: BTreeSet<u32> = BTreeSet::new();
let root_repr = match resolve_pdf_resources_object(doc, page_dict) {
Some(obj) => convert_pdf_object(doc, obj, &mut out, &mut seen),
None => ObjRepr::Dict(Vec::new()),
};
out.insert(0, (0, root_repr));
ImportedObjects(out)
}
#[cfg(feature = "pdf-image")]
fn resolve_pdf_resources_object<'a>(
doc: &'a lopdf::Document,
page_dict: &'a lopdf::Dictionary,
) -> Option<&'a lopdf::Object> {
let mut cur = page_dict;
let mut seen: BTreeSet<(u32, u16)> = BTreeSet::new();
loop {
if let Ok(obj) = cur.get(b"Resources") {
return Some(obj);
}
match cur.get(b"Parent").and_then(|o| o.as_reference()) {
Ok(parent_id) => {
if !seen.insert(parent_id) {
return None;
}
cur = doc.get_dictionary(parent_id).ok()?;
}
Err(_) => return None,
}
}
}
#[cfg(feature = "pdf-image")]
fn convert_pdf_object(
doc: &lopdf::Document,
obj: &lopdf::Object,
out: &mut Vec<(u32, ObjRepr)>,
seen: &mut BTreeSet<u32>,
) -> ObjRepr {
use lopdf::Object as LObj;
match obj {
LObj::Null => ObjRepr::Null,
LObj::Boolean(b) => ObjRepr::Bool(*b),
LObj::Integer(n) => ObjRepr::Int(*n),
LObj::Real(r) => ObjRepr::Real(*r as f64),
LObj::Name(n) => ObjRepr::Name(n.clone()),
LObj::String(s, _) => ObjRepr::String(s.clone()),
LObj::Array(items) => ObjRepr::Array(
items
.iter()
.map(|it| convert_pdf_object(doc, it, out, seen))
.collect(),
),
LObj::Dictionary(d) => ObjRepr::Dict(convert_pdf_dict(doc, d, out, seen)),
LObj::Stream(s) => {
let dict_entries = convert_pdf_dict(doc, &s.dict, out, seen)
.into_iter()
.filter(|(k, _)| k.as_slice() != b"Length")
.collect();
ObjRepr::Stream(dict_entries, s.content.clone())
}
LObj::Reference((obj_num, gen)) => {
let (obj_num, gen) = (*obj_num, *gen);
if obj_num != 0 && seen.insert(obj_num) {
if let Ok(target) = doc.get_object((obj_num, gen)) {
let repr = convert_pdf_object(doc, target, out, seen);
out.push((obj_num, repr));
}
}
ObjRepr::Ref(obj_num)
}
}
}
#[cfg(feature = "pdf-image")]
fn convert_pdf_dict(
doc: &lopdf::Document,
dict: &lopdf::Dictionary,
out: &mut Vec<(u32, ObjRepr)>,
seen: &mut BTreeSet<u32>,
) -> Vec<(Vec<u8>, ObjRepr)> {
dict.iter()
.map(|(k, v)| (k.clone(), convert_pdf_object(doc, v, out, seen)))
.collect()
}
fn prim_read_file(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let path_str = as_str(args.pop().unwrap())?;
let path = std::path::Path::new(&path_str);
if path.is_absolute() {
return eval_error(
"read-file: cannot access files by using an absolute path (job-directory containment)",
);
}
if path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return eval_error("cannot access files by using '..'");
}
let bytes = std::fs::read(path).map_err(|e| EvalError {
span: None,
msg: format!("read-file: cannot open '{path_str}': {e}"),
})?;
let text = String::from_utf8(bytes).map_err(|_| EvalError {
span: None,
msg: format!("read-file '{path_str}': not valid UTF-8"),
})?;
let mut lines: Vec<Value> = text
.split('\n')
.map(|s| Value::Str(s.to_string()))
.collect();
if matches!(lines.last(), Some(Value::Str(s)) if s.is_empty()) {
lines.pop();
}
Ok(Value::List(lines))
}
fn as_option_string(v: Value) -> Result<Option<String>, EvalError> {
match v {
Value::Ctor(name, payload) => match (name.as_str(), payload.map(|b| *b)) {
("None", None) => Ok(None),
("Some", Some(Value::Str(s))) => Ok(Some(s)),
(other, _) => eval_error(format!(
"expected a string option (None / Some(string)), got variant '{other}'"
)),
},
other => eval_error(format!("expected an option, got {}", other.type_name())),
}
}
fn prim_register_document_information(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let fields = match args.pop().unwrap() {
Value::Record(m) => m,
other => {
return eval_error(format!(
"register-document-information: expected a document-information-dictionary \
record, got {}",
other.type_name()
))
}
};
let record_name = "document-information-dictionary";
let title = as_option_string(record_field(&fields, record_name, "title")?)?;
let subject = as_option_string(record_field(&fields, record_name, "subject")?)?;
let author = as_option_string(record_field(&fields, record_name, "author")?)?;
let keywords = as_list(record_field(&fields, record_name, "keywords")?)?
.into_iter()
.map(as_str)
.collect::<Result<Vec<_>, _>>()?;
interp.doc_info = Some(DocInfo {
title,
subject,
author,
keywords,
});
Ok(Value::Unit)
}
fn prim_start_path(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let start = as_point(args.pop().unwrap())?;
Ok(Value::PrePath(PrePath {
start,
segs: Vec::new(),
}))
}
fn prim_line_to(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mut pp = as_prepath(args.pop().unwrap())?;
let pt = as_point(args.pop().unwrap())?;
pp.segs.push(PathSeg::Line(pt));
Ok(Value::PrePath(pp))
}
fn prim_terminate_path(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let pp = as_prepath(args.pop().unwrap())?;
Ok(Value::Path(Path {
subpaths: vec![Subpath {
start: pp.start,
segs: pp.segs,
closing: Closing::Open,
}],
}))
}
fn prim_close_with_line(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let pp = as_prepath(args.pop().unwrap())?;
Ok(Value::Path(Path {
subpaths: vec![Subpath {
start: pp.start,
segs: pp.segs,
closing: Closing::Line,
}],
}))
}
fn prim_fill(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let path = as_path(args.pop().unwrap())?;
let color = as_color(args.pop().unwrap())?;
Ok(Value::Graphics(GraphicsElem::Fill(color, path)))
}
fn prim_stroke(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let path = as_path(args.pop().unwrap())?;
let color = as_color(args.pop().unwrap())?;
let wid = as_length(args.pop().unwrap())?;
Ok(Value::Graphics(GraphicsElem::Stroke(wid, color, path)))
}
fn apply_graphics_callback(
interp: &mut Interp,
version: RustyfiVersion,
apply: impl FnOnce(&mut Interp) -> Result<Value, EvalError>,
) -> Result<Vec<GraphicsElem>, EvalError> {
let defer = interp.current_page.is_none();
let saved = if defer {
interp.pending_dests.replace(Vec::new())
} else {
None
};
let result = apply(interp).and_then(|v| coerce_graphics_result_for(version, v));
let pending = if defer {
interp.pending_dests.take().unwrap_or_default()
} else {
Vec::new()
};
if defer {
interp.pending_dests = saved;
}
let mut elems = result?;
elems.extend(
pending
.into_iter()
.map(|(key, pt)| GraphicsElem::Destination { key, pt }),
);
Ok(elems)
}
fn prim_inline_graphics(
interp: &mut Interp,
version: RustyfiVersion,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let gfun = args.pop().unwrap();
let d = as_length(args.pop().unwrap())?;
let h = as_length(args.pop().unwrap())?;
let w = as_length(args.pop().unwrap())?;
let gf = gfun.clone();
let elems = apply_graphics_callback(interp, version, move |it| {
let origin = make_point_value((Length::ZERO, Length::ZERO));
it.apply(gf, origin)
})?;
let origin_independent = {
let probe = make_point_value((Length::pt(4096.0), Length::pt(2731.0)));
match apply_graphics_callback(interp, version, move |it| it.apply(gfun, probe)) {
Ok(e2) => e2 == elems,
Err(_) => false,
}
};
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::Graphics {
width: w,
height: h,
depth: d,
elems,
origin_independent,
},
)]))
}
fn prim_inline_graphics_outer(
interp: &mut Interp,
version: RustyfiVersion,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let gfun = args.pop().unwrap();
let d = as_length(args.pop().unwrap())?;
let h = as_length(args.pop().unwrap())?;
interp.outer_graphics.push((gfun, version));
let fn_id = GraphicsFnId(interp.outer_graphics.len() - 1);
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::GraphicsOuter {
height: h,
depth: d,
width: Length::ZERO,
fn_id,
},
)]))
}
fn resolve_outer_graphics_in_contents(
interp: &mut Interp,
contents: &mut [(Length, PureHorzBox)],
) -> Result<(), EvalError> {
for (_, bx) in contents.iter_mut() {
if let PureHorzBox::GraphicsOuter {
height,
depth,
width,
fn_id,
} = bx
{
let (w, h, d) = (*width, *height, *depth);
let (gfun, gver) = match interp.outer_graphics.get(fn_id.0) {
Some((f, v)) => (f.clone(), *v),
None => {
return eval_error(format!(
"inline-graphics-outer: dangling callback index {}",
fn_id.0
))
}
};
let elems = apply_graphics_callback(interp, gver, move |it| {
let partial = it.apply(gfun, Value::Length(w))?;
it.apply(partial, make_point_value((Length::ZERO, Length::ZERO)))
})?;
*bx = PureHorzBox::Graphics {
width: w,
height: h,
depth: d,
elems,
origin_independent: false,
};
}
}
Ok(())
}
fn prim_tabular(
interp: &mut Interp,
version: RustyfiVersion,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let rulesf = args.pop().unwrap();
let rows = as_cell_grid(args.pop().unwrap())?;
let mut solved = rustyfi_backend::tabular::main(rows);
for cell in &mut solved.cells {
resolve_outer_graphics_in_contents(interp, &mut cell.contents)?;
}
let xs = make_length_list(&solved.xs);
let ys = make_length_list(&solved.ys);
let partial = interp.apply(rulesf, xs)?;
let gval = interp.apply(partial, ys)?;
let rules = coerce_graphics_result_for(version, gval)?;
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::Tabular(TabularBox {
width: solved.width,
height: solved.height,
depth: Length::ZERO,
cells: solved.cells,
rules,
}),
)]))
}
fn prim_bezier_to(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mut pp = as_prepath(args.pop().unwrap())?;
let pt1 = as_point(args.pop().unwrap())?;
let pt_t = as_point(args.pop().unwrap())?;
let pt_s = as_point(args.pop().unwrap())?;
pp.segs.push(PathSeg::Bezier(pt_s, pt_t, pt1));
Ok(Value::PrePath(pp))
}
fn prim_close_with_bezier(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let pp = as_prepath(args.pop().unwrap())?;
let pt_t = as_point(args.pop().unwrap())?;
let pt_s = as_point(args.pop().unwrap())?;
Ok(Value::Path(Path {
subpaths: vec![Subpath {
start: pp.start,
segs: pp.segs,
closing: Closing::Bezier(pt_s, pt_t),
}],
}))
}
fn prim_shift_path(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let path = as_path(args.pop().unwrap())?;
let v = as_point(args.pop().unwrap())?;
Ok(Value::Path(shift_path(v, &path)))
}
fn prim_linear_transform_path(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let path = as_path(args.pop().unwrap())?;
let d = as_float(args.pop().unwrap())?;
let c = as_float(args.pop().unwrap())?;
let b = as_float(args.pop().unwrap())?;
let a = as_float(args.pop().unwrap())?;
Ok(Value::Path(linear_transform_path((a, b, c, d), &path)))
}
fn prim_shift_graphics(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let g = as_graphics(args.pop().unwrap())?;
let v = as_point(args.pop().unwrap())?;
Ok(Value::Graphics(shift_graphics(v, &g)))
}
fn prim_linear_transform_graphics(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let g = as_graphics(args.pop().unwrap())?;
let d = as_float(args.pop().unwrap())?;
let c = as_float(args.pop().unwrap())?;
let b = as_float(args.pop().unwrap())?;
let a = as_float(args.pop().unwrap())?;
Ok(Value::Graphics(linear_transform_graphics((a, b, c, d), &g)))
}
fn prim_get_graphics_bbox_v006(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let g = as_graphics(args.pop().unwrap())?;
let (pmin, pmax) =
graphics_bbox(&g).unwrap_or(((Length::ZERO, Length::ZERO), (Length::ZERO, Length::ZERO)));
Ok(Value::Tuple(vec![
make_point_value(pmin),
make_point_value(pmax),
]))
}
fn prim_get_graphics_bbox_v01(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let g = as_graphics(args.pop().unwrap())?;
Ok(match graphics_bbox(&g) {
Some((pmin, pmax)) => Value::Ctor(
"Some".to_string(),
Some(Box::new(Value::Tuple(vec![
make_point_value(pmin),
make_point_value(pmax),
]))),
),
None => Value::Ctor("None".to_string(), None),
})
}
fn prim_unite_graphics(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let items = as_list(args.pop().unwrap())?;
let mut elems = Vec::with_capacity(items.len());
for it in items {
elems.push(as_graphics(it)?);
}
Ok(Value::Graphics(GraphicsElem::Group(elems)))
}
fn prim_clip_graphics_by_path(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let g = as_graphics(args.pop().unwrap())?;
let path = as_path(args.pop().unwrap())?;
Ok(Value::Graphics(GraphicsElem::Clip(path, vec![g])))
}
fn prim_get_path_bbox(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let path = as_path(args.pop().unwrap())?;
let (pmin, pmax) = path_bbox(&path);
Ok(Value::Tuple(vec![
make_point_value(pmin),
make_point_value(pmax),
]))
}
fn prim_dashed_stroke(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let path = as_path(args.pop().unwrap())?;
let color = as_color(args.pop().unwrap())?;
let dash = as_dash(args.pop().unwrap())?;
let wid = as_length(args.pop().unwrap())?;
Ok(Value::Graphics(GraphicsElem::DashedStroke(
wid, dash, color, path,
)))
}
fn prim_draw_text(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ib = as_inline_boxes(args.pop().unwrap())?;
let pt = as_point(args.pop().unwrap())?;
let (width, height, depth) = natural_metrics(&ib);
let (mut contents, _, _) = fit_cell(ib, width);
resolve_outer_graphics_in_contents(interp, &mut contents)?;
Ok(Value::Graphics(GraphicsElem::Text {
pt,
contents,
width,
height,
depth,
transform: None,
}))
}
fn prim_get_natural_metrics(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ib = as_inline_boxes(args.pop().unwrap())?;
let (width, height, depth) = natural_metrics(&ib);
Ok(Value::Tuple(vec![
Value::Length(width),
Value::Length(height),
Value::Length(depth),
]))
}
fn make_inline_frame(
interp: &mut Interp,
version: RustyfiVersion,
(pad_l, pad_r, pad_t, pad_b): (Length, Length, Length, Length),
deco: Value,
inner: Vec<HorzBox>,
) -> Value {
let (w, _, _) = natural_metrics(&inner);
let (contents, height, depth) = fit_cell(inner, w);
let contents = contents.into_iter().map(|(x, b)| (x + pad_l, b)).collect();
let id = DecoId(interp.decos.len());
interp.decos.push(DecoEntry::Inline { deco, version });
Value::InlineBoxes(vec![HorzBox::Pure(PureHorzBox::Frame {
width: pad_l + w + pad_r,
height: height + pad_t,
depth: depth + pad_b,
deco: id,
contents,
})])
}
fn prim_inline_frame_outer(
interp: &mut Interp,
version: RustyfiVersion,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let inner = as_inline_boxes(args.pop().unwrap())?;
let deco = args.pop().unwrap();
let pads = as_paddings(args.pop().unwrap())?;
Ok(make_inline_frame(interp, version, pads, deco, inner))
}
fn prim_inline_frame_inner(
interp: &mut Interp,
version: RustyfiVersion,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let inner = as_inline_boxes(args.pop().unwrap())?;
let deco = args.pop().unwrap();
let pads = as_paddings(args.pop().unwrap())?;
Ok(make_inline_frame(interp, version, pads, deco, inner))
}
fn prim_set_manual_rising(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let rising = as_length(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
manual_rising: rising,
..ctx
})))
}
fn prim_script_guard(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ib = as_inline_boxes(args.pop().unwrap())?;
let _script = args.pop().unwrap();
Ok(Value::InlineBoxes(ib))
}
fn into_pure(boxes: Vec<HorzBox>) -> Vec<PureHorzBox> {
boxes.into_iter().map(|HorzBox::Pure(p)| p).collect()
}
fn prim_discretionary(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let post_break = as_inline_boxes(args.pop().unwrap())?;
let pre_break = as_inline_boxes(args.pop().unwrap())?;
let no_break = as_inline_boxes(args.pop().unwrap())?;
let penalty = as_int(args.pop().unwrap())?;
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::Discretionary {
penalty: penalty.clamp(i32::MIN as i64, i32::MAX as i64) as i32,
pre_break: into_pure(pre_break),
post_break: into_pure(post_break),
no_break: into_pure(no_break),
},
)]))
}
fn prim_get_axis_height(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let mc = MathC::of(interp, &ctx);
Ok(Value::Length(mc.axis(ctx.font_size)))
}
fn prim_hook_page_break(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let closure = args.pop().unwrap();
let id = HookId(interp.hooks.len());
interp.hooks.push(closure);
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::HookPageBreak { id },
)]))
}
fn prim_hook_page_break_block(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let closure = args.pop().unwrap();
let id = HookId(interp.hooks.len());
interp.hooks.push(closure);
Ok(Value::BlockBoxes(vec![VertBox::HookPageBreak(id)]))
}
fn prim_register_cross_reference(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let value = as_str(args.pop().unwrap())?;
let key = as_str(args.pop().unwrap())?;
interp.crossrefs.borrow_mut().register(key, value);
Ok(Value::Unit)
}
fn prim_get_cross_reference(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let key = as_str(args.pop().unwrap())?;
Ok(match interp.crossrefs.borrow_mut().get(&key) {
Some(v) => Value::Ctor("Some".to_string(), Some(Box::new(Value::Str(v)))),
None => Value::Ctor("None".to_string(), None),
})
}
fn prim_probe_cross_reference(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let key = as_str(args.pop().unwrap())?;
Ok(match interp.crossrefs.borrow().probe(&key) {
Some(v) => Value::Ctor("Some".to_string(), Some(Box::new(Value::Str(v)))),
None => Value::Ctor("None".to_string(), None),
})
}
fn prim_get_leftmost_script(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let _ib = as_inline_boxes(args.pop().unwrap())?;
Ok(Value::Ctor("None".to_string(), None))
}
fn prim_get_rightmost_script(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let _ib = as_inline_boxes(args.pop().unwrap())?;
Ok(Value::Ctor("None".to_string(), None))
}
fn prim_inline_frame_breakable(
interp: &mut Interp,
version: RustyfiVersion,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let inner = as_inline_boxes(args.pop().unwrap())?;
let decoset = as_decoset(args.pop().unwrap())?;
let (pad_l, pad_r, pad_t, pad_b) = as_paddings(args.pop().unwrap())?;
let (_, height, depth) = natural_metrics(&inner);
let id = DecoId(interp.decos.len());
interp.decos.push(DecoEntry::InlineBreakable {
pads: Paddings {
l: pad_l,
r: pad_r,
t: pad_t,
b: pad_b,
},
decoset,
version,
});
let marker = |end| {
HorzBox::Pure(PureHorzBox::InlineFrameMarker {
id,
end,
height: height + pad_t,
depth: depth + pad_b,
})
};
let mut out = Vec::with_capacity(inner.len() + 4);
out.push(marker(false));
if pad_l != Length::ZERO {
out.push(HorzBox::Pure(PureHorzBox::FixedEmpty { width: pad_l }));
}
out.extend(inner);
if pad_r != Length::ZERO {
out.push(HorzBox::Pure(PureHorzBox::FixedEmpty { width: pad_r }));
}
out.push(marker(true));
Ok(Value::InlineBoxes(out))
}
fn as_decoset(v: Value) -> Result<[Value; 4], EvalError> {
match v {
Value::Tuple(vs) if vs.len() == 4 => {
let mut it = vs.into_iter();
let a = it.next().unwrap();
let b = it.next().unwrap();
let c = it.next().unwrap();
let d = it.next().unwrap();
Ok([a, b, c, d])
}
other => eval_error(format!(
"expected a deco-set (4-tuple of decorations), got {}",
other.type_name()
)),
}
}
fn coerce_graphics_result_for(
version: RustyfiVersion,
v: Value,
) -> Result<Vec<GraphicsElem>, EvalError> {
if version.graphics_is_collection() {
Ok(vec![as_graphics(v)?])
} else {
as_list(v)?.into_iter().map(as_graphics).collect()
}
}
pub(crate) fn apply_deco(
interp: &mut Interp,
version: RustyfiVersion,
deco: Value,
pt: Point,
w: Length,
h: Length,
d: Length,
) -> Result<Vec<GraphicsElem>, EvalError> {
let v = interp.apply(deco, make_point_value(pt))?;
let v = interp.apply(v, Value::Length(w))?;
let v = interp.apply(v, Value::Length(h))?;
let v = interp.apply(v, Value::Length(d))?;
coerce_graphics_result_for(version, v)
}
fn as_border_option(v: Value) -> Result<Option<(Length, Color)>, EvalError> {
match v {
Value::Ctor(name, payload) => match (name.as_str(), payload.map(|b| *b)) {
("None", None) => Ok(None),
("Some", Some(Value::Tuple(vs))) if vs.len() == 2 => {
let mut it = vs.into_iter();
let w = as_length(it.next().unwrap())?;
let c = as_color(it.next().unwrap())?;
Ok(Some((w, c)))
}
(other, _) => eval_error(format!(
"expected a border option (None / Some(length * color)), got variant '{other}'"
)),
},
other => eval_error(format!("expected an option, got {}", other.type_name())),
}
}
fn prim_register_destination(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let (x, y) = as_point(args.pop().unwrap())?;
let key = as_str(args.pop().unwrap())?;
if interp.current_page.is_none() {
if let Some(pending) = interp.pending_dests.as_mut() {
pending.push((key, (x, y)));
return Ok(Value::Unit);
}
}
let Some(page) = interp.current_page else {
return eval_error(
"register-destination can only be called during page breaking \
(from a page-break hook or a decoration)",
);
};
let name = interp.dest_name(&key);
if let Some(deco_id) = interp.current_deco_id {
interp.dest_decos.push((deco_id, name.clone()));
}
interp.destinations.push(NamedDest { page, name, x, y });
Ok(Value::Unit)
}
fn register_link(
interp: &mut Interp,
mut args: Vec<Value>,
prim_name: &str,
make_action: impl FnOnce(&mut Interp, String) -> AnnotAction,
) -> Result<Value, EvalError> {
let border = as_border_option(args.pop().unwrap())?;
let dpt = as_length(args.pop().unwrap())?;
let hgt = as_length(args.pop().unwrap())?;
let wid = as_length(args.pop().unwrap())?;
let (x, y) = as_point(args.pop().unwrap())?;
let target = as_str(args.pop().unwrap())?;
let Some(page) = interp.current_page else {
return eval_error(format!(
"{prim_name} can only be called during page breaking \
(from a page-break hook or a decoration)"
));
};
let action = make_action(interp, target);
if let Some(deco_id) = interp.current_deco_id {
interp.link_decos.push((deco_id, action.clone()));
}
interp.annotations.push(Annot {
page,
rect: (x, y - dpt, x + wid, y + hgt),
action,
border,
});
Ok(Value::Unit)
}
fn prim_register_link_to_uri(interp: &mut Interp, args: Vec<Value>) -> Result<Value, EvalError> {
register_link(interp, args, "register-link-to-uri", |_, uri| {
AnnotAction::Uri(uri)
})
}
fn prim_register_link_to_location(
interp: &mut Interp,
args: Vec<Value>,
) -> Result<Value, EvalError> {
register_link(interp, args, "register-link-to-location", |interp, key| {
AnnotAction::GotoName(interp.dest_name(&key))
})
}
use crate::value::{Math, MathElement, MathVariantStyle};
fn as_math_kind(v: Value) -> Result<MathKind, EvalError> {
match v {
Value::Ctor(name, None) => match name.as_str() {
"MathOrd" => Ok(MathKind::Ord),
"MathBin" => Ok(MathKind::Bin),
"MathRel" => Ok(MathKind::Rel),
"MathOp" => Ok(MathKind::Op),
"MathPunct" => Ok(MathKind::Punct),
"MathOpen" => Ok(MathKind::Open),
"MathClose" => Ok(MathKind::Close),
"MathPrefix" => Ok(MathKind::Prefix),
"MathInner" => Ok(MathKind::Inner),
other => eval_error(format!("expected a math-class constructor, got '{other}'")),
},
other => eval_error(format!("expected a math-class, got {}", other.type_name())),
}
}
fn as_math_char_class(v: Value) -> Result<MathCharClass, EvalError> {
match v {
Value::Ctor(name, None) => match name.as_str() {
"MathItalic" => Ok(MathCharClass::Italic),
"MathBoldItalic" => Ok(MathCharClass::BoldItalic),
"MathRoman" => Ok(MathCharClass::Roman),
"MathBoldRoman" => Ok(MathCharClass::BoldRoman),
"MathScript" => Ok(MathCharClass::Script),
"MathBoldScript" => Ok(MathCharClass::BoldScript),
"MathFraktur" => Ok(MathCharClass::Fraktur),
"MathBoldFraktur" => Ok(MathCharClass::BoldFraktur),
"MathDoubleStruck" => Ok(MathCharClass::DoubleStruck),
"MathSansSerif" => Ok(MathCharClass::SansSerif),
"MathBoldSansSerif" => Ok(MathCharClass::BoldSansSerif),
"MathItalicSansSerif" => Ok(MathCharClass::ItalicSansSerif),
"MathBoldItalicSansSerif" => Ok(MathCharClass::BoldItalicSansSerif),
"MathTypewriter" => Ok(MathCharClass::Typewriter),
other => eval_error(format!(
"expected a math-char-class constructor, got '{other}'"
)),
},
other => eval_error(format!(
"expected a math-char-class, got {}",
other.type_name()
)),
}
}
fn as_math_variant_style(v: Value) -> Result<MathVariantStyle, EvalError> {
match v {
Value::Record(mut fields) => {
let mut take = |label: &str| -> Result<String, EvalError> {
match fields.remove(label) {
Some(v) => as_str(v),
None => eval_error(format!(
"math-variant-char style record missing field '{label}'"
)),
}
};
Ok(MathVariantStyle {
italic: take("italic")?,
bold_italic: take("bold-italic")?,
roman: take("roman")?,
bold_roman: take("bold-roman")?,
script: take("script")?,
bold_script: take("bold-script")?,
fraktur: take("fraktur")?,
bold_fraktur: take("bold-fraktur")?,
double_struck: take("double-struck")?,
})
}
other => eval_error(format!(
"expected a math-variant-char style record, got {}",
other.type_name()
)),
}
}
fn as_math(interp: &mut Interp, v: Value) -> Result<Rc<Vec<Math>>, EvalError> {
match v {
Value::Math(m) => Ok(m),
Value::MathText { elems, env } => {
let mut out = Vec::new();
for e in elems.iter() {
reflect_math_elem(interp, e, &env, &mut out)?;
}
Ok(Rc::new(out))
}
other => eval_error(format!("expected math, got {}", other.type_name())),
}
}
fn reflect_math_elem(
interp: &mut Interp,
elem: &MathElem,
env: &Env,
out: &mut Vec<Math>,
) -> Result<(), EvalError> {
match elem {
MathElem::Chars(s) => {
out.push(Math::Pure(MathElement::VariantCharPending(s.clone())));
Ok(())
}
MathElem::Group(elems) => {
for e in elems {
reflect_math_elem(interp, e, env, out)?;
}
Ok(())
}
MathElem::Sub(base, script) => {
let mut base_v = Vec::new();
reflect_math_elem(interp, base, env, &mut base_v)?;
let mut script_v = Vec::new();
for e in script {
reflect_math_elem(interp, e, env, &mut script_v)?;
}
out.push(Math::Sub(base_v, script_v));
Ok(())
}
MathElem::Sup(base, script) => {
let mut base_v = Vec::new();
reflect_math_elem(interp, base, env, &mut base_v)?;
let mut script_v = Vec::new();
for e in script {
reflect_math_elem(interp, e, env, &mut script_v)?;
}
out.push(Math::Sup(base_v, script_v));
Ok(())
}
MathElem::Primes(base, n) => {
let mut base_v = Vec::new();
reflect_math_elem(interp, base, env, &mut base_v)?;
let primes: String = std::iter::repeat('\u{2032}').take(*n).collect();
out.push(Math::Sup(
base_v,
vec![Math::Pure(MathElement::Char {
class: MathKind::Ord,
big: false,
chars: primes,
})],
));
Ok(())
}
MathElem::Cmd { cmd, args, .. } => {
let mut v = cmd.run(env, interp)?;
for arg in args {
let mut opt_vals = Vec::with_capacity(arg.opts.len());
for (label, e) in &arg.opts {
opt_vals.push((label.clone(), e.run(env, interp)?));
}
let arg_v = arg.arg.run(env, interp)?;
v = interp.apply_with_opts(v, opt_vals, arg_v)?;
}
let m = as_math(interp, v)?;
out.extend(m.iter().cloned());
Ok(())
}
MathElem::Embed { expr, span: _ } => {
let v = expr.run(env, interp)?;
let m = as_math(interp, v)?;
out.extend(m.iter().cloned());
Ok(())
}
}
}
fn single_math(m: Math) -> Value {
Value::Math(Rc::new(vec![m]))
}
fn single_math_boxes(m: Math) -> Value {
Value::MathBoxes(Rc::new(vec![m]))
}
fn as_math_boxes(v: Value) -> Result<Rc<Vec<Math>>, EvalError> {
match v {
Value::MathBoxes(m) => Ok(m),
other => eval_error(format!(
"expected math-boxes, got {} (V0_1: math-text and math-boxes \
are distinct types — bridge with `read-math`)",
other.type_name()
)),
}
}
fn as_math_text(v: Value) -> Result<(Rc<Vec<MathElem>>, Env), EvalError> {
match v {
Value::MathText { elems, env } => Ok((elems, env)),
other => eval_error(format!("expected math-text, got {}", other.type_name())),
}
}
fn as_option_math_text(v: Value) -> Result<Option<(Rc<Vec<MathElem>>, Env)>, EvalError> {
match v {
Value::Ctor(name, None) if name == "None" => Ok(None),
Value::Ctor(name, Some(payload)) if name == "Some" => {
let (elems, env) = as_math_text(*payload)?;
Ok(Some((elems, env)))
}
other => eval_error(format!(
"expected an option (None/Some), got {}",
other.type_name()
)),
}
}
fn option_math_text_value(opt: Option<&[MathElem]>, env: &Env) -> Value {
match opt {
None => Value::Ctor("None".to_string(), None),
Some(elems) => Value::Ctor(
"Some".to_string(),
Some(Box::new(Value::MathText {
elems: Rc::new(elems.to_vec()),
env: env.clone(),
})),
),
}
}
fn math_char_class_ctor_name(c: MathCharClass) -> &'static str {
match c {
MathCharClass::Italic => "MathItalic",
MathCharClass::BoldItalic => "MathBoldItalic",
MathCharClass::Roman => "MathRoman",
MathCharClass::BoldRoman => "MathBoldRoman",
MathCharClass::Script => "MathScript",
MathCharClass::BoldScript => "MathBoldScript",
MathCharClass::Fraktur => "MathFraktur",
MathCharClass::BoldFraktur => "MathBoldFraktur",
MathCharClass::DoubleStruck => "MathDoubleStruck",
MathCharClass::SansSerif => "MathSansSerif",
MathCharClass::BoldSansSerif => "MathBoldSansSerif",
MathCharClass::ItalicSansSerif => "MathItalicSansSerif",
MathCharClass::BoldItalicSansSerif => "MathBoldItalicSansSerif",
MathCharClass::Typewriter => "MathTypewriter",
}
}
fn math_char_class_value(c: MathCharClass) -> Value {
Value::Ctor(math_char_class_ctor_name(c).to_string(), None)
}
fn enter_script(interp: &Interp, ctx: &Context) -> Context {
let mc = MathC::of(interp, ctx);
let (scale, next_level) = match ctx.math_script_level {
MathScriptLevel::Base => (
mc.c.map(|c| c.script_scale_down).unwrap_or(0.7),
MathScriptLevel::Script,
),
MathScriptLevel::Script => (
mc.c.map(|c| c.script_script_scale_down / c.script_scale_down)
.unwrap_or(5.0 / 7.0),
MathScriptLevel::ScriptScript,
),
MathScriptLevel::ScriptScript => return ctx.clone(),
};
Context {
font_size: ctx.font_size * scale,
math_script_level: next_level,
..ctx.clone()
}
}
fn flatten_math_scripts(elem: &MathElem) -> (&MathElem, Option<&[MathElem]>, Option<&[MathElem]>) {
match elem {
MathElem::Sup(base, sup) => match base.as_ref() {
MathElem::Sub(inner, sub) => {
(inner.as_ref(), Some(sub.as_slice()), Some(sup.as_slice()))
}
_ => (base.as_ref(), None, Some(sup.as_slice())),
},
MathElem::Sub(base, sub) => (base.as_ref(), Some(sub.as_slice()), None),
_ => unreachable!("flatten_math_scripts called on a non-Sub/Sup MathElem"),
}
}
fn attach_scripts(
interp: &mut Interp,
ctx: &Context,
base: Vec<Math>,
sub_opt: Option<(Rc<Vec<MathElem>>, Env)>,
sup_opt: Option<(Rc<Vec<MathElem>>, Env)>,
) -> Result<Vec<Math>, EvalError> {
if sub_opt.is_none() && sup_opt.is_none() {
return Ok(base);
}
let script_ctx = enter_script(interp, ctx);
let mut cur = base;
if let Some((elems, senv)) = sub_opt {
let mut sub_v = Vec::new();
for e in elems.iter() {
reflect_math_elem_v01(interp, &script_ctx, e, &senv, &mut sub_v)?;
}
cur = vec![Math::Sub(cur, sub_v)];
}
if let Some((elems, senv)) = sup_opt {
let mut sup_v = Vec::new();
for e in elems.iter() {
reflect_math_elem_v01(interp, &script_ctx, e, &senv, &mut sup_v)?;
}
cur = vec![Math::Sup(cur, sup_v)];
}
Ok(cur)
}
fn reflect_scripted_v01(
interp: &mut Interp,
ctx: &Context,
base: &MathElem,
sub: Option<&[MathElem]>,
sup: Option<&[MathElem]>,
env: &Env,
out: &mut Vec<Math>,
) -> Result<(), EvalError> {
if let MathElem::Cmd { cmd, args, .. } = base {
let mut v = cmd.run(env, interp)?;
for arg in args {
let mut opt_vals = Vec::with_capacity(arg.opts.len());
for (label, e) in &arg.opts {
opt_vals.push((label.clone(), e.run(env, interp)?));
}
let arg_v = arg.arg.run(env, interp)?;
v = interp.apply_with_opts(v, opt_vals, arg_v)?;
}
if matches!(v, Value::Math(_) | Value::MathText { .. }) {
let base_v = as_math(interp, v)?.as_ref().clone();
let sub_opt = sub.map(|s| (Rc::new(s.to_vec()), env.clone()));
let sup_opt = sup.map(|s| (Rc::new(s.to_vec()), env.clone()));
let attached = attach_scripts(interp, ctx, base_v, sub_opt, sup_opt)?;
out.extend(attached);
return Ok(());
}
v = interp.apply(v, Value::Context(Box::new(ctx.clone())))?;
v = interp.apply(v, option_math_text_value(sub, env))?;
v = interp.apply(v, option_math_text_value(sup, env))?;
let m = as_math_boxes(v)?;
out.extend(m.iter().cloned());
return Ok(());
}
let mut base_v = Vec::new();
reflect_math_elem_v01(interp, ctx, base, env, &mut base_v)?;
let sub_opt = sub.map(|s| (Rc::new(s.to_vec()), env.clone()));
let sup_opt = sup.map(|s| (Rc::new(s.to_vec()), env.clone()));
let attached = attach_scripts(interp, ctx, base_v, sub_opt, sup_opt)?;
out.extend(attached);
Ok(())
}
fn reflect_math_elem_v01(
interp: &mut Interp,
ctx: &Context,
elem: &MathElem,
env: &Env,
out: &mut Vec<Math>,
) -> Result<(), EvalError> {
match elem {
MathElem::Chars(s) => {
out.push(Math::Pure(MathElement::VariantCharPending(s.clone())));
Ok(())
}
MathElem::Group(elems) => {
for e in elems {
reflect_math_elem_v01(interp, ctx, e, env, out)?;
}
Ok(())
}
MathElem::Primes(base, n) => {
let mut base_v = Vec::new();
reflect_math_elem_v01(interp, ctx, base, env, &mut base_v)?;
let primes: String = std::iter::repeat('\u{2032}').take(*n).collect();
out.push(Math::Sup(
base_v,
vec![Math::Pure(MathElement::Char {
class: MathKind::Ord,
big: false,
chars: primes,
})],
));
Ok(())
}
MathElem::Sub(_, _) | MathElem::Sup(_, _) => {
let (base, sub, sup) = flatten_math_scripts(elem);
reflect_scripted_v01(interp, ctx, base, sub, sup, env, out)
}
MathElem::Cmd { .. } => reflect_scripted_v01(interp, ctx, elem, None, None, env, out),
MathElem::Embed { expr, span: _ } => {
let v = expr.run(env, interp)?;
let (elems2, env2) = as_math_text(v)?;
for e in elems2.iter() {
reflect_math_elem_v01(interp, ctx, e, &env2, out)?;
}
Ok(())
}
}
}
fn prim_read_math(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mt = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let (elems, env) = as_math_text(mt)?;
let mut out = Vec::new();
for e in elems.iter() {
reflect_math_elem_v01(interp, &ctx, e, &env, &mut out)?;
}
Ok(Value::MathBoxes(Rc::new(vec![Math::WithContext(
Box::new(ctx),
out,
)])))
}
fn prim_stringify_math(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let _mt = args.pop().unwrap();
let _tctx = args.pop().unwrap();
eval_error(
"stringify-math: the text-mode backend is out of scope for this PDF port \
(see primitives.rs's prim_convert_string_for_math doc comment)"
.to_string(),
)
}
fn prim_set_math_char(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mut ctx = as_context(args.pop().unwrap())?;
let kind = as_math_kind(args.pop().unwrap())?;
let cpto = as_int(args.pop().unwrap())?;
let cpfrom = as_int(args.pop().unwrap())?;
let from = u32::try_from(cpfrom)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| EvalError {
span: None,
msg: format!("set-math-char: {cpfrom} is not a valid Unicode codepoint"),
})?;
let to = u32::try_from(cpto)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| EvalError {
span: None,
msg: format!("set-math-char: {cpto} is not a valid Unicode codepoint"),
})?;
Arc::make_mut(&mut ctx.math_class_map).insert(from.to_string(), (to.to_string(), kind));
Ok(Value::Context(Box::new(ctx)))
}
fn prim_set_math_char_class(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let cls = as_math_char_class(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
math_char_class: cls,
..ctx
})))
}
fn prim_get_math_char_class(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
Ok(math_char_class_value(ctx.math_char_class))
}
fn prim_embed_inline_to_math(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ib = as_inline_boxes(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
Ok(single_math_boxes(Math::Pure(MathElement::EmbeddedBoxes {
class,
boxes: ib,
})))
}
fn prim_get_math_axis_height_ratio(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let ratio = MathC::of(interp, &ctx)
.c
.map(|c| c.axis_height)
.unwrap_or(0.25);
Ok(Value::Float(ratio))
}
fn prim_math_attach_scripts(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let sup_v = args.pop().unwrap();
let sub_v = args.pop().unwrap();
let base_v = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let base = as_math_boxes(base_v)?;
let sub_opt = as_option_math_text(sub_v)?;
let sup_opt = as_option_math_text(sup_v)?;
let out = attach_scripts(interp, &ctx, (*base).clone(), sub_opt, sup_opt)?;
Ok(Value::MathBoxes(Rc::new(out)))
}
fn prim_load_hyphenation_dictionary(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let arg = as_str(args.pop().unwrap())?;
let stem = std::path::Path::new(&arg)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(arg.as_str())
.to_ascii_lowercase();
let tag = match stem.as_str() {
"english" | "en-us" => HyphenLang::EnglishUS,
"british" | "en-gb" | "british-english" => HyphenLang::EnglishGB,
_ => {
return eval_error(format!(
"load-hyphenation-dictionary: unknown dictionary {arg:?} \
(supported: \"english\"/\"en-US\", \"british\"/\"en-GB\"/\"british-english\", \
bare or as a `.../<name>.rustyfi-hyph`-style path)"
))
}
};
Ok(Value::Hyphenation(tag))
}
fn prim_load_unicode_char_database(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
args.truncate(0);
Ok(Value::Unit)
}
fn prim_set_hyphenation_dictionary(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let tag = as_hyphenation(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
hyphen_dictionary: Some(tag),
..ctx
})))
}
fn prim_set_unicode_char_database(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = args.pop().unwrap();
let _db = args.pop().unwrap();
Ok(ctx)
}
fn prim_math_char_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ = interp;
Ok(single_math(Math::Pure(MathElement::Char {
class,
big: false,
chars: s,
})))
}
fn prim_math_char_v01(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ctx = as_context(args.pop().unwrap())?;
let _ = interp;
Ok(single_math_boxes(Math::Pure(MathElement::Char {
class,
big: false,
chars: s,
})))
}
fn prim_math_big_char_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ = interp;
Ok(single_math(Math::Pure(MathElement::Char {
class,
big: true,
chars: s,
})))
}
fn prim_math_big_char_v01(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ctx = as_context(args.pop().unwrap())?;
let _ = interp;
Ok(single_math_boxes(Math::Pure(MathElement::Char {
class,
big: true,
chars: s,
})))
}
fn prim_math_char_with_kern_v006(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let kern_r = args.pop().unwrap();
let kern_l = args.pop().unwrap();
let s = as_str(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ = interp;
Ok(single_math(Math::Pure(MathElement::CharWithKern {
class,
big: false,
chars: s,
kern_l: Box::new(kern_l),
kern_r: Box::new(kern_r),
})))
}
fn prim_math_char_with_kern_v01(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let kern_r = args.pop().unwrap();
let kern_l = args.pop().unwrap();
let s = as_str(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ctx = as_context(args.pop().unwrap())?;
let _ = interp;
Ok(single_math_boxes(Math::Pure(MathElement::CharWithKern {
class,
big: false,
chars: s,
kern_l: Box::new(kern_l),
kern_r: Box::new(kern_r),
})))
}
fn prim_math_big_char_with_kern_v006(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let kern_r = args.pop().unwrap();
let kern_l = args.pop().unwrap();
let s = as_str(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ = interp;
Ok(single_math(Math::Pure(MathElement::CharWithKern {
class,
big: true,
chars: s,
kern_l: Box::new(kern_l),
kern_r: Box::new(kern_r),
})))
}
fn prim_math_big_char_with_kern_v01(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let kern_r = args.pop().unwrap();
let kern_l = args.pop().unwrap();
let s = as_str(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ctx = as_context(args.pop().unwrap())?;
let _ = interp;
Ok(single_math_boxes(Math::Pure(MathElement::CharWithKern {
class,
big: true,
chars: s,
kern_l: Box::new(kern_l),
kern_r: Box::new(kern_r),
})))
}
fn prim_math_concat_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = args.pop().unwrap();
let m1 = args.pop().unwrap();
let m1 = as_math(interp, m1)?;
let m2 = as_math(interp, m2)?;
let mut out = (*m1).clone();
out.extend((*m2).iter().cloned());
Ok(Value::Math(Rc::new(out)))
}
fn prim_math_concat_v01(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = as_math_boxes(args.pop().unwrap())?;
let m1 = as_math_boxes(args.pop().unwrap())?;
let mut out = (*m1).clone();
out.extend((*m2).iter().cloned());
Ok(Value::MathBoxes(Rc::new(out)))
}
fn prim_math_group_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m = args.pop().unwrap();
let cls2 = as_math_kind(args.pop().unwrap())?;
let cls1 = as_math_kind(args.pop().unwrap())?;
let inner = as_math(interp, m)?;
Ok(single_math(Math::Group(cls1, cls2, (*inner).clone())))
}
fn prim_math_group_v01(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m = as_math_boxes(args.pop().unwrap())?;
let cls2 = as_math_kind(args.pop().unwrap())?;
let cls1 = as_math_kind(args.pop().unwrap())?;
Ok(single_math_boxes(Math::Group(cls1, cls2, (*m).clone())))
}
fn prim_math_sup_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = args.pop().unwrap();
let m1 = args.pop().unwrap();
let base = as_math(interp, m1)?;
let script = as_math(interp, m2)?;
Ok(single_math(Math::Sup((*base).clone(), (*script).clone())))
}
fn prim_math_sup_v01(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let f = args.pop().unwrap();
let base_v = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let base = as_math_boxes(base_v)?;
let script_ctx = enter_script(interp, &ctx);
let script_v = interp.apply(f, Value::Context(Box::new(script_ctx)))?;
let script = as_math_boxes(script_v)?;
Ok(single_math_boxes(Math::Sup(
(*base).clone(),
(*script).clone(),
)))
}
fn prim_math_sub_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = args.pop().unwrap();
let m1 = args.pop().unwrap();
let base = as_math(interp, m1)?;
let script = as_math(interp, m2)?;
Ok(single_math(Math::Sub((*base).clone(), (*script).clone())))
}
fn prim_math_sub_v01(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let f = args.pop().unwrap();
let base_v = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let base = as_math_boxes(base_v)?;
let script_ctx = enter_script(interp, &ctx);
let script_v = interp.apply(f, Value::Context(Box::new(script_ctx)))?;
let script = as_math_boxes(script_v)?;
Ok(single_math_boxes(Math::Sub(
(*base).clone(),
(*script).clone(),
)))
}
fn prim_math_frac_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = args.pop().unwrap();
let m1 = args.pop().unwrap();
let num = as_math(interp, m1)?;
let den = as_math(interp, m2)?;
Ok(single_math(Math::Fraction((*num).clone(), (*den).clone())))
}
fn prim_math_frac_v01(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = as_math_boxes(args.pop().unwrap())?;
let m1 = as_math_boxes(args.pop().unwrap())?;
let _ctx = as_context(args.pop().unwrap())?;
Ok(single_math_boxes(Math::Fraction(
(*m1).clone(),
(*m2).clone(),
)))
}
fn prim_math_radical_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = args.pop().unwrap();
let opt = args.pop().unwrap();
let radicand = as_math(interp, m2)?;
let degree = match opt {
Value::Ctor(name, payload) if name == "None" && payload.is_none() => None,
Value::Ctor(name, Some(payload)) if name == "Some" => {
Some((*as_math(interp, *payload)?).clone())
}
other => {
return eval_error(format!(
"expected a math option (None/Some), got {}",
other.type_name()
))
}
};
Ok(single_math(Math::Radical(degree, (*radicand).clone())))
}
fn prim_math_radical_v01(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = args.pop().unwrap();
let opt = args.pop().unwrap();
let _ctx = as_context(args.pop().unwrap())?;
let radicand = as_math_boxes(m2)?;
let degree = match opt {
Value::Ctor(name, payload) if name == "None" && payload.is_none() => None,
Value::Ctor(name, Some(payload)) if name == "Some" => {
Some((*as_math_boxes(*payload)?).clone())
}
other => {
return eval_error(format!(
"expected a math-boxes option (None/Some), got {}",
other.type_name()
))
}
};
Ok(single_math_boxes(Math::Radical(
degree,
(*radicand).clone(),
)))
}
fn prim_math_lower_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = args.pop().unwrap();
let m1 = args.pop().unwrap();
let base = as_math(interp, m1)?;
let lower = as_math(interp, m2)?;
Ok(single_math(Math::LowerLimit(
(*base).clone(),
(*lower).clone(),
)))
}
fn prim_math_lower_v01(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let f = args.pop().unwrap();
let base_v = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let base = as_math_boxes(base_v)?;
let script_ctx = enter_script(interp, &ctx);
let script_v = interp.apply(f, Value::Context(Box::new(script_ctx)))?;
let lower = as_math_boxes(script_v)?;
Ok(single_math_boxes(Math::LowerLimit(
(*base).clone(),
(*lower).clone(),
)))
}
fn prim_math_upper_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m2 = args.pop().unwrap();
let m1 = args.pop().unwrap();
let base = as_math(interp, m1)?;
let upper = as_math(interp, m2)?;
Ok(single_math(Math::UpperLimit(
(*base).clone(),
(*upper).clone(),
)))
}
fn prim_math_upper_v01(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let f = args.pop().unwrap();
let base_v = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let base = as_math_boxes(base_v)?;
let script_ctx = enter_script(interp, &ctx);
let script_v = interp.apply(f, Value::Context(Box::new(script_ctx)))?;
let upper = as_math_boxes(script_v)?;
Ok(single_math_boxes(Math::UpperLimit(
(*base).clone(),
(*upper).clone(),
)))
}
fn prim_math_pull_in_scripts(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let resolver = args.pop().unwrap();
let cls2 = as_math_kind(args.pop().unwrap())?;
let cls1 = as_math_kind(args.pop().unwrap())?;
let _ = interp;
Ok(single_math(Math::PullInScripts(
cls1,
cls2,
Box::new(resolver),
)))
}
fn prim_math_color(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m = args.pop().unwrap();
let color = as_color(args.pop().unwrap())?;
let inner = as_math(interp, m)?;
Ok(single_math(Math::ChangeColor(color, (*inner).clone())))
}
fn prim_math_char_class(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m = args.pop().unwrap();
let cls = as_math_char_class(args.pop().unwrap())?;
let inner = as_math(interp, m)?;
Ok(single_math(Math::ChangeCharClass(cls, (*inner).clone())))
}
fn prim_math_variant_char(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let style = as_math_variant_style(args.pop().unwrap())?;
let class = as_math_kind(args.pop().unwrap())?;
let _ = interp;
Ok(single_math(Math::Pure(MathElement::VariantChar {
class,
big: false,
style: Box::new(style),
})))
}
fn prim_math_paren_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m = args.pop().unwrap();
let paren_r = args.pop().unwrap();
let paren_l = args.pop().unwrap();
let inner = as_math(interp, m)?;
Ok(single_math(Math::Paren(
Box::new(paren_l),
Box::new(paren_r),
(*inner).clone(),
)))
}
fn prim_math_paren_v01(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m = args.pop().unwrap();
let paren_r = args.pop().unwrap();
let paren_l = args.pop().unwrap();
let _ctx = as_context(args.pop().unwrap())?;
let inner = as_math_boxes(m)?;
Ok(single_math_boxes(Math::Paren(
Box::new(paren_l),
Box::new(paren_r),
(*inner).clone(),
)))
}
fn prim_math_paren_with_middle_v006(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let mlst = args.pop().unwrap();
let middle = args.pop().unwrap();
let paren_r = args.pop().unwrap();
let paren_l = args.pop().unwrap();
let items = as_list(mlst)?;
let mut mlstlst = Vec::with_capacity(items.len());
for it in items {
mlstlst.push((*as_math(interp, it)?).clone());
}
Ok(single_math(Math::ParenWithMiddle(
Box::new(paren_l),
Box::new(paren_r),
Box::new(middle),
mlstlst,
)))
}
fn prim_math_paren_with_middle_v01(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let mlst = args.pop().unwrap();
let middle = args.pop().unwrap();
let paren_r = args.pop().unwrap();
let paren_l = args.pop().unwrap();
let _ctx = as_context(args.pop().unwrap())?;
let items = as_list(mlst)?;
let mut mlstlst = Vec::with_capacity(items.len());
for it in items {
mlstlst.push((*as_math_boxes(it)?).clone());
}
Ok(single_math_boxes(Math::ParenWithMiddle(
Box::new(paren_l),
Box::new(paren_r),
Box::new(middle),
mlstlst,
)))
}
fn prim_text_in_math(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let body = args.pop().unwrap();
let class = as_math_kind(args.pop().unwrap())?;
let _ = interp;
Ok(single_math(Math::Pure(MathElement::EmbeddedText {
class,
body: Box::new(body),
})))
}
fn prim_convert_string_for_math(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
let class = as_math_char_class(args.pop().unwrap())?;
let ctx = as_context(args.pop().unwrap())?;
if let Some((target, _mk)) = ctx.math_class_map.get(&s) {
return Ok(Value::Str(target.clone()));
}
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
let mapped = ctx
.math_variant_char_map
.get(&(ch, class))
.copied()
.or_else(|| default_math_variant_char(class, ch))
.unwrap_or(ch);
out.push(mapped);
}
Ok(Value::Str(out))
}
fn prim_set_math_variant_char_v006(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let mut ctx = as_context(args.pop().unwrap())?;
let cpto = as_int(args.pop().unwrap())?;
let cpfrom = as_int(args.pop().unwrap())?;
let cls = as_math_char_class(args.pop().unwrap())?;
let from = u32::try_from(cpfrom)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| EvalError {
span: None,
msg: format!("set-math-variant-char: {cpfrom} is not a valid Unicode codepoint"),
})?;
let to = u32::try_from(cpto)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| EvalError {
span: None,
msg: format!("set-math-variant-char: {cpto} is not a valid Unicode codepoint"),
})?;
Arc::make_mut(&mut ctx.math_variant_char_map).insert((from, cls), to);
Ok(Value::Context(Box::new(ctx)))
}
fn prim_set_math_variant_char_v01(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let mut ctx = as_context(args.pop().unwrap())?;
let selector = args.pop().unwrap();
let cpfrom = as_int(args.pop().unwrap())?;
let from = u32::try_from(cpfrom)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| EvalError {
span: None,
msg: format!("set-math-variant-char: {cpfrom} is not a valid Unicode codepoint"),
})?;
const CLASSES: [MathCharClass; 9] = [
MathCharClass::Italic,
MathCharClass::BoldItalic,
MathCharClass::Roman,
MathCharClass::BoldRoman,
MathCharClass::Script,
MathCharClass::BoldScript,
MathCharClass::Fraktur,
MathCharClass::BoldFraktur,
MathCharClass::DoubleStruck,
];
for cls in CLASSES {
let cpto_v = interp.apply(selector.clone(), math_char_class_value(cls))?;
let cpto = as_int(cpto_v)?;
let to = u32::try_from(cpto)
.ok()
.and_then(char::from_u32)
.ok_or_else(|| EvalError {
span: None,
msg: format!("set-math-variant-char: {cpto} is not a valid Unicode codepoint"),
})?;
Arc::make_mut(&mut ctx.math_variant_char_map).insert((from, cls), to);
}
Ok(Value::Context(Box::new(ctx)))
}
fn math_element_kind(ctx: &Context, me: &MathElement) -> MathKind {
match me {
MathElement::Char { class, .. }
| MathElement::CharWithKern { class, .. }
| MathElement::EmbeddedText { class, .. }
| MathElement::VariantChar { class, .. }
| MathElement::EmbeddedBoxes { class, .. } => *class,
MathElement::VariantCharPending(s) => ctx
.math_class_map
.get(s.as_str())
.map(|(_, kind)| *kind)
.unwrap_or(MathKind::Ord),
}
}
fn boundary_math_kind(ctx: &Context, ms: &[Math], left: bool) -> MathKind {
let m = if left { ms.first() } else { ms.last() };
let Some(m) = m else {
return MathKind::End;
};
match m {
Math::Pure(me) => math_element_kind(ctx, me),
Math::Group(cls1, cls2, _) => {
if left {
*cls1
} else {
*cls2
}
}
Math::PullInScripts(cls1, cls2, _) => {
if left {
*cls1
} else {
*cls2
}
}
Math::Sup(base, _)
| Math::Sub(base, _)
| Math::UpperLimit(base, _)
| Math::LowerLimit(base, _) => boundary_math_kind(ctx, base, left),
Math::Fraction(..) | Math::Radical(..) => MathKind::Inner,
Math::Paren(..) | Math::ParenWithMiddle(..) => {
if left {
MathKind::Open
} else {
MathKind::Close
}
}
Math::ChangeColor(_, inner) | Math::ChangeCharClass(_, inner) => {
boundary_math_kind(ctx, inner, left)
}
Math::WithContext(_, inner) => boundary_math_kind(ctx, inner, left),
}
}
fn left_math_kind(ctx: &Context, ms: &[Math]) -> MathKind {
boundary_math_kind(ctx, ms, true)
}
fn right_math_kind(ctx: &Context, ms: &[Math]) -> MathKind {
boundary_math_kind(ctx, ms, false)
}
fn make_math_class_option_value(mk: MathKind) -> Value {
let name = match mk {
MathKind::Ord => "MathOrd",
MathKind::Bin => "MathBin",
MathKind::Rel => "MathRel",
MathKind::Op => "MathOp",
MathKind::Punct => "MathPunct",
MathKind::Open => "MathOpen",
MathKind::Close => "MathClose",
MathKind::Prefix => "MathPrefix",
MathKind::Inner => "MathInner",
MathKind::End => return Value::Ctor("None".to_string(), None),
};
Value::Ctor(
"Some".to_string(),
Some(Box::new(Value::Ctor(name.to_string(), None))),
)
}
fn prim_get_left_math_class_v006(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let m = as_math(interp, args.pop().unwrap())?;
let ctx = as_context(args.pop().unwrap())?;
Ok(make_math_class_option_value(left_math_kind(&ctx, &m)))
}
fn prim_get_left_math_class_v01(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let m = as_math_boxes(args.pop().unwrap())?;
let ctx = Context::initial(Length::ZERO);
Ok(make_math_class_option_value(left_math_kind(&ctx, &m)))
}
fn prim_get_right_math_class_v006(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let m = as_math(interp, args.pop().unwrap())?;
let ctx = as_context(args.pop().unwrap())?;
Ok(make_math_class_option_value(right_math_kind(&ctx, &m)))
}
fn prim_get_right_math_class_v01(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let m = as_math_boxes(args.pop().unwrap())?;
let ctx = Context::initial(Length::ZERO);
Ok(make_math_class_option_value(right_math_kind(&ctx, &m)))
}
fn prim_set_math_command(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mut ctx = as_context(args.pop().unwrap())?;
let cmd = args.pop().unwrap();
ctx.math_command = Some(interp.register_math_command(cmd));
Ok(Value::Context(Box::new(ctx)))
}
fn resolve_font_abbrev(abbrev: &str) -> FontKey {
let lower = abbrev.to_ascii_lowercase();
if lower.contains("bold") {
FONT_BOLD
} else if lower.contains("it") || lower.contains("obl") || lower.contains("slant") {
FONT_OBLIQUE
} else {
FONT_REGULAR
}
}
fn prim_set_math_font_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let abbrev = as_str(args.pop().unwrap())?;
let math_font = interp
.metrics
.resolve_font_abbrev(&abbrev)
.unwrap_or_else(|| resolve_font_abbrev(&abbrev));
Ok(Value::Context(Box::new(Context { math_font, ..ctx })))
}
fn prim_set_math_font_v01(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let math_font = as_font_key(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context { math_font, ..ctx })))
}
fn prim_load_single_font(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let abbrev = as_str(args.pop().unwrap())?;
let key = interp
.metrics
.resolve_font_abbrev(&abbrev)
.unwrap_or_else(|| resolve_font_abbrev(&abbrev));
Ok(Value::Font(key))
}
fn prim_space_between_maths_v006(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let _m2 = args.pop().unwrap();
let _m1 = args.pop().unwrap();
let _ctx = as_context(args.pop().unwrap())?;
Ok(Value::Ctor("None".to_string(), None))
}
fn prim_space_between_maths_v01(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let _m2 = as_math_boxes(args.pop().unwrap())?;
let _m1 = as_math_boxes(args.pop().unwrap())?;
let _ctx = as_context(args.pop().unwrap())?;
Ok(Value::Ctor("None".to_string(), None))
}
fn prim_raise_inline(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ib = as_inline_boxes(args.pop().unwrap())?;
let _len = as_length(args.pop().unwrap())?;
Ok(Value::InlineBoxes(ib))
}
fn prim_embed_block_breakable(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let bb = as_block_boxes(args.pop().unwrap())?;
let ctx = as_context(args.pop().unwrap())?;
let block = match make_embedded_block(ctx.paragraph_width, bb, false, true) {
Value::InlineBoxes(boxes) => boxes,
other => return Ok(other),
};
let forced = || {
HorzBox::Pure(PureHorzBox::Discretionary {
penalty: FORCED_BREAK_PENALTY,
pre_break: Vec::new(),
post_break: Vec::new(),
no_break: Vec::new(),
})
};
let mut out = vec![forced()];
out.extend(block);
out.push(forced());
Ok(Value::InlineBoxes(out))
}
fn prim_unite_path(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let p2 = as_path(args.pop().unwrap())?;
let p1 = as_path(args.pop().unwrap())?;
let mut subpaths = p1.subpaths;
subpaths.extend(p2.subpaths);
Ok(Value::Path(Path { subpaths }))
}
fn prim_set_min_gap_of_lines(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let _len = as_length(args.pop().unwrap())?;
Ok(Value::Context(Box::new(ctx)))
}
fn prim_embed_math_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let elems = as_math(interp, m)?;
let boxed = layout_math_value(interp, &ctx, &elems)?;
Ok(Value::InlineBoxes(vec![HorzBox::Pure(boxed)]))
}
fn prim_embed_math_v01(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let m = args.pop().unwrap();
let ctx = as_context(args.pop().unwrap())?;
let elems = as_math_boxes(m)?;
let boxed = layout_math_value(interp, &ctx, &elems)?;
Ok(Value::InlineBoxes(vec![HorzBox::Pure(boxed)]))
}
fn layout_math_value(
interp: &mut Interp,
ctx: &Context,
elems: &[Math],
) -> Result<PureHorzBox, EvalError> {
let (glyphs, rules, width, _left, _right) =
layout_math_list(interp, ctx, elems, ctx.font_size)?;
let mut height = Length::ZERO;
let mut depth = Length::ZERO;
for g in &glyphs {
height = height.max(g.dy + g.height);
depth = depth.max(g.depth - g.dy);
}
for r in &rules {
if let Some(((_, min_y), (_, max_y))) = graphics_bbox(r) {
height = height.max(max_y);
depth = depth.max(-min_y);
}
}
Ok(PureHorzBox::Math {
width,
height,
depth,
glyphs,
rules,
})
}
fn layout_math_list(
interp: &mut Interp,
ctx: &Context,
elems: &[Math],
size: Length,
) -> Result<
(
Vec<MathGlyph>,
Vec<GraphicsElem>,
Length,
MathKind,
MathKind,
),
EvalError,
> {
let mut laid: Vec<(
Vec<MathGlyph>,
Vec<GraphicsElem>,
Length,
MathKind,
MathKind,
)> = Vec::with_capacity(elems.len());
for atom in elems {
laid.push(layout_math_atom(interp, ctx, atom, size)?);
}
let mut glyphs = Vec::new();
let mut rules = Vec::new();
let mut x = Length::ZERO;
let mut last_kind: Option<MathKind> = None;
let mut first_kind: Option<MathKind> = None;
let in_script = math_in_script(ctx, size);
for (i, (atom_glyphs, atom_rules, atom_width, left_raw, right_raw)) in
laid.iter().cloned().enumerate()
{
let prev_raw = if i == 0 { MathKind::End } else { laid[i - 1].4 };
let next_raw = laid.get(i + 1).map_or(MathKind::End, |a| a.3);
let left = normalize_math_kind(prev_raw, next_raw, left_raw);
let right = normalize_math_kind(prev_raw, next_raw, right_raw);
if let Some(prev) = last_kind {
x += space_before(prev, left, in_script, size);
}
first_kind.get_or_insert(left);
let base_x = x;
for mut g in atom_glyphs {
g.dx = base_x + g.dx;
glyphs.push(g);
}
for r in &atom_rules {
rules.push(shift_graphics((base_x, Length::ZERO), r));
}
x = base_x + atom_width;
last_kind = Some(right);
}
let left = first_kind.unwrap_or(MathKind::Ord);
let right = last_kind.unwrap_or(MathKind::Ord);
Ok((glyphs, rules, x, left, right))
}
fn check_subscript(base: &[Math]) -> Option<(Vec<Math>, Vec<Math>)> {
let (last, head) = base.split_last()?;
match last {
Math::Sub(inner_base, sub_script) => {
let mut new_base = head.to_vec();
new_base.extend(inner_base.iter().cloned());
Some((sub_script.clone(), new_base))
}
Math::ChangeColor(color, inner) => {
let (sub_script, inner_new) = check_subscript(inner)?;
let mut new_base = head.to_vec();
new_base.push(Math::ChangeColor(color.clone(), inner_new));
Some((vec![Math::ChangeColor(color.clone(), sub_script)], new_base))
}
Math::ChangeCharClass(cls, inner) => {
let (sub_script, inner_new) = check_subscript(inner)?;
let mut new_base = head.to_vec();
new_base.push(Math::ChangeCharClass(cls.clone(), inner_new));
Some((
vec![Math::ChangeCharClass(cls.clone(), sub_script)],
new_base,
))
}
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
fn layout_pull_in_scripts(
interp: &mut Interp,
ctx: &Context,
head: &[Math],
cls1: MathKind,
cls2: MathKind,
resolver: &Value,
sub: Option<&[Math]>,
sup: Option<&[Math]>,
size: Length,
) -> Result<
(
Vec<MathGlyph>,
Vec<GraphicsElem>,
Length,
MathKind,
MathKind,
),
EvalError,
> {
let opt_math = |o: Option<&[Math]>| match o {
Some(m) => Value::Ctor(
"Some".to_string(),
Some(Box::new(Value::Math(Rc::new(m.to_vec())))),
),
None => Value::Ctor("None".to_string(), None),
};
let partial = interp.apply(resolver.clone(), opt_math(sub))?;
let result = interp.apply(partial, opt_math(sup))?;
let resolved = as_math(interp, result)?;
let mut items: Vec<Math> = head.to_vec();
items.push(Math::Group(cls1, cls2, (*resolved).clone()));
layout_math_list(interp, ctx, &items, size)
}
fn resolve_variant_char(interp: &Interp, ctx: &Context, c: char, size: Length) -> char {
let mapped = ctx
.math_variant_char_map
.get(&(c, ctx.math_char_class))
.copied()
.or_else(|| default_math_variant_char(ctx.math_char_class, c));
match mapped {
Some(m) if math_char_available(interp, ctx, m, size) => m,
_ => c,
}
}
fn make_paren_run(
interp: &mut Interp,
ctx: &Context,
paren: &Value,
h_in: Length,
d_in: Length,
axis: Length,
size: Length,
) -> Result<(Vec<MathGlyph>, Vec<GraphicsElem>, Length, Value), EvalError> {
let mut v = paren.clone();
if interp.version.math_is_split() {
let mut c2 = ctx.clone();
c2.font_size = size;
let args = [
Value::Length(h_in),
Value::Length(-d_in),
Value::Context(Box::new(c2)),
];
for a in args {
v = interp.apply(v, a)?;
}
} else {
let args = [
Value::Length(h_in),
Value::Length(-d_in),
Value::Length(axis),
Value::Length(size),
make_color_value(ctx.text_color),
];
for a in args {
v = interp.apply(v, a)?;
}
}
let (boxes_v, kernf) = match v {
Value::Tuple(mut items) if items.len() == 2 => {
let kernf = items.pop().unwrap();
(items.pop().unwrap(), kernf)
}
other => {
return eval_error(format!(
"math-paren: a paren closure must return (inline-boxes, length -> length), got {}",
other.type_name()
))
}
};
let boxes = as_inline_boxes(boxes_v)?;
let (glyphs, rules, width) = math_boxes_of_inline_boxes(&boxes);
Ok((glyphs, rules, width, kernf))
}
fn paren_variant_fallback(
interp: &mut Interp,
ctx: &Context,
parts: Vec<(Vec<MathGlyph>, Vec<GraphicsElem>, Length)>,
h_in: Length,
d_in: Length,
axis: Length,
size: Length,
) -> Result<
(
Vec<MathGlyph>,
Vec<GraphicsElem>,
Length,
MathKind,
MathKind,
),
EvalError,
> {
let target = (h_in - axis).max(axis + d_in) * 2.0;
let mut glyphs = Vec::new();
let mut rules = Vec::new();
let mut x = Length::ZERO;
push_delimiter_glyph(interp, ctx, '(', size, target, axis, &mut glyphs, &mut x)?;
for (i, (pg, pr, pw)) in parts.into_iter().enumerate() {
if i > 0 {
push_delimiter_glyph(interp, ctx, '|', size, target, axis, &mut glyphs, &mut x)?;
}
append_at(&mut glyphs, &mut rules, &mut x, pg, pr, pw);
}
push_delimiter_glyph(interp, ctx, ')', size, target, axis, &mut glyphs, &mut x)?;
Ok((glyphs, rules, x, MathKind::Open, MathKind::Close))
}
fn paren_trailing_kernf(
interp: &mut Interp,
ctx: &Context,
base: &[Math],
size: Length,
) -> Option<Value> {
let (r, h_in, d_in) = match base.last()? {
Math::Paren(_, r, inner) => {
let (g, ru, ..) = layout_math_list(interp, ctx, inner, size).ok()?;
let (h, d) = inner_ink_extent(&g, &ru);
(r, h, d)
}
Math::ParenWithMiddle(_, r, _, parts) => {
let mut h = Length::ZERO;
let mut d = Length::ZERO;
for p in parts {
let (g, ru, ..) = layout_math_list(interp, ctx, p, size).ok()?;
let (ph, pd) = inner_ink_extent(&g, &ru);
h = h.max(ph);
d = d.max(pd);
}
(r, h, d)
}
_ => return None,
};
let mc = MathC::of(interp, ctx);
let axis = mc.axis(size);
let (_, _, _, kernf) = make_paren_run(interp, ctx, r, h_in, d_in, axis, size).ok()?;
Some(kernf)
}
fn dense_kern(interp: &mut Interp, kernf: &Value, corrhgt: Length) -> Length {
match interp.apply(kernf.clone(), Value::Length(corrhgt)) {
Ok(Value::Length(l)) => -l,
_ => Length::ZERO,
}
}
fn layout_math_atom(
interp: &mut Interp,
ctx: &Context,
atom: &Math,
size: Length,
) -> Result<
(
Vec<MathGlyph>,
Vec<GraphicsElem>,
Length,
MathKind,
MathKind,
),
EvalError,
> {
match atom {
Math::Pure(MathElement::Char { class, big, chars })
| Math::Pure(MathElement::CharWithKern {
class, big, chars, ..
}) => {
let mut glyphs = Vec::new();
let mut x = Length::ZERO;
for c in chars.chars() {
if *big {
push_big_char_glyph(interp, ctx, c, size, &mut glyphs, &mut x)?;
} else {
push_char_glyph(interp, ctx, c, size, &mut glyphs, &mut x)?;
}
}
Ok((glyphs, Vec::new(), x, *class, *class))
}
Math::Pure(MathElement::VariantChar { class, style, .. }) => {
let text = match ctx.math_char_class {
MathCharClass::Italic => &style.italic,
MathCharClass::BoldItalic => &style.bold_italic,
MathCharClass::Roman => &style.roman,
MathCharClass::BoldRoman => &style.bold_roman,
MathCharClass::Script => &style.script,
MathCharClass::BoldScript => &style.bold_script,
MathCharClass::Fraktur => &style.fraktur,
MathCharClass::BoldFraktur => &style.bold_fraktur,
MathCharClass::DoubleStruck => &style.double_struck,
MathCharClass::SansSerif | MathCharClass::Typewriter => &style.roman,
MathCharClass::ItalicSansSerif => &style.italic,
MathCharClass::BoldSansSerif => &style.bold_roman,
MathCharClass::BoldItalicSansSerif => &style.bold_italic,
};
let mut glyphs = Vec::new();
let mut x = Length::ZERO;
for c in text.chars() {
push_char_glyph(interp, ctx, c, size, &mut glyphs, &mut x)?;
}
Ok((glyphs, Vec::new(), x, *class, *class))
}
Math::Pure(MathElement::VariantCharPending(s)) => {
let mut glyphs = Vec::new();
let mut x = Length::ZERO;
if let Some((target, kind)) = ctx.math_class_map.get(s.as_str()) {
let kind = *kind;
let all_renderable = target
.chars()
.all(|c| math_char_available(interp, ctx, c, size));
let chosen = if all_renderable {
target.clone()
} else {
s.clone()
};
for c in chosen.chars() {
push_char_glyph(interp, ctx, c, size, &mut glyphs, &mut x)?;
}
return Ok((glyphs, Vec::new(), x, kind, kind));
}
for c in s.chars() {
let chosen = resolve_variant_char(interp, ctx, c, size);
push_char_glyph(interp, ctx, chosen, size, &mut glyphs, &mut x)?;
}
Ok((glyphs, Vec::new(), x, MathKind::Ord, MathKind::Ord))
}
Math::Pure(MathElement::EmbeddedText { class, body }) => {
let v = interp.apply((**body).clone(), Value::Context(Box::new(ctx.clone())))?;
let boxes = as_inline_boxes(v)?;
let (glyphs, rules, width) = math_boxes_of_inline_boxes(&boxes);
Ok((glyphs, rules, width, *class, *class))
}
Math::Pure(MathElement::EmbeddedBoxes { class, boxes }) => {
let (glyphs, rules, width) = math_boxes_of_inline_boxes(boxes);
Ok((glyphs, rules, width, *class, *class))
}
Math::Group(cls1, cls2, inner) => {
let (glyphs, rules, width, _, _) = layout_math_list(interp, ctx, inner, size)?;
Ok((glyphs, rules, width, *cls1, *cls2))
}
Math::Sup(base, script) => {
if let Some((sub_script, new_base)) = check_subscript(base) {
if let Some((Math::PullInScripts(cls1, cls2, resolver), head)) =
new_base.split_last()
{
return layout_pull_in_scripts(
interp,
ctx,
head,
*cls1,
*cls2,
resolver,
Some(&sub_script),
Some(script),
size,
);
}
let (mut glyphs, mut rules, base_width, left, _) =
layout_math_list(interp, ctx, &new_base, size)?;
let mc = MathC::of(interp, ctx);
let script_size = size * mc.script_scale();
let paren_kernf = paren_trailing_kernf(interp, ctx, &new_base, size);
let sub_ctx = Context {
math_cramped: true,
..ctx.clone()
};
let (sub_glyphs, sub_rules, sub_width, _, _) =
layout_math_list(interp, &sub_ctx, &sub_script, script_size)?;
let (sup_glyphs, sup_rules, sup_width, _, _) =
layout_math_list(interp, ctx, script, script_size)?;
let (h_base, d_base) = inner_ink_extent(&glyphs, &rules);
let (_, d_sup) = inner_ink_extent(&sup_glyphs, &sup_rules);
let (h_sub, _) = inner_ink_extent(&sub_glyphs, &sub_rules);
let sup_shift_raw = mc.sup_shift_clamped(ctx.font_size, h_base, d_sup);
let sub_shift_raw = mc.sub_shift_clamped(ctx.font_size, d_base, h_sub);
let (sup_shift, sub_shift) = mc.correct_script_gap(
ctx.font_size,
d_sup,
h_sub,
sup_shift_raw,
sub_shift_raw,
);
let kern = match &paren_kernf {
Some(kf) => dense_kern(interp, kf, sup_shift - d_sup),
None => superscript_kern(
interp,
ctx,
size,
script_size,
&glyphs,
&sup_glyphs,
sup_shift,
h_base,
d_sup,
),
};
let sub_kern = paren_kernf
.as_ref()
.map(|kf| dense_kern(interp, kf, h_sub - d_base))
.unwrap_or(Length::ZERO);
shift_and_append(
&mut glyphs,
&mut rules,
sub_glyphs,
sub_rules,
base_width + sub_kern,
-sub_shift,
);
shift_and_append(
&mut glyphs,
&mut rules,
sup_glyphs,
sup_rules,
base_width + kern,
sup_shift,
);
return Ok((
glyphs,
rules,
base_width + (sub_kern + sub_width).max(kern + sup_width),
left,
MathKind::Ord,
));
}
if let Some((Math::PullInScripts(cls1, cls2, resolver), head)) = base.split_last() {
return layout_pull_in_scripts(
interp,
ctx,
head,
*cls1,
*cls2,
resolver,
None,
Some(script),
size,
);
}
let (mut glyphs, mut rules, base_width, left, _) =
layout_math_list(interp, ctx, base, size)?;
let mc = MathC::of(interp, ctx);
let script_size = size * mc.script_scale();
let (script_glyphs, script_rules, script_width, _, _) =
layout_math_list(interp, ctx, script, script_size)?;
let (h_base, _) = inner_ink_extent(&glyphs, &rules);
let (_, d_sup) = inner_ink_extent(&script_glyphs, &script_rules);
let sup_shift = mc.sup_shift_clamped(ctx.font_size, h_base, d_sup);
let kern = match paren_trailing_kernf(interp, ctx, base, size) {
Some(kf) => dense_kern(interp, &kf, sup_shift - d_sup),
None => superscript_kern(
interp,
ctx,
size,
script_size,
&glyphs,
&script_glyphs,
sup_shift,
h_base,
d_sup,
),
};
shift_and_append(
&mut glyphs,
&mut rules,
script_glyphs,
script_rules,
base_width + kern,
sup_shift,
);
Ok((
glyphs,
rules,
base_width + kern + script_width,
left,
MathKind::Ord,
))
}
Math::Sub(base, script) => {
if let Some((Math::PullInScripts(cls1, cls2, resolver), head)) = base.split_last() {
return layout_pull_in_scripts(
interp,
ctx,
head,
*cls1,
*cls2,
resolver,
Some(script),
None,
size,
);
}
let (mut glyphs, mut rules, base_width, left, _) =
layout_math_list(interp, ctx, base, size)?;
let mc = MathC::of(interp, ctx);
let script_size = size * mc.script_scale();
let sub_ctx = Context {
math_cramped: true,
..ctx.clone()
};
let (script_glyphs, script_rules, script_width, _, _) =
layout_math_list(interp, &sub_ctx, script, script_size)?;
let (_, d_base) = inner_ink_extent(&glyphs, &rules);
let (h_sub, _) = inner_ink_extent(&script_glyphs, &script_rules);
let sub_shift = mc.sub_shift_clamped(ctx.font_size, d_base, h_sub);
let kern = match paren_trailing_kernf(interp, ctx, base, size) {
Some(kf) => dense_kern(interp, &kf, h_sub - d_base),
None => Length::ZERO,
};
shift_and_append(
&mut glyphs,
&mut rules,
script_glyphs,
script_rules,
base_width + kern,
-sub_shift,
);
Ok((
glyphs,
rules,
base_width + kern + script_width,
left,
MathKind::Ord,
))
}
Math::ChangeColor(_, inner) => {
let (glyphs, rules, width, left, right) = layout_math_list(interp, ctx, inner, size)?;
Ok((glyphs, rules, width, left, right))
}
Math::ChangeCharClass(cls, inner) => {
let ctx2 = Context {
math_char_class: *cls,
..ctx.clone()
};
let (glyphs, rules, width, left, right) = layout_math_list(interp, &ctx2, inner, size)?;
Ok((glyphs, rules, width, left, right))
}
Math::Fraction(num, den) => {
let (num_glyphs, num_rules, num_w, ..) = layout_math_list(interp, ctx, num, size)?;
let den_ctx = Context {
math_cramped: true,
..ctx.clone()
};
let (den_glyphs, den_rules, den_w, ..) = layout_math_list(interp, &den_ctx, den, size)?;
let w = num_w.max(den_w);
let num_dx = (w - num_w) * 0.5;
let den_dx = (w - den_w) * 0.5;
let (_, d_numer) = inner_ink_extent(&num_glyphs, &num_rules);
let (h_denom, _) = inner_ink_extent(&den_glyphs, &den_rules);
let mc = MathC::of(interp, ctx);
let numer_shift = mc.frac_numer_shift(size, d_numer);
let denom_shift = mc.frac_denom_shift(size, h_denom);
let axis = mc.axis(size);
let rule = mc.frac_rule(size);
let mut glyphs = Vec::new();
let mut rules = Vec::new();
shift_and_append(
&mut glyphs,
&mut rules,
num_glyphs,
num_rules,
num_dx,
numer_shift,
);
shift_and_append(
&mut glyphs,
&mut rules,
den_glyphs,
den_rules,
den_dx,
denom_shift,
);
rules.push(GraphicsElem::Fill(
ctx.text_color,
rect_path((Length::ZERO, axis), (w, rule)),
));
Ok((glyphs, rules, w, MathKind::Inner, MathKind::Inner))
}
Math::Radical(_degree, inner) => {
let radicand_ctx = Context {
math_cramped: true,
..ctx.clone()
};
let (inner_glyphs, inner_rules, inner_w, ..) =
layout_math_list(interp, &radicand_ctx, inner, size)?;
let (h_cont, d_cont) = inner_ink_extent(&inner_glyphs, &inner_rules);
let mc = MathC::of(interp, ctx);
let (h_bar, t_bar, l_extra) = mc.radical_bar_metrics(size, h_cont);
let (sign_path, sign_w, _nonnegdpt) = radical_sign_geometry(size, h_bar, t_bar, d_cont);
let mut rules = vec![GraphicsElem::Fill(ctx.text_color, sign_path)];
rules.push(GraphicsElem::Fill(
ctx.text_color,
rect_path((sign_w, h_bar), (inner_w, t_bar)),
));
rules.push(GraphicsElem::Fill(
ctx.text_color,
Path {
subpaths: vec![Subpath {
start: (Length::ZERO, h_bar + t_bar + l_extra),
segs: Vec::new(),
closing: Closing::Open,
}],
},
));
let mut glyphs = Vec::new();
for mut g in inner_glyphs {
g.dx = sign_w + g.dx;
glyphs.push(g);
}
for r in &inner_rules {
rules.push(shift_graphics((sign_w, Length::ZERO), r));
}
Ok((
glyphs,
rules,
sign_w + inner_w,
MathKind::Inner,
MathKind::Inner,
))
}
Math::Paren(l, r, inner) => {
let (inner_glyphs, inner_rules, inner_w, ..) =
layout_math_list(interp, ctx, inner, size)?;
let (h_in, d_in) = inner_ink_extent(&inner_glyphs, &inner_rules);
let mc = MathC::of(interp, ctx);
let axis = mc.axis(size);
let closure_route =
make_paren_run(interp, ctx, l, h_in, d_in, axis, size).and_then(|left| {
let right = make_paren_run(interp, ctx, r, h_in, d_in, axis, size)?;
Ok((left, right))
});
match closure_route {
Ok(((lg, lr, lw, _), (rg, rr, rw, _))) => {
let mut glyphs = Vec::new();
let mut rules = Vec::new();
let mut x = Length::ZERO;
append_at(&mut glyphs, &mut rules, &mut x, lg, lr, lw);
append_at(
&mut glyphs,
&mut rules,
&mut x,
inner_glyphs,
inner_rules,
inner_w,
);
append_at(&mut glyphs, &mut rules, &mut x, rg, rr, rw);
Ok((glyphs, rules, x, MathKind::Open, MathKind::Close))
}
Err(_) => paren_variant_fallback(
interp,
ctx,
vec![(inner_glyphs, inner_rules, inner_w)],
h_in,
d_in,
axis,
size,
),
}
}
Math::ParenWithMiddle(l, r, m, mlstlst) => {
let mut parts = Vec::with_capacity(mlstlst.len());
let mut h_in = Length::ZERO;
let mut d_in = Length::ZERO;
for part in mlstlst {
let (part_glyphs, part_rules, part_w, ..) =
layout_math_list(interp, ctx, part, size)?;
let (h, d) = inner_ink_extent(&part_glyphs, &part_rules);
h_in = h_in.max(h);
d_in = d_in.max(d);
parts.push((part_glyphs, part_rules, part_w));
}
let mc = MathC::of(interp, ctx);
let axis = mc.axis(size);
let closure_route =
make_paren_run(interp, ctx, l, h_in, d_in, axis, size).and_then(|left| {
let right = make_paren_run(interp, ctx, r, h_in, d_in, axis, size)?;
let middle = make_paren_run(interp, ctx, m, h_in, d_in, axis, size)?;
Ok((left, right, middle))
});
match closure_route {
Ok(((lg, lr, lw, _), (rg, rr, rw, _), (mg, mr, mw, _))) => {
let mut glyphs = Vec::new();
let mut rules = Vec::new();
let mut x = Length::ZERO;
append_at(&mut glyphs, &mut rules, &mut x, lg, lr, lw);
for (i, (part_glyphs, part_rules, part_w)) in parts.into_iter().enumerate() {
if i > 0 {
append_at(&mut glyphs, &mut rules, &mut x, mg.clone(), mr.clone(), mw);
}
append_at(
&mut glyphs,
&mut rules,
&mut x,
part_glyphs,
part_rules,
part_w,
);
}
append_at(&mut glyphs, &mut rules, &mut x, rg, rr, rw);
Ok((glyphs, rules, x, MathKind::Open, MathKind::Close))
}
Err(_) => paren_variant_fallback(interp, ctx, parts, h_in, d_in, axis, size),
}
}
Math::UpperLimit(base, upper) => {
let (mut glyphs, mut rules, base_width, left, right) =
layout_math_list(interp, ctx, base, size)?;
let mc = MathC::of(interp, ctx);
let script_size = size * mc.script_scale();
let (script_glyphs, script_rules, script_width, _, _) =
layout_math_list(interp, ctx, upper, script_size)?;
let (h_base, _) = inner_ink_extent(&glyphs, &rules);
let (_, d_up) = inner_ink_extent(&script_glyphs, &script_rules);
let up_shift = mc.upper_limit_shift(ctx.font_size, h_base, d_up);
let (base_dx, script_dx) = center_offsets(base_width, script_width);
shift_existing(&mut glyphs, &mut rules, base_dx);
shift_and_append(
&mut glyphs,
&mut rules,
script_glyphs,
script_rules,
script_dx,
up_shift,
);
Ok((glyphs, rules, base_width.max(script_width), left, right))
}
Math::LowerLimit(base, lower) => {
let (mut glyphs, mut rules, base_width, left, right) =
layout_math_list(interp, ctx, base, size)?;
let mc = MathC::of(interp, ctx);
let script_size = size * mc.script_scale();
let (script_glyphs, script_rules, script_width, _, _) =
layout_math_list(interp, ctx, lower, script_size)?;
let (_, d_base) = inner_ink_extent(&glyphs, &rules);
let (h_low, _) = inner_ink_extent(&script_glyphs, &script_rules);
let low_shift = mc.lower_limit_shift(ctx.font_size, d_base, h_low);
let (base_dx, script_dx) = center_offsets(base_width, script_width);
shift_existing(&mut glyphs, &mut rules, base_dx);
shift_and_append(
&mut glyphs,
&mut rules,
script_glyphs,
script_rules,
script_dx,
-low_shift,
);
Ok((glyphs, rules, base_width.max(script_width), left, right))
}
Math::PullInScripts(cls1, cls2, resolver) => {
layout_pull_in_scripts(interp, ctx, &[], *cls1, *cls2, resolver, None, None, size)
}
Math::WithContext(stored, inner) => {
layout_math_list(interp, stored, inner, stored.font_size)
}
}
}
fn append_at(
out_glyphs: &mut Vec<MathGlyph>,
out_rules: &mut Vec<GraphicsElem>,
x: &mut Length,
glyphs: Vec<MathGlyph>,
rules: Vec<GraphicsElem>,
width: Length,
) {
let base_x = *x;
for mut g in glyphs {
g.dx = base_x + g.dx;
out_glyphs.push(g);
}
for r in &rules {
out_rules.push(shift_graphics((base_x, Length::ZERO), r));
}
*x = base_x + width;
}
fn center_offsets(base_width: Length, script_width: Length) -> (Length, Length) {
if base_width < script_width {
((script_width - base_width) * 0.5, Length::ZERO)
} else {
(Length::ZERO, (base_width - script_width) * 0.5)
}
}
fn shift_existing(glyphs: &mut [MathGlyph], rules: &mut [GraphicsElem], dx: Length) {
if dx == Length::ZERO {
return;
}
for g in glyphs.iter_mut() {
g.dx = g.dx + dx;
}
for r in rules.iter_mut() {
*r = shift_graphics((dx, Length::ZERO), r);
}
}
fn shift_and_append(
out_glyphs: &mut Vec<MathGlyph>,
out_rules: &mut Vec<GraphicsElem>,
glyphs: Vec<MathGlyph>,
rules: Vec<GraphicsElem>,
dx_shift: Length,
dy_shift: Length,
) {
for mut g in glyphs {
g.dx = dx_shift + g.dx;
g.dy = g.dy + dy_shift;
out_glyphs.push(g);
}
for r in &rules {
out_rules.push(shift_graphics((dx_shift, dy_shift), r));
}
}
fn rect_path(origin: Point, size: (Length, Length)) -> Path {
let (x, y) = origin;
let (w, h) = size;
Path {
subpaths: vec![Subpath {
start: (x, y),
segs: vec![
PathSeg::Line((x + w, y)),
PathSeg::Line((x + w, y + h)),
PathSeg::Line((x, y + h)),
],
closing: Closing::Line,
}],
}
}
fn radical_sign_geometry(
size: Length,
hgt_bar: Length,
t_bar: Length,
dpt: Length,
) -> (Path, Length, Length) {
let w_m = size * 0.02;
let w1 = size * 0.1;
let w2 = size * 0.15;
let w3 = size * 0.4;
let w_a = size * 0.18;
let h1 = size * 0.3;
let h2 = size * 0.375;
let nonnegdpt = dpt + size * 0.1;
let l_r = hgt_bar + nonnegdpt;
let wid = w_m + w1 + w2 + w3;
let a1 = (h2 - h1) / w1;
let a2 = h2 / w2;
let a3 = l_r / w3;
let t1 = t_bar * (1.0 + a1 * a1).sqrt();
let t3 = t_bar * (((1.0 + a3 * a3).sqrt() - 1.0) / a3);
let h_a = h1 + t1 + w_a * a1;
let w_b = (l_r + t_bar - h_a - (w1 + w2 + w3 - t3 - w_a) * a3) * (-1.0 / (a2 + a3));
let h_b = h_a - w_b * a2;
let path = Path {
subpaths: vec![Subpath {
start: (wid, hgt_bar),
segs: vec![
PathSeg::Line((w_m + w1 + w2, -nonnegdpt)),
PathSeg::Line((w_m + w1, -nonnegdpt + h2)),
PathSeg::Line((w_m, -nonnegdpt + h1)),
PathSeg::Line((w_m, -nonnegdpt + h1 + t1)),
PathSeg::Line((w_m + w_a, -nonnegdpt + h_a)),
PathSeg::Line((w_m + w_a + w_b, -nonnegdpt + h_b)),
PathSeg::Line((wid - t3, hgt_bar + t_bar)),
PathSeg::Line((wid, hgt_bar + t_bar)),
],
closing: Closing::Line,
}],
};
(path, wid, nonnegdpt)
}
#[allow(dead_code)]
fn math_glyphs_of_inline_boxes(boxes: &[HorzBox]) -> (Vec<MathGlyph>, Length) {
fn go(pure: &PureHorzBox, out: &mut Vec<MathGlyph>, x: &mut Length) {
match pure {
PureHorzBox::InnerString {
info,
text,
width,
height,
depth,
} => {
out.push(MathGlyph {
info: info.clone(),
text: text.clone(),
gid: None,
dx: *x,
dy: Length::ZERO,
width: *width,
height: *height,
depth: *depth,
});
*x += *width;
}
PureHorzBox::OuterEmpty { natural, .. } => *x += *natural,
PureHorzBox::OuterFil => {}
PureHorzBox::FixedEmpty { width } => *x += *width,
PureHorzBox::Image { width, .. } => *x += *width,
PureHorzBox::Discretionary { no_break, .. } => {
for p in no_break {
go(p, out, x);
}
}
PureHorzBox::Graphics { width, .. } => *x += *width,
PureHorzBox::GraphicsOuter { width, .. } => *x += *width,
PureHorzBox::Math { width, glyphs, .. } => {
for g in glyphs {
let mut g = g.clone();
g.dx = *x + g.dx;
out.push(g);
}
*x += *width;
}
PureHorzBox::HookPageBreak { .. } => {}
PureHorzBox::Tabular(tab) => *x += tab.width,
PureHorzBox::EmbeddedBlock { width, .. } => *x += *width,
PureHorzBox::Frame { width, .. } => *x += *width,
PureHorzBox::FrameMarker { .. } => {}
PureHorzBox::InlineFrameMarker { .. } => {}
PureHorzBox::Footnote { .. } => {}
PureHorzBox::InlineMark(_) => {}
}
}
let mut glyphs = Vec::new();
let mut x = Length::ZERO;
for HorzBox::Pure(p) in boxes {
go(p, &mut glyphs, &mut x);
}
(glyphs, x)
}
fn math_boxes_of_inline_boxes(boxes: &[HorzBox]) -> (Vec<MathGlyph>, Vec<GraphicsElem>, Length) {
fn go(
pure: &PureHorzBox,
out: &mut Vec<MathGlyph>,
rules: &mut Vec<GraphicsElem>,
x: &mut Length,
dy: Length,
) {
match pure {
PureHorzBox::InnerString {
info,
text,
width,
height,
depth,
} => {
out.push(MathGlyph {
info: info.clone(),
text: text.clone(),
gid: None,
dx: *x,
dy,
width: *width,
height: *height,
depth: *depth,
});
*x += *width;
}
PureHorzBox::OuterEmpty { natural, .. } => *x += *natural,
PureHorzBox::OuterFil => {}
PureHorzBox::FixedEmpty { width } => *x += *width,
PureHorzBox::Image { width, .. } => *x += *width,
PureHorzBox::Discretionary { no_break, .. } => {
for p in no_break {
go(p, out, rules, x, dy);
}
}
PureHorzBox::Graphics { width, elems, .. } => {
for e in elems {
rules.push(shift_graphics((*x, dy), e));
}
*x += *width;
}
PureHorzBox::GraphicsOuter { width, .. } => *x += *width,
PureHorzBox::Math {
width,
glyphs,
rules: inner_rules,
..
} => {
for g in glyphs {
let mut g = g.clone();
g.dx = *x + g.dx;
g.dy += dy;
out.push(g);
}
for r in inner_rules {
rules.push(shift_graphics((*x, dy), r));
}
*x += *width;
}
PureHorzBox::HookPageBreak { .. } => {}
PureHorzBox::Tabular(tab) => *x += tab.width,
PureHorzBox::EmbeddedBlock {
width,
block,
anchor_last,
..
} => {
let placed = place_block_at((Length::ZERO, Length::ZERO), block.clone());
let anchor = if *anchor_last {
placed.last()
} else {
placed.first()
};
if let Some(anchor) = anchor {
let anchor_y = anchor.baseline_y;
for line in &placed {
let line_dy = dy - (line.baseline_y - anchor_y);
for (cdx, cbx) in &line.contents {
let mut cx = *x + line.x + *cdx;
go(cbx, out, rules, &mut cx, line_dy);
}
}
}
*x += *width;
}
PureHorzBox::Frame { width, .. } => *x += *width,
PureHorzBox::FrameMarker { .. } => {}
PureHorzBox::InlineFrameMarker { .. } => {}
PureHorzBox::Footnote { .. } => {}
PureHorzBox::InlineMark(_) => {}
}
}
let mut glyphs = Vec::new();
let mut rules = Vec::new();
let mut x = Length::ZERO;
for HorzBox::Pure(p) in boxes {
go(p, &mut glyphs, &mut rules, &mut x, Length::ZERO);
}
(glyphs, rules, x)
}
fn make_color_value(c: Color) -> Value {
match c {
Color::Gray(g) => Value::Ctor("Gray".to_string(), Some(Box::new(Value::Float(g)))),
Color::Rgb(r, g, b) => Value::Ctor(
"RGB".to_string(),
Some(Box::new(Value::Tuple(vec![
Value::Float(r),
Value::Float(g),
Value::Float(b),
]))),
),
Color::Cmyk(c, m, y, k) => Value::Ctor(
"CMYK".to_string(),
Some(Box::new(Value::Tuple(vec![
Value::Float(c),
Value::Float(m),
Value::Float(y),
Value::Float(k),
]))),
),
}
}
fn as_font(v: Value) -> Result<(String, f64, f64), EvalError> {
match v {
Value::Tuple(vs) if vs.len() == 3 => {
let mut it = vs.into_iter();
let abbrev = as_str(it.next().unwrap())?;
let size_ratio = as_float(it.next().unwrap())?;
let rising_ratio = as_float(it.next().unwrap())?;
Ok((abbrev, size_ratio, rising_ratio))
}
other => eval_error(format!(
"expected a font (string * float * float), got {}",
other.type_name()
)),
}
}
fn as_font_with_ratio(v: Value) -> Result<(FontKey, f64, f64), EvalError> {
match v {
Value::Tuple(vs) if vs.len() == 3 => {
let mut it = vs.into_iter();
let key = as_font_key(it.next().unwrap())?;
let size_ratio = as_float(it.next().unwrap())?;
let rising_ratio = as_float(it.next().unwrap())?;
Ok((key, size_ratio, rising_ratio))
}
other => eval_error(format!(
"expected a font (font * float * float), got {}",
other.type_name()
)),
}
}
fn as_font_key(v: Value) -> Result<FontKey, EvalError> {
match v {
Value::Font(key) => Ok(key),
other => eval_error(format!("expected a font, got {}", other.type_name())),
}
}
fn prim_set_text_color(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let color = as_color(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
text_color: color,
..ctx
})))
}
fn prim_get_text_color(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
Ok(make_color_value(ctx.text_color))
}
fn prim_set_hyphen_penalty(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let n = as_int(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
hyphen_badness: n,
..ctx
})))
}
fn prim_set_hyphen_min(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let right = as_int(args.pop().unwrap())?.max(0);
let left = as_int(args.pop().unwrap())?.max(0);
Ok(Value::Context(Box::new(Context {
left_hyphen_min: left,
right_hyphen_min: right,
..ctx
})))
}
fn prim_set_space_ratio(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let stretch = as_float(args.pop().unwrap())?.max(0.0);
let shrink = as_float(args.pop().unwrap())?.max(0.0);
let natural = as_float(args.pop().unwrap())?.max(0.0);
Ok(Value::Context(Box::new(Context {
space_natural: natural,
space_shrink: shrink,
space_stretch: stretch,
..ctx
})))
}
fn prim_set_space_ratio_between_scripts(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let script2 = as_script(args.pop().unwrap())?;
let script1 = as_script(args.pop().unwrap())?;
let _stretch = as_float(args.pop().unwrap())?;
let _shrink = as_float(args.pop().unwrap())?;
let natural = as_float(args.pop().unwrap())?.max(0.0);
let mut script_space_map = ctx.script_space_map;
script_space_map[script1 as usize][script2 as usize] = natural;
Ok(Value::Context(Box::new(Context {
script_space_map,
..ctx
})))
}
fn prim_split_into_lines(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let s = as_str(args.pop().unwrap())?;
let mut out = Vec::new();
for line in s.split('\n') {
let indent = line.chars().take_while(|c| *c == ' ').count();
let rest: String = line.chars().skip(indent).collect();
out.push(Value::Tuple(vec![
Value::Int(indent as i64),
Value::Str(rest),
]));
}
Ok(Value::List(out))
}
fn indent_left(block: Vec<VertBox>, pad_l: Length) -> Vec<VertBox> {
block
.into_iter()
.map(|vb| match vb {
VertBox::Line {
height,
depth,
leading,
contents,
} => VertBox::Line {
height,
depth,
leading,
contents: contents
.into_iter()
.map(|(x, bx)| (x + pad_l, bx))
.collect(),
},
other => other,
})
.collect()
}
fn strip_outer_margins(body: &mut Vec<VertBox>) {
let is_margin = |vb: &VertBox| matches!(vb, VertBox::Skip(_) | VertBox::ParagTop(_));
if body.first().is_some_and(is_margin) {
body.remove(0);
}
if body.last().is_some_and(is_margin) {
body.pop();
}
}
fn prim_block_frame_breakable(
interp: &mut Interp,
version: RustyfiVersion,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let k = args.pop().unwrap();
let decoset = as_decoset(args.pop().unwrap())?;
let (pad_l, pad_r, pad_t, pad_b) = as_paddings(args.pop().unwrap())?;
let ctx = as_context(args.pop().unwrap())?;
let id = DecoId(interp.decos.len());
interp.decos.push(DecoEntry::Block {
pads: Paddings {
l: pad_l,
r: pad_r,
t: pad_t,
b: pad_b,
},
width: ctx.paragraph_width,
decoset,
version,
});
let inner_ctx = Context {
paragraph_width: ctx.paragraph_width - pad_l - pad_r,
..ctx
};
let inner = as_block_boxes(interp.apply(k, Value::Context(Box::new(inner_ctx)))?)?;
let mut indented = indent_left(inner, pad_l);
strip_outer_margins(&mut indented);
let mut out = Vec::with_capacity(indented.len() + 6);
out.push(VertBox::Skip(ctx.paragraph_top));
out.push(VertBox::FrameStart(id));
out.push(VertBox::FramePad(pad_t));
out.extend(indented);
out.push(VertBox::FramePad(pad_b));
out.push(VertBox::FrameEnd(id));
out.push(VertBox::Skip(ctx.paragraph_bottom));
Ok(Value::BlockBoxes(out))
}
fn make_embedded_block(
width: Length,
block: Vec<VertBox>,
anchor_last: bool,
breakable: bool,
) -> Value {
let first_line_height = block.iter().find_map(|vb| match vb {
VertBox::Line { height, .. } => Some(*height),
_ => None,
});
let last_line_depth = block.iter().rev().find_map(|vb| match vb {
VertBox::Line { depth, .. } => Some(*depth),
_ => None,
});
let (height, depth) = match (first_line_height, last_line_depth) {
(Some(first_h), Some(last_d)) => {
let placed = place_block_at((Length::ZERO, Length::ZERO), block.clone());
let last_baseline = placed.last().map(|l| l.baseline_y).unwrap_or(first_h);
let bottom_edge = last_baseline + last_d;
if anchor_last {
(last_baseline, last_d)
} else {
(first_h, bottom_edge - first_h)
}
}
_ => measure_block(&block),
};
Value::InlineBoxes(vec![HorzBox::Pure(PureHorzBox::EmbeddedBlock {
width,
height,
depth,
block,
anchor_last,
breakable,
})])
}
fn prim_embed_block_top(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let k = args.pop().unwrap();
let wid = as_length(args.pop().unwrap())?;
let ctx = as_context(args.pop().unwrap())?;
let inner_ctx = Context {
paragraph_width: wid,
..ctx
};
let block = as_block_boxes(interp.apply(k, Value::Context(Box::new(inner_ctx)))?)?;
Ok(make_embedded_block(wid, block, false, false))
}
fn prim_embed_block_bottom(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let k = args.pop().unwrap();
let wid = as_length(args.pop().unwrap())?;
let ctx = as_context(args.pop().unwrap())?;
let inner_ctx = Context {
paragraph_width: wid,
..ctx
};
let block = as_block_boxes(interp.apply(k, Value::Context(Box::new(inner_ctx)))?)?;
Ok(make_embedded_block(wid, block, true, false))
}
fn prim_line_stack_bottom(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
line_stack(args.pop().unwrap(), true)
}
fn prim_line_stack_top(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
line_stack(args.pop().unwrap(), false)
}
fn line_stack(arg: Value, anchor_last: bool) -> Result<Value, EvalError> {
let hblstlst = as_list(arg)?
.into_iter()
.map(as_inline_boxes)
.collect::<Result<Vec<_>, _>>()?;
let wid = hblstlst
.iter()
.map(|hbs| natural_metrics(hbs).0)
.fold(Length::ZERO, |acc, w| if w > acc { w } else { acc });
let mut block = Vec::with_capacity(hblstlst.len());
let mut prev_depth = Length::ZERO;
for (idx, hbs) in hblstlst.into_iter().enumerate() {
let (contents, height, depth) = fit_cell(hbs, wid);
let leading = if idx == 0 {
height + depth
} else {
prev_depth + height
};
block.push(VertBox::Line {
height,
depth,
leading,
contents,
});
prev_depth = depth;
}
Ok(make_embedded_block(wid, block, anchor_last, false))
}
fn prim_add_footnote(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let block = as_block_boxes(args.pop().unwrap())?;
Ok(Value::InlineBoxes(vec![HorzBox::Pure(
PureHorzBox::Footnote { block },
)]))
}
fn prim_set_font_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mut ctx = as_context(args.pop().unwrap())?;
let (abbrev, size_ratio, rising_ratio) = as_font(args.pop().unwrap())?;
let script = as_script(args.pop().unwrap())?;
let font = interp
.metrics
.resolve_font_abbrev(&abbrev)
.unwrap_or_else(|| resolve_font_abbrev(&abbrev));
install_script_font(&mut ctx, script, font, size_ratio, rising_ratio);
Ok(Value::Context(Box::new(ctx)))
}
fn prim_set_font_v01(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let mut ctx = as_context(args.pop().unwrap())?;
let (font, size_ratio, rising_ratio) = as_font_with_ratio(args.pop().unwrap())?;
let script = as_script(args.pop().unwrap())?;
install_script_font(&mut ctx, script, font, size_ratio, rising_ratio);
Ok(Value::Context(Box::new(ctx)))
}
fn prim_get_font_v006(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let script = as_script(args.pop().unwrap())?;
let sf = script_font(&ctx, script);
let abbrev = interp.metrics.font_abbrev(sf.font).unwrap_or_default();
Ok(Value::Tuple(vec![
Value::Str(abbrev),
Value::Float(sf.ratio),
Value::Float(sf.rising),
]))
}
fn prim_get_font_v01(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let script = as_script(args.pop().unwrap())?;
let sf = script_font(&ctx, script);
Ok(Value::Tuple(vec![
Value::Font(sf.font),
Value::Float(sf.ratio),
Value::Float(sf.rising),
]))
}
fn install_script_font(ctx: &mut Context, script: Script, font: FontKey, ratio: f64, rising: f64) {
ctx.font_scheme[script as usize] = ScriptFont {
font,
ratio,
rising,
};
if script == Script::Latin {
ctx.font = font;
}
}
fn prim_set_code_text_command(
interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let mut ctx = as_context(args.pop().unwrap())?;
let cmd = args.pop().unwrap();
ctx.code_text_command = Some(interp.register_math_command(cmd));
Ok(Value::Context(Box::new(ctx)))
}
fn prim_get_natural_length(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let bb = as_block_boxes(args.pop().unwrap())?;
let (height, depth) = measure_block(&bb);
Ok(Value::Length(height + depth))
}
fn prim_set_dominant_wide_script(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let dominant_wide_script = as_script(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
dominant_wide_script,
..ctx
})))
}
fn prim_set_dominant_narrow_script(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let dominant_narrow_script = as_script(args.pop().unwrap())?;
Ok(Value::Context(Box::new(Context {
dominant_narrow_script,
..ctx
})))
}
fn prim_set_language(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let langsys = as_language(args.pop().unwrap())?;
let script = as_script(args.pop().unwrap())?;
let mut langsys_scheme = ctx.langsys_scheme;
langsys_scheme[script as usize] = langsys;
Ok(Value::Context(Box::new(Context {
langsys_scheme,
..ctx
})))
}
fn prim_get_dominant_wide_script(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
Ok(make_script_value(ctx.dominant_wide_script))
}
fn prim_get_dominant_narrow_script(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
Ok(make_script_value(ctx.dominant_narrow_script))
}
fn prim_get_language(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let script = as_script(args.pop().unwrap())?;
Ok(make_language_value(ctx.langsys_scheme[script as usize]))
}
fn prim_set_every_word_break(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let ctx = as_context(args.pop().unwrap())?;
let _after = args.pop().unwrap();
let _before = args.pop().unwrap();
Ok(Value::Context(Box::new(ctx)))
}
fn prim_register_outline(interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let entries = as_list(args.pop().unwrap())?;
let mut out = Vec::with_capacity(entries.len());
for e in entries {
let Value::Tuple(vs) = e else {
return eval_error("register-outline expects a list of (int * string * string * bool)");
};
if vs.len() != 4 {
return eval_error("register-outline expects 4-tuples (level, text, key, is-open)");
}
let mut it = vs.into_iter();
let level = as_int(it.next().unwrap())?;
let text = as_str(it.next().unwrap())?;
let key = as_str(it.next().unwrap())?;
let is_open = as_bool(it.next().unwrap())?;
let dest_name = interp.dest_name(&key);
out.push(OutlineEntry {
level,
text,
dest_name,
is_open,
});
}
interp.outline = out; Ok(Value::Unit)
}
fn extract_string_pure_one(phb: &PureHorzBox) -> String {
match phb {
PureHorzBox::InnerString { text, .. } => text.clone(),
PureHorzBox::Discretionary { no_break, .. } => {
no_break.iter().map(extract_string_pure_one).collect()
}
PureHorzBox::Frame { contents, .. } => contents
.iter()
.map(|(_, b)| extract_string_pure_one(b))
.collect(),
_ => String::new(),
}
}
fn extract_string_one(hb: &HorzBox) -> String {
match hb {
HorzBox::Pure(phb) => extract_string_pure_one(phb),
}
}
fn prim_extract_string(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let boxes = as_inline_boxes(args.pop().unwrap())?;
let s: String = boxes.iter().map(extract_string_one).collect();
Ok(Value::Str(s))
}
fn prim_get_initial_text_info_v006(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let _unit = args.pop().unwrap();
Ok(Value::TextInfo(TextInfo { indent: 0 }))
}
fn prim_get_initial_text_info_v01(
_interp: &mut Interp,
mut args: Vec<Value>,
) -> Result<Value, EvalError> {
let _stringifier = args.pop().unwrap();
let _default_math_cmd = args.pop().unwrap();
Ok(Value::TextInfo(TextInfo { indent: 0 }))
}
fn prim_deepen_indent(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let tinfo = as_text_info(args.pop().unwrap())?;
let i = as_int(args.pop().unwrap())?;
Ok(Value::TextInfo(TextInfo {
indent: tinfo.indent + i.max(0),
}))
}
fn prim_break(_interp: &mut Interp, mut args: Vec<Value>) -> Result<Value, EvalError> {
let tinfo = as_text_info(args.pop().unwrap())?;
let mut s = String::with_capacity(1 + tinfo.indent as usize);
s.push('\n');
for _ in 0..tinfo.indent {
s.push(' ');
}
Ok(Value::Str(s))
}
#[cfg(test)]
mod page_model_tests {
use super::*;
#[test]
fn as_page_maps_every_nullary_ctor_to_the_right_paper_size() {
let cases: &[(&str, PaperSize)] = &[
("A0Paper", PaperSize::A0),
("A1Paper", PaperSize::A1),
("A2Paper", PaperSize::A2),
("A3Paper", PaperSize::A3),
("A4Paper", PaperSize::A4),
("A5Paper", PaperSize::A5),
("USLetter", PaperSize::USLetter),
("USLegal", PaperSize::USLegal),
];
for (name, expected) in cases {
let v = Value::Ctor((*name).to_string(), None);
assert_eq!(as_page(v).unwrap(), *expected, "ctor {name}");
}
}
#[test]
fn as_page_unwraps_user_defined_papers_tuple_payload() {
let v = Value::Ctor(
"UserDefinedPaper".to_string(),
Some(Box::new(Value::Tuple(vec![
Value::Length(Length::pt(100.0)),
Value::Length(Length::pt(200.0)),
]))),
);
assert_eq!(
as_page(v).unwrap(),
PaperSize::UserDefined(Length::pt(100.0), Length::pt(200.0))
);
}
#[test]
fn a4_paper_dims_are_595_by_842_points() {
let (w, h) = PaperSize::A4.dims();
assert!((w.0 - 595.0).abs() < 1.0, "width: {}", w.0);
assert!((h.0 - 842.0).abs() < 1.0, "height: {}", h.0);
}
#[test]
fn read_content_scheme_extracts_origin_and_height() {
let mut fields = BTreeMap::new();
fields.insert(
"text-origin".to_string(),
Value::Tuple(vec![
Value::Length(Length::pt(10.0)),
Value::Length(Length::pt(20.0)),
]),
);
fields.insert("text-height".to_string(), Value::Length(Length::pt(300.0)));
let (origin, height) = read_content_scheme(Value::Record(fields)).unwrap();
assert_eq!(origin, (Length::pt(10.0), Length::pt(20.0)));
assert_eq!(height, Length::pt(300.0));
}
#[test]
fn read_content_scheme_errors_on_a_missing_field() {
let mut fields = BTreeMap::new();
fields.insert(
"text-origin".to_string(),
Value::Tuple(vec![
Value::Length(Length::ZERO),
Value::Length(Length::ZERO),
]),
);
let err = read_content_scheme(Value::Record(fields)).unwrap_err();
assert!(
err.msg.contains("text-height"),
"error should name the missing field: {}",
err.msg
);
}
#[test]
fn read_parts_scheme_extracts_all_four_fields() {
let mut fields = BTreeMap::new();
fields.insert(
"header-origin".to_string(),
Value::Tuple(vec![
Value::Length(Length::ZERO),
Value::Length(Length::ZERO),
]),
);
fields.insert("header-content".to_string(), Value::BlockBoxes(Vec::new()));
fields.insert(
"footer-origin".to_string(),
Value::Tuple(vec![
Value::Length(Length::pt(1.0)),
Value::Length(Length::pt(2.0)),
]),
);
fields.insert("footer-content".to_string(), Value::BlockBoxes(Vec::new()));
let (horg, hbb, forg, fbb) = read_parts_scheme(Value::Record(fields)).unwrap();
assert_eq!(horg, (Length::ZERO, Length::ZERO));
assert!(hbb.is_empty());
assert_eq!(forg, (Length::pt(1.0), Length::pt(2.0)));
assert!(fbb.is_empty());
}
#[test]
fn read_parts_scheme_errors_on_a_missing_field() {
let err = read_parts_scheme(Value::Record(BTreeMap::new())).unwrap_err();
assert!(
err.msg.contains("header-origin"),
"error should name the missing field: {}",
err.msg
);
}
}
#[cfg(test)]
mod math_split_tests {
use super::*;
use rustyfi_backend::FontMetrics;
struct NoMath;
impl FontMetrics for NoMath {
fn advance(&self, _f: FontKey, c: char, size: Length) -> Option<Length> {
if c.is_ascii() {
Some(size * 0.5)
} else {
None
}
}
fn ascender(&self, _f: FontKey, size: Length) -> Length {
size * 0.75
}
fn descender(&self, _f: FontKey, size: Length) -> Length {
size * 0.25
}
}
#[test]
fn enter_script_scales_and_saturates() {
let metrics = NoMath;
let interp = Interp::new(&metrics);
let ctx = Context::initial(Length::pt(400.0));
assert_eq!(ctx.math_script_level, MathScriptLevel::Base);
assert_eq!(ctx.font_size, Length::pt(12.0));
let s1 = enter_script(&interp, &ctx);
assert_eq!(s1.math_script_level, MathScriptLevel::Script);
assert!(
(s1.font_size.0 - ctx.font_size.0 * 0.7).abs() < 1e-9,
"expected {} * 0.7, got {}",
ctx.font_size.0,
s1.font_size.0
);
let s2 = enter_script(&interp, &s1);
assert_eq!(s2.math_script_level, MathScriptLevel::ScriptScript);
assert!(
(s2.font_size.0 - s1.font_size.0 * (5.0 / 7.0)).abs() < 1e-9,
"expected {} * 5/7, got {}",
s1.font_size.0,
s2.font_size.0
);
let s3 = enter_script(&interp, &s2);
assert_eq!(s3.math_script_level, MathScriptLevel::ScriptScript);
assert_eq!(s3.font_size, s2.font_size);
}
}