#![expect(
clippy::allow_attributes,
reason = "vendored from `vy-core`: macro-internal `#[allow(non_snake_case)]` attrs whose underlying lint fires only for some tuple-arity expansions"
)]
use std::{
borrow::Cow,
fmt::{self, Write as _},
};
pub trait IntoHtml {
fn into_html(self) -> impl IntoHtml;
#[inline]
fn escape_and_write(self, buf: &mut String)
where
Self: Sized,
{
self.into_html().escape_and_write(buf);
}
#[inline]
fn size_hint(&self) -> usize {
0
}
fn into_string(self) -> String
where
Self: Sized,
{
let html = self.into_html();
let size = html.size_hint();
let mut buf = String::with_capacity(size + (size / 10));
html.escape_and_write(&mut buf);
buf
}
}
#[inline]
pub fn escape_into(output: &mut String, input: &str) {
let bytes = input.as_bytes();
let mut start = 0;
for (i, &b) in bytes.iter().enumerate() {
let replacement = match b {
b'&' => "&",
b'<' => "<",
b'>' => ">",
b'"' => """,
b'\'' => "'",
_ => continue,
};
output.push_str(&input[start..i]);
output.push_str(replacement);
start = i + 1;
}
output.push_str(&input[start..]);
}
pub(crate) fn escape_attr_value_into(output: &mut Vec<u8>, value: &[u8]) {
let mut start = 0;
for (i, &b) in value.iter().enumerate() {
let replacement: &[u8] = match b {
b'&' => b"&",
b'"' => b""",
_ => continue,
};
output.extend_from_slice(&value[start..i]);
output.extend_from_slice(replacement);
start = i + 1;
}
output.extend_from_slice(&value[start..]);
}
#[inline]
pub fn escape(input: &str) -> Cow<'_, str> {
if !input
.bytes()
.any(|b| matches!(b, b'&' | b'<' | b'>' | b'"' | b'\''))
{
return Cow::Borrowed(input);
}
let mut output = String::with_capacity(input.len() + 8);
escape_into(&mut output, input);
Cow::Owned(output)
}
const MAX_ENTITY_LEN: usize = 32;
#[must_use]
pub fn decode_entities(input: &str) -> Cow<'_, str> {
let Some(first) = input.find('&') else {
return Cow::Borrowed(input);
};
let mut out = String::with_capacity(input.len());
out.push_str(&input[..first]);
let mut rest = &input[first..];
loop {
let after = &rest[1..];
if let Some(semi) = after.find(';').filter(|&i| i <= MAX_ENTITY_LEN)
&& let Some(ch) = decode_entity(&after[..semi])
{
out.push(ch);
rest = &after[semi + 1..];
} else {
out.push('&');
rest = after;
}
let Some(next) = rest.find('&') else {
out.push_str(rest);
break;
};
out.push_str(&rest[..next]);
rest = &rest[next..];
}
Cow::Owned(out)
}
fn decode_entity(body: &str) -> Option<char> {
if let Some(num) = body.strip_prefix('#') {
let code = match num.strip_prefix(['x', 'X']) {
Some(hex) => u32::from_str_radix(hex, 16).ok()?,
None => num.parse::<u32>().ok()?,
};
return char::from_u32(code);
}
Some(match body {
"amp" => '&',
"lt" => '<',
"gt" => '>',
"quot" => '"',
"apos" => '\'',
"nbsp" => '\u{a0}',
"hellip" => '…',
"mdash" => '—',
"ndash" => '–',
"lsquo" => '\u{2018}',
"rsquo" => '\u{2019}',
"ldquo" => '\u{201C}',
"rdquo" => '\u{201D}',
"laquo" => '«',
"raquo" => '»',
"copy" => '©',
"reg" => '®',
"trade" => '™',
"deg" => '°',
"middot" | "bull" => '•',
"euro" => '€',
"pound" => '£',
"cent" => '¢',
"sect" => '§',
"times" => '×',
"divide" => '÷',
_ => return None,
})
}
#[inline]
pub fn marker<S: AsRef<str>>(name: S) -> Marker<S> {
Marker(name)
}
#[derive(Debug, Clone, Copy)]
pub struct Marker<S>(pub S);
impl<S: AsRef<str>> IntoHtml for Marker<S> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
fn escape_and_write(self, buf: &mut String) {
buf.push_str(r#"<?marker name=""#);
escape_into(buf, self.0.as_ref());
buf.push_str(r#"">"#);
}
fn size_hint(&self) -> usize {
17 + self.0.as_ref().len()
}
}
#[inline]
pub fn start<S: AsRef<str>>(name: S) -> Start<S> {
Start(name)
}
#[derive(Debug, Clone, Copy)]
pub struct Start<S>(pub S);
impl<S: AsRef<str>> IntoHtml for Start<S> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
fn escape_and_write(self, buf: &mut String) {
buf.push_str(r#"<?start name=""#);
escape_into(buf, self.0.as_ref());
buf.push_str(r#"">"#);
}
fn size_hint(&self) -> usize {
16 + self.0.as_ref().len()
}
}
#[inline]
pub fn end() -> End {
End
}
#[derive(Debug, Clone, Copy)]
pub struct End;
impl IntoHtml for End {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
fn escape_and_write(self, buf: &mut String) {
buf.push_str(r#"<?end>"#);
}
fn size_hint(&self) -> usize {
6 }
}
#[derive(Debug, Clone, Copy)]
pub struct PreEscaped<T>(pub T);
impl<T: fmt::Display> fmt::Display for PreEscaped<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl IntoHtml for PreEscaped<&str> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
buf.push_str(self.0);
}
#[inline]
fn size_hint(&self) -> usize {
self.0.len()
}
}
impl IntoHtml for PreEscaped<String> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
buf.push_str(&self.0);
}
#[inline]
fn size_hint(&self) -> usize {
self.0.len()
}
}
impl IntoHtml for PreEscaped<char> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
buf.push(self.0);
}
#[inline]
fn size_hint(&self) -> usize {
self.0.len_utf8()
}
}
impl IntoHtml for PreEscaped<Cow<'static, str>> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
buf.push_str(&self.0);
}
#[inline]
fn size_hint(&self) -> usize {
self.0.len()
}
}
impl IntoHtml for PreEscaped<Box<str>> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
buf.push_str(&self.0);
}
#[inline]
fn size_hint(&self) -> usize {
self.0.len()
}
}
impl IntoHtml for &str {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
escape_into(buf, self)
}
#[inline]
fn size_hint(&self) -> usize {
self.len()
}
}
impl IntoHtml for char {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
escape_into(buf, self.encode_utf8(&mut [0; 4]));
}
#[inline]
fn size_hint(&self) -> usize {
self.len_utf8()
}
}
impl IntoHtml for String {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
escape_into(buf, &self)
}
#[inline]
fn size_hint(&self) -> usize {
self.len()
}
}
impl IntoHtml for &String {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
escape_into(buf, self)
}
#[inline]
fn size_hint(&self) -> usize {
self.len()
}
}
impl IntoHtml for Box<str> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
escape_into(buf, &self)
}
#[inline]
fn size_hint(&self) -> usize {
self.len()
}
}
impl IntoHtml for Cow<'static, str> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
escape_into(buf, self.as_ref())
}
#[inline]
fn size_hint(&self) -> usize {
self.as_ref().len()
}
}
impl IntoHtml for bool {
#[inline]
fn into_html(self) -> impl IntoHtml {
if self { "true" } else { "false" }
}
#[inline]
fn size_hint(&self) -> usize {
5
}
}
impl<T: IntoHtml> IntoHtml for Option<T> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
if let Some(x) = self {
x.escape_and_write(buf)
}
}
#[inline]
fn size_hint(&self) -> usize {
match self {
Some(x) => x.size_hint(),
None => 0,
}
}
}
impl IntoHtml for () {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, _: &mut String) {}
}
impl<F: FnOnce(&mut String)> IntoHtml for F {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
(self)(buf)
}
}
impl<B: IntoHtml, I: ExactSizeIterator, F> IntoHtml for std::iter::Map<I, F>
where
F: FnMut(I::Item) -> B,
{
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
let len = self.len();
for (i, x) in self.enumerate() {
if i == 0 {
buf.reserve(len * x.size_hint());
}
x.escape_and_write(buf);
}
}
}
impl<T: IntoHtml> IntoHtml for Vec<T> {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
for x in self {
x.escape_and_write(buf);
}
}
#[inline]
fn size_hint(&self) -> usize {
self.iter().map(IntoHtml::size_hint).sum()
}
}
impl<T: IntoHtml, const N: usize> IntoHtml for [T; N] {
#[inline]
fn into_html(self) -> impl IntoHtml {
self
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
for x in self {
x.escape_and_write(buf);
}
}
#[inline]
fn size_hint(&self) -> usize {
self.iter().map(IntoHtml::size_hint).sum()
}
}
macro_rules! impl_tuple {
( ( $($i:ident,)+ ) ) => {
impl<$($i,)+> IntoHtml for ($($i,)+)
where
$($i: IntoHtml,)+
{
#[inline]
fn into_html(self) -> impl IntoHtml {
#[allow(non_snake_case)]
let ($($i,)+) = self;
($($i.into_html(),)+)
}
#[inline]
fn escape_and_write(self, buf: &mut String) {
#[allow(non_snake_case)]
let ($($i,)+) = self;
$( $i.escape_and_write(buf); )+
}
#[inline]
fn size_hint(&self) -> usize {
#[allow(non_snake_case)]
let ($($i,)+) = self;
let mut n = 0;
$( n += $i.size_hint(); )+
n
}
}
};
($f:ident) => {
impl_tuple!(($f,));
};
($f:ident $($i:ident)+) => {
impl_tuple!(($f, $($i,)+));
impl_tuple!($($i)+);
};
}
impl_tuple!(A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A_ B_ C_ D_ E_ F_ G_ H_ I_ J_ K_);
macro_rules! via_display {
($($ty:ty)*) => {
$(
impl IntoHtml for $ty {
#[inline]
fn into_html(self) -> impl IntoHtml { self }
#[inline]
fn escape_and_write(self, buf: &mut String) {
_ = write!(buf, "{self}");
}
}
)*
};
}
via_display! { isize i8 i16 i32 i64 i128 usize u8 u16 u32 u64 u128 f32 f64 }