quicklatex 0.1.0

A program to help me write LaTeX quickly
Documentation
use std::{io::ErrorKind, path::Path};

use anyhow::{Context, Error};
use diskit::{diskit_extend::DiskitExt, Diskit};

/// A position in an input file
///
/// This stores the line and column of a position in a file.  Line
/// numbers go from `1`, while column numbers from `0`.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct Pos
{
    pub line: u32,
    pub column: u32,
}

/// A range of an input file
///
/// This stores an range in an input file, i.e. a start and end.
/// `start` is the first character in the range, and `end` is the
/// last.  Note that this means that the number of characters is not
/// the difference `end - start`, but `end - start + 1`, e.g. a single
/// character span has the same start and end.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct Span
{
    pub start: Pos,
    pub end: Pos,
}

impl Default for Pos
{
    fn default() -> Self
    {
        Self { line: 1, column: 0 }
    }
}

impl Pos
{
    pub const fn update(&mut self, c: char)
    {
        if c == '\n'
        {
            self.line += 1;
            self.column = 0;
        }
        else
        {
            self.column += 1;
        }
    }
}

fn is_same_start(a: &[char], b: &str) -> bool
{
    a.iter().zip(b.chars()).all(|(&x, y)| x == y)
}

pub fn starts_with(a: &[char], b: &str) -> bool
{
    is_same_start(a, b) && a.len() >= b.chars().count()
}

pub fn is_eq(a: &[char], b: &str) -> bool
{
    is_same_start(a, b) && a.len() == b.chars().count()
}

// Look in lib.rs for justification.
#[allow(clippy::needless_pass_by_value)]
pub fn try_read_to_string<D: Diskit>(path: &Path, d: D) -> Result<Option<String>, Error>
{
    match d.read_to_string(path)
    {
        Ok(s) => Ok(Some(s)),
        Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
        Err(e) => Err(e).with_context(|| format!("Couldn't read {path:?}")),
    }
}

// Look in lib.rs for justification.
#[allow(clippy::needless_pass_by_value)]
pub fn read_to_string<D: Diskit>(path: &Path, d: D) -> Result<String, Error>
{
    if let Some(s) = try_read_to_string(path, d.clone()).context("Couldn't read file to string")?
    {
        return Ok(s);
    }

    let mut path = path
        .to_str()
        .context("Input file doesn't exist and its path is not UTF8")?
        .to_string();

    // I'm not using `.TEX` and I don't think really anyone else is
    // either.  If this is wrong, please open an issue.
    #[allow(clippy::case_sensitive_file_extension_comparisons)]
    if path.ends_with(".t")
    {
        path.push_str("ex");
        d.read_to_string(&path).context("Couldn't read path with added \"ex\"")
    }
    else if path.ends_with('.')
    {
        path.push_str("tex");
        d.read_to_string(&path).context("Couldn't read path with added \"tex\"")
    }
    else
    {
        path.push_str(".tex");
        d.read_to_string(&path)
            .context("Couldn't read path with added \".tex\"")
    }
}

#[cfg(test)]
mod tests
{
    use crate::misc::Pos;

    #[test]
    fn pos_test()
    {
        let tests = vec![
            ("abc\ndef", vec![(1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2)]),
            ("äöü\nÄÖÜ", vec![(1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2)]),
            (
                "\n\n\na\n\nb",
                vec![(1, 0), (2, 0), (3, 0), (4, 0), (4, 1), (5, 0), (6, 0)],
            ),
            (
                "\n\n\na\n\nb\n",
                vec![(1, 0), (2, 0), (3, 0), (4, 0), (4, 1), (5, 0), (6, 0), (6, 1)],
            ),
            (
                "\n\n\na\n\nb\n\n",
                vec![(1, 0), (2, 0), (3, 0), (4, 0), (4, 1), (5, 0), (6, 0), (6, 1), (7, 0)],
            ),
        ];

        for (string, poses) in tests
        {
            let mut pos = Pos::default();

            for (c, &(line, column)) in string.chars().zip(poses.iter())
            {
                assert_eq!(pos, Pos { line, column });

                pos.update(c);
            }

            assert_eq!(string.chars().count(), poses.len());
        }
    }
}