Refactor to allow more optimized escaping

This commit is contained in:
Michael Pfaff 2024-03-15 19:28:03 -04:00
parent 19559e5c09
commit faabe73cb2
4 changed files with 86 additions and 27 deletions

View File

@ -1,21 +1,47 @@
use std::marker::PhantomData;
use array_vec::ArrayStr;
use super::Buffer;
pub(crate) struct CommonIdents<E: Escape>(PhantomData<E>);
pub const fn are_all_chars_identity<E: Escape + ~const EscapeMeta>(set: &[char]) -> bool {
let mut i = 0;
while i < set.len() {
if !E::is_identity(set[i]) {
return false;
}
i += 1;
}
true
}
impl<E: Escape> CommonIdents<E> {
/// True if `true` and `false` will never need escaping.
pub const BOOLS: bool =
are_all_chars_identity::<E>(&['t', 'r', 'u', 'e', 'f', 'a', 'l', 's']);
/// True if unsigned integers will never need escaping.
pub const UINTS: bool =
are_all_chars_identity::<E>(&['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
/// True if signed integers will never need escaping.
pub const INTS: bool = Self::UINTS && are_all_chars_identity::<E>(&['-']);
/// True if floats (using [`ryu`]'s formatting) will never need escaping.
pub const FLOATS: bool = Self::INTS && are_all_chars_identity::<E>(&['.', 'e']);
}
#[const_trait]
pub trait EscapeMeta {
/// Returns `true` if the escaping scheme will never map the given character, regardless of its
/// configuration.
fn is_identity(c: char) -> bool;
}
/// A scheme for escaping strings.
pub trait Escape {
pub trait Escape: const EscapeMeta {
/// The type of an escaped character.
type Escaped: AsRef<str>;
/// True if `true` and `false` will never need escaping.
const IDENT_BOOLS: bool = false;
/// True if unsigned integers will never need escaping.
const IDENT_UINTS: bool = false;
/// True if signed integers will never need escaping.
const IDENT_INTS: bool = false;
/// True if floats (using [`ryu`]'s formatting) will never need escaping.
const IDENT_FLOATS: bool = false;
/// If the character needs to be escaped, does so and returns it as a string. Otherwise,
/// returns `None`.
fn escape(&self, c: char) -> Option<Self::Escaped>;
@ -56,14 +82,16 @@ pub trait Escape {
/// A scheme for escaping strings for safe insertion into JSON strings.
pub struct EscapeJsonString;
impl const EscapeMeta for EscapeJsonString {
#[inline]
fn is_identity(c: char) -> bool {
!matches!(c, '"' | '\\' | '\u{0000}'..='\u{001F}')
}
}
impl Escape for EscapeJsonString {
type Escaped = ArrayStr<4>;
const IDENT_BOOLS: bool = true;
const IDENT_UINTS: bool = true;
const IDENT_INTS: bool = true;
const IDENT_FLOATS: bool = true;
#[inline]
fn escape(&self, c: char) -> Option<Self::Escaped> {
match c {
@ -87,3 +115,16 @@ impl Escape for EscapeJsonString {
}
}
}
#[cfg(test)]
mod tests {
use super::{CommonIdents, EscapeJsonString};
#[test]
fn check_idents() {
assert!(CommonIdents::<EscapeJsonString>::BOOLS);
assert!(CommonIdents::<EscapeJsonString>::UINTS);
assert!(CommonIdents::<EscapeJsonString>::INTS);
assert!(CommonIdents::<EscapeJsonString>::FLOATS);
}
}

View File

@ -34,20 +34,24 @@ static ESCAPE_LUT: [u8; 256] = [
const ESCAPED: [&str; 5] = ["&quot;", "&amp;", "&#039;", "&lt;", "&gt;"];
const ESCAPED_LEN: usize = 5;
use crate::escape::EscapeMeta;
use super::{Buffer, Escape};
/// A scheme for escaping strings for safe insertion into HTML.
#[derive(Debug, Clone, Copy)]
pub struct EscapeHtml;
impl const EscapeMeta for EscapeHtml {
#[inline]
fn is_identity(c: char) -> bool {
!matches!(c, '\"' | '&' | '<' | '>' | '\'')
}
}
impl Escape for EscapeHtml {
type Escaped = &'static str;
const IDENT_BOOLS: bool = true;
const IDENT_UINTS: bool = true;
const IDENT_INTS: bool = true;
const IDENT_FLOATS: bool = true;
#[inline(always)]
fn escape(&self, c: char) -> Option<Self::Escaped> {
match c {
@ -128,8 +132,18 @@ fn escape_to_buf(feed: &str, buf: &mut Buffer) {
#[cfg(test)]
mod tests {
use crate::escape;
use super::*;
#[test]
fn check_idents() {
assert!(escape::CommonIdents::<EscapeHtml>::BOOLS);
assert!(escape::CommonIdents::<EscapeHtml>::UINTS);
assert!(escape::CommonIdents::<EscapeHtml>::INTS);
assert!(escape::CommonIdents::<EscapeHtml>::FLOATS);
}
fn escape(feed: &str) -> String {
let mut s = String::new();
EscapeHtml.escape_to_string(&mut s, feed);

View File

@ -33,6 +33,8 @@
)]
#![cfg_attr(sailfish_nightly, feature(core_intrinsics))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![feature(const_trait_impl)]
#![feature(effects)]
#![feature(maybe_uninit_slice)]
#![feature(specialization)]
#![allow(clippy::redundant_closure)]
@ -42,7 +44,7 @@
mod utils;
mod buffer;
mod escape;
pub mod escape;
pub mod filter;
mod html_escape;
mod render;

View File

@ -9,6 +9,8 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Arc, MutexGuard, RwLockReadGuard, RwLockWriteGuard};
use crate::escape;
use super::{Buffer, Escape, SizeHint};
/// types which can be rendered inside buffer block (`<%- %>`) by reference
@ -303,7 +305,7 @@ impl Render for bool {
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
if E::IDENT_BOOLS {
if escape::CommonIdents::<E>::BOOLS {
self.render(b)
} else {
if *self {
@ -341,7 +343,7 @@ macro_rules! render_int {
#[inline]
fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
if E::$ident_tag {
if escape::CommonIdents::<E>::$ident_tag {
// push_str without escape
self.render(b)
} else {
@ -353,8 +355,8 @@ macro_rules! render_int {
}
}
render_int!(IDENT_UINTS, u8, u16, u32, u64, u128, usize);
render_int!(IDENT_INTS, i8, i16, i32, i64, i128, isize);
render_int!(UINTS, u8, u16, u32, u64, u128, usize);
render_int!(INTS, i8, i16, i32, i64, i128, isize);
impl Render for f32 {
#[cfg_attr(feature = "perf-inline", inline)]
@ -384,7 +386,7 @@ impl Render for f32 {
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
if E::IDENT_FLOATS {
if escape::CommonIdents::<E>::FLOATS {
self.render(b)
} else {
default_render_escaped(self, b, e)
@ -420,7 +422,7 @@ impl Render for f64 {
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
if E::IDENT_FLOATS {
if escape::CommonIdents::<E>::FLOATS {
self.render(b)
} else {
default_render_escaped(self, b, e)