Let |indent use AsIndent

This commit is contained in:
René Kijewski 2025-04-15 13:47:54 +02:00 committed by René Kijewski
parent 2c1e86e410
commit ba33974996
4 changed files with 132 additions and 115 deletions

View File

@ -1,8 +1,11 @@
use alloc::format;
use alloc::borrow::Cow;
use alloc::string::String;
use alloc::str;
use core::cell::Cell;
use core::convert::Infallible;
use core::fmt::{self, Write};
use core::ops::Deref;
use core::pin::Pin;
use super::escape::HtmlSafeOutput;
use super::{FastWritable, MAX_LEN};
@ -504,7 +507,7 @@ fn flush_trim(dest: &mut (impl fmt::Write + ?Sized), collector: TrimCollector) -
dest.write_str(collector.0.trim_end())
}
/// Indent lines with `width` spaces
/// Indent lines with spaces or a prefix
///
/// ```
/// # #[cfg(feature = "code-in-doc")] {
@ -524,58 +527,74 @@ fn flush_trim(dest: &mut (impl fmt::Write + ?Sized), collector: TrimCollector) -
/// );
/// # }
/// ```
///
/// ```
/// # #[cfg(feature = "code-in-doc")] {
/// # use askama::Template;
/// /// ```jinja
/// /// <div>{{ example|indent("$$$ ") }}</div>
/// /// ```
/// #[derive(Template)]
/// #[template(ext = "html", in_doc = true)]
/// struct Example<'a> {
/// example: &'a str,
/// }
///
/// assert_eq!(
/// Example { example: "hello\nfoo\nbar" }.to_string(),
/// "<div>hello\n$$$ foo\n$$$ bar</div>"
/// );
/// # }
/// ```
#[inline]
pub fn indent<S: fmt::Display>(source: S, width: usize) -> Result<Indent<S>, Infallible> {
Ok(Indent { source, width })
pub fn indent<S, I: AsIndent>(source: S, indent: I) -> Result<Indent<S, I>, Infallible> {
Ok(Indent { source, indent })
}
pub struct Indent<S> {
pub struct Indent<S, I> {
source: S,
width: usize,
indent: I,
}
impl<S: fmt::Display> fmt::Display for Indent<S> {
impl<S: fmt::Display, I: AsIndent> fmt::Display for Indent<S, I> {
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { ref source, width } = *self;
if width >= MAX_LEN || width == 0 {
let Self { source, indent } = self;
let indent = indent.as_indent();
if indent.len() >= MAX_LEN || indent.is_empty() {
write!(dest, "{source}")
} else {
let mut buffer;
flush_indent(dest, width, try_to_str!(source => buffer))
flush_indent(dest, &indent, try_to_str!(source => buffer))
}
}
}
impl<S: FastWritable> FastWritable for Indent<S> {
impl<S: FastWritable, I: AsIndent> FastWritable for Indent<S, I> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let Self { ref source, width } = *self;
if width >= MAX_LEN || width == 0 {
let Self { ref source, indent } = self;
let indent = indent.as_indent();
if indent.len() >= MAX_LEN || indent.is_empty() {
source.write_into(dest, values)
} else {
let mut buffer = String::new();
source.write_into(&mut buffer, values)?;
Ok(flush_indent(dest, width, &buffer)?)
Ok(flush_indent(dest, &indent, &buffer)?)
}
}
}
fn flush_indent(dest: &mut (impl fmt::Write + ?Sized), width: usize, s: &str) -> fmt::Result {
fn flush_indent(dest: &mut (impl fmt::Write + ?Sized), indent: &str, s: &str) -> fmt::Result {
if s.len() >= MAX_LEN {
return dest.write_str(s);
}
let mut prefix = String::new();
for (idx, line) in s.split_inclusive('\n').enumerate() {
if idx > 0 {
// only allocate prefix if needed, i.e. not for single-line outputs
if prefix.is_empty() {
prefix = format!("{: >1$}", "", width);
}
dest.write_str(&prefix)?;
dest.write_str(indent)?;
}
dest.write_str(line)?;
}
@ -776,6 +795,92 @@ fn flush_title(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
Ok(())
}
/// A prefix usable for indenting [prettified JSON data](json_pretty)
///
/// ```
/// # use askama::filters::AsIndent;
/// assert_eq!(4.as_indent(), " ");
/// assert_eq!(" -> ".as_indent(), " -> ");
/// ```
pub trait AsIndent {
/// Borrow `self` as prefix to use.
fn as_indent(&self) -> &str;
}
impl AsIndent for str {
#[inline]
fn as_indent(&self) -> &str {
self
}
}
#[cfg(feature = "alloc")]
impl AsIndent for alloc::string::String {
#[inline]
fn as_indent(&self) -> &str {
self
}
}
impl AsIndent for usize {
#[inline]
fn as_indent(&self) -> &str {
spaces(*self)
}
}
impl AsIndent for std::num::Wrapping<usize> {
#[inline]
fn as_indent(&self) -> &str {
spaces(self.0)
}
}
impl AsIndent for std::num::NonZeroUsize {
#[inline]
fn as_indent(&self) -> &str {
spaces(self.get())
}
}
fn spaces(width: usize) -> &'static str {
const MAX_SPACES: usize = 16;
const SPACES: &str = match str::from_utf8(&[b' '; MAX_SPACES]) {
Ok(spaces) => spaces,
Err(_) => panic!(),
};
&SPACES[..width.min(SPACES.len())]
}
#[cfg(feature = "alloc")]
impl<T: AsIndent + alloc::borrow::ToOwned + ?Sized> AsIndent for Cow<'_, T> {
#[inline]
fn as_indent(&self) -> &str {
T::as_indent(self)
}
}
crate::impl_for_ref! {
impl AsIndent for T {
#[inline]
fn as_indent(&self) -> &str {
<T>::as_indent(self)
}
}
}
impl<T> AsIndent for Pin<T>
where
T: Deref,
<T as Deref>::Target: AsIndent,
{
#[inline]
fn as_indent(&self) -> &str {
self.as_ref().get_ref().as_indent()
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;

View File

@ -1,12 +1,10 @@
use std::convert::Infallible;
use std::ops::Deref;
use std::pin::Pin;
use std::{fmt, io, str};
use serde::Serialize;
use serde_json::ser::{CompactFormatter, PrettyFormatter, Serializer};
use super::FastWritable;
use super::{AsIndent, FastWritable};
use crate::ascii_str::{AsciiChar, AsciiStr};
use crate::{NO_VALUES, Values};
@ -102,92 +100,6 @@ struct ToJsonPretty<S, I> {
indent: I,
}
/// A prefix usable for indenting [prettified JSON data](json_pretty)
///
/// ```
/// # use askama::filters::AsIndent;
/// assert_eq!(4.as_indent(), " ");
/// assert_eq!(" -> ".as_indent(), " -> ");
/// ```
pub trait AsIndent {
/// Borrow `self` as prefix to use.
fn as_indent(&self) -> &str;
}
impl AsIndent for str {
#[inline]
fn as_indent(&self) -> &str {
self
}
}
#[cfg(feature = "alloc")]
impl AsIndent for alloc::string::String {
#[inline]
fn as_indent(&self) -> &str {
self
}
}
impl AsIndent for usize {
#[inline]
fn as_indent(&self) -> &str {
spaces(*self)
}
}
impl AsIndent for std::num::Wrapping<usize> {
#[inline]
fn as_indent(&self) -> &str {
spaces(self.0)
}
}
impl AsIndent for std::num::NonZeroUsize {
#[inline]
fn as_indent(&self) -> &str {
spaces(self.get())
}
}
fn spaces(width: usize) -> &'static str {
const MAX_SPACES: usize = 16;
const SPACES: &str = match str::from_utf8(&[b' '; MAX_SPACES]) {
Ok(spaces) => spaces,
Err(_) => panic!(),
};
&SPACES[..width.min(SPACES.len())]
}
#[cfg(feature = "alloc")]
impl<T: AsIndent + alloc::borrow::ToOwned + ?Sized> AsIndent for alloc::borrow::Cow<'_, T> {
#[inline]
fn as_indent(&self) -> &str {
T::as_indent(self)
}
}
crate::impl_for_ref! {
impl AsIndent for T {
#[inline]
fn as_indent(&self) -> &str {
<T>::as_indent(self)
}
}
}
impl<T> AsIndent for Pin<T>
where
T: Deref,
<T as Deref>::Target: AsIndent,
{
#[inline]
fn as_indent(&self) -> &str {
self.as_ref().get_ref().as_indent()
}
}
impl<S: Serialize> FastWritable for ToJson<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(&self, f: &mut W, _: &dyn Values) -> crate::Result<()> {

View File

@ -22,8 +22,8 @@ mod urlencode;
#[cfg(feature = "alloc")]
pub use self::alloc::{
capitalize, fmt, format, indent, linebreaks, linebreaksbr, lower, lowercase, paragraphbreaks,
title, trim, upper, uppercase, wordcount,
AsIndent, capitalize, fmt, format, indent, linebreaks, linebreaksbr, lower, lowercase,
paragraphbreaks, title, trim, upper, uppercase, wordcount,
};
pub use self::builtin::{PluralizeCount, center, join, pluralize, truncate};
pub use self::escape::{
@ -32,7 +32,7 @@ pub use self::escape::{
};
pub use self::humansize::filesizeformat;
#[cfg(feature = "serde_json")]
pub use self::json::{AsIndent, json, json_pretty};
pub use self::json::{json, json_pretty};
#[cfg(feature = "urlencode")]
pub use self::urlencode::{urlencode, urlencode_strict};

View File

@ -154,7 +154,7 @@ HELLO
fn filter_block_chaining_paren_followed_by_whitespace() {
#[derive(Template)]
#[template(
source = r#"{% filter lower|indent(2) -%}
source = r#"{% filter lower|indent("> ") -%}
HELLO
{{v}}
{%- endfilter %}
@ -183,7 +183,7 @@ HELLO
assert_eq!(
template.render().unwrap(),
r"hello
pika
> pika
hello
pika