use std::io::Write;
use crate::Engine;
use crate::input::Level;
use crate::nodes::NodeRef;
use crate::primitives_gen::assign_int as ip;
pub type Fields = (u32, i32);
enum Sink {
Plain(Vec<u8>),
Gz(flate2::write::GzEncoder<Vec<u8>>, Vec<u8>),
}
struct Out {
sink: Sink,
size: u64,
last_bang: u64,
count: u64,
line: Vec<u8>,
}
impl Out {
fn emit(&mut self, counted: bool) {
self.size += self.line.len() as u64;
match &mut self.sink {
Sink::Plain(v) => v.extend_from_slice(&self.line),
Sink::Gz(g, pending) => {
pending.extend_from_slice(&self.line);
if pending.len() >= 1 << 16 {
let _ = g.write_all(pending);
pending.clear();
}
}
}
self.line.clear();
if counted {
self.count += 1;
}
}
fn bang(&mut self) {
let off = self.size - self.last_bang;
self.last_bang = self.size;
let _ = writeln!(self.line, "!{off}");
self.emit(true);
}
}
#[derive(Default)]
pub struct Synctex {
pub option: Option<i32>,
pub files: Vec<Vec<u8>>,
cwd: Option<Vec<u8>>,
out: Option<Out>,
pub dir: Option<std::path::PathBuf>,
in_page: bool,
origin: (i32, i32),
ctx: Fields,
last: (i32, i32),
seen: bool,
pending: Option<(Fields, i32)>,
run: bool,
}
impl Synctex {
fn input_name(&mut self, path: &std::path::Path) -> Vec<u8> {
let name = path.to_string_lossy().into_owned().into_bytes();
if path.is_absolute() {
return name;
}
let cwd = self.cwd.get_or_insert_with(|| {
std::env::current_dir()
.map(|d| d.to_string_lossy().into_owned().into_bytes())
.unwrap_or_default()
});
let mut full = cwd.clone();
full.push(b'/');
full.extend_from_slice(&name);
full
}
fn add_input(&mut self, path: &std::path::Path) -> u32 {
let name = self.input_name(path);
let tag = self.files.len() as u32 + 1;
if let Some(out) = self.out.as_mut() {
out.line
.extend_from_slice(format!("Input:{tag}:").as_bytes());
out.line.extend_from_slice(&name);
out.line.push(b'\n');
out.emit(false);
}
self.files.push(name);
tag
}
fn open(&mut self, pdf: bool, mag: i32) -> &mut Out {
let compressed = self.option.is_none_or(|n| n > 0);
let files = &self.files;
self.out.get_or_insert_with(|| {
let sink = if compressed {
Sink::Gz(
flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast()),
Vec::new(),
)
} else {
Sink::Plain(Vec::new())
};
let mut out = Out {
sink,
size: 0,
last_bang: 0,
count: 0,
line: Vec::new(),
};
out.line.extend_from_slice(b"SyncTeX Version:1\n");
for (i, f) in files.iter().enumerate() {
out.line
.extend_from_slice(format!("Input:{}:", i + 1).as_bytes());
out.line.extend_from_slice(f);
out.line.push(b'\n');
}
let _ = write!(
out.line,
"Output:{}\nMagnification:{mag}\nUnit:1\nX Offset:0\nY Offset:0\nContent:\n",
if pdf { "pdf" } else { "dvi" }
);
out.emit(false);
out
})
}
fn begin_sheet(&mut self, n: u32, origin: (i32, i32), pdf: bool, mag: i32) {
let out = self.open(pdf, mag);
out.bang();
let _ = writeln!(out.line, "{{{n}");
out.emit(true);
self.origin = origin;
self.ctx = (0, 0);
self.last = (0, 0);
self.seen = false;
self.pending = None;
self.run = false;
}
fn end_sheet(&mut self, n: u32) {
if let Some(out) = self.out.as_mut() {
out.bang();
let _ = writeln!(out.line, "}}{n}");
out.emit(true);
}
}
fn out(&mut self) -> &mut Out {
self.out.as_mut().expect("a sheet is open")
}
fn record(&mut self, c: u8, f: Fields, h: i32, v: i32, rest: &[i32]) {
let (oh, ov) = self.origin;
let out = self.out();
out.line.push(c);
let _ = write!(out.line, "{},{}:{},{}", f.0, f.1, h + oh, v + ov);
for (i, x) in rest.iter().enumerate() {
out.line.push(if i == 0 { b':' } else { b',' });
let _ = write!(out.line, "{x}");
}
out.line.push(b'\n');
out.emit(c != b'x');
}
fn flush_kern(&mut self, next: Option<Fields>) {
if let Some((f, w)) = self.pending.take()
&& Some(f) != next
{
let (h, v) = self.last;
self.record(b'k', f, h, v, &[w]);
}
}
#[inline]
pub fn end_run(&mut self, h: i32, v: i32) {
if self.run {
self.run = false;
let f = self.ctx;
self.record(b'x', f, h, v, &[]);
}
}
#[inline]
pub fn glyph(&mut self) {
self.run = true;
}
pub fn box_open(&mut self, hlist: bool, f: Fields, h: i32, v: i32, size: [i32; 3]) {
self.record(if hlist { b'(' } else { b'[' }, f, h, v, &size);
self.ctx = f;
self.last = (h, v);
self.seen = false;
self.pending = None;
}
pub fn box_close(&mut self, hlist: bool, f: Fields, h: i32, v: i32) {
let out = self.out();
out.line.push(if hlist { b')' } else { b']' });
out.line.push(b'\n');
out.emit(true);
self.ctx = f;
self.last = (h, v);
self.seen = true;
self.pending = None;
}
pub fn void_box(&mut self, hlist: bool, f: Fields, h: i32, v: i32, size: [i32; 3]) {
self.flush_kern(None);
self.record(if hlist { b'h' } else { b'v' }, f, h, v, &size);
self.ctx = f;
self.last = (h, v);
self.seen = true;
}
pub fn glue(&mut self, f: Fields, h: i32, v: i32) {
if f.0 == 0 || f.1 == 0 {
return;
}
self.pending = None;
self.record(b'g', f, h, v, &[]);
self.ctx = f;
self.last = (h, v);
self.seen = true;
}
pub fn rule(&mut self, f: Fields, h: i32, v: i32, size: [i32; 3]) {
if f.0 == 0 || f.1 == 0 {
return;
}
self.pending = None;
self.record(b'r', f, h, v, &size);
self.ctx = f;
self.last = (h, v);
self.seen = true;
}
pub fn math(&mut self, f: Fields, h: i32, v: i32) {
if f.0 == 0 || f.1 == 0 {
return;
}
self.flush_kern(Some(f));
self.record(b'$', f, h, v, &[]);
self.ctx = f;
self.last = (h, v);
self.seen = true;
}
pub fn kern(&mut self, f: Fields, w: i32) {
if f.0 == 0 || f.1 == 0 {
return;
}
if self.seen && f != self.ctx {
self.flush_kern(Some(f));
let (h, v) = self.last;
self.record(b'k', f, h, v, &[w]);
} else {
self.pending = Some((f, w));
}
self.ctx = f;
self.seen = true;
}
fn finish(&mut self) -> Option<(Vec<u8>, bool)> {
let mut out = self.out.take()?;
out.bang();
let _ = write!(out.line, "Postamble:\nCount:{}\n", out.count);
out.emit(false);
out.bang();
out.line.extend_from_slice(b"Post scriptum:\n");
out.emit(false);
match out.sink {
Sink::Plain(v) => Some((v, false)),
Sink::Gz(mut g, pending) => {
let _ = g.write_all(&pending);
g.finish().ok().map(|v| (v, true))
}
}
}
}
impl Engine {
fn synctex_allowed(&self) -> bool {
self.synctex.option != Some(0)
}
pub fn synctex_changed(&mut self) {
if self.eqtb.int(ip::SYNCTEX) != 0 && self.synctex_allowed() && self.nodes.sync.is_none() {
self.nodes.enable_sync();
self.sync_input_changed();
}
}
#[inline]
pub(crate) fn sync_input_changed(&mut self) {
if let Some(s) = self.nodes.sync.as_deref_mut() {
s.cur = self
.input
.stack
.iter()
.rev()
.find_map(|l| match l {
Level::Text(t) => Some(t.sync),
Level::Tokens(_) => None,
})
.unwrap_or((0, 0));
}
}
pub(crate) fn synctex_input_opened(&mut self, path: &std::path::Path) {
let tag = self.synctex.add_input(path);
if let Some(Level::Text(t)) = self.input.stack.last_mut() {
t.sync = (tag, 0);
}
}
pub(crate) fn synctex_recording(&self) -> bool {
self.nodes.sync.is_some() && self.eqtb.int(ip::SYNCTEX) != 0
}
pub(crate) fn synctex_begin_page(&mut self, origin: (i32, i32)) {
let pdf = self.eqtb.int(ip::OUTPUTMODE) > 0;
let mag = self.eqtb.int(ip::MAG);
self.synctex
.begin_sheet(self.total_pages + 1, origin, pdf, mag);
self.synctex.in_page = true;
}
pub(crate) fn synctex_end_page(&mut self) {
self.synctex.in_page = false;
self.synctex.end_sheet(self.total_pages + 1);
}
pub(crate) fn synctex_begin_form(&mut self, total: i32) {
let pdf = self.eqtb.int(ip::OUTPUTMODE) > 0;
let mag = self.eqtb.int(ip::MAG);
let n = self.total_pages + 1 + u32::from(self.synctex.in_page);
let page_height = self
.eqtb
.dimen(crate::primitives_gen::assign_dimen::PAGEHEIGHT);
self.synctex
.begin_sheet(n, (0, page_height - total), pdf, mag);
}
pub(crate) fn synctex_end_form(&mut self) {
let n = self.total_pages + u32::from(self.synctex.in_page);
self.synctex.end_sheet(n);
}
#[inline]
pub(crate) fn sync_of(&self, p: NodeRef) -> Fields {
self.nodes.sync_fields(p)
}
pub(crate) fn finish_synctex(&mut self) {
let job =
String::from_utf8_lossy(self.job_name.as_deref().unwrap_or(b"texput")).into_owned();
let dir = self
.synctex
.dir
.clone()
.or_else(|| self.files.output_dir.clone())
.unwrap_or_default();
let gz = dir.join(format!("{job}.synctex.gz"));
let plain = dir.join(format!("{job}.synctex"));
let result = if self.total_pages > 0 {
self.synctex.finish()
} else {
None
};
let Some((data, compressed)) = result else {
let _ = std::fs::remove_file(&gz);
let _ = std::fs::remove_file(&plain);
return;
};
let (path, other) = if compressed { (gz, plain) } else { (plain, gz) };
let _ = std::fs::remove_file(&other);
if std::fs::write(&path, &data).is_err() {
return;
}
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let sel = self.printer.selector;
self.printer.selector = crate::print::Selector::TermOnly;
self.printer.print_nl("SyncTeX written on ");
self.printer.print_bytes(name.as_bytes());
self.printer.print_char('.' as u32);
self.printer.selector = sel;
}
}