-
-
Couldn't load subscription status.
- Fork 133
Add support for new escape sequences and Python compatible rendering #804
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| [package] | ||
| name = "pycompat-rendering" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
|
|
||
| [dependencies] | ||
| minijinja = { path = "../../minijinja" } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Python-Compatible Rendering Example | ||
|
|
||
| This example demonstrates MiniJinja's Python-compatible rendering mode. | ||
|
|
||
| ## Background | ||
|
|
||
| By default, MiniJinja renders values in its own style: | ||
| - Booleans: `true`/`false` | ||
| - None: `none` | ||
| - Strings: Rust-style escaping and quoting | ||
|
|
||
| When Python compatibility is needed (e.g., for SQL templating where precise output matters), you can enable PyCompat mode to match Python Jinja2's output: | ||
| - Booleans: `True`/`False` | ||
| - None: `None` | ||
| - Strings: Python-style escaping and quoting | ||
|
|
||
| ## Usage | ||
|
|
||
| ```rust | ||
| use minijinja::Environment; | ||
|
|
||
| let mut env = Environment::new(); | ||
|
|
||
| // Enable Python-compatible rendering | ||
| env.set_pycompat_rendering(true); | ||
|
|
||
| // Now templates will render values like Python Jinja2 | ||
| let tmpl = env.template_from_str("{{ [true, false, none, 'hello'] }}")?; | ||
| let result = tmpl.render(minijinja::context!{})?; | ||
| // Output: [True, False, None, 'hello'] | ||
| ``` | ||
|
|
||
| ## Running this Example | ||
|
|
||
| ```bash | ||
| cargo run --example pycompat-rendering | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| use minijinja::{context, Environment}; | ||
|
|
||
| fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
| println!("=== MiniJinja Rendering Comparison ==="); | ||
|
|
||
| // Create environments for both modes to avoid borrowing issues | ||
| let mut env_default = Environment::new(); | ||
| let mut env_pycompat = Environment::new(); | ||
|
|
||
| // Add the same template to both environments | ||
| let template_str = r#"{{ [true, false, none, 'foo', "bar'baz", '\x13'] }}"#; | ||
| env_default.add_template("demo", template_str)?; | ||
| env_pycompat.add_template("demo", template_str)?; | ||
|
|
||
| // Configure rendering modes | ||
| env_default.set_pycompat_rendering(false); | ||
| env_pycompat.set_pycompat_rendering(true); | ||
|
|
||
| // Test rendering | ||
| let result_default = env_default.get_template("demo")?.render(context! {})?; | ||
| let result_pycompat = env_pycompat.get_template("demo")?.render(context! {})?; | ||
|
|
||
| println!("Default rendering: {}", result_default); | ||
| println!("PyCompat rendering: {}", result_pycompat); | ||
|
|
||
| println!("\n=== Individual Value Comparison ==="); | ||
|
|
||
| // Test individual values | ||
| env_default.add_template("bool_true", "{{ true }}")?; | ||
| env_default.add_template("bool_false", "{{ false }}")?; | ||
| env_default.add_template("none_val", "{{ none }}")?; | ||
|
|
||
| env_pycompat.add_template("bool_true", "{{ true }}")?; | ||
| env_pycompat.add_template("bool_false", "{{ false }}")?; | ||
| env_pycompat.add_template("none_val", "{{ none }}")?; | ||
|
|
||
| println!("Default mode:"); | ||
| println!( | ||
| " true -> {}", | ||
| env_default.get_template("bool_true")?.render(context! {})? | ||
| ); | ||
| println!( | ||
| " false -> {}", | ||
| env_default | ||
| .get_template("bool_false")? | ||
| .render(context! {})? | ||
| ); | ||
| println!( | ||
| " none -> {}", | ||
| env_default.get_template("none_val")?.render(context! {})? | ||
| ); | ||
|
|
||
| println!("PyCompat mode:"); | ||
| println!( | ||
| " true -> {}", | ||
| env_pycompat | ||
| .get_template("bool_true")? | ||
| .render(context! {})? | ||
| ); | ||
| println!( | ||
| " false -> {}", | ||
| env_pycompat | ||
| .get_template("bool_false")? | ||
| .render(context! {})? | ||
| ); | ||
| println!( | ||
| " none -> {}", | ||
| env_pycompat.get_template("none_val")?.render(context! {})? | ||
| ); | ||
|
|
||
| Ok(()) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -277,6 +277,14 @@ impl Unescaper { | |
| let val = ok!(self.parse_u16(&mut char_iter)); | ||
| ok!(self.push_u16(val)); | ||
| } | ||
| 'x' => { | ||
| let val = ok!(self.parse_hex_byte(&mut char_iter)); | ||
| ok!(self.push_char(val as char)); | ||
| } | ||
| '0'..='7' => { | ||
| let val = ok!(self.parse_octal_byte(d, &mut char_iter)); | ||
| ok!(self.push_char(val as char)); | ||
| } | ||
| _ => return Err(ErrorKind::BadEscape.into()), | ||
| }, | ||
| } | ||
|
|
@@ -297,6 +305,33 @@ impl Unescaper { | |
| u16::from_str_radix(&hexnum, 16).map_err(|_| ErrorKind::BadEscape.into()) | ||
| } | ||
|
|
||
| fn parse_hex_byte(&self, chars: &mut Chars) -> Result<u8, Error> { | ||
| let hexnum = chars.chain(repeat('\0')).take(2).collect::<String>(); | ||
|
||
| u8::from_str_radix(&hexnum, 16).map_err(|_| ErrorKind::BadEscape.into()) | ||
| } | ||
|
|
||
| fn parse_octal_byte(&self, first_digit: char, chars: &mut Chars) -> Result<u8, Error> { | ||
| let mut octal_str = String::new(); | ||
| octal_str.push(first_digit); | ||
|
|
||
| // Collect up to 2 more octal digits (0-7) | ||
| for _ in 0..2 { | ||
| let next_char = chars.as_str().chars().next(); | ||
| if let Some(c) = next_char { | ||
| if ('0'..='7').contains(&c) { | ||
| octal_str.push(c); | ||
| chars.next(); // consume the character | ||
| } else { | ||
| break; | ||
| } | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| u8::from_str_radix(&octal_str, 8).map_err(|_| ErrorKind::BadEscape.into()) | ||
| } | ||
|
|
||
| fn push_u16(&mut self, c: u16) -> Result<(), Error> { | ||
| match (self.pending_surrogate, (0xD800..=0xDFFF).contains(&c)) { | ||
| (0, false) => match decode_utf16(once(c)).next() { | ||
|
|
@@ -450,6 +485,26 @@ mod tests { | |
| assert_eq!(unescape(r"\t\b\f\r\n\\\/").unwrap(), "\t\x08\x0c\r\n\\/"); | ||
| assert_eq!(unescape("foobarbaz").unwrap(), "foobarbaz"); | ||
| assert_eq!(unescape(r"\ud83d\udca9").unwrap(), "💩"); | ||
|
|
||
| // Test new escape sequences | ||
| assert_eq!(unescape(r"\0").unwrap(), "\0"); | ||
| assert_eq!(unescape(r"foo\0bar").unwrap(), "foo\0bar"); | ||
| assert_eq!(unescape(r"\x00").unwrap(), "\0"); | ||
| assert_eq!(unescape(r"\x42").unwrap(), "B"); | ||
| assert_eq!(unescape(r"\xab").unwrap(), "\u{ab}"); | ||
| assert_eq!(unescape(r"foo\x42bar").unwrap(), "fooBbar"); | ||
| assert_eq!(unescape(r"\x0a").unwrap(), "\n"); | ||
| assert_eq!(unescape(r"\x0d").unwrap(), "\r"); | ||
|
|
||
| // Test octal escape sequences | ||
| assert_eq!(unescape(r"\0").unwrap(), "\0"); // octal 0 = null | ||
| assert_eq!(unescape(r"\1").unwrap(), "\x01"); // octal 1 = SOH | ||
| assert_eq!(unescape(r"\12").unwrap(), "\n"); // octal 12 = 10 decimal = LF | ||
| assert_eq!(unescape(r"\123").unwrap(), "S"); // octal 123 = 83 decimal = 'S' | ||
| assert_eq!(unescape(r"\141").unwrap(), "a"); // octal 141 = 97 decimal = 'a' | ||
| assert_eq!(unescape(r"\177").unwrap(), "\x7f"); // octal 177 = 127 decimal = DEL | ||
| assert_eq!(unescape(r"foo\123bar").unwrap(), "fooSbar"); // 'S' in the middle | ||
| assert_eq!(unescape(r"\101\102\103").unwrap(), "ABC"); // octal for A, B, C | ||
| } | ||
|
|
||
| #[test] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We really don't need this example here.