pub mod animations;
pub mod arrows;
pub mod basic;
pub mod calendar;
pub mod chains;
pub mod circuits;
pub mod coords;
pub mod datavis;
pub mod decorations;
pub mod fadings;
pub mod folding;
pub mod functional;
pub mod graphs;
pub mod intersections;
pub mod libraries;
pub mod lsystem;
pub mod marks;
pub mod matrix;
pub mod mindmap;
pub mod nodes;
pub mod nonlinear;
pub mod options;
pub mod output;
pub mod path;
pub mod patterns;
pub mod pgfplots;
pub mod pics;
pub mod shadings;
pub mod shapes;
pub mod spy;
pub mod state;
pub mod three_d;
pub mod tikzmath;
pub mod trees;
pub mod turtle;
pub mod user_shapes;
pub mod views;
use std::collections::HashMap;
use std::rc::Rc;
use crate::canvas::{Canvas, Item};
use crate::color::{self, Color};
use crate::scene::{Point, Transform};
use crate::tex::{self, Cat, Meaning, Prim, Tex, Token};
use crate::Error;
pub use state::GState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum Cmd {
Path,
Draw,
Fill,
Filldraw,
Clip,
Shade,
Shadedraw,
Pattern,
Node,
Coordinate,
Useasboundingbox,
ResetBoundingBox,
Matrix,
PgfMatrix,
Chainin,
TikzMath,
DeclareFading,
DeclarePatternForm,
DeclarePatternColored,
SetFillPattern,
TikzDeclarePattern,
PgfDeclarePattern,
ArrowsDeclare,
DeclareShape,
PositionNodeLater,
PositionNodeNow,
DeclareShadingH,
DeclareShadingV,
DeclareShadingR,
UseShading,
ShadePath,
SetFading,
TransformNonlinear,
ApproximateNonlinear,
ApproximateNonlinearTranslation,
SetCurvilinear,
PointCurvilinear,
PointCurvilinearPolar,
ToNumber,
MathSinCos,
SetFadingForPath,
SetFadingForPathStroked,
SetSeed,
Pic,
Graph,
Tikz,
Begin,
End,
Scoped,
Tikzset,
Pgfkeys,
Pgfqkeys,
Pgfkeysalso,
Pgfset,
Pgfplotsset,
Tikzstyle,
Key,
SetPath,
Foreach,
Definecolor,
Colorlet,
Color,
Textcolor,
Pgfqbox,
Pgfmathsetmacro,
Pgfmathparse,
PgfmathFunction,
Pgfmathtruncatemacro,
Pgfmathsetlengthmacro,
Pgfmathsetcount,
Pgfmathsetlength,
Pgfmathdeclarefunction,
Pgfmathprintnumber,
Usetikzlibrary,
Usepackage,
Documentclass,
Ignore1,
Ignore2,
Hspace,
Fbox,
Hskip,
FontSize,
FontSeries,
Unsupported,
OnLayer,
EndOnLayer,
NamePrefix,
Tikzfading,
SetLayers,
Frame,
Arrow,
ArrowReversed,
DecorationBegin,
DecorationEnd,
DecorateCurrentPath,
DataVis,
Spy,
SpyBegin,
SpyEnd,
Quad,
}
const COMMANDS: &[(&str, Cmd)] = &[
("tikra@arrow", Cmd::Arrow),
("tikra@arrowreversed", Cmd::ArrowReversed),
("tikra@decoration", Cmd::DecorationBegin),
("tikra@enddecoration", Cmd::DecorationEnd),
("tikra@decoratecurrentpath", Cmd::DecorateCurrentPath),
("path", Cmd::Path),
("draw", Cmd::Draw),
("fill", Cmd::Fill),
("filldraw", Cmd::Filldraw),
("clip", Cmd::Clip),
("shade", Cmd::Shade),
("shadedraw", Cmd::Shadedraw),
("pattern", Cmd::Pattern),
("node", Cmd::Node),
("coordinate", Cmd::Coordinate),
("useasboundingbox", Cmd::Useasboundingbox),
("pgfresetboundingbox", Cmd::ResetBoundingBox),
("matrix", Cmd::Matrix),
("pgfmatrix", Cmd::PgfMatrix),
("chainin", Cmd::Chainin),
("tikzmath", Cmd::TikzMath),
("pgfdeclarefading", Cmd::DeclareFading),
("pgfdeclarepatternformonly", Cmd::DeclarePatternForm),
("pgfdeclarepatterninherentlycolored", Cmd::DeclarePatternColored),
("pgfsetfillpattern", Cmd::SetFillPattern),
("tikzdeclarepattern", Cmd::TikzDeclarePattern),
("pgfdeclarepattern", Cmd::PgfDeclarePattern),
("pgfarrowsdeclare", Cmd::ArrowsDeclare),
("pgfdeclareshape", Cmd::DeclareShape),
("pgfpositionnodelater", Cmd::PositionNodeLater),
("pgfpositionnodenow", Cmd::PositionNodeNow),
("pgfdeclarehorizontalshading", Cmd::DeclareShadingH),
("pgfdeclareverticalshading", Cmd::DeclareShadingV),
("pgfdeclareradialshading", Cmd::DeclareShadingR),
("pgfuseshading", Cmd::UseShading),
("pgfshadepath", Cmd::ShadePath),
("pgfsetfading", Cmd::SetFading),
("pgftransformnonlinear", Cmd::TransformNonlinear),
("pgfapproximatenonlineartransformation", Cmd::ApproximateNonlinear),
("pgfapproximatenonlineartranslation", Cmd::ApproximateNonlinearTranslation),
("pgfsetcurvilinearbeziercurve", Cmd::SetCurvilinear),
("pgfpointcurvilinearbezierorthogonal", Cmd::PointCurvilinear),
("pgfpointcurvilinearbezierpolar", Cmd::PointCurvilinearPolar),
("pgf@sys@tonumber", Cmd::ToNumber),
("pgfmathsincos@", Cmd::MathSinCos),
("pgfsetfadingforcurrentpath", Cmd::SetFadingForPath),
("pgfsetfadingforcurrentpathstroked", Cmd::SetFadingForPathStroked),
("tikra@setseed", Cmd::SetSeed),
("pic", Cmd::Pic),
("graph", Cmd::Graph),
("tikz", Cmd::Tikz),
("begin", Cmd::Begin),
("end", Cmd::End),
("scoped", Cmd::Scoped),
("tikzset", Cmd::Tikzset),
("pgfkeys", Cmd::Pgfkeys),
("pgfqkeys", Cmd::Pgfqkeys),
("pgfkeysalso", Cmd::Pgfkeysalso),
("pgfset", Cmd::Pgfset),
("pgfplotsset", Cmd::Pgfplotsset),
("tikzstyle", Cmd::Tikzstyle),
("tikra@key", Cmd::Key),
("tikra@setpath", Cmd::SetPath),
("foreach", Cmd::Foreach),
("definecolor", Cmd::Definecolor),
("providecolor", Cmd::Definecolor),
("colorlet", Cmd::Colorlet),
("color", Cmd::Color),
("textcolor", Cmd::Textcolor),
("pgfqbox", Cmd::Pgfqbox),
("pgfmathsetmacro", Cmd::Pgfmathsetmacro),
("pgfmathparse", Cmd::Pgfmathparse),
("pgfmathadd", Cmd::PgfmathFunction),
("pgfmathsubtract", Cmd::PgfmathFunction),
("pgfmathmultiply", Cmd::PgfmathFunction),
("pgfmathdivide", Cmd::PgfmathFunction),
("pgfmathmax", Cmd::PgfmathFunction),
("pgfmathmin", Cmd::PgfmathFunction),
("pgfmathmod", Cmd::PgfmathFunction),
("pgfmathpow", Cmd::PgfmathFunction),
("pgfmathveclen", Cmd::PgfmathFunction),
("pgfmathatan2", Cmd::PgfmathFunction),
("pgfmathabs", Cmd::PgfmathFunction),
("pgfmathneg", Cmd::PgfmathFunction),
("pgfmathsqrt", Cmd::PgfmathFunction),
("pgfmathsin", Cmd::PgfmathFunction),
("pgfmathcos", Cmd::PgfmathFunction),
("pgfmathtan", Cmd::PgfmathFunction),
("pgfmathexp", Cmd::PgfmathFunction),
("pgfmathln", Cmd::PgfmathFunction),
("pgfmathround", Cmd::PgfmathFunction),
("pgfmathfloor", Cmd::PgfmathFunction),
("pgfmathceil", Cmd::PgfmathFunction),
("pgfmathint", Cmd::PgfmathFunction),
("pgfmathdeg", Cmd::PgfmathFunction),
("pgfmathrad", Cmd::PgfmathFunction),
("pgfmathsign", Cmd::PgfmathFunction),
("pgfmathtruncatemacro", Cmd::Pgfmathtruncatemacro),
("pgfmathsetlengthmacro", Cmd::Pgfmathsetlengthmacro),
("pgfmathsetcount", Cmd::Pgfmathsetcount),
("pgfmathsetlength", Cmd::Pgfmathsetlength),
("pgfmathdeclarefunction", Cmd::Pgfmathdeclarefunction),
("pgfmathprintnumber", Cmd::Pgfmathprintnumber),
("usetikzlibrary", Cmd::Usetikzlibrary),
("usepgflibrary", Cmd::Usetikzlibrary),
("usepgfplotslibrary", Cmd::Usetikzlibrary),
("usepgfmodule", Cmd::Usetikzlibrary),
("usepackage", Cmd::Usepackage),
("RequirePackage", Cmd::Usepackage),
("documentclass", Cmd::Documentclass),
("pgfplotsset", Cmd::Pgfplotsset),
("label", Cmd::Ignore1),
("hspace", Cmd::Hspace),
("fbox", Cmd::Fbox),
("hskip", Cmd::Hskip),
("vspace", Cmd::Ignore1),
("pgfdeclarelayer", Cmd::Ignore1),
("pgfsetlayers", Cmd::SetLayers),
("pgfonlayer", Cmd::OnLayer),
("endpgfonlayer", Cmd::EndOnLayer),
("tikra@frame", Cmd::Frame),
("tikra@nameprefix", Cmd::NamePrefix),
("tikzfading", Cmd::Tikzfading),
("tikzexternalize", Cmd::Ignore1),
("pgfrealjobname", Cmd::Ignore1),
("pagestyle", Cmd::Ignore1),
("thispagestyle", Cmd::Ignore1),
("caption", Cmd::Ignore1),
("tikzsetnextfilename", Cmd::Ignore1),
("pgfplotstableset", Cmd::Ignore1),
("tiny", Cmd::FontSize),
("scriptsize", Cmd::FontSize),
("footnotesize", Cmd::FontSize),
("small", Cmd::FontSize),
("normalsize", Cmd::FontSize),
("large", Cmd::FontSize),
("Large", Cmd::FontSize),
("LARGE", Cmd::FontSize),
("huge", Cmd::FontSize),
("Huge", Cmd::FontSize),
("bfseries", Cmd::FontSeries),
("mdseries", Cmd::FontSeries),
("itshape", Cmd::FontSeries),
("upshape", Cmd::FontSeries),
("slshape", Cmd::FontSeries),
("scshape", Cmd::FontSeries),
("ttfamily", Cmd::FontSeries),
("sffamily", Cmd::FontSeries),
("rmfamily", Cmd::FontSeries),
("centering", Cmd::FontSeries),
("raggedright", Cmd::FontSeries),
("raggedleft", Cmd::FontSeries),
("noindent", Cmd::FontSeries),
("maketitle", Cmd::FontSeries),
("selectfont", Cmd::FontSeries),
("quad", Cmd::Quad),
("qquad", Cmd::Quad),
("spy", Cmd::Spy),
("tikra@spybegin", Cmd::SpyBegin),
("tikra@spyend", Cmd::SpyEnd),
];
fn path_command(cmd: Cmd) -> Option<&'static str> {
Some(match cmd {
Cmd::Path => "",
Cmd::Draw => "draw",
Cmd::Fill => "fill",
Cmd::Filldraw => "fill,draw",
Cmd::Clip => "clip",
Cmd::Shade => "shade",
Cmd::Shadedraw => "shade,draw",
Cmd::Pattern => "pattern",
Cmd::Useasboundingbox => "use as bounding box",
_ => return None,
})
}
#[derive(Debug, Clone, Default)]
struct PgfScope {
ends: usize,
fading_last: bool,
pub clipped: bool,
}
pub const MAX_STEPS: usize = 100_000;
pub const MAX_DEPTH: usize = 64;
pub struct Engine {
pub tex: Tex,
pub st: GState,
saved: Vec<GState>,
pub canvas: Canvas,
pub nodes: HashMap<String, nodes::NodeInfo>,
pub colors: HashMap<String, Color>,
scopes: Vec<PgfScope>,
pub picture_depth: usize,
pub pictures: usize,
pub steps: usize,
pub depth: usize,
text_reported: bool,
key_fns: Vec<options::KeyFn>,
pub canvas_frozen: bool,
nesting: usize,
pub pgf_path: basic::PgfPath,
pub plot_stream: Vec<crate::scene::Point>,
scope_ends: Vec<Vec<Token>>,
brace_scopes: Vec<bool>,
layer_order: Vec<String>,
layers: HashMap<String, Vec<Item>>,
on_layer: Vec<(String, Vec<Item>)>,
pub last_node: Option<nodes::NodeInfo>,
pub anim_shifts: Vec<Point>,
pub last_node_name: Option<String>,
tree_count: usize,
pub chain_list: std::collections::HashMap<String, chains::Chain>,
pub math_kinds: HashMap<String, tikzmath::Kind>,
pub after_node_path: Vec<Token>,
pub local_boxes: Vec<(String, usize)>,
interrupted_boxes: Vec<(Option<Point>, Option<Point>)>,
pub path_fadings: Vec<Vec<(String, usize, bool, Transform)>>,
pub carried_fadings: Vec<(String, bool, Transform)>,
pub path_fading_settings: Vec<(Transform, bool)>,
pub turn_from: (crate::scene::Point, crate::scene::Point),
pub subpath_start: crate::scene::Point,
pub fadings: HashMap<String, crate::canvas::Shading>,
pub picture_fadings: HashMap<String, fadings::PictureFading>,
pub basic_shadings: HashMap<String, shadings::BasicShading>,
pub functional_shadings: HashMap<String, functional::Functional>,
pub plot_marks_declared: HashMap<String, Vec<Token>>,
pub plot_handlers: HashMap<String, String>,
pub saved_paths: HashMap<String, Vec<crate::scene::Segment>>,
pub pgf_solutions: Vec<Point>,
pub user_patterns: HashMap<String, patterns::UserPattern>,
pub user_shapes: HashMap<String, user_shapes::UserShape>,
pub later_node: Option<(Vec<Item>, nodes::NodeInfo, Option<String>)>,
pub forced_box: Option<(f64, f64, f64)>,
pub text_shading: Option<(String, f64, f64)>,
pub text_picture: Option<(Vec<Item>, Point, Point)>,
pub tree_child_names: Vec<Option<String>>,
pub tree_edge_out: Option<(Vec<Token>, Vec<Token>)>,
pub behind_path: Vec<Item>,
pub decoration_envs: Vec<decorations::Env>,
pub named_paths: HashMap<String, Vec<crate::scene::Segment>>,
boxes: Vec<PictureBox>,
layout: Vec<Layout>,
words: Vec<(String, String)>,
pub first_char_syntax: bool,
pub keys_run: usize,
pub datavis: Vec<datavis::Dv>,
pub current_key: String,
pub axis: Option<Box<pgfplots::Axis>>,
pub frame: Option<std::rc::Rc<pgfplots::Frame>>,
pub plot_group: Option<Box<pgfplots::group::Group>>,
pub graphs: graphs::State,
pub spy_scopes: Vec<spy::SpyScope>,
pub facts: crate::describe::Facts,
}
#[derive(Debug, Clone)]
struct PictureBox {
canvas: Canvas,
baseline: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Layout {
Picture(usize),
Word(usize),
Space,
Skip(f64),
Par,
Break,
Center(bool),
TabStart(u32),
Cell,
Row,
TabEnd,
Width(f64),
}
pub const MAX_NESTING: usize = 150;
pub const MAX_KEYS: usize = 50_000;
pub const NATIVE_KEY_BASE: u16 = 1000;
thread_local! {
static PROTOTYPE: std::cell::RefCell<Option<Engine>> = const { std::cell::RefCell::new(None) };
}
impl Clone for Engine {
fn clone(&self) -> Self {
Engine {
tex: self.tex.clone(),
st: self.st.clone(),
saved: self.saved.clone(),
canvas: self.canvas.clone(),
nodes: self.nodes.clone(),
colors: self.colors.clone(),
scopes: self.scopes.clone(),
picture_depth: self.picture_depth,
pictures: self.pictures,
steps: self.steps,
depth: self.depth,
text_reported: self.text_reported,
key_fns: self.key_fns.clone(),
canvas_frozen: self.canvas_frozen,
nesting: self.nesting,
keys_run: self.keys_run,
first_char_syntax: self.first_char_syntax,
datavis: self.datavis.clone(),
pgf_path: self.pgf_path.clone(),
plot_stream: Vec::new(),
decoration_envs: self.decoration_envs.clone(),
named_paths: self.named_paths.clone(),
last_node: self.last_node.clone(),
last_node_name: self.last_node_name.clone(),
tree_count: self.tree_count,
chain_list: self.chain_list.clone(),
math_kinds: self.math_kinds.clone(),
after_node_path: Vec::new(),
local_boxes: Vec::new(),
interrupted_boxes: Vec::new(),
path_fadings: Vec::new(),
carried_fadings: Vec::new(),
path_fading_settings: Vec::new(),
turn_from: Default::default(),
subpath_start: Default::default(),
tree_edge_out: self.tree_edge_out.clone(),
behind_path: Vec::new(),
tree_child_names: self.tree_child_names.clone(),
forced_box: self.forced_box,
text_shading: None,
text_picture: None,
fadings: self.fadings.clone(),
picture_fadings: self.picture_fadings.clone(),
basic_shadings: self.basic_shadings.clone(),
functional_shadings: self.functional_shadings.clone(),
plot_marks_declared: self.plot_marks_declared.clone(),
plot_handlers: self.plot_handlers.clone(),
saved_paths: self.saved_paths.clone(),
pgf_solutions: self.pgf_solutions.clone(),
anim_shifts: Vec::new(),
user_patterns: self.user_patterns.clone(),
user_shapes: self.user_shapes.clone(),
later_node: None,
scope_ends: self.scope_ends.clone(),
brace_scopes: Vec::new(),
layer_order: self.layer_order.clone(),
layers: self.layers.clone(),
on_layer: self.on_layer.clone(),
boxes: self.boxes.clone(),
layout: self.layout.clone(),
words: self.words.clone(),
current_key: self.current_key.clone(),
axis: self.axis.clone(),
frame: self.frame.clone(),
plot_group: self.plot_group.clone(),
graphs: self.graphs.clone(),
spy_scopes: self.spy_scopes.clone(),
facts: self.facts.clone(),
}
}
}
impl Engine {
pub fn new() -> Engine {
arrows::clear_declared();
PROTOTYPE.with(|p| {
let mut p = p.borrow_mut();
if p.is_none() {
*p = Some(Engine::build());
}
p.as_ref().expect("built").clone()
})
}
fn build() -> Engine {
let mut e = Engine {
tex: Tex::new(),
st: GState::default(),
saved: Vec::new(),
canvas: Canvas::default(),
nodes: HashMap::new(),
colors: HashMap::new(),
scopes: Vec::new(),
picture_depth: 0,
pictures: 0,
steps: 0,
depth: 0,
text_reported: false,
key_fns: Vec::new(),
canvas_frozen: false,
nesting: 0,
keys_run: 0,
first_char_syntax: false,
datavis: Vec::new(),
pgf_path: basic::PgfPath::default(),
plot_stream: Vec::new(),
decoration_envs: Vec::new(),
named_paths: HashMap::new(),
last_node: None,
last_node_name: None,
tree_count: 0,
chain_list: Default::default(),
math_kinds: Default::default(),
after_node_path: Vec::new(),
local_boxes: Vec::new(),
interrupted_boxes: Vec::new(),
path_fadings: Vec::new(),
carried_fadings: Vec::new(),
path_fading_settings: Vec::new(),
turn_from: Default::default(),
subpath_start: Default::default(),
tree_edge_out: None,
behind_path: Vec::new(),
tree_child_names: Vec::new(),
forced_box: None,
text_shading: None,
text_picture: None,
fadings: HashMap::new(),
picture_fadings: HashMap::new(),
basic_shadings: HashMap::new(),
functional_shadings: HashMap::new(),
plot_marks_declared: HashMap::new(),
plot_handlers: HashMap::new(),
saved_paths: HashMap::new(),
pgf_solutions: Vec::new(),
anim_shifts: Vec::new(),
user_patterns: HashMap::new(),
user_shapes: HashMap::new(),
later_node: None,
scope_ends: Vec::new(),
brace_scopes: Vec::new(),
layer_order: vec!["background".into(), "main".into()],
layers: HashMap::new(),
on_layer: Vec::new(),
boxes: Vec::new(),
layout: Vec::new(),
words: Vec::new(),
current_key: String::new(),
axis: None,
frame: None,
plot_group: None,
graphs: graphs::State::default(),
spy_scopes: Vec::new(),
facts: Default::default(),
};
for (name, cmd) in COMMANDS {
e.tex.define(name, Meaning::Native(*cmd as u16));
}
e.colors.insert("transparent".into(), Color::BLACK);
e.colors.insert("pgftransparent".into(), Color::BLACK);
e.tex.define_text(r"\let\bgroup={\let\egroup=}\def\pgfkeysnovalue{\pgfkeysnovalue}\def\pgfkeysvalueof#1{\csname pgfk@#1\endcsname}\def\pgfkeysgetvalue#1#2{\expandafter\let\expandafter#2\csname pgfk@#1\endcsname}\def\pgfkeyssetvalue#1#2{\expandafter\def\csname pgfk@#1\endcsname{#2}}\def\pgfkeysifdefined#1{\ifcsname pgfk@#1\endcsname\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi}\def\pgfmathresult{}\def\tikzlastnode{}");
basic::define(&mut e);
nonlinear::define(&mut e);
calendar::define(&mut e);
folding::define(&mut e);
mindmap::define(&mut e);
lsystem::define(&mut e);
options::define_keys(&mut e);
datavis::define(&mut e);
pgfplots::define(&mut e);
graphs::define(&mut e);
e
}
pub fn warn(&mut self, s: impl Into<String>) {
self.tex.warn(s);
}
pub fn run(&mut self) -> Result<(), Error> {
while let Some(t) = self.tex.get_x_token()? {
self.execute(t)?;
}
Ok(())
}
pub fn run_tokens(&mut self, tokens: Vec<Token>) -> Result<(), Error> {
if self.nesting >= MAX_NESTING {
self.warn(format!("styles or macros nested more than {MAX_NESTING} levels (one calls itself?): the rest ignored"));
return Ok(());
}
self.nesting += 1;
let depth = self.tex.depth();
self.tex.push_frame(tokens);
let r = (|| {
while let Some(t) = self.tex.get_x_token_until(depth)? {
self.execute(t)?;
}
Ok(())
})();
if r.is_err() {
self.tex.drain_to(depth);
}
self.nesting -= 1;
r
}
fn count_step(&mut self) -> Result<(), Error> {
self.steps += 1;
if self.steps > MAX_STEPS {
return Err(Error::Figure(format!("figure too large: more than {MAX_STEPS} statements")));
}
Ok(())
}
pub fn execute(&mut self, t: Token) -> Result<(), Error> {
let m = self.tex.meaning(&t);
match m {
Meaning::Char(_, Cat::Begin) if self.picture_depth > 0 => {
match self.tex.next_nonspace() {
Some(n) if n.is_char('[') => {
let o = self.tex.read_until_char(']', false)?;
self.brace_scopes.push(true);
let r = self.open_tikz_scope(Some(o), false);
self.recover(r, &t)?;
}
other => {
if let Some(n) = other {
self.tex.back(n);
}
self.brace_scopes.push(false);
self.begin_group();
}
}
}
Meaning::Char(_, Cat::End) if self.picture_depth > 0 => {
if self.brace_scopes.pop() == Some(true) {
let r = self.close_tikz_scope();
self.recover(r, &t)?;
} else {
self.end_group();
}
}
Meaning::Char(_, Cat::Begin) => self.begin_group(),
Meaning::Char(_, Cat::End) => self.end_group(),
Meaning::Char(_, Cat::Math) if self.picture_depth == 0 => {
let mut formula = Vec::new();
while let Some(n) = self.tex.get_token() {
if matches!(self.tex.meaning(&n), Meaning::Char(_, Cat::Math)) {
break;
}
formula.push(n);
}
if formula.iter().any(|x| x.is_cs("tikz") || x.is_cs("begin")) {
self.tex.push_tokens(formula);
return Ok(());
}
let text = format!("${}$", tex::tokens_to_string(&formula));
match self.layout.last() {
Some(Layout::Word(i)) => self.words[*i].1.push_str(&text),
_ => {
self.layout.push(Layout::Word(self.words.len()));
self.words.push((self.st.font.clone(), text));
}
}
}
Meaning::Prim(Prim::Par) if self.picture_depth == 0 => {
if !self.layout.is_empty() && self.layout.last() != Some(&Layout::Par) {
self.layout.push(Layout::Par);
}
}
Meaning::Prim(Prim::Begingroup) => self.begin_group(),
Meaning::Prim(Prim::Endgroup) => self.end_group(),
Meaning::Native(id) if id >= NATIVE_KEY_BASE => {
}
Meaning::Native(id) => {
if id == Cmd::Key as u16 || id == Cmd::SetPath as u16 {
return self.command(id, &t);
}
self.count_step()?;
self.tex.new_statement();
let r = self.command(id, &t);
self.recover(r, &t)?;
}
Meaning::Char(c, _) if matches!(t, Token::Char(..)) => self.text(c),
Meaning::Undefined => {
if let Token::Cs(name) = &t {
self.unknown_command(name)?;
} else if matches!(t, Token::Char('~', Cat::Active)) && self.picture_depth == 0 {
self.layout.push(Layout::Skip(SPACE));
}
}
_ => {
if !self.tex.execute_basic(t.clone())? {
if let Meaning::Char(c, _) = self.tex.meaning(&t) {
self.text(c);
}
}
}
}
Ok(())
}
pub fn typeset_word(&mut self, text: String) {
self.layout.push(Layout::Word(self.words.len()));
self.words.push((self.st.font.clone(), text));
}
fn in_table(&self) -> bool {
let opened = self.layout.iter().filter(|l| matches!(l, Layout::TabStart(_))).count();
opened > self.layout.iter().filter(|l| matches!(l, Layout::TabEnd)).count()
}
fn text(&mut self, c: char) {
if self.picture_depth == 0 && c == '&' && self.in_table() {
self.layout.push(Layout::Cell);
return;
}
if self.picture_depth == 0 && c.is_whitespace() {
if matches!(self.layout.last(), Some(Layout::Picture(_) | Layout::Word(_) | Layout::Space | Layout::Skip(_))) {
self.layout.push(Layout::Space);
}
return;
}
if self.picture_depth == 0 {
match self.layout.last() {
Some(Layout::Word(i)) => self.words[*i].1.push(c),
_ => {
self.layout.push(Layout::Word(self.words.len()));
self.words.push((self.st.font.clone(), c.to_string()));
}
}
return;
}
if c.is_whitespace() || c == ';' || self.text_reported {
return;
}
if self.picture_depth > 0 {
self.text_reported = true;
self.warn("text outside a statement ignored");
}
}
fn unknown_command(&mut self, name: &str) -> Result<(), Error> {
if self.picture_depth == 0 && name == "\\" {
if let Some(t) = self.tex.next_nonspace() {
if t.is_char('[') {
let _ = self.tex.read_until_char(']', false)?;
} else {
self.tex.back(t);
}
}
self.layout.push(if self.in_table() { Layout::Row } else { Layout::Break });
return Ok(());
}
if self.picture_depth > 0 {
let rest = self.skip_statement()?;
self.warn(format!("\\{name} not supported: '{}' ignored", rest.chars().take(40).collect::<String>().trim()));
}
Ok(())
}
pub fn skip_statement(&mut self) -> Result<String, Error> {
let mut out = Vec::new();
let mut depth = 0usize;
while let Some(t) = self.tex.get_token() {
match t {
Token::Char(_, Cat::Begin) => depth += 1,
Token::Char(_, Cat::End) => {
if depth == 0 {
self.tex.back(t);
break;
}
depth -= 1;
}
Token::Char(';', _) if depth == 0 => break,
_ => {}
}
out.push(t);
if out.len() > tex::MAX_TOKENS {
break;
}
}
Ok(tex::tokens_to_string(&out))
}
pub fn begin_group(&mut self) {
self.tex.begin_group();
self.saved.push(self.st.clone());
}
pub fn end_group(&mut self) {
if self.tex.end_group() {
if let Some(s) = self.saved.pop() {
self.st = s;
}
self.canvas.overlay = self.st.overlay;
self.close_local_boxes();
} else {
self.warn("extra '}'");
}
}
pub fn open_local_box(&mut self, name: String) {
self.local_boxes.push((name, self.saved.len()));
self.canvas.local.push((None, None));
}
fn close_local_boxes(&mut self) {
while self.local_boxes.last().is_some_and(|b| b.1 > self.saved.len()) {
let (name, _) = self.local_boxes.pop().expect("checked");
if let Some((Some(lo), Some(hi))) = self.canvas.local.pop() {
self.nodes.insert(name, libraries::box_node(lo, hi));
}
}
}
pub fn begin_scope(&mut self) {
self.scopes.push(PgfScope::default());
}
pub fn end_scope(&mut self) {
if let Some(s) = self.scopes.pop() {
for _ in 0..s.ends {
self.canvas.push(Item::End);
}
}
}
pub fn open_in_scope(&mut self, item: Item) {
let fading = matches!(item, Item::Fade { .. } | Item::FadePicture { .. });
if fading && self.scopes.last().is_some_and(|s| s.fading_last && s.ends > 0) {
self.canvas.push(Item::End);
if let Some(s) = self.scopes.last_mut() {
s.ends -= 1;
}
}
self.canvas.push(item);
match self.scopes.last_mut() {
Some(s) => {
s.ends += 1;
s.fading_last = fading;
}
None => self.canvas.push(Item::End),
}
}
pub fn argument(&mut self) -> Result<Vec<Token>, Error> {
self.tex.read_undelimited()
}
pub fn argument_string(&mut self) -> Result<String, Error> {
let a = self.argument()?;
Ok(self.tex.expand_to_string(a)?.trim().to_string())
}
pub fn optional(&mut self) -> Result<Option<Vec<Token>>, Error> {
match self.tex.next_nonspace() {
Some(t) if t.is_char('[') => Ok(Some(self.tex.read_until_char(']', false)?)),
Some(t) => {
self.tex.back(t);
Ok(None)
}
None => Ok(None),
}
}
pub fn math_string(&mut self, tokens: Vec<Token>) -> Result<String, Error> {
let t = self.tex.expand_fully(tokens)?;
let mut s = String::new();
let mut bare = false;
for (i, x) in t.iter().enumerate() {
if x.is_cs("pgf@sys@tonumber") {
bare = true;
continue;
}
match self.tex.meaning(x) {
Meaning::Dimen(r) if bare && matches!(x, Token::Cs(_)) => {
bare = false;
let sp = self.tex.dimens.get(&r).copied().unwrap_or(0);
s.push_str(&pgf_number(sp as f64 / 65536.0));
}
Meaning::Count(r) if matches!(x, Token::Cs(_)) => s.push_str(&self.tex.counts.get(&r).copied().unwrap_or(0).to_string()),
Meaning::CharDef(v) if matches!(x, Token::Cs(_)) => s.push_str(&v.to_string()),
Meaning::Dimen(r) | Meaning::Skip(r) if matches!(x, Token::Cs(_)) => {
if s.ends_with(|c: char| c.is_ascii_digit() || c == '.') {
s.push('*');
}
let sp = self.tex.dimens.get(&r).copied().unwrap_or(0);
s.push_str(&format!("{}pt", sp as f64 / 65536.0));
}
_ => s.push_str(&tex::tokens_to_string(&t[i..i + 1])),
}
if let (Token::Cs(_), Some(Token::Char(c, _))) = (x, t.get(i + 1)) {
if c.is_alphabetic() && !s.ends_with(' ') {
s.push(' ');
}
}
}
Ok(s)
}
pub fn vars(&self) -> crate::expr::Variables {
let sans = self.st.font.contains("\\sffamily");
crate::expr::Variables {
values: HashMap::new(),
functions: (*self.st.functions).clone(),
font_scale: Some(self.st.ambient_size / 10.0),
in_pt: false,
sans,
trig_rad: self.st.trig_rad,
}
}
pub fn number(&mut self, text: &str) -> Result<f64, Error> {
let t = tex::string_tokens(text);
let s = self.math_string(t)?;
let mut vars = self.vars();
vars.in_pt = true;
crate::expr::eval(&s, &vars)
}
pub fn length(&mut self, text: &str) -> Result<f64, Error> {
let s = self.math_string(tikra_tokens(text))?;
let (v, unit) = crate::expr::eval_length(&s, &self.vars())?;
Ok(if unit { v } else { v * crate::PT })
}
pub fn number_of(&mut self, tokens: Vec<Token>) -> Result<f64, Error> {
let s = self.math_string(tokens)?;
crate::expr::eval(&s, &self.vars())
}
pub fn length_of(&mut self, tokens: Vec<Token>) -> Result<f64, Error> {
let s = self.math_string(tokens)?;
let (v, unit) = crate::expr::eval_length(&s, &self.vars())?;
Ok(if unit { v } else { v * crate::PT })
}
pub fn color(&self, text: &str) -> Option<Color> {
if text.split('!').any(|p| p.trim() == ".") {
let mut colors = self.colors.clone();
colors.insert(".".into(), self.st.color);
return color::parse(text.trim(), &colors);
}
color::parse(text.trim(), &self.colors)
}
fn command(&mut self, id: u16, t: &Token) -> Result<(), Error> {
if basic::is_basic(id) {
return self.basic_command(id);
}
if id == Cmd::DataVis as u16 {
return self.datavis_command(t);
}
if calendar::is_calendar(id) {
return self.calendar_command(id);
}
if folding::is_folding(id) {
return self.folding_command(id);
}
if mindmap::is_mindmap(id) {
return self.mindmap_command(id);
}
if lsystem::is_lsystem(id) {
return self.lsystem_command(id);
}
if pgfplots::is_command(id) {
return self.pgfplots_command(id);
}
let Some(&(name, cmd)) = COMMANDS.iter().find(|(_, c)| *c as u16 == id) else { return Ok(()) };
if self.axis.is_some() && pgfplots::is_deferred(name) {
return self.defer_statement(t);
}
if let Some(defaults) = path_command(cmd) {
let r = path::statement(self, defaults);
return self.recover(r, t);
}
match cmd {
Cmd::Node => {
if let Some(o) = self.tex.next_nonspace() {
let mut back = vec![o.clone()];
let mut matrix = false;
if o.is_char('[') {
let options = self.tex.read_until_char(']', false)?;
matrix = crate::keys::split_list(&options).iter().any(|i| i.key == "matrix" && i.value.is_none());
back.extend(options);
back.push(Token::other(']'));
}
self.tex.push_tokens(back);
if matrix {
return self.matrix();
}
}
self.tex.push_tokens(tikra_tokens("node "));
let r = path::statement(self, "");
self.recover(r, t)
}
Cmd::Coordinate => {
self.tex.push_tokens(tikra_tokens("coordinate "));
let r = path::statement(self, "");
self.recover(r, t)
}
Cmd::Pic => {
self.tex.push_tokens(tikra_tokens("pic "));
let r = path::statement(self, "");
self.recover(r, t)
}
Cmd::NamePrefix => {
let p = self.argument_string()?;
self.st.name_prefix = if p == ".." { String::new() } else { p };
Ok(())
}
Cmd::Matrix => self.matrix(),
Cmd::PgfMatrix => self.pgf_matrix(),
Cmd::Chainin => self.chainin(),
Cmd::TikzMath => self.tikzmath(),
Cmd::DeclareFading => self.declare_fading(),
Cmd::DeclarePatternForm => self.declare_pattern(false),
Cmd::DeclarePatternColored => self.declare_pattern(true),
Cmd::SetFillPattern => self.set_fill_pattern(),
Cmd::TikzDeclarePattern => self.tikz_declare_pattern(true),
Cmd::PgfDeclarePattern => self.tikz_declare_pattern(false),
Cmd::ArrowsDeclare => self.arrows_declare(),
Cmd::DeclareShape => self.declare_shape(),
Cmd::PositionNodeLater => {
let _ = self.argument()?;
self.st.position_later = true;
Ok(())
}
Cmd::PositionNodeNow => self.position_node_now(),
Cmd::DeclareShadingH => self.declare_shading('h'),
Cmd::DeclareShadingV => self.declare_shading('v'),
Cmd::DeclareShadingR => self.declare_shading('r'),
Cmd::UseShading => self.use_shading(),
Cmd::ShadePath => self.shade_path(),
Cmd::SetFading => self.set_fading(),
Cmd::TransformNonlinear => self.transform_nonlinear(),
Cmd::ApproximateNonlinear => self.approximate_nonlinear(true),
Cmd::ApproximateNonlinearTranslation => self.approximate_nonlinear(false),
Cmd::SetCurvilinear => self.set_curvilinear(),
Cmd::PointCurvilinear => self.point_curvilinear(),
Cmd::PointCurvilinearPolar => self.point_curvilinear_polar(),
Cmd::ToNumber => self.to_number(),
Cmd::MathSinCos => self.math_sincos(),
Cmd::SetFadingForPath => self.set_fading_for_path(false),
Cmd::SetFadingForPathStroked => self.set_fading_for_path(true),
Cmd::SetSeed => {
let v = self.argument_string()?;
let n: f64 = v.trim().parse().unwrap_or(1.0);
crate::expr::set_seed(n.trunc() as i64);
Ok(())
}
Cmd::Graph => {
self.tex.push_tokens(tikra_tokens("graph "));
let r = path::statement(self, "");
self.recover(r, t)
}
Cmd::Unsupported => {
let name = tex::show_token(t);
let rest = if self.picture_depth > 0 { self.skip_statement()? } else { String::new() };
self.warn(format!("{name} not supported: '{}' ignored", rest.chars().take(40).collect::<String>().trim()));
Ok(())
}
Cmd::Tikz => self.tikz_command(),
Cmd::Arrow => self.arrow_command(false),
Cmd::ArrowReversed => self.arrow_command(true),
Cmd::DecorationBegin => self.decoration_begin(),
Cmd::DecorationEnd => self.decoration_end(),
Cmd::DecorateCurrentPath => self.decorate_current_path(),
Cmd::Spy => self.spy_command(),
Cmd::SpyBegin => {
self.spy_begin();
Ok(())
}
Cmd::SpyEnd => self.spy_end(),
Cmd::Begin => self.begin_environment(),
Cmd::End => self.end_environment(),
Cmd::Scoped => {
let mut o = self.optional()?;
while let Some(t) = self.tex.next_nonspace() {
if !t.is_char(':') {
self.tex.back(t);
break;
}
if let Some(a) = self.read_animation()? {
let v = o.get_or_insert_with(Vec::new);
if !v.is_empty() {
v.push(Token::other(','));
}
let name = tex::tokens_to_string(&a);
if ["rotate", "shift", "xshift", "yshift", "scale", "xscale", "yscale", "xslant", "yslant"].iter().any(|k| name.starts_with(&format!("{k}="))) {
v.extend(tikra_tokens("transform canvas="));
v.push(Token::Char('{', Cat::Begin));
v.extend(a);
v.push(Token::Char('}', Cat::End));
} else {
v.extend(a);
}
}
}
self.open_tikz_scope(o, false)?;
let body = self.next_statement()?;
self.run_tokens(body)?;
self.close_tikz_scope()
}
Cmd::Tikzset => {
let a = self.argument()?;
self.keys_command(a, Some("/tikz"), true)?;
if self.picture_depth > 0 {
self.st.apply_pending();
}
Ok(())
}
Cmd::Pgfset => {
let a = self.argument()?;
self.keys_command(a, Some("/pgf"), true)
}
Cmd::Pgfplotsset => {
let a = self.argument()?;
self.keys_command(a, Some("/pgfplots"), true)
}
Cmd::Pgfkeys => {
let a = self.argument()?;
self.keys_command(a, Some("/"), true)
}
Cmd::Pgfqkeys => {
let p = self.argument_string()?;
let a = self.argument()?;
self.keys_command(a, Some(&p), true)
}
Cmd::Pgfkeysalso => {
let a = self.argument()?;
self.keys_command(a, None, false)
}
Cmd::Tikzstyle => {
let name = self.argument_string()?;
let mut append = false;
if let Some(x) = self.tex.next_nonspace() {
if x.is_char('+') {
append = true;
} else if !x.is_char('=') {
self.tex.back(x);
}
}
if append {
if let Some(x) = self.tex.next_nonspace() {
if !x.is_char('=') {
self.tex.back(x);
}
}
}
let o = self.optional()?.unwrap_or_default();
let mut v = tikra_tokens(&format!("{name}/.{}=", if append { "append style" } else { "style" }));
v.push(Token::Char('{', Cat::Begin));
v.extend(o);
v.push(Token::Char('}', Cat::End));
self.keys_command(v, Some("/tikz"), true)
}
Cmd::Key => self.invoke_key_command(),
Cmd::SetPath => {
let p = self.argument_string()?;
self.st.key_path = p;
Ok(())
}
Cmd::Foreach => self.foreach(),
Cmd::Definecolor => {
let _ = self.optional()?;
let name = self.argument_string()?;
let model = self.argument_string()?;
let values = self.argument_string()?;
match color::define(&model, &values) {
Some(c) => {
self.colors.insert(name, c);
}
None => self.warn(format!("\\definecolor{{{name}}}{{{model}}}: model not supported")),
}
Ok(())
}
Cmd::Colorlet => {
let _ = self.optional()?;
let name = self.argument_string()?;
let e = self.argument_string()?;
match self.color(&e) {
Some(c) => {
self.colors.insert(name, c);
}
None => self.warn(format!("\\colorlet{{{name}}}: unknown color '{e}'")),
}
Ok(())
}
Cmd::Color => {
let _ = self.optional()?;
let e = self.argument_string()?;
if let Some(c) = self.color(&e) {
self.st.color = c;
self.st.draw_color = None;
self.st.fill_color = None;
self.st.text_color = None;
}
Ok(())
}
Cmd::Pgfqbox => {
let b = self.argument()?;
self.warn(format!("\\pgfqbox{{{}}}: the box is not drawn", tex::tokens_to_string(&b).trim()));
Ok(())
}
Cmd::Textcolor => {
let _ = self.argument()?;
let _ = self.argument()?;
Ok(())
}
Cmd::Pgfmathsetmacro | Cmd::Pgfmathtruncatemacro | Cmd::Pgfmathsetlengthmacro => {
let name = self.argument()?;
let expr = self.argument()?;
let Some(name) = name.into_iter().find(|x| !x.is_space()) else { return Ok(()) };
let text = self.math_string(expr)?;
let mut vars = self.vars();
vars.in_pt = true;
let v = crate::expr::eval_length(&text, &vars).map(|(v, _)| v);
match v {
Ok(v) if v.is_finite() => {
let s = match cmd {
Cmd::Pgfmathtruncatemacro => format!("{}", v.trunc() as i64),
Cmd::Pgfmathsetlengthmacro => format!("{}pt", pgf_number(v)),
_ => pgf_number(v),
};
self.tex.set_meaning(&name, macro_from(&s), false);
self.tex.define_string("pgfmathresult", &s, false);
}
Ok(v) => self.warn(format!("\\pgfmathsetmacro{{{}}}: non-finite result ({v}), macro not defined", tex::show_token(&name))),
Err(e) => self.warn(format!("\\pgfmathsetmacro{{{}}}: {e}", tex::show_token(&name))),
}
Ok(())
}
Cmd::Pgfmathparse => {
let expr = self.argument()?;
let text = self.math_string(expr)?;
let mut vars = self.vars();
vars.in_pt = true;
match crate::expr::eval_length(&text, &vars).map(|(v, _)| v) {
Ok(v) => self.tex.define_string("pgfmathresult", &pgf_number(v), false),
Err(e) => self.warn(format!("\\pgfmathparse: {e}")),
}
Ok(())
}
Cmd::PgfmathFunction => {
let name = match t {
Token::Cs(n) => n.trim_start_matches("pgfmath").to_string(),
_ => return Ok(()),
};
let n = if matches!(name.as_str(), "add" | "subtract" | "multiply" | "divide" | "max" | "min" | "mod" | "pow" | "veclen" | "atan2") { 2 } else { 1 };
let mut args = Vec::new();
for _ in 0..n {
let a = self.argument()?;
args.push(format!("({})", self.math_string(a)?));
}
let text = match name.as_str() {
"add" => format!("{}+{}", args[0], args[1]),
"subtract" => format!("{}-{}", args[0], args[1]),
"multiply" => format!("{}*{}", args[0], args[1]),
"divide" => format!("{}/{}", args[0], args[1]),
"neg" => format!("-{}", args[0]),
other => format!("{other}({})", args.join(",")),
};
match crate::expr::eval_length(&text, &self.vars()).map(|(v, u)| if u { v / crate::PT } else { v }) {
Ok(v) => self.tex.define_string("pgfmathresult", &pgf_number(v), false),
Err(e) => self.warn(format!("\\pgfmath{name}: {e}")),
}
Ok(())
}
Cmd::Pgfmathsetcount | Cmd::Pgfmathsetlength => {
let target = self.argument()?;
let expr = self.argument()?;
let text = self.math_string(expr)?;
let Some(name) = target.into_iter().find(|x| !x.is_space()) else { return Ok(()) };
match self.tex.meaning(&name) {
Meaning::Count(r) => {
let v = crate::expr::eval(&text, &self.vars())?;
self.tex.set_count(r, v.trunc() as i64, false);
}
Meaning::Dimen(r) | Meaning::Skip(r) => {
let (v, u) = crate::expr::eval_length(&text, &self.vars())?;
let pt = if u { v / crate::PT } else { v };
self.tex.set_dimen(r, (pt * 65536.0).round() as i64, false);
}
_ => {}
}
Ok(())
}
Cmd::Pgfmathdeclarefunction => {
let name = self.argument_string()?;
let n: usize = self.argument_string()?.parse().unwrap_or(0);
let body = self.argument()?;
let text = tex::tokens_to_string(&body);
let expr = text.split("\\pgfmathparse").nth(1).map(|r| r.trim().trim_start_matches('{').trim_end_matches('}').to_string());
match expr {
Some(e) => {
let params = (1..=n).map(|k| format!("#{k}")).collect();
Rc::make_mut(&mut self.st.functions).insert(name, (params, e));
}
None => self.warn(format!("\\pgfmathdeclarefunction{{{name}}}: only a \\pgfmathparse body is supported")),
}
Ok(())
}
Cmd::Pgfmathprintnumber => {
let o = self.optional()?;
let a = self.argument()?;
let v = self.number_of(a)?;
let saved = self.st.number_format.clone();
if let Some(o) = o {
let path = self.st.key_path.clone();
self.process_keys(o, Some("/pgf/number format"))?;
self.st.key_path = path;
}
let s = self.st.number_format.print(v);
self.st.number_format = saved;
self.tex.push_tokens(tex::string_tokens(&s));
Ok(())
}
Cmd::Usetikzlibrary | Cmd::Usepackage | Cmd::Documentclass => {
let o = if cmd == Cmd::Documentclass { self.documentclass_options() } else { self.optional()? };
let a = match self.tex.next_nonspace() {
Some(Token::Char(_, Cat::Begin)) => {
let body = self.tex.read_group_body()?;
self.tex.expand_to_string(body)?
}
Some(t) => {
self.tex.back(t);
String::new()
}
None => String::new(),
};
if cmd == Cmd::Documentclass && a.contains("standalone") {
if let Some(o) = o {
self.standalone_options(o)?;
}
}
Ok(())
}
Cmd::OnLayer => {
let name = self.argument_string()?;
self.begin_on_layer(name);
Ok(())
}
Cmd::EndOnLayer => {
self.end_on_layer();
Ok(())
}
Cmd::SetLayers => {
let list = self.argument_string()?;
self.layer_order = list.split(',').map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect();
Ok(())
}
Cmd::Frame => self.frame_macros(),
Cmd::Tikzfading => self.tikzfading(),
Cmd::Ignore1 => {
let _ = self.optional()?;
let _ = self.argument()?;
Ok(())
}
Cmd::Fbox => {
let arg = self.argument()?;
let start = self.boxes.len();
self.begin_group();
let r = self.run_tokens(arg);
self.end_group();
r?;
if self.picture_depth == 0 {
let sep = self.tex.dimens.get(&247).copied().unwrap_or(0) as f64 / 65536.0 * crate::PT;
let rule = self.tex.dimens.get(&246).copied().unwrap_or(0) as f64 / 65536.0 * crate::PT;
for b in &mut self.boxes[start..] {
let (Some(lo), Some(hi)) = (b.canvas.min, b.canvas.max) else { continue };
let d = sep + rule / 2.0;
let (lo, hi) = (Point::new(lo.x - d, lo.y - d), Point::new(hi.x + d, hi.y + d));
use crate::scene::Segment as S;
let segments = vec![S::MoveTo(lo), S::LineTo(Point::new(hi.x, lo.y)), S::LineTo(hi), S::LineTo(Point::new(lo.x, hi.y)), S::Close];
let stroke = crate::canvas::Stroke {
color: crate::color::Color::BLACK,
width: rule,
dash: None,
cap: crate::canvas::Cap::Butt,
join: crate::canvas::Join::Miter,
miter_limit: 10.0,
opacity: 1.0,
};
b.canvas.items.push(Item::Path { segments, fill: None, stroke: Some(stroke) });
b.canvas.include(Point::new(lo.x - rule / 2.0, lo.y - rule / 2.0));
b.canvas.include(Point::new(hi.x + rule / 2.0, hi.y + rule / 2.0));
}
}
Ok(())
}
Cmd::Hspace => {
match self.tex.next_nonspace() {
Some(t) if t.is_char('*') => {}
Some(t) => self.tex.back(t),
None => {}
}
let arg = self.argument()?;
if self.picture_depth == 0 {
let w = self.length_of(arg)?;
self.layout.push(Layout::Skip(w));
}
Ok(())
}
Cmd::Hskip => {
let w = self.tex.scan_dimen()?;
for k in ["plus", "minus"] {
if self.tex.scan_keyword(k)? {
let _ = self.tex.scan_dimen();
let _ = self.tex.scan_keyword("fill")? || self.tex.scan_keyword("fil")?;
}
}
if self.picture_depth == 0 {
self.layout.push(Layout::Skip(w as f64 / 65536.0 * crate::PT));
}
Ok(())
}
Cmd::ResetBoundingBox => {
(self.canvas.min, self.canvas.max) = (None, None);
Ok(())
}
Cmd::Ignore2 => {
let _ = self.argument()?;
let _ = self.argument()?;
Ok(())
}
Cmd::FontSize => {
if let Token::Cs(n) = t {
self.st.font_size = font_size(n);
self.st.ambient_size = self.st.font_size;
self.st.font.push_str(&format!("\\{n}"));
}
Ok(())
}
Cmd::Quad => {
if self.picture_depth == 0 {
let em = if t.is_cs("qquad") { 2.0 } else { 1.0 };
self.layout.push(Layout::Skip(em * 10.0 * crate::PT));
}
Ok(())
}
Cmd::FontSeries => {
if let Token::Cs(n) = t {
self.st.font.push_str(&format!("\\{n}"));
}
Ok(())
}
_ => Ok(()),
}
}
fn recover(&mut self, r: Result<(), Error>, t: &Token) -> Result<(), Error> {
match r {
Ok(()) => Ok(()),
Err(e) if is_limit(&e) => Err(e),
Err(e) => {
self.warn(format!("{}: {e}; statement ignored", tex::show_token(t)));
let _ = self.skip_statement();
Ok(())
}
}
}
fn documentclass_options(&mut self) -> Option<Vec<Token>> {
let t = self.tex.next_nonspace()?;
if !t.is_char('[') {
self.tex.back(t);
return None;
}
let mut out = Vec::new();
let mut depth = 0usize;
while let Some(x) = self.tex.get_token() {
match &x {
Token::Char(_, Cat::Begin) => depth += 1,
Token::Char(_, Cat::End) => depth = depth.saturating_sub(1),
x if depth == 0 && x.is_char(']') => return Some(out),
x if x.is_cs("begin") => {
self.tex.back(x.clone());
return Some(out);
}
_ => {}
}
out.push(x);
}
Some(out)
}
fn standalone_options(&mut self, o: Vec<Token>) -> Result<(), Error> {
for item in crate::keys::split_list(&o) {
if item.key == "border" {
if let Some(v) = item.value {
let s = self.tex.expand_to_string(v)?;
let first = s.split_whitespace().next().unwrap_or("0pt").to_string();
if let Ok(m) = self.length(&first) {
self.canvas.margin = m;
}
}
}
}
Ok(())
}
pub fn next_statement(&mut self) -> Result<Vec<Token>, Error> {
match self.tex.next_nonspace() {
Some(Token::Char(_, Cat::Begin)) => self.tex.read_group_body(),
Some(t) if t.is_cs("foreach") => {
let mut out = vec![t];
while let Some(x) = self.tex.get_token() {
if x.is_char('[') {
out.push(x);
out.extend(self.tex.read_until_char(']', false)?);
out.push(Token::other(']'));
continue;
}
let is_i = x.is_char('i');
out.push(x);
if is_i {
match self.tex.get_token() {
Some(n) if n.is_char('n') => {
out.push(n);
break;
}
Some(n) => self.tex.back(n),
None => break,
}
}
}
for _ in 0..2 {
match self.tex.next_nonspace() {
Some(b @ Token::Char(_, Cat::Begin)) => {
out.push(b);
out.extend(self.tex.read_group_body()?);
out.push(Token::Char('}', Cat::End));
}
Some(x) if x.is_char('[') => {
out.push(x);
out.extend(self.tex.read_until_char(']', false)?);
out.push(Token::other(']'));
continue;
}
Some(x) => {
if out.last().is_some_and(|t| matches!(t, Token::Char(_, Cat::End))) {
self.tex.back(x);
} else {
out.push(x);
}
}
None => {}
}
break;
}
out.push(Token::Char(' ', Cat::Space));
out.extend(self.next_statement_braced()?);
Ok(out)
}
Some(t) => {
self.tex.back(t);
let mut out = Vec::new();
let mut depth = 0usize;
while let Some(x) = self.tex.get_token() {
match x {
Token::Char(_, Cat::Begin) => depth += 1,
Token::Char(_, Cat::End) => depth = depth.saturating_sub(1),
_ => {}
}
let end = depth == 0 && x.is_char(';');
out.push(x);
if end || out.len() > tex::MAX_TOKENS {
break;
}
}
Ok(out)
}
None => Ok(Vec::new()),
}
}
fn next_statement_braced(&mut self) -> Result<Vec<Token>, Error> {
match self.tex.next_nonspace() {
Some(b @ Token::Char(_, Cat::Begin)) => {
let mut out = vec![b];
out.extend(self.tex.read_group_body()?);
out.push(Token::Char('}', Cat::End));
Ok(out)
}
Some(t) => {
self.tex.back(t);
self.next_statement()
}
None => Ok(Vec::new()),
}
}
fn begin_picture(&mut self, o: Option<Vec<Token>>) -> Result<(), Error> {
if self.depth >= MAX_DEPTH {
return Err(Error::Figure(format!("scope/\\foreach nesting went past {MAX_DEPTH} levels")));
}
self.depth += 1;
if self.picture_depth == 0 {
self.pictures += 1;
let margin = self.canvas.margin;
self.canvas = Canvas { margin, ..Canvas::default() };
self.canvas_frozen = false;
}
self.picture_depth += 1;
self.begin_group();
self.begin_scope();
self.run_tokens(tikra_tokens("\\tikz@picture@start"))?;
if let Some(o) = o {
self.process_keys(o, Some("/tikz")).map_err(|e| Error::Figure(format!("picture options: {e}")))?;
}
self.st.apply_pending();
self.open_transparency_group();
let mut begin = std::mem::take(&mut self.st.at_begin_scope);
begin.append(&mut self.st.at_begin_picture);
let mut end = std::mem::take(&mut self.st.at_end_picture);
end.append(&mut self.st.at_end_scope);
self.scope_ends.push(end);
self.run_tokens(begin)
}
fn open_transparency_group(&mut self) {
if std::mem::take(&mut self.st.transparency_group) {
let opacity = self.st.fill_opacity.min(self.st.draw_opacity);
self.open_in_scope(Item::Group { opacity });
self.st.draw_opacity = 1.0;
self.st.fill_opacity = 1.0;
self.st.text_opacity = 1.0;
}
}
fn open_tikz_scope(&mut self, o: Option<Vec<Token>>, fatal: bool) -> Result<(), Error> {
self.begin_group();
self.begin_scope();
self.run_tokens(tikra_tokens("\\tikz@scope@options"))?;
if let Some(o) = o {
let r = self.process_keys(o, Some("/tikz"));
if fatal {
r.map_err(|e| Error::Figure(format!("scope options: {e}")))?;
} else {
r?;
}
}
self.st.apply_pending();
self.open_transparency_group();
let begin = std::mem::take(&mut self.st.at_begin_scope);
let end = std::mem::take(&mut self.st.at_end_scope);
self.scope_ends.push(end);
self.run_tokens(begin)
}
fn close_tikz_scope(&mut self) -> Result<(), Error> {
let end = self.scope_ends.pop().unwrap_or_default();
let r = self.run_tokens(end);
self.end_scope();
self.end_group();
r
}
fn tikzfading(&mut self) -> Result<(), Error> {
let o = self.optional()?.unwrap_or_default();
let mut name = String::new();
let mut opts = Vec::new();
for item in crate::keys::split_list(&o) {
if item.key == "name" {
name = self.tex.expand_to_string(item.value.unwrap_or_default())?.trim().to_string();
} else {
if !opts.is_empty() {
opts.push(Token::other(','));
}
opts.extend(tikra_tokens(&item.key));
if let Some(v) = item.value {
opts.push(Token::other('='));
opts.push(Token::Char('{', Cat::Begin));
opts.extend(v);
opts.push(Token::Char('}', Cat::End));
}
}
}
self.begin_group();
let r = self.process_keys(opts, Some("/tikz"));
let square = [crate::scene::Segment::MoveTo(Point::new(-25.0, -25.0)), crate::scene::Segment::LineTo(Point::new(25.0, 25.0))];
let sh = self.shading_of(&square);
self.end_group();
r?;
if !name.is_empty() {
self.fadings.insert(name, sh);
}
Ok(())
}
fn begin_on_layer(&mut self, name: String) {
self.begin_group();
let main = std::mem::take(&mut self.canvas.items);
self.on_layer.push((name, main));
}
fn end_on_layer(&mut self) {
let Some((name, main)) = self.on_layer.pop() else {
self.warn("\\endpgfonlayer without \\pgfonlayer");
return;
};
let drawn = std::mem::replace(&mut self.canvas.items, main);
self.layers.entry(name).or_default().extend(drawn);
self.end_group();
}
fn frame_macros(&mut self) -> Result<(), Error> {
if matches!(self.tex.meaning_of("tikra@xa"), Meaning::Macro(_)) {
return Ok(());
}
let (Some(lo), Some(hi)) = (self.canvas.min, self.canvas.max) else { return Ok(()) };
let sep = |e: &mut Engine, name: &str| -> f64 {
let v = match e.tex.meaning_of(name) {
Meaning::Macro(m) => tex::tokens_to_string(&tex::substitute(&m.body, &[])),
_ => "0pt".into(),
};
e.length(&v).unwrap_or(0.0)
};
let (fx, fy) = (sep(self, "tikz@framexsep"), sep(self, "tikz@frameysep"));
let (ox, oy) = (sep(self, "tikz@outerframexsep"), sep(self, "tikz@outerframeysep"));
let pt = |v: f64| format!("{}pt", v / crate::PT);
for (name, v) in [
("tikra@xa", lo.x - fx),
("tikra@ya", lo.y - fy),
("tikra@xb", hi.x + fx),
("tikra@yb", hi.y + fy),
("tikra@oxa", lo.x - fx - ox),
("tikra@oya", lo.y - fy - oy),
("tikra@oxb", hi.x + fx + ox),
("tikra@oyb", hi.y + fy + oy),
] {
self.tex.define_string(name, &pt(v), false);
}
Ok(())
}
fn end_picture(&mut self) {
let end = self.scope_ends.pop().unwrap_or_default();
if let Err(e) = self.run_tokens(end) {
self.warn(format!("execute at end picture: {e}"));
}
while !self.on_layer.is_empty() {
self.end_on_layer();
}
self.end_scope();
let outermost = self.picture_depth == 1;
if outermost && !self.layers.is_empty() {
let main = std::mem::take(&mut self.canvas.items);
let mut layers = std::mem::take(&mut self.layers);
let mut order = self.layer_order.clone();
if !order.iter().any(|l| l == "main") {
order.push("main".into());
}
let mut main = Some(main);
for name in order {
if name == "main" {
self.canvas.items.extend(main.take().unwrap_or_default());
} else if let Some(v) = layers.remove(&name) {
self.canvas.items.extend(v);
}
}
for (_, v) in layers {
self.canvas.items.extend(v);
}
}
let baseline = if outermost { self.picture_baseline() } else { 0.0 };
if outermost {
self.trim_picture();
}
self.end_group();
self.picture_depth = self.picture_depth.saturating_sub(1);
self.depth = self.depth.saturating_sub(1);
if outermost {
let margin = self.canvas.margin;
let canvas = std::mem::replace(&mut self.canvas, Canvas { margin, ..Canvas::default() });
let hsize = self.tex.dimens.get(&255).copied().unwrap_or(0);
if hsize != 345 * 65536 && hsize > 0 {
self.layout.push(Layout::Width(hsize as f64 / 65536.0 * crate::PT));
}
self.layout.push(Layout::Picture(self.boxes.len()));
self.boxes.push(PictureBox { canvas, baseline });
}
}
fn trim_picture(&mut self) {
for (spec, left) in [(self.st.trim_left.clone(), true), (self.st.trim_right.clone(), false)] {
let Some(spec) = spec else { continue };
let s = spec.trim();
let x = match s.strip_prefix('(').and_then(|r| r.strip_suffix(')')) {
Some(inner) => self.coordinate(inner).map(|c| c.p.x),
None if s.is_empty() => Ok(0.0),
None => self.length(s),
};
let Ok(x) = x else { continue };
match (left, self.canvas.min.as_mut(), self.canvas.max.as_mut()) {
(true, Some(m), _) => m.x = x,
(false, _, Some(m)) => m.x = x,
_ => {}
}
}
}
fn picture_baseline(&mut self) -> f64 {
let lower = self.canvas.min.map_or(0.0, |m| m.y);
let Some(spec) = self.st.baseline.clone() else { return lower };
let s = spec.trim();
if s.is_empty() {
return 0.0;
}
if let Some(inner) = s.strip_prefix('(').and_then(|r| r.strip_suffix(')')) {
return self.coordinate(inner).map(|c| c.p.y).unwrap_or(lower);
}
if let Some(point) = s.strip_prefix("tikra@point:") {
let t = self.st.transform;
return self.point_of(tikra_tokens(point)).map(|p| t.apply(p).y).unwrap_or(lower);
}
self.length(s).unwrap_or(lower)
}
fn tikz_command(&mut self) -> Result<(), Error> {
let o = self.optional()?;
if let Some(t) = self.tex.next_nonspace() {
let dv = t.is_cs("datavisualization");
self.tex.back(t);
if dv {
let _ = self.tex.get_token();
self.begin_picture(o)?;
let r = self.datavisualization();
self.end_picture();
return r;
}
}
let body = self.next_statement()?;
self.begin_picture(o)?;
self.run_tokens(body)?;
self.end_picture();
Ok(())
}
fn begin_environment(&mut self) -> Result<(), Error> {
let name = self.argument_string()?;
self.tex.env_stack.push(name.clone());
match name.as_str() {
"tikzpicture" => {
let o = self.optional()?;
self.begin_picture(o)
}
"pgfpicture" => self.begin_picture(None),
"pgfscope" => {
self.begin_group();
self.begin_scope();
Ok(())
}
"scope" => {
if self.depth >= MAX_DEPTH {
return Err(Error::Figure(format!("scope/\\foreach nesting went past {MAX_DEPTH} levels")));
}
self.depth += 1;
let o = self.optional()?;
self.open_tikz_scope(o, true)
}
n if pgfplots::is_axis(n) => self.begin_axis(n),
"groupplot" => self.begin_groupplot(),
"pgfonlayer" => {
let layer = self.argument_string()?;
self.begin_on_layer(layer);
Ok(())
}
"pgfviewboxscope" => self.begin_view_box_scope(),
"pgfinterruptboundingbox" => {
self.interrupted_boxes.push((self.canvas.min, self.canvas.max));
self.begin_group();
Ok(())
}
"tikzfadingfrompicture" => {
self.tex.env_stack.pop();
self.fading_from_picture()
}
"tabular" if self.picture_depth == 0 => {
let _ = self.optional()?;
let spec = self.argument_string()?;
let cols: Vec<u32> = spec
.chars()
.filter_map(|c| match c {
'l' => Some(0),
'c' => Some(1),
'r' => Some(2),
_ => None,
})
.take(12)
.collect();
let code = cols.iter().enumerate().fold(cols.len() as u32, |acc, (k, a)| acc | (a << (8 + 2 * k)));
self.layout.push(Layout::TabStart(code));
self.begin_group();
Ok(())
}
"document" | "center" | "figure" | "flushleft" | "flushright" | "minipage" => {
if name == "center" && self.picture_depth == 0 {
self.layout.extend([Layout::Par, Layout::Center(true)]);
}
if name == "minipage" {
let _ = self.optional()?;
let width = self.argument()?;
if self.picture_depth == 0 {
let w = self.length_of(width)?;
self.layout.push(Layout::Width(w));
}
} else if name == "figure" {
let _ = self.optional()?;
}
self.begin_group();
Ok(())
}
_ => {
if let Some(code) = self.tex.environment_begin(&name)? {
self.begin_group();
self.tex.push_tokens(code);
return Ok(());
}
self.warn(format!("environment {name} not supported: content read as statements"));
self.begin_group();
Ok(())
}
}
}
fn end_environment(&mut self) -> Result<(), Error> {
let name = self.argument_string()?;
let Some(pos) = self.tex.env_stack.iter().rposition(|n| *n == name) else {
self.warn(format!("\\end{{{name}}} without a matching \\begin: ignored"));
return Ok(());
};
while self.tex.env_stack.len() > pos + 1 {
let inner = self.tex.env_stack.pop().unwrap_or_default();
self.warn(format!("\\begin{{{inner}}} without \\end: closed at \\end{{{name}}}"));
self.close_environment(&inner)?;
}
self.tex.env_stack.pop();
self.close_environment(&name)
}
fn close_environment(&mut self, name: &str) -> Result<(), Error> {
match name {
"tikzpicture" | "pgfpicture" => self.end_picture(),
n if pgfplots::is_axis(n) => self.end_axis()?,
"groupplot" => self.end_groupplot()?,
"pgfscope" | "pgfviewboxscope" => {
self.end_scope();
self.end_group();
}
"pgfonlayer" => self.end_on_layer(),
"pgfinterruptboundingbox" => {
self.end_group();
if let Some((min, max)) = self.interrupted_boxes.pop() {
(self.canvas.min, self.canvas.max) = (min, max);
}
}
"scope" => {
self.close_tikz_scope()?;
self.depth = self.depth.saturating_sub(1);
}
_ => {
if let Some((_, end)) = self.tex.environments.get(name).cloned() {
self.run_tokens(end)?;
}
self.end_group();
if name == "center" && self.picture_depth == 0 {
self.layout.extend([Layout::Par, Layout::Center(false)]);
}
if name == "tabular" && self.picture_depth == 0 {
self.layout.push(Layout::TabEnd);
}
}
}
Ok(())
}
fn math_text_value(&mut self, text: &str) -> Result<Option<String>, Error> {
let t = text.trim();
let t = t.strip_prefix('{').and_then(|x| x.strip_suffix('}')).unwrap_or(t).trim();
if !t.contains('"') {
return Ok(None);
}
if let Some(q) = t.strip_prefix('"').and_then(|x| x.strip_suffix('"')).filter(|q| !q.contains('"')) {
return Ok(Some(q.to_string()));
}
let Some(args) = t.strip_prefix("ifthenelse").map(str::trim).and_then(|a| a.strip_prefix('(')).map(|a| a.strip_suffix(')').unwrap_or(a)) else { return Ok(None) };
let mut parts = vec![String::new()];
let (mut depth, mut quoted) = (0, false);
for c in args.chars() {
match c {
'"' => quoted = !quoted,
'(' if !quoted => depth += 1,
')' if !quoted => depth -= 1,
',' if !quoted && depth == 0 => {
parts.push(String::new());
continue;
}
_ => {}
}
parts.last_mut().unwrap().push(c);
}
let [cond, a, b] = &parts[..] else { return Ok(None) };
let pick = if self.number(cond)? != 0.0 { a } else { b };
Ok(Some(self.math_text_value(pick)?.unwrap_or_else(|| pick.trim().to_string())))
}
fn foreach(&mut self) -> Result<(), Error> {
let mut vars: Vec<Token> = Vec::new();
let mut opts: Vec<Token> = Vec::new();
loop {
let Some(t) = self.tex.get_token() else { return Err(Error::Figure("\\foreach without 'in'".into())) };
if t.is_char('[') {
opts.extend(self.tex.read_until_char(']', false)?);
continue;
}
if t.is_char('i') {
if let Some(n) = self.tex.get_token() {
if n.is_char('n') {
break;
}
self.tex.back(n);
}
}
vars.push(t);
if vars.len() > 1000 {
return Err(Error::Figure("\\foreach without 'in'".into()));
}
}
let names: Vec<Token> = vars.iter().filter(|t| matches!(t, Token::Cs(_))).cloned().collect();
let list_tokens = match self.tex.next_nonspace() {
Some(Token::Char(_, Cat::Begin)) => self.tex.read_group_body()?,
Some(t) => vec![t],
None => Vec::new(),
};
let list = self.tex.expand_fully(list_tokens)?;
let items = foreach_items(&list);
if let Some(t) = self.tex.next_nonspace() {
if t.is_char('[') {
opts.extend(tex::string_tokens(","));
opts.extend(self.tex.read_until_char(']', false)?);
} else {
self.tex.back(t);
}
}
let body = self.next_statement()?;
let o = foreach_options(&opts);
if self.depth >= MAX_DEPTH {
return Err(Error::Figure(format!("scope/\\foreach nesting went past {MAX_DEPTH} levels")));
}
self.depth += 1;
let mut memory = o.initial_memory();
for (k, item) in items.iter().enumerate() {
self.count_step()?;
self.begin_group();
let parts = split_slash(item);
for (i, name) in names.iter().enumerate() {
let v = parts.get(i).or(parts.last()).cloned().unwrap_or_default();
self.tex.set_meaning(name, macro_tokens(v), false);
}
self.foreach_iteration(&o, k, &memory)?;
let r = self.run_tokens(body.clone());
self.foreach_remember(&o, &mut memory);
self.end_group();
r?;
if matches!(self.tex.meaning_of("tikra@foreach@break"), Meaning::Macro(m) if !m.body.is_empty()) {
self.tex.set_meaning(&Token::cs("tikra@foreach@break"), macro_tokens(Vec::new()), true);
break;
}
}
self.depth -= 1;
Ok(())
}
pub fn foreach_iteration(&mut self, o: &ForeachOptions, k: usize, memory: &[Option<Vec<Token>>]) -> Result<(), Error> {
if let Some((n, start)) = &o.count {
self.tex.set_meaning(n, macro_from(&(start + k as i64).to_string()), false);
}
for ((_, as_name, _), m) in o.remember.iter().zip(memory) {
if let Some(v) = m {
self.tex.set_meaning(as_name, macro_tokens(v.clone()), false);
}
}
for (var, target, using) in &o.evaluate {
let expr = match using {
Some(u) => u.clone(),
None => vec![var.clone()],
};
let dest = target.clone().unwrap_or_else(|| var.clone());
let text = self.tex.expand_to_string(expr.clone())?;
if let Some(s) = self.math_text_value(&text)? {
self.tex.set_meaning(&dest, macro_tokens(tikra_tokens(&s)), false);
continue;
}
let v = self.number_of(expr)?;
self.tex.set_meaning(&dest, macro_from(&pgf_number(v)), false);
}
Ok(())
}
pub fn foreach_remember(&self, o: &ForeachOptions, memory: &mut [Option<Vec<Token>>]) {
for (i, (var, _, _)) in o.remember.iter().enumerate() {
if let Meaning::Macro(m) = self.tex.meaning(var) {
memory[i] = Some(tex::substitute(&m.body, &[]));
}
}
}
}
#[derive(Default)]
pub struct ForeachOptions {
count: Option<(Token, i64)>,
remember: Vec<(Token, Token, Option<Vec<Token>>)>,
evaluate: Vec<(Token, Option<Token>, Option<Vec<Token>>)>,
}
impl ForeachOptions {
pub fn initial_memory(&self) -> Vec<Option<Vec<Token>>> {
self.remember.iter().map(|r| r.2.clone()).collect()
}
}
pub fn foreach_options(opts: &[Token]) -> ForeachOptions {
let options = crate::keys::split_list(opts);
let mut count: Option<(Token, i64)> = None;
let mut remember: Vec<(Token, Token, Option<Vec<Token>>)> = Vec::new();
let mut evaluate: Vec<(Token, Option<Token>, Option<Vec<Token>>)> = Vec::new();
for o in &options {
let v = o.value.clone().unwrap_or_default();
let parts: Vec<Token> = v.iter().filter(|t| !t.is_space()).cloned().collect();
match o.key.as_str() {
"count" => {
let name = parts.first().cloned();
let s = tex::tokens_to_string(&v);
let start = s.split("from").nth(1).and_then(|x| x.trim().parse().ok()).unwrap_or(1);
if let Some(n) = name {
count = Some((n, start));
}
}
"remember" => {
let s = tex::tokens_to_string(&v);
let initially = s.split("initially").nth(1).map(|x| tex::string_tokens(x.trim().trim_start_matches('(').trim_end_matches(')')));
let cs: Vec<Token> = v.iter().filter(|t| matches!(t, Token::Cs(_))).cloned().collect();
if cs.len() >= 2 {
remember.push((cs[0].clone(), cs[1].clone(), initially));
}
}
"evaluate" if !tex::tokens_to_string(&v).contains(" as ") && tex::tokens_to_string(&v).contains('=') => {
let s = tex::tokens_to_string(&v);
for statement in s.split(';') {
if let Some((name, expr)) = statement.split_once('=') {
if let Some(var) = tikra_tokens(name.trim()).into_iter().find(|t| matches!(t, Token::Cs(_))) {
evaluate.push((var.clone(), Some(var), Some(tikra_tokens(expr.trim()))));
}
}
}
}
"evaluate" => {
let s = tex::tokens_to_string(&v);
let cs: Vec<Token> = v.iter().filter(|t| matches!(t, Token::Cs(_))).cloned().collect();
let using = s.split("using").nth(1).map(|x| tikra_tokens(x.trim()));
let target = if s.contains(" as ") { cs.get(1).cloned() } else { None };
if let Some(var) = cs.first() {
evaluate.push((var.clone(), target, using));
}
}
"parse" => {}
_ => {}
}
}
ForeachOptions { count, remember, evaluate }
}
impl Default for Engine {
fn default() -> Self {
Engine::new()
}
}
pub fn is_limit(e: &Error) -> bool {
let s = e.to_string();
s.contains("figure too large") || s.contains("scope/\\foreach nesting went past") || s.contains("picture options:") || s.contains("scope options:")
}
pub fn macro_from(s: &str) -> Meaning {
macro_tokens(tex::string_tokens(s))
}
pub fn macro_tokens(v: Vec<Token>) -> Meaning {
Meaning::Macro(Rc::new(tex::Macro { params: Vec::new(), body: v.into_iter().map(tex::Body::Tok).collect(), optional: None, protected: false }))
}
pub fn tikra_tokens(text: &str) -> Vec<Token> {
let mut t = Tex::new_lexer();
t.push_text(text);
let mut v = Vec::new();
while let Some(x) = t.get_token() {
v.push(x);
}
v
}
pub fn pgf_number(v: f64) -> String {
let r = (v * 100000.0).round() / 100000.0;
if r.fract() == 0.0 && r.abs() < 1e15 {
format!("{}", r as i64)
} else {
format!("{r}")
}
}
fn font_size(name: &str) -> f64 {
match name {
"tiny" => 5.0,
"scriptsize" => 7.0,
"footnotesize" => 8.0,
"small" => 9.0,
"large" => 12.0,
"Large" => 14.4,
"LARGE" => 17.28,
"huge" => 20.74,
"Huge" => 24.88,
_ => 10.0,
}
}
fn split_slash(item: &[Token]) -> Vec<Vec<Token>> {
let mut parts = vec![Vec::new()];
let mut depth = 0usize;
for t in item {
match t {
Token::Char(_, Cat::Begin) => depth += 1,
Token::Char(_, Cat::End) => depth = depth.saturating_sub(1),
Token::Char('/', _) if depth == 0 => {
parts.push(Vec::new());
continue;
}
_ => {}
}
if let Some(p) = parts.last_mut() {
p.push(t.clone());
}
}
parts.into_iter().map(|p| tex::strip_outer_braces(crate::keys::trim(&p).to_vec())).collect()
}
pub fn foreach_items(list: &[Token]) -> Vec<Vec<Token>> {
let mut raw: Vec<Vec<Token>> = Vec::new();
let mut current: Vec<Token> = Vec::new();
let mut depth = 0usize;
let mut parens = 0usize;
for t in list {
match t {
Token::Char(_, Cat::Begin) => depth += 1,
Token::Char(_, Cat::End) => depth = depth.saturating_sub(1),
Token::Char('(', _) if depth == 0 && (parens > 0 || current.iter().all(Token::is_space)) => parens += 1,
Token::Char(')', _) if depth == 0 && parens > 0 => parens -= 1,
Token::Char(',', _) if depth == 0 && parens == 0 => {
raw.push(std::mem::take(&mut current));
continue;
}
_ => {}
}
current.push(t.clone());
}
raw.push(current);
let raw: Vec<Vec<Token>> = raw.into_iter().map(|p| tex::strip_outer_braces(crate::keys::trim(&p).to_vec())).filter(|p| !p.is_empty()).collect();
let text = |v: &[Token]| tex::tokens_to_string(v).trim().to_string();
let mut out: Vec<Vec<Token>> = Vec::new();
let mut i = 0;
while i < raw.len() {
if text(&raw[i]) == "..." && !out.is_empty() && i + 1 < raw.len() {
let end_s = text(&raw[i + 1]);
let last_s = text(&out[out.len() - 1]);
let before = if out.len() >= 2 { Some(text(&out[out.len() - 2])) } else { None };
let letter = |s: &str| if s.chars().count() == 1 && s.chars().all(|c| c.is_ascii_alphabetic()) { s.chars().next() } else { None };
if let (Some(a), Some(b)) = (letter(&last_s), letter(&end_s)) {
let step: i32 = before.as_deref().and_then(letter).map_or(1, |p| a as i32 - p as i32).max(1);
let mut c = a as i32 + step;
while c <= b as i32 && out.len() < 100_000 {
out.push(tex::string_tokens(&char::from_u32(c as u32).unwrap_or('?').to_string()));
c += step;
}
i += 2;
continue;
}
let (Ok(last), Ok(end)) = (last_s.parse::<f64>(), end_s.parse::<f64>()) else {
i += 1;
continue;
};
let step = before.and_then(|b| b.parse::<f64>().ok()).map_or(if end >= last { 1.0 } else { -1.0 }, |b| last - b);
if step.abs() > 1e-12 {
let mut k = 1.0;
loop {
let v = last + step * k;
if (step > 0.0 && v > end + 1e-9) || (step < 0.0 && v < end - 1e-9) || out.len() > 100_000 {
break;
}
out.push(tex::string_tokens(&pgf_number(v)));
k += 1.0;
}
}
i += 2;
continue;
}
out.push(raw[i].clone());
i += 1;
}
out
}
const SPACE: f64 = 3.33333 * crate::PT;
const BASELINE_SKIP: f64 = 12.0 * crate::PT;
const LINE_SKIP: f64 = 1.0 * crate::PT;
fn typeset(boxes: &[PictureBox], layout: &[Layout], words: &[(String, String)], margin: f64) -> Canvas {
let mut out = Canvas { margin, ..Canvas::default() };
let mut warnings = Vec::new();
let labels: Vec<crate::scene::Label> = words.iter().map(|(font, w)| crate::label::compose_label(w, font, None, 'l', crate::color::Color::BLACK, &mut warnings)).collect();
let (table_boxes, layout) = tables(boxes, layout, &labels);
let boxes = &table_boxes[..];
let layout = &layout[..];
let width_of = |l: &Layout| match l {
Layout::Picture(i) => boxes[*i].canvas.min.zip(boxes[*i].canvas.max).map_or(0.0, |(a, b)| b.x - a.x),
Layout::Word(i) => labels[*i].width(),
Layout::Skip(w) => *w,
Layout::Space => SPACE,
_ => 0.0,
};
struct Line {
items: Vec<Layout>,
center: bool,
empty_kept: bool,
}
let line = |center| Line { items: Vec::new(), center, empty_kept: false };
let mut lines = vec![line(false)];
let mut center = false;
let mut hsize: Option<f64> = None;
let mut after_break = false;
for l in layout {
match l {
Layout::Par => {
if after_break {
lines.last_mut().expect("a line").empty_kept = true;
}
lines.push(line(center));
}
Layout::Break => lines.push(line(center)),
Layout::Center(c) => {
center = *c;
lines.last_mut().expect("a line").center = center;
}
other => {
if let Layout::Width(w) = other {
hsize = Some(*w);
}
lines.last_mut().expect("a line").items.push(*other);
}
}
after_break = matches!(l, Layout::Break) || (after_break && matches!(l, Layout::Center(_)));
}
if let Some(w) = hsize {
let mut broken = Vec::new();
for l in lines {
let mut cur = Line { items: Vec::new(), center: l.center, empty_kept: l.empty_kept };
let (mut x, mut pending) = (0.0, 0.0);
let mut started = false;
let mut chunk: Vec<Layout> = Vec::new();
let mut chunk_w = 0.0;
let flush = |cur: &mut Line, broken: &mut Vec<Line>, chunk: &mut Vec<Layout>, chunk_w: &mut f64, x: &mut f64, pending: &mut f64, started: &mut bool| {
if chunk.is_empty() {
return;
}
let content = chunk.iter().any(|c| matches!(c, Layout::Picture(_) | Layout::Word(_)));
if *started && content && *x + *pending + *chunk_w > w + 1e-9 {
let next = Line { items: Vec::new(), center: cur.center, empty_kept: false };
broken.push(std::mem::replace(cur, next));
*x = 0.0;
*pending = 0.0;
} else if *started {
cur.items.push(Layout::Space);
*x += *pending;
}
*pending = 0.0;
cur.items.append(chunk);
*x += *chunk_w;
*chunk_w = 0.0;
*started |= content;
};
for item in l.items {
if matches!(item, Layout::Space) {
flush(&mut cur, &mut broken, &mut chunk, &mut chunk_w, &mut x, &mut pending, &mut started);
if started {
pending = SPACE;
}
} else {
chunk_w += width_of(&item);
chunk.push(item);
}
}
flush(&mut cur, &mut broken, &mut chunk, &mut chunk_w, &mut x, &mut pending, &mut started);
broken.push(cur);
}
lines = broken;
}
let mut baseline_y = 0.0;
let mut previous_depth: Option<f64> = None;
let has_picture = layout.iter().any(|x| matches!(x, Layout::Picture(_)));
let shown = |l: &Line| l.items.iter().any(|x| matches!(x, Layout::Picture(_))) || (has_picture && (l.empty_kept || l.items.iter().any(|x| matches!(x, Layout::Word(_)))));
for line in lines.iter().filter(|l| shown(l)) {
let (mut height, mut depth) = (0.0f64, 0.0f64);
for l in &line.items {
match l {
Layout::Picture(i) => {
let b = &boxes[*i];
let (lo, hi) = (b.canvas.min.map_or(0.0, |m| m.y), b.canvas.max.map_or(0.0, |m| m.y));
height = height.max(hi - b.baseline);
depth = depth.max(b.baseline - lo);
}
Layout::Word(i) => {
height = height.max(labels[*i].height());
depth = depth.max(labels[*i].depth());
}
_ => {}
}
}
if let Some(d) = previous_depth {
let gap = BASELINE_SKIP - d - height;
baseline_y -= if gap >= 0.0 { BASELINE_SKIP } else { d + LINE_SKIP + height };
}
let mut x = 0.0;
if let (true, Some(w)) = (line.center, hsize) {
let first = line.items.iter().position(|l| matches!(l, Layout::Picture(_) | Layout::Word(_) | Layout::Skip(_))).unwrap_or(line.items.len());
let natural: f64 = line.items[first..].iter().map(&width_of).sum();
x = ((w - natural) / 2.0).max(0.0);
}
let mut started = false;
for l in &line.items {
match l {
Layout::Space if started => x += SPACE,
Layout::Skip(w) => x += w,
Layout::Width(w) => {
out.include(Point::new(0.0, baseline_y));
out.include(Point::new(*w, baseline_y));
}
Layout::Word(i) => {
let label = &labels[*i];
out.include(Point::new(x, baseline_y - label.depth()));
out.include(Point::new(x + label.width(), baseline_y + label.height()));
out.items.push(Item::Text { label: label.clone(), place: crate::scene::Transform::translation(x, baseline_y), opacity: 1.0 });
x += label.width();
started = true;
}
Layout::Picture(i) => {
let b = &boxes[*i];
let (Some(min), Some(max)) = (b.canvas.min, b.canvas.max) else {
if !started {
out.include(Point::new(x, baseline_y));
}
started = true;
continue;
};
let d = Point::new(x - min.x, baseline_y - b.baseline);
for item in &b.canvas.items {
let mut it = item.clone();
it.translate(d);
out.items.push(it);
}
out.include(Point::new((min.x + d.x).max(0.0), min.y + d.y));
out.include(max.plus(d));
x += max.x - min.x;
started = true;
}
_ => {}
}
}
previous_depth = Some(depth);
}
if let (Some(w), Some(lo), Some(hi)) = (hsize, out.min.as_mut(), out.max.as_mut()) {
lo.x = 0.0;
hi.x = w;
}
out
}
const TABCOLSEP: f64 = 6.0 * crate::PT;
const STRUT: (f64, f64) = (8.4 * crate::PT, 3.6 * crate::PT);
const MATH_AXIS: f64 = 2.5 * crate::PT;
fn tables(boxes: &[PictureBox], layout: &[Layout], labels: &[crate::scene::Label]) -> (Vec<PictureBox>, Vec<Layout>) {
let mut all: Vec<PictureBox> = boxes.to_vec();
if !layout.iter().any(|l| matches!(l, Layout::TabStart(_))) {
return (all, layout.to_vec());
}
let measure = |all: &[PictureBox], l: &Layout| -> Option<(f64, f64, f64)> {
match l {
Layout::Picture(i) => {
let b = &all[*i];
let (lo, hi) = b.canvas.min.zip(b.canvas.max)?;
Some((hi.x - lo.x, hi.y - b.baseline, b.baseline - lo.y))
}
Layout::Word(i) => Some((labels[*i].width(), labels[*i].height(), labels[*i].depth())),
Layout::Skip(w) => Some((*w, 0.0, 0.0)),
_ => None,
}
};
let mut out = Vec::new();
let mut i = 0;
while i < layout.len() {
let Layout::TabStart(code) = layout[i] else {
out.push(layout[i]);
i += 1;
continue;
};
let ncols = (code & 0xff) as usize;
let align = |k: usize| if k < ncols { (code >> (8 + 2 * k)) & 3 } else { 0 };
let mut rows: Vec<Vec<Vec<Layout>>> = vec![vec![Vec::new()]];
i += 1;
while i < layout.len() && !matches!(layout[i], Layout::TabEnd) {
match layout[i] {
Layout::Cell => rows.last_mut().expect("a row").push(Vec::new()),
Layout::Row => rows.push(vec![Vec::new()]),
Layout::Picture(_) | Layout::Word(_) | Layout::Skip(_) => rows.last_mut().expect("a row").last_mut().expect("a cell").push(layout[i]),
Layout::Width(w) if !out.contains(&Layout::Width(w)) => out.push(Layout::Width(w)),
_ => {}
}
i += 1;
}
i += 1;
if rows.len() > 1 && rows.last().is_some_and(|r| r.iter().all(Vec::is_empty)) {
rows.pop();
}
let size = |cell: &[Layout]| cell.iter().filter_map(|l| measure(&all, l)).fold((0.0f64, 0.0f64, 0.0f64), |(w, h, d), (a, b, c)| (w + a, h.max(b), d.max(c)));
let n = rows.iter().map(Vec::len).max().unwrap_or(0);
let widths: Vec<f64> = (0..n).map(|k| rows.iter().filter_map(|r| r.get(k)).map(|c| size(c).0).fold(0.0, f64::max)).collect();
let mut canvas = Canvas::default();
let mut y = 0.0;
for row in &rows {
let (h, d) = row.iter().map(|c| size(c)).fold(STRUT, |(h, d), (_, a, b)| (h.max(a), d.max(b)));
let base = y - h;
let mut x = 0.0;
for (k, w) in widths.iter().enumerate() {
let cell = row.get(k).map_or(&[][..], Vec::as_slice);
let room = w - size(cell).0;
let mut cx = x
+ TABCOLSEP
+ match align(k) {
2 => room,
1 => room / 2.0,
_ => 0.0,
};
for l in cell {
match l {
Layout::Picture(p) => {
let b = &all[*p];
let Some((lo, hi)) = b.canvas.min.zip(b.canvas.max) else { continue };
let shift = Point::new(cx - lo.x, base - b.baseline);
for item in &b.canvas.items {
let mut it = item.clone();
it.translate(shift);
canvas.items.push(it);
}
canvas.include(lo.plus(shift));
canvas.include(hi.plus(shift));
cx += hi.x - lo.x;
}
Layout::Word(wd) => {
let label = &labels[*wd];
canvas.items.push(Item::Text { label: label.clone(), place: crate::scene::Transform::translation(cx, base), opacity: 1.0 });
canvas.include(Point::new(cx, base - label.depth()));
canvas.include(Point::new(cx + label.width(), base + label.height()));
cx += label.width();
}
Layout::Skip(s) => cx += s,
_ => {}
}
}
x += w + 2.0 * TABCOLSEP;
}
canvas.include(Point::new(0.0, y));
canvas.include(Point::new(x, base - d));
y = base - d;
}
let baseline = y / 2.0 - MATH_AXIS;
out.push(Layout::Picture(all.len()));
all.push(PictureBox { canvas, baseline });
}
(all, out)
}
pub fn run_document(code: &str) -> Result<(Canvas, Vec<String>), Error> {
run_document_noting(code).map(|(canvas, warnings, _)| (canvas, warnings))
}
pub fn run_document_noting(code: &str) -> Result<(Canvas, Vec<String>, crate::describe::Facts), Error> {
let mut e = Engine::new();
crate::expr::reset_random();
e.canvas.margin = 2.0 * crate::PT;
e.tex.push_text(code);
e.run()?;
while e.picture_depth > 0 {
e.end_picture();
}
while !e.scopes.is_empty() {
e.end_scope();
}
let margin = e.canvas.margin;
e.canvas = typeset(&e.boxes, &e.layout, &e.words, margin);
let mut warnings = std::mem::take(&mut e.tex.warnings);
warnings.dedup();
let _ = Transform::default();
Ok((e.canvas, warnings, std::mem::take(&mut e.facts)))
}