tidier 0.5.6

Format HTML, XHTML and XML documents
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright 2023 Taylan Gökkaya

#![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>;

/// An HTML or XML document.
#[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 {
	/// Parse the provided HTML or XML string to create a [Doc].
	///
	/// Tip: If you can afford it, passing an owned [String] to this function
	/// will avoid an allocation for the overwhelming majority of cases.
	/// Or as an alternative, passing a null-terminated string will also avoid
	/// an allocation.
	/// #### Errors
	/// This function will return [Error::Doc] if the input document has errors
	/// Tidy can't deal with; and it will return [Error::ContainsNullByte] if
	/// the input contains a null byte at the start or in the middle.
	pub fn new<S: AsRef<str> + Into<String>>(input: S, xml: bool) -> Result<Self> {
		let s = input.as_ref();
		// Check for null bytes in the middle; it's okay at the end.
		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') {
				// It's already null terminated.
				tidyParseString(doc, s.as_ptr() as *const _)
			} else {
				// We have to push a null byte because C...
				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 {
				// We probably have errors in the buffer
				let errs = x.diagnostics();
				Err(Error::Doc(errs))
			}
		}
	}

	/// Pretty-format the document with the given options.
	///
	/// #### Errors
	/// The error conditions are the same as [format_to](Self::format_to).
	#[inline]
	pub fn format(&self, opts: &FormatOptions) -> Result<String> {
		let s = self.format_bytes(opts)?;
		String::from_utf8(s).map_err(|_| Error::ParseUtf8)
	}

	/// Pretty-format the document with the given options, not parsing the generated bytes into a [String].
	///
	/// Tidy will be configured to emit UTF-8 output but validating it as UTF-8 has a small cost.<br>
	/// This function exists for when you don't need a string and can work with bytes.
	/// #### Errors
	/// The error conditions are the same as [format_bytes_to](Self::format_bytes_to).
	#[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)
	}

	/// Write formatted output to a string buffer.
	///
	/// This function will clear the contents of the buffer on startup and on error; in other words, it overwrites the contents of the buffer.
	/// #### Errors
	/// In addition to the error conditions of [format_bytes_to](Self::format_bytes_to), this function will return `[Error::ParseUtf8] if the output produced by Tidy cannot be validated as UTF-8. Note that this is highly unlikely as Tidy is configured to use UTF-8.
	#[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) {
			// Failed, blank the string so it's in a safe state and still retains the allocation
			v.clear();
			*buf = unsafe { String::from_utf8_unchecked(v) };
			return Err(e);
		}

		match String::from_utf8(v) {
			Ok(s) => {
				*buf = s;
				Ok(())
			}
			Err(e) => {
				// Failed, at least give back an empty string to preserve the same allocation
				let mut v = e.into_bytes();
				v.clear();
				*buf = unsafe { String::from_utf8_unchecked(v) };
				Err(Error::ParseUtf8)
			}
		}
	}

	/// Writes formatted output to a byte buffer.
	///
	/// This function will clear the contents of the buffer on startup and on error; in other words, it overwrites the contents of the buffer.<br>
	/// Tidy will be configured to emit UTF-8 output but validating it as UTF-8 has a small cost.<br>
	/// This function exists for when you don't need a string and can work with bytes.<br>
	/// #### Errors
	/// - Returns [Error::Doc] if Tidy produces any error, usually relating to the input document having severe syntax errors.
	/// - Returns [Error::MaxSizeReached] if the formatted output exceeds 4 GiB (`u32::MAX`). This is due to Tidy using u32 for the length.
	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);
				}
				// We need more space on the buffer.
				// Tidy sets the len value to some reasonable number so we use that.
				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 {
				// Severe error, res is errno negated
				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(())
	}

	/// Cleans and repairs the document.
	///
	/// Only calls [tidy_sys::tidyCleanAndRepair].
	pub fn repair(&self) -> Result<()> {
		unsafe {
			match tidyCleanAndRepair(self.doc) {
				0 | 1 => Ok(()),
				2.. => Err(Error::Doc(self.diagnostics())),
				n => Err(Error::Errno(-n)),
			}
		}
	}

	/// Return all the [Diagnostic]s that were generated.
	///
	/// The returned Vec may contain both errors and warnings.
	pub fn diagnostics(&self) -> Vec<Diagnostic> {
		Diagnostic::parse_all(self.err_buf)
	}

	/// Returns the number of errors generated.
	pub fn error_count(&self) -> u32 {
		unsafe { tidyErrorCount(self.doc) as u32 }
	}

	/// Returns the number of warnings generated.
	pub fn warning_count(&self) -> u32 {
		unsafe { tidyWarningCount(self.doc) as u32 }
	}

	/// Returns true if `self.error_count() > 0`.
	pub fn has_errors(&self) -> bool {
		self.error_count() > 0
	}

	/// Returns true if `self.warning_count() > 0`.
	pub fn has_warnings(&self) -> bool {
		self.warning_count() > 0
	}

	/// Returns true if there are any errors or warnings.
	pub fn has_issues(&self) -> bool {
		self.has_warnings() || self.has_errors()
	}
}

/// A convenience function for [Doc::format].
pub fn format<S: AsRef<str> + Into<String>>(
	doc: S,
	xml: bool,
	opts: &FormatOptions,
) -> Result<String> {
	Doc::new(doc, xml)?.format(opts)
}

/// A convenience function for [Doc::format_bytes].
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)
}

/// A convenience function for [Doc::format_to].
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)
}

/// A convenience function for [Doc::format_bytes_to].
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)
}