use std::collections::BTreeSet;
use std::fmt::{self, Write as _};
use std::marker::PhantomData;
use std::str::FromStr;
use itertools::{Itertools as _, Position};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub(crate) struct SortedTemplate<F> {
format: PhantomData<F>,
before: String,
after: String,
fragments: BTreeSet<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Offset {
start: usize,
fragment_lengths: Vec<usize>,
}
impl<F> SortedTemplate<F> {
pub(crate) fn from_template(template: &str, delimiter: &str) -> Result<Self, Error> {
let mut split = template.split(delimiter);
let before = split.next().ok_or(Error("delimiter should appear at least once"))?;
let after = split.next().ok_or(Error("delimiter should appear at least once"))?;
if split.next().is_some() {
return Err(Error("delimiter should appear at most once"));
}
Ok(Self::from_before_after(before, after))
}
pub(crate) fn from_before_after<S: ToString, T: ToString>(before: S, after: T) -> Self {
let before = before.to_string();
let after = after.to_string();
Self { format: PhantomData, before, after, fragments: Default::default() }
}
}
impl<F> SortedTemplate<F> {
pub(crate) fn append(&mut self, insert: String) {
self.fragments.insert(insert);
}
}
impl<F: FileFormat> fmt::Display for SortedTemplate<F> {
fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut fragment_lengths = Vec::default();
write!(f, "{}", self.before)?;
for (p, fragment) in self.fragments.iter().with_position() {
let mut f = DeltaWriter { inner: &mut f, delta: 0 };
let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
write!(f, "{}{}", sep, fragment)?;
fragment_lengths.push(f.delta);
}
let offset = Offset { start: self.before.len(), fragment_lengths };
let offset = serde_json::to_string(&offset).unwrap();
write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)
}
}
impl<F: FileFormat> FromStr for SortedTemplate<F> {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (s, offset) = s
.rsplit_once("\n")
.ok_or(Error("invalid format: should have a newline on the last line"))?;
let offset = offset
.strip_prefix(F::COMMENT_START)
.ok_or(Error("last line expected to start with a comment"))?;
let offset = offset
.strip_suffix(F::COMMENT_END)
.ok_or(Error("last line expected to end with a comment"))?;
let offset: Offset = serde_json::from_str(&offset).map_err(|_| {
Error("could not find insertion location descriptor object on last line")
})?;
let (before, mut s) =
s.split_at_checked(offset.start).ok_or(Error("invalid start: out of bounds"))?;
let mut fragments = BTreeSet::default();
for (p, &index) in offset.fragment_lengths.iter().with_position() {
let (fragment, rest) =
s.split_at_checked(index).ok_or(Error("invalid fragment length: out of bounds"))?;
s = rest;
let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
let fragment = fragment
.strip_prefix(sep)
.ok_or(Error("invalid fragment length: expected to find separator here"))?;
fragments.insert(fragment.to_string());
}
Ok(Self {
format: PhantomData,
before: before.to_string(),
after: s.to_string(),
fragments,
})
}
}
pub(crate) trait FileFormat {
const COMMENT_START: &'static str;
const COMMENT_END: &'static str;
const SEPARATOR: &'static str;
}
#[derive(Debug, Clone)]
pub(crate) struct Html;
impl FileFormat for Html {
const COMMENT_START: &'static str = "<!--";
const COMMENT_END: &'static str = "-->";
const SEPARATOR: &'static str = "";
}
#[derive(Debug, Clone)]
pub(crate) struct Js;
impl FileFormat for Js {
const COMMENT_START: &'static str = "//";
const COMMENT_END: &'static str = "";
const SEPARATOR: &'static str = ",";
}
#[derive(Debug, Clone)]
pub(crate) struct Error(&'static str);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid template: {}", self.0)
}
}
struct DeltaWriter<W> {
inner: W,
delta: usize,
}
impl<W: fmt::Write> fmt::Write for DeltaWriter<W> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.inner.write_str(s)?;
self.delta += s.len();
Ok(())
}
}
#[cfg(test)]
mod tests;