Filter linebreaks only needs core

This commit is contained in:
René Kijewski 2025-06-14 11:16:17 +02:00
parent 477bfe4a3c
commit 781d88042c
5 changed files with 111 additions and 82 deletions

View File

@ -95,62 +95,6 @@ pub fn fmt() {}
/// Compare with [fmt](./fn.fmt.html).
pub fn format() {}
/// Replaces line breaks in plain text with appropriate HTML
///
/// A single newline becomes an HTML line break `<br>` and a new line
/// followed by a blank line becomes a paragraph break `<p>`.
///
/// ```
/// # #[cfg(feature = "code-in-doc")] {
/// # use askama::Template;
/// /// ```jinja
/// /// <div>{{ example|linebreaks }}</div>
/// /// ```
/// #[derive(Template)]
/// #[template(ext = "html", in_doc = true)]
/// struct Example<'a> {
/// example: &'a str,
/// }
///
/// assert_eq!(
/// Example { example: "Foo\nBar\n\nBaz" }.to_string(),
/// "<div><p>Foo<br/>Bar</p><p>Baz</p></div>"
/// );
/// # }
/// ```
#[inline]
pub fn linebreaks<S: fmt::Display>(source: S) -> Result<HtmlSafeOutput<Linebreaks<S>>, Infallible> {
Ok(HtmlSafeOutput(Linebreaks(source)))
}
pub struct Linebreaks<S>(S);
impl<S: fmt::Display> fmt::Display for Linebreaks<S> {
#[inline]
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut buffer;
flush_linebreaks(dest, try_to_str!(self.0 => buffer))
}
}
impl<S: FastWritable> FastWritable for Linebreaks<S> {
#[inline]
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
let mut buffer = String::new();
self.0.write_into(&mut buffer, values)?;
Ok(flush_linebreaks(dest, &buffer)?)
}
}
fn flush_linebreaks(dest: &mut (impl fmt::Write + ?Sized), s: &str) -> fmt::Result {
let linebroken = s.replace("\n\n", "</p><p>").replace('\n', "<br/>");
write!(dest, "<p>{linebroken}</p>")
}
/// Converts all newlines in a piece of plain text to HTML line breaks
///
/// ```
@ -656,18 +600,6 @@ mod tests {
use super::*;
#[test]
fn test_linebreaks() {
assert_eq!(
linebreaks("Foo\nBar Baz").unwrap().to_string(),
"<p>Foo<br/>Bar Baz</p>"
);
assert_eq!(
linebreaks("Foo\nBar\n\nBaz").unwrap().to_string(),
"<p>Foo<br/>Bar</p><p>Baz</p>"
);
}
#[test]
fn test_linebreaksbr() {
assert_eq!(linebreaksbr("Foo\nBar").unwrap().to_string(), "Foo<br/>Bar");

View File

@ -1,10 +1,12 @@
use core::cell::Cell;
use core::convert::Infallible;
use core::fmt::{self, Write};
use core::mem::replace;
use core::ops::Deref;
use core::pin::Pin;
use super::MAX_LEN;
use crate::filters::HtmlSafeOutput;
use crate::{Error, FastWritable, Result, Values};
/// Limit string length, appends '...' if truncated
@ -615,6 +617,94 @@ impl<'a> fmt::Write for WordCountWriter<'a> {
}
}
/// Replaces line breaks in plain text with appropriate HTML
///
/// A single newline becomes an HTML line break `<br>` and a new line
/// followed by a blank line becomes a paragraph break `<p>`.
///
/// ```
/// # #[cfg(feature = "code-in-doc")] {
/// # use askama::Template;
/// /// ```jinja
/// /// <div>{{ example|linebreaks }}</div>
/// /// ```
/// #[derive(Template)]
/// #[template(ext = "html", in_doc = true)]
/// struct Example<'a> {
/// example: &'a str,
/// }
///
/// assert_eq!(
/// Example { example: "Foo\nBar\n\nBaz" }.to_string(),
/// "<div><p>Foo<br/>Bar</p><p>Baz</p></div>"
/// );
/// # }
/// ```
#[inline]
pub fn linebreaks<S: fmt::Display>(source: S) -> Result<HtmlSafeOutput<Linebreaks<S>>, Infallible> {
Ok(HtmlSafeOutput(Linebreaks(source)))
}
pub struct Linebreaks<S>(S);
impl<S: fmt::Display> fmt::Display for Linebreaks<S> {
fn fmt(&self, dest: &mut fmt::Formatter<'_>) -> fmt::Result {
dest.write_str("<p>")?;
let mut formatter = LinebreakFormatter { dest, counter: -1 };
write!(formatter, "{}", self.0)?;
formatter.dest.write_str("</p>")
}
}
impl<S: FastWritable> FastWritable for Linebreaks<S> {
fn write_into<W: fmt::Write + ?Sized>(
&self,
dest: &mut W,
values: &dyn crate::Values,
) -> crate::Result<()> {
dest.write_str("<p>")?;
let mut formatter = LinebreakFormatter { dest, counter: -1 };
self.0.write_into(&mut formatter, values)?;
dest.write_str("</p>")?;
Ok(())
}
}
struct LinebreakFormatter<'a, W: ?Sized> {
dest: &'a mut W,
counter: isize,
}
impl<W: fmt::Write + ?Sized> fmt::Write for LinebreakFormatter<'_, W> {
fn write_str(&mut self, s: &str) -> fmt::Result {
if s.is_empty() {
return Ok(());
}
for line in s.split_inclusive('\n') {
let (has_eol, line) = if let Some(line) = line.strip_suffix("\r\n") {
(true, line)
} else if let Some(line) = line.strip_suffix('\n') {
(true, line)
} else {
(false, line)
};
if !line.is_empty() {
match replace(&mut self.counter, if has_eol { 1 } else { 0 }) {
..=0 => {}
1 => self.dest.write_str("<br/>")?,
2.. => self.dest.write_str("</p><p>")?,
}
self.dest.write_str(line)?;
} else if has_eol && self.counter >= 0 {
self.counter += 1;
}
}
Ok(())
}
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use alloc::string::{String, ToString};
@ -725,4 +815,16 @@ mod tests {
assert_eq!(wrap("hello\n\n bar"), 2);
assert_eq!(wrap(" hello\n\n bar "), 2);
}
#[test]
fn test_linebreaks() {
assert_eq!(
linebreaks("Foo\nBar Baz").unwrap().to_string(),
"<p>Foo<br/>Bar Baz</p>"
);
assert_eq!(
linebreaks("Foo\nBar\n\nBaz").unwrap().to_string(),
"<p>Foo<br/>Bar</p><p>Baz</p>"
);
}
}

View File

@ -25,11 +25,11 @@ mod urlencode;
#[cfg(feature = "alloc")]
pub use self::alloc::{
capitalize, fmt, format, linebreaks, linebreaksbr, lower, lowercase, paragraphbreaks, title,
titlecase, trim, upper, uppercase,
capitalize, fmt, format, linebreaksbr, lower, lowercase, paragraphbreaks, title, titlecase,
trim, upper, uppercase,
};
pub use self::core::{
PluralizeCount, center, join, pluralize, reject, reject_with, truncate, wordcount,
PluralizeCount, center, join, linebreaks, pluralize, reject, reject_with, truncate, wordcount,
};
pub use self::escape::{
AutoEscape, AutoEscaper, Escaper, Html, HtmlSafe, HtmlSafeOutput, MaybeSafe, Safe, Text,

View File

@ -325,7 +325,8 @@ impl<'a> Generator<'a, '_> {
args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>,
) -> Result<DisplayWrap, CompileError> {
self.visit_linebreaks_filters(ctx, buf, "paragraphbreaks", args, node)
ensure_filter_has_feature_alloc(ctx, "paragraphbreaks", node)?;
self.visit_linebreaks_filters(ctx, buf, "paragraphbreaks", args)
}
fn visit_linebreaksbr_filter(
@ -335,7 +336,8 @@ impl<'a> Generator<'a, '_> {
args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>,
) -> Result<DisplayWrap, CompileError> {
self.visit_linebreaks_filters(ctx, buf, "linebreaksbr", args, node)
ensure_filter_has_feature_alloc(ctx, "linebreaksbr", node)?;
self.visit_linebreaks_filters(ctx, buf, "linebreaksbr", args)
}
fn visit_linebreaks_filter(
@ -343,9 +345,9 @@ impl<'a> Generator<'a, '_> {
ctx: &Context<'_>,
buf: &mut Buffer,
args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>,
_node: Span<'_>,
) -> Result<DisplayWrap, CompileError> {
self.visit_linebreaks_filters(ctx, buf, "linebreaks", args, node)
self.visit_linebreaks_filters(ctx, buf, "linebreaks", args)
}
fn visit_linebreaks_filters(
@ -354,9 +356,7 @@ impl<'a> Generator<'a, '_> {
buf: &mut Buffer,
name: &str,
args: &[WithSpan<'a, Expr<'a>>],
node: Span<'_>,
) -> Result<DisplayWrap, CompileError> {
ensure_filter_has_feature_alloc(ctx, name, node)?;
let arg = no_arguments(ctx, name, args)?;
buf.write(format_args!(
"askama::filters::{name}(&(&&askama::filters::AutoEscaper::new(&(",

View File

@ -297,11 +297,6 @@ foo, bar, bazz
### linebreaks
[#linebreaks]: #linebreaks
<blockquote class="right" style="padding:0.5ex 1ex; margin:0 0 1ex 1ex; font-size:80%">
enabled by <code>"alloc"</code><br/>
enabled by <code>"default"</code>
</blockquote>
```jinja
{{ text_to_break | linebreaks }}
```