Skip to content

Implement upper filter #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub enum FilterType {
Lower(LowerFilter),
Safe(SafeFilter),
Slugify(SlugifyFilter),
Upper(UpperFilter),
}

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -81,3 +82,6 @@ pub struct SafeFilter;

#[derive(Clone, Debug, PartialEq)]
pub struct SlugifyFilter;

#[derive(Clone, Debug, PartialEq)]
pub struct UpperFilter;
5 changes: 5 additions & 0 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use crate::filters::FilterType;
use crate::filters::LowerFilter;
use crate::filters::SafeFilter;
use crate::filters::SlugifyFilter;
use crate::filters::UpperFilter;
use crate::lex::START_TAG_LEN;
use crate::lex::autoescape::{AutoescapeEnabled, AutoescapeError, lex_autoescape_argument};
use crate::lex::common::LexerError;
Expand Down Expand Up @@ -124,6 +125,10 @@ impl Filter {
Some(right) => return Err(unexpected_argument("slugify", right)),
None => FilterType::Slugify(SlugifyFilter),
},
"upper" => match right {
Some(right) => return Err(unexpected_argument("upper", right)),
None => FilterType::Upper(UpperFilter),
},
external => {
let external = match parser.external_filters.get(external) {
Some(external) => external.clone().unbind(),
Expand Down
10 changes: 5 additions & 5 deletions src/render/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::BTreeMap;

use pyo3::prelude::*;

use super::types::{Content, Context};
use super::types::{Content, ContentString, Context};
use super::{Evaluate, Render, RenderResult, Resolve, ResolveFailures, ResolveResult};
use crate::error::RenderError;
use crate::parse::{TagElement, TokenTree};
Expand Down Expand Up @@ -74,10 +74,10 @@ impl Resolve for Text {
_failures: ResolveFailures,
) -> ResolveResult<'t, 'py> {
let resolved = Cow::Borrowed(template.content(self.at));
Ok(Some(match context.autoescape {
false => Content::String(resolved),
true => Content::HtmlSafe(resolved),
}))
Ok(Some(Content::String(match context.autoescape {
false => ContentString::String(resolved),
true => ContentString::HtmlSafe(resolved),
})))
}
}

Expand Down
158 changes: 116 additions & 42 deletions src/render/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ use pyo3::types::PyType;

use crate::filters::{
AddFilter, AddSlashesFilter, CapfirstFilter, DefaultFilter, EscapeFilter, ExternalFilter,
FilterType, LowerFilter, SafeFilter, SlugifyFilter,
FilterType, LowerFilter, SafeFilter, SlugifyFilter, UpperFilter,
};
use crate::parse::Filter;
use crate::render::types::{Content, Context};
use crate::render::types::{Content, ContentString, Context};
use crate::render::{Resolve, ResolveFailures, ResolveResult};
use crate::types::TemplateString;
use regex::Regex;
Expand Down Expand Up @@ -43,13 +43,13 @@ where
'a: 't,
{
fn as_content(&'a self) -> Option<Content<'t, 'py>> {
Some(Content::String(Cow::Borrowed(self)))
Some(Content::String(ContentString::String(Cow::Borrowed(self))))
}
}

impl<'t, 'py> IntoOwnedContent<'t, 'py> for String {
fn into_content(self) -> Option<Content<'t, 'py>> {
Some(Content::String(Cow::Owned(self)))
Some(Content::String(ContentString::String(Cow::Owned(self))))
}
}

Expand All @@ -72,6 +72,7 @@ impl Resolve for Filter {
FilterType::Lower(filter) => filter.resolve(left, py, template, context),
FilterType::Safe(filter) => filter.resolve(left, py, template, context),
FilterType::Slugify(filter) => filter.resolve(left, py, template, context),
FilterType::Upper(filter) => filter.resolve(left, py, template, context),
};
result
}
Expand Down Expand Up @@ -189,25 +190,27 @@ impl ResolveFilter for EscapeFilter {
_template: TemplateString<'t>,
_context: &mut Context,
) -> ResolveResult<'t, 'py> {
Ok(match variable {
Some(content) => match content {
Content::HtmlSafe(content) => Some(Content::HtmlSafe(content)),
Content::String(content) => {
let mut encoded = String::new();
encode_quoted_attribute_to_string(&content, &mut encoded);
Some(Content::HtmlSafe(Cow::Owned(encoded)))
}
Content::Int(n) => Some(Content::HtmlSafe(Cow::Owned(n.to_string()))),
Content::Float(n) => Some(Content::HtmlSafe(Cow::Owned(n.to_string()))),
Content::Py(object) => {
let content = object.str()?.extract::<String>()?;
let mut encoded = String::new();
encode_quoted_attribute_to_string(&content, &mut encoded);
Some(Content::HtmlSafe(Cow::Owned(encoded)))
}
Ok(Some(Content::String(ContentString::HtmlSafe(
match variable {
Some(content) => match content {
Content::String(ContentString::HtmlSafe(content)) => content,
Content::String(content) => {
let mut encoded = String::new();
encode_quoted_attribute_to_string(content.as_raw(), &mut encoded);
Cow::Owned(encoded)
}
Content::Int(n) => Cow::Owned(n.to_string()),
Content::Float(n) => Cow::Owned(n.to_string()),
Content::Py(object) => {
let content = object.str()?.extract::<String>()?;
let mut encoded = String::new();
encode_quoted_attribute_to_string(&content, &mut encoded);
Cow::Owned(encoded)
}
},
None => Cow::Borrowed(""),
},
None => Some(Content::HtmlSafe(Cow::Borrowed(""))),
})
))))
}
}

Expand Down Expand Up @@ -260,20 +263,20 @@ impl ResolveFilter for SafeFilter {
_template: TemplateString<'t>,
_context: &mut Context,
) -> ResolveResult<'t, 'py> {
let content = match variable {
Some(content) => match content {
Content::HtmlSafe(content) => Some(Content::HtmlSafe(content)),
Content::String(content) => Some(Content::HtmlSafe(content)),
Content::Int(n) => Some(Content::HtmlSafe(Cow::Owned(n.to_string()))),
Content::Float(n) => Some(Content::HtmlSafe(Cow::Owned(n.to_string()))),
Content::Py(object) => {
let content = object.str()?.extract::<String>()?;
Some(Content::HtmlSafe(Cow::Owned(content)))
}
Ok(Some(Content::String(ContentString::HtmlSafe(
match variable {
Some(content) => match content {
Content::String(content) => content.into_raw(),
Content::Int(n) => Cow::Owned(n.to_string()),
Content::Float(n) => Cow::Owned(n.to_string()),
Content::Py(object) => {
let content = object.str()?.extract::<String>()?;
Cow::Owned(content)
}
},
None => Cow::Borrowed(""),
},
None => Some(Content::HtmlSafe(Cow::Borrowed(""))),
};
Ok(content)
))))
}
}

Expand Down Expand Up @@ -306,26 +309,48 @@ impl ResolveFilter for SlugifyFilter {
#[allow(non_snake_case)]
let SafeData = SAFEDATA.import(py, "django.utils.safestring", "SafeData")?;
match content.is_instance(SafeData)? {
true => Some(Content::HtmlSafe(slug)),
false => Some(Content::String(slug)),
true => Some(Content::String(ContentString::HtmlSafe(slug))),
false => Some(Content::String(ContentString::HtmlUnsafe(slug))),
}
}
// Int and Float requires no slugify, we only need to turn it into a string.
Content::Int(content) => Some(Content::String(Cow::Owned(content.to_string()))),
Content::Float(content) => Some(Content::String(Cow::Owned(content.to_string()))),
Content::String(content) => Some(Content::String(slugify(content))),
Content::HtmlSafe(content) => Some(Content::HtmlSafe(slugify(content))),
Content::Int(content) => Some(Content::String(ContentString::String(Cow::Owned(
content.to_string(),
)))),
Content::Float(content) => Some(Content::String(ContentString::String(
Cow::Owned(content.to_string()),
))),
Content::String(content) => Some(content.map_content(slugify)),
},
None => "".as_content(),
};
Ok(content)
}
}

impl ResolveFilter for UpperFilter {
fn resolve<'t, 'py>(
&self,
variable: Option<Content<'t, 'py>>,
_py: Python<'py>,
_template: TemplateString<'t>,
context: &mut Context,
) -> ResolveResult<'t, 'py> {
let content = match variable {
Some(content) => {
let content = content.resolve_string(context)?;
Some(content.map_content(|content| Cow::Owned(content.to_uppercase())))
}
None => "".as_content(),
};
Ok(content)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::filters::{AddSlashesFilter, DefaultFilter, LowerFilter};
use crate::filters::{AddSlashesFilter, DefaultFilter, LowerFilter, UpperFilter};
use crate::parse::TagElement;
use crate::render::Render;
use crate::template::django_rusty_templates::{EngineData, Template};
Expand Down Expand Up @@ -800,4 +825,53 @@ mod tests {
assert_eq!(rendered, "bryony");
})
}

#[test]
fn test_render_filter_upper() {
pyo3::prepare_freethreaded_python();

Python::with_gil(|py| {
let name = PyString::new(py, "Foo").into_any();
let context = HashMap::from([("name".to_string(), name.unbind())]);
let mut context = Context {
context,
request: None,
autoescape: false,
};
let template = TemplateString("{{ name|upper }}");
let variable = Variable::new((3, 4));
let filter = Filter {
at: (8, 5),
left: TagElement::Variable(variable),
filter: FilterType::Upper(UpperFilter),
};

let rendered = filter.render(py, template, &mut context).unwrap();
assert_eq!(rendered, "FOO");
})
}

#[test]
fn test_render_filter_upper_missing_left() {
pyo3::prepare_freethreaded_python();

Python::with_gil(|py| {
let context = HashMap::new();
let mut context = Context {
context,
request: None,
autoescape: false,
};
let template = TemplateString("{{ name|upper }}");
let variable = Variable::new((3, 4));
let filter = Filter {
at: (8, 5),
left: TagElement::Variable(variable),
filter: FilterType::Upper(UpperFilter),
};

let rendered = filter.render(py, template, &mut context).unwrap();
assert_eq!(rendered, "");
})
}
}
Loading
Loading