use crate::tex::Token;
use crate::Error;
use super::{tikra_tokens, Engine};
#[derive(Debug, Clone, PartialEq)]
pub enum Direction {
Going(String),
Placed(String),
}
#[derive(Debug, Clone)]
pub struct Chain {
pub count: usize,
pub direction: Direction,
pub start: String,
}
impl Chain {
fn previous(&self, name: &str) -> String {
if self.count == 0 {
self.start.clone()
} else {
format!("{name}-{}", self.count)
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OnChain {
pub chain: String,
pub direction: Option<Direction>,
pub joins: Vec<(Option<String>, String)>,
}
fn parse(spec: &str) -> (String, Option<Direction>) {
let s = spec.trim();
let split = |key: &str| -> Option<(String, String)> {
let i = s.find(key).filter(|&i| i == 0 || s[..i].ends_with(' '))?;
Some((s[..i].trim().to_string(), s[i + key.len()..].trim().to_string()))
};
if let Some((name, d)) = split("going ") {
return (name, Some(Direction::Going(d)));
}
if let Some((name, d)) = split("placed ") {
let d = d.trim_start_matches('{').trim_end_matches('}').to_string();
return (name, Some(Direction::Placed(d)));
}
(s.to_string(), None)
}
impl Engine {
pub fn start_chain(&mut self, spec: &str) {
let (name, direction) = parse(spec);
let name = if name.is_empty() { "chain".to_string() } else { name };
let direction = direction.unwrap_or_else(|| self.st.chain_direction.clone());
self.chain_list.insert(name.clone(), Chain { count: 0, direction, start: String::new() });
self.st.current_chain = name;
}
pub fn start_branch(&mut self, spec: &str) {
let (name, direction) = parse(spec);
let parent = self.st.current_chain.clone();
let Some(p) = self.chain_list.get(&parent) else {
self.warn(format!("start branch: no chain to branch from ('{parent}')"));
return;
};
let start = p.previous(&parent);
let direction = direction.unwrap_or_else(|| p.direction.clone());
let name = format!("{parent}/{name}");
self.chain_list.insert(name.clone(), Chain { count: 0, direction, start });
self.st.current_chain = name;
}
pub fn continue_branch(&mut self, spec: &str) {
let (name, direction) = parse(spec);
let name = format!("{}/{name}", self.st.current_chain);
self.make_current(name, direction, "continue branch");
}
pub fn continue_chain(&mut self, spec: &str) {
let (name, direction) = parse(spec);
let name = if name.is_empty() { self.st.current_chain.clone() } else { name };
self.make_current(name, direction, "continue chain");
}
fn make_current(&mut self, name: String, direction: Option<Direction>, key: &str) {
match self.chain_list.get_mut(&name) {
Some(c) => {
if let Some(d) = direction {
c.direction = d;
}
self.st.current_chain = name;
}
None => self.warn(format!("{key}: unknown chain '{name}'")),
}
}
pub fn chain_default_direction(&mut self, spec: &str) {
if let (_, Some(d)) = parse(spec) {
self.st.chain_direction = d;
}
}
pub fn on_chain(&mut self, spec: &str) -> Result<(), Error> {
let (name, direction) = parse(spec);
let name = if name.is_empty() { self.st.current_chain.clone() } else { name };
let Some(chain) = self.chain_list.get(&name).cloned() else {
self.warn(format!("on chain: unknown chain '{name}'"));
return Ok(());
};
let previous = chain.previous(&name);
self.tex.define_string("tikzchainprevious", &previous, true);
self.tex.define_string("tikzchaincount", &(chain.count + 1).to_string(), true);
self.place_on_chain(direction.as_ref().unwrap_or(&chain.direction), &previous)?;
self.st.on_chain = Some(OnChain { chain: name, direction, joins: Vec::new() });
Ok(())
}
fn place_on_chain(&mut self, direction: &Direction, previous: &str) -> Result<(), Error> {
let (d, always) = match direction {
Direction::Going(d) => (d, false),
Direction::Placed(d) => (d, true),
};
if !always && previous.is_empty() {
return Ok(());
}
let d = crate::keys::strip_braces(d.trim());
let keys = if d.contains('=') { d.to_string() } else { format!("{d}=of {previous}") };
self.process_keys(tikra_tokens(&keys), Some("/tikz"))
}
pub fn join(&mut self, spec: &str) {
let s = spec.trim();
let (with, by) = match s.strip_prefix("with ") {
Some(rest) => match rest.split_once(" by ") {
Some((w, b)) => (Some(w.trim().to_string()), b.trim().to_string()),
None => (Some(rest.trim().to_string()), String::new()),
},
None => (None, s.strip_prefix("by").unwrap_or(s).trim().to_string()),
};
match self.st.on_chain.as_mut() {
Some(c) => c.joins.push((with, by)),
None => self.st.pending_joins.push((with, by)),
}
}
pub fn chain_node_found(&mut self) -> Result<(Vec<String>, Vec<Token>), Error> {
let Some(mut on) = self.st.on_chain.take() else { return Ok((Vec::new(), Vec::new())) };
on.joins.splice(0..0, std::mem::take(&mut self.st.pending_joins));
let Some(chain) = self.chain_list.get_mut(&on.chain) else { return Ok((Vec::new(), Vec::new())) };
let previous = chain.previous(&on.chain);
chain.count += 1;
let k = chain.count;
let direction = on.direction.clone().unwrap_or_else(|| chain.direction.clone());
let current = format!("{}-{k}", on.chain);
self.tex.define_string("tikzchaincurrent", ¤t, true);
let mut names = vec![current.clone(), format!("{}-end", on.chain)];
if k == 1 {
names.push(format!("{}-begin", on.chain));
}
let (transform, pending) = (self.st.transform, self.st.pending);
self.place_on_chain(&direction, &previous)?;
(self.st.transform, self.st.pending) = (transform, pending);
self.st.on_chain = Some(OnChain { joins: Vec::new(), ..on.clone() });
self.process_keys(tikra_tokens("every on chain/.try"), Some("/tikz"))?;
if let Some(more) = self.st.on_chain.take() {
on.joins.extend(more.joins);
}
let mut path = Vec::new();
for (with, by) in on.joins {
let from = match with {
Some(w) => w,
None if previous.is_empty() => continue,
None => previous.clone(),
};
path.extend(tikra_tokens(&format!("({from}) edge[every join/.try,")));
path.extend(tikra_tokens(&by));
path.extend(tikra_tokens(&format!("] ({current}) ")));
}
Ok((names, path))
}
pub fn chainin(&mut self) -> Result<(), Error> {
let name = match self.tex.next_nonspace() {
Some(t) if t.is_char('(') => {
let n = super::coords::read_coordinate_tokens(self)?;
self.tex.expand_to_string(n)?.trim().to_string()
}
_ => return Err(Error::Figure("\\chainin without (node)".into())),
};
let options = match self.tex.next_nonspace() {
Some(t) if t.is_char('[') => self.tex.read_until_char(']', false)?,
Some(t) => {
self.tex.back(t);
Vec::new()
}
None => Vec::new(),
};
let Some(info) = self.nodes.get(&name).cloned() else {
return Err(Error::Figure(format!("\\chainin: unknown node ({name})")));
};
self.begin_group();
let r = (|| {
let mut v = tikra_tokens("on chain,every chain in/.try,");
v.extend(options);
self.process_keys(v, Some("/tikz"))?;
self.chain_node_found()
})();
self.end_group();
let (names, path) = r?;
for n in names {
self.nodes.insert(n, info.clone());
}
self.tex.push_tokens(path);
super::path::statement(self, "")
}
}