use std::{
mem::take,
path::{Path, PathBuf},
};
const MAX_RECURSION_DEPTH: i32 = 1000;
const MAX_REPETITION_COUNT: u32 = 1000;
const MAX_LENGTH_MULTIPLICATION: usize = 100;
const MAX_LENGTH_MULTIPLICATION_OFFSET: usize = 1000;
use anyhow::{bail, Context, Error};
use diskit::{diskit_extend::DiskitExt, Diskit};
use home::home_dir;
use crate::{
misc::{starts_with, Span},
noisy::Noisy,
second_pass::{Block, Text, TextPiece},
Timers,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Replacement
{
pub(crate) from: String,
pub(crate) to: String,
pub(crate) noisy: bool,
pub(crate) repeated: bool,
pub(crate) condition: Option<String>,
pub(crate) refkind: bool,
}
impl Block
{
fn fix_math(self) -> Self
{
if starts_with(&self.kind, "align")
{
let span = Span {
start: self.content.pieces[0].1.start,
end: self.content.pieces[self.content.pieces.len() - 1].1.end,
};
let text = format!("{}", self.content);
let lines = text
.lines()
.filter(|line| !line.chars().all(char::is_whitespace))
.collect::<Vec<_>>();
let mut text = vec!['\n'];
for (i, line) in lines.iter().enumerate()
{
let line: &str = line;
text.extend_from_slice(&line.chars().collect::<Vec<_>>());
if i != lines.len() - 1
{
text.extend_from_slice(&[' ', '\\', '\\']);
}
text.extend_from_slice(&['\n']);
}
Self {
kind: self.kind,
content: Text {
pieces: vec![(TextPiece::Text(text), span)],
},
}
}
else
{
Self {
kind: self.kind,
content: self.content.fix_math(),
}
}
}
}
impl TextPiece
{
fn fix_math(self) -> Self
{
match self
{
Self::Block(block) => Self::Block(block.fix_math()),
Self::Braces(braces) => Self::Braces(braces.fix_math()),
val => val,
}
}
}
impl Text
{
pub fn fix_math(self) -> Self
{
Self {
pieces: self
.pieces
.into_iter()
.map(|(piece, span)| (piece.fix_math(), span))
.collect(),
}
}
}
fn parse_filename(path: &str) -> Result<PathBuf, Error>
{
let mut rv = String::with_capacity(path.len());
let mut in_quote = false;
for c in path.chars()
{
let was_in_quote = in_quote;
match (in_quote, c)
{
(false, '\\') => in_quote = true,
(false, c) => rv.push(c),
(true, '\\') => rv.push('\\'),
(true, 'n') => rv.push('\n'),
(true, 't') => rv.push('\t'),
(true, 's') => rv.push(' '),
(true, c) => bail!("Unknown escape sequence \\{c:?}"),
}
if was_in_quote
{
in_quote = false;
}
}
Ok(rv.into())
}
#[allow(clippy::fn_params_excessive_bools)]
fn parse_replacement(
replacement: &str,
noisy: bool,
repeated: bool,
condition: bool,
refkind: bool,
) -> Result<Replacement, Error>
{
let mut fragments = vec![];
let mut fragment = String::new();
let mut in_quote = false;
for c in replacement.chars()
{
let was_in_quote = in_quote;
match (in_quote, c)
{
(false, '\\') => in_quote = true,
(false, ';') => fragments.push(take(&mut fragment)),
(false, c) => fragment.push(c),
(true, '\\') => fragment.push('\\'),
(true, 'n') => fragment.push('\n'),
(true, 's') => fragment.push(' '),
(true, ';') => fragment.push(';'),
(true, c) => bail!("Unknown escape sequence \\{c:?}"),
}
if was_in_quote
{
in_quote = false;
}
}
fragments.push(take(&mut fragment));
if fragments.len() != (2 + condition as u8 as usize)
{
bail!("Wrong number of fragments found");
}
let condition = if condition { Some(take(&mut fragments[2])) } else { None };
Ok(Replacement {
from: take(&mut fragments[0]),
to: take(&mut fragments[1]),
noisy,
repeated,
condition,
refkind,
})
}
#[allow(clippy::needless_pass_by_value)]
fn read_replacements<D: Diskit>(file: &Path, recursion_depth: i32, d: D) -> Result<Vec<Replacement>, Error>
{
let mut replacements = vec![];
if recursion_depth <= 0
{
bail!("Recursion depth limit exceeded");
}
for line in d
.clone()
.read_to_string(file)
.with_context(|| format!("Couldn't read replacement file {file:?}"))?
.lines()
{
let (command, value) = line
.split_once(' ')
.with_context(|| format!("Couldn't read all lines in replacement file {file:?}"))?;
if command == "include" || command == "include_default"
{
let root = if command == "include"
{
file.parent()
.with_context(|| format!("Bug in quicklatex about path {file:?}"))?
.to_owned()
}
else
{
home_dir()
.context("Couldn't get home directory")?
.join(PathBuf::from("./.zvavybir/quicklatex/"))
};
replacements.append(
&mut read_replacements(
&root.join(
parse_filename(value)
.with_context(|| format!("Couldn't read import statement in replacement file {file:?}"))?,
),
recursion_depth - 1,
d.clone(),
)
.with_context(|| format!("Included from replacement file {file:?}"))?,
);
}
else if command.starts_with("replace")
{
replacements.push(
parse_replacement(
value,
command.contains("_noisy"),
command.contains("_repeated"),
command.contains("_conditionally"),
command.contains("_refkind"),
)
.with_context(|| format!("Couldn't read replacement from replacement file {file:?}"))?,
);
}
else
{
bail!("Unknown command {command:?} at replacement file {file:?}")
}
}
Ok(replacements)
}
pub fn literal<D: Diskit>(
mut s: String,
path: &Path,
timers: &mut Timers,
d: D,
) -> Result<(String, Noisy, Vec<Replacement>), Error>
{
timers.start("repl_full")?;
let primary_file = path.with_extension("qlr");
let default_file = home_dir()
.context("Couldn't get home directory")?
.join(PathBuf::from("./.zvavybir/quicklatex/default.qlr"));
let replacements = if primary_file.exists()
{
read_replacements(&primary_file, MAX_RECURSION_DEPTH, d)
.context("Couldn't read corresponding replacement file")?
}
else if default_file.exists()
{
read_replacements(&default_file, MAX_RECURSION_DEPTH, d).context("Couldn't read default replacement file")?
}
else
{
vec![]
};
timers.start("repl_inner")?;
let mut noisy = vec![];
for replacement in &replacements
{
if replacement.refkind
{
continue;
}
if let Some(condition) = &replacement.condition
{
if !s.contains(condition)
{
continue;
}
}
#[allow(clippy::collapsible_else_if)]
if replacement.repeated
{
let initial_len = s.len();
if s.contains(&replacement.from)
{
for _ in 0..MAX_REPETITION_COUNT
{
let new_s = s.replace(&replacement.from, &replacement.to);
if s == new_s
|| new_s.len() > (initial_len * MAX_LENGTH_MULTIPLICATION + MAX_LENGTH_MULTIPLICATION_OFFSET)
{
break;
}
s = new_s;
}
if replacement.noisy
{
noisy.push(replacement.clone());
}
}
}
else
{
if s.contains(&replacement.from)
{
s = s.replace(&replacement.from, &replacement.to);
if replacement.noisy
{
noisy.push(replacement.clone());
}
}
}
}
timers.stop("repl_inner")?;
timers.stop("repl_full")?;
Ok((s, Noisy::from_replacements(noisy), replacements))
}