#![doc = include_str!("../readme.md")]
mod error;
mod options;
#[cfg(test)]
mod tests;
use std::{
mem,
ptr::{
self,
addr_of_mut,
},
};
use tidy_sys::*;
pub use self::{
error::*,
options::*,
};
const ENOMEM: i32 = 12;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug)]
pub struct Doc {
doc: TidyDoc,
input_len: usize,
err_buf: *mut TidyBuffer,
xml: bool,
}
impl Drop for Doc {
fn drop(&mut self) {
unsafe {
if !(*self.err_buf).bp.is_null() {
tidyBufFree(self.err_buf);
}
let _ = Box::from_raw(self.err_buf);
tidyRelease(self.doc);
}
}
}
impl Doc {
pub fn new<S: AsRef<str> + Into<String>>(input: S, xml: bool) -> Result<Self> {
let s = input.as_ref();
if let Some(i) = memchr::memchr(0, s.as_bytes()) {
if s.as_bytes()[i + 1..].iter().any(|&c| c != 0) {
return Err(Error::ContainsNullByte(i));
}
}
let err_buf = Box::into_raw(Box::new(TidyBuffer {
allocator: ptr::null_mut(),
bp: ptr::null_mut(),
size: 0,
allocated: 0,
next: 0,
}));
unsafe {
let doc = tidyCreate();
tidySetErrorBuffer(doc, err_buf);
reset_opts(doc, xml);
let input_len = s.len().min(u32::MAX as usize);
let res = if s.ends_with('\0') {
tidyParseString(doc, s.as_ptr() as *const _)
} else {
let mut s = input.into();
s.push('\0');
tidyParseString(doc, s.as_ptr() as *const _)
};
let x = Self {
doc,
xml,
input_len,
err_buf,
};
if res < 0 {
Err(Error::Errno(-res))
} else if res < 2 {
Ok(x)
} else {
let errs = x.diagnostics();
Err(Error::Doc(errs))
}
}
}
#[inline]
pub fn format(&self, opts: &FormatOptions) -> Result<String> {
let s = self.format_bytes(opts)?;
String::from_utf8(s).map_err(|_| Error::ParseUtf8)
}
#[inline]
pub fn format_bytes(&self, opts: &FormatOptions) -> Result<Vec<u8>> {
let mut buf = Vec::new();
self.format_bytes_to(&mut buf, opts)?;
Ok(buf)
}
#[inline]
pub fn format_to(&self, buf: &mut String, opts: &FormatOptions) -> Result<()> {
let mut v = mem::take(buf).into_bytes();
if let Err(e) = self.format_bytes_to(&mut v, opts) {
v.clear();
*buf = unsafe { String::from_utf8_unchecked(v) };
return Err(e);
}
match String::from_utf8(v) {
Ok(s) => {
*buf = s;
Ok(())
}
Err(e) => {
let mut v = e.into_bytes();
v.clear();
*buf = unsafe { String::from_utf8_unchecked(v) };
Err(Error::ParseUtf8)
}
}
}
pub fn format_bytes_to(&self, buf: &mut Vec<u8>, opts: &FormatOptions) -> Result<()> {
opts.apply(self.doc);
buf.clear();
buf.reserve(self.input_len.saturating_sub(buf.len()));
let mut len = u32::try_from(buf.capacity()).unwrap_or(u32::MAX);
loop {
let res =
unsafe { tidySaveString(self.doc, buf.as_mut_ptr() as *mut _, addr_of_mut!(len)) };
if res == -ENOMEM {
if len == u32::MAX {
reset_opts(self.doc, self.xml);
return Err(Error::MaxSizeReached);
}
let reserve = if len as usize > buf.capacity() {
len as usize
} else {
usize::min(u32::MAX as usize, buf.capacity() * 2)
};
buf.reserve(reserve);
len = u32::try_from(buf.capacity()).unwrap_or(u32::MAX);
} else if res >= 2 {
reset_opts(self.doc, self.xml);
return Err(Error::Doc(self.diagnostics()));
} else if res < 0 {
reset_opts(self.doc, self.xml);
return Err(Error::Errno(-res));
} else {
break;
}
}
reset_opts(self.doc, self.xml);
unsafe {
buf.set_len(len as usize);
}
Ok(())
}
pub fn repair(&self) -> Result<()> {
unsafe {
match tidyCleanAndRepair(self.doc) {
0 | 1 => Ok(()),
2.. => Err(Error::Doc(self.diagnostics())),
n => Err(Error::Errno(-n)),
}
}
}
pub fn diagnostics(&self) -> Vec<Diagnostic> {
Diagnostic::parse_all(self.err_buf)
}
pub fn error_count(&self) -> u32 {
unsafe { tidyErrorCount(self.doc) as u32 }
}
pub fn warning_count(&self) -> u32 {
unsafe { tidyWarningCount(self.doc) as u32 }
}
pub fn has_errors(&self) -> bool {
self.error_count() > 0
}
pub fn has_warnings(&self) -> bool {
self.warning_count() > 0
}
pub fn has_issues(&self) -> bool {
self.has_warnings() || self.has_errors()
}
}
pub fn format<S: AsRef<str> + Into<String>>(
doc: S,
xml: bool,
opts: &FormatOptions,
) -> Result<String> {
Doc::new(doc, xml)?.format(opts)
}
pub fn format_bytes<S: AsRef<str> + Into<String>>(
doc: S,
xml: bool,
opts: &FormatOptions,
) -> Result<Vec<u8>> {
Doc::new(doc, xml)?.format_bytes(opts)
}
pub fn format_to<S: AsRef<str> + Into<String>>(
doc: S,
buf: &mut String,
xml: bool,
opts: &FormatOptions,
) -> Result<()> {
Doc::new(doc, xml)?.format_to(buf, opts)
}
pub fn format_bytes_to<S: AsRef<str> + Into<String>>(
doc: S,
buf: &mut Vec<u8>,
xml: bool,
opts: &FormatOptions,
) -> Result<()> {
Doc::new(doc, xml)?.format_bytes_to(buf, opts)
}