Repository: djc/askama
Pull request #189 (2019-01-05 11:35:08)

Add support for loop.last
I managed to support for loop.last.
If necessary, please move ::askama::helpers::enumerate to appropriate position.


Patch:
diff --git a/askama/src/lib.rs b/askama/src/lib.rs
index d1bb639..725c3de 100644
--- a/askama/src/lib.rs
+++ b/askama/src/lib.rs
@@ -433,6 +433,7 @@ pub trait Template {
 }
 
 pub use crate::shared::filters;
+pub use crate::shared::helpers;
 pub use crate::shared::{read_config_file, Error, MarkupDisplay, Result};
 pub use askama_derive::*;
 
diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs
index 67ec2c2..288ddae 100644
--- a/askama_derive/src/generator.rs
+++ b/askama_derive/src/generator.rs
@@ -467,11 +467,17 @@ impl<'a> Generator<'a> {
         let expr_code = self.visit_expr_root(iter);
 
         self.write_buf_writable(buf);
-        buf.write("for (_loop_index, ");
+        buf.write("for (");
         self.visit_target(buf, var);
         match iter {
-            Expr::Range(_, _, _) => buf.writeln(&format!(") in ({}).enumerate() {{", expr_code)),
-            _ => buf.writeln(&format!(") in (&{}).into_iter().enumerate() {{", expr_code)),
+            Expr::Range(_, _, _) => buf.writeln(&format!(
+                ", _loop_item) in ::askama::helpers::TemplateLoop::new({}) {{",
+                expr_code
+            )),
+            _ => buf.writeln(&format!(
+                ", _loop_item) in ::askama::helpers::TemplateLoop::new((&{}).into_iter()) {{",
+                expr_code
+            )),
         };
 
         self.handle(ctx, body, buf, AstLevel::Nested);
@@ -907,13 +913,16 @@ impl<'a> Generator<'a> {
         if let Expr::Var(name) = *obj {
             if name == "loop" {
                 if attr == "index" {
-                    buf.write("(_loop_index + 1)");
+                    buf.write("(_loop_item.index + 1)");
                     return DisplayWrap::Unwrapped;
                 } else if attr == "index0" {
-                    buf.write("_loop_index");
+                    buf.write("_loop_item.index");
                     return DisplayWrap::Unwrapped;
                 } else if attr == "first" {
-                    buf.write("(_loop_index == 0)");
+                    buf.write("_loop_item.first");
+                    return DisplayWrap::Unwrapped;
+                } else if attr == "last" {
+                    buf.write("_loop_item.last");
                     return DisplayWrap::Unwrapped;
                 } else {
                     panic!("unknown loop variable");
diff --git a/askama_derive/src/parser.rs b/askama_derive/src/parser.rs
index c5e610e..c4cb52d 100644
--- a/askama_derive/src/parser.rs
+++ b/askama_derive/src/parser.rs
@@ -178,14 +178,14 @@ fn take_content<'a>(
 }
 
 fn identifier(input: Input) -> Result<(Input, &str), nom::Err<Input>> {
-    if !nom::is_alphabetic(input[0]) && input[0] != b'_' {
+    if !nom::is_alphabetic(input[0]) && input[0] != b'_' && !non_ascii(input[0]) {
         return Err(nom::Err::Error(error_position!(
             input,
             nom::ErrorKind::Custom(0)
         )));
     }
     for (i, ch) in input.iter().enumerate() {
-        if i == 0 || nom::is_alphanumeric(*ch) || *ch == b'_' {
+        if i == 0 || nom::is_alphanumeric(*ch) || *ch == b'_' || non_ascii(*ch) {
             continue;
         }
         return Ok((Input(&input[i..]), str::from_utf8(&input[..i]).unwrap()));
@@ -193,6 +193,11 @@ fn identifier(input: Input) -> Result<(Input, &str), nom::Err<Input>> {
     Ok((Input(&input[1..]), str::from_utf8(&input[..1]).unwrap()))
 }
 
+#[inline]
+fn non_ascii(chr: u8) -> bool {
+    chr >= 0x80 && chr <= 0xFD
+}
+
 named!(num_lit<Input, &str>, map!(nom::digit,
     |s| str::from_utf8(s.0).unwrap()
 ));
@@ -291,6 +296,7 @@ named!(target_tuple<Input, Target>, do_parse!(
            res
         })
     )) >>
+    opt!(ws!(tag!(","))) >>
     tag!(")") >>
     (Target::Tuple(args.unwrap_or_default()))
 ));
diff --git a/askama_escape/src/lib.rs b/askama_escape/src/lib.rs
index 8c23530..a591a9c 100644
--- a/askama_escape/src/lib.rs
+++ b/askama_escape/src/lib.rs
@@ -1,4 +1,5 @@
 use std::fmt::{self, Display, Formatter};
+use std::io::{self, prelude::*};
 use std::str;
 
 #[derive(Debug, PartialEq)]
@@ -37,12 +38,33 @@ where
 {
     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
         match *self {
-            MarkupDisplay::Unsafe(ref t) => escape(&t.to_string()).fmt(f),
+            MarkupDisplay::Unsafe(ref t) => {
+                let mut w = EscapeWriter { fmt: f };
+                write!(w, "{}", t).map_err(|_e| fmt::Error)
+            }
             MarkupDisplay::Safe(ref t) => t.fmt(f),
         }
     }
 }
 
+pub struct EscapeWriter<'a, 'b: 'a> {
+    fmt: &'a mut fmt::Formatter<'b>,
+}
+
+impl io::Write for EscapeWriter<'_, '_> {
+    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
+        let escaped = Escaped { bytes };
+        escaped
+            .fmt(self.fmt)
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
+        Ok(bytes.len())
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        Ok(())
+    }
+}
+
 pub fn escape(s: &str) -> Escaped<'_> {
     Escaped {
         bytes: s.as_bytes(),
diff --git a/askama_shared/src/helpers/mod.rs b/askama_shared/src/helpers/mod.rs
new file mode 100644
index 0000000..87316fd
--- /dev/null
+++ b/askama_shared/src/helpers/mod.rs
@@ -0,0 +1,49 @@
+use std::iter::Enumerate;
+use std::iter::Peekable;
+
+pub struct TemplateLoop<I>
+where
+    I: Iterator,
+{
+    iter: Peekable<Enumerate<I>>,
+}
+
+impl<I> TemplateLoop<I>
+where
+    I: Iterator,
+{
+    #[inline]
+    pub fn new(iter: I) -> Self {
+        TemplateLoop {
+            iter: iter.enumerate().peekable(),
+        }
+    }
+}
+
+impl<I> Iterator for TemplateLoop<I>
+where
+    I: Iterator,
+{
+    type Item = (<I as Iterator>::Item, LoopItem);
+
+    #[inline]
+    fn next(&mut self) -> Option<(<I as Iterator>::Item, LoopItem)> {
+        self.iter.next().map(|(index, item)| {
+            (
+                item,
+                LoopItem {
+                    index,
+                    first: index == 0,
+                    last: self.iter.peek().is_none(),
+                },
+            )
+        })
+    }
+}
+
+#[derive(Copy, Clone)]
+pub struct LoopItem {
+    pub index: usize,
+    pub first: bool,
+    pub last: bool,
+}
diff --git a/askama_shared/src/lib.rs b/askama_shared/src/lib.rs
index 850cb8b..a0c9321 100644
--- a/askama_shared/src/lib.rs
+++ b/askama_shared/src/lib.rs
@@ -16,6 +16,7 @@ pub use askama_escape::MarkupDisplay;
 use std::collections::BTreeMap;
 
 pub mod filters;
+pub mod helpers;
 
 #[derive(Debug)]
 pub struct Config<'a> {


Test patch:
diff --git a/testing/templates/for-range.html b/testing/templates/for-range.html
index 9b109bb..3688478 100644
--- a/testing/templates/for-range.html
+++ b/testing/templates/for-range.html
@@ -1,5 +1,5 @@
 {% for s in 0..2 -%}
-    foo
+    foo{% if loop.first %} (first){% endif %}{% if loop.last %} (last){% endif %}
 {% endfor -%}
 
 {% for s in init..1 -%}
diff --git a/testing/templates/for.html b/testing/templates/for.html
index f749752..3d4533b 100644
--- a/testing/templates/for.html
+++ b/testing/templates/for.html
@@ -1,6 +1,6 @@
 {% for s in strings %}
   {{- loop.index0 }}. {{ s }}{% if loop.first %} (first){% endif %}
 {% endfor %}
-{% for (s1, s2) in tuple_strings %}
+{% for (s1, s2, ) in tuple_strings %}
   {{- loop.index0 }}. {{ s1 }},{{ s2 }}{% if loop.first %} (first){% endif %}
 {% endfor %}
diff --git a/testing/templates/precedence-for.html b/testing/templates/precedence-for.html
index c8d2cdd..57c900b 100644
--- a/testing/templates/precedence-for.html
+++ b/testing/templates/precedence-for.html
@@ -1,3 +1,3 @@
 {% for s in strings %}
-    {{- loop.index0 }}. {{ s }}{{ 2 * loop.index }}{% if !loop.first %}{% else %} (first){% endif %}
+    {{- loop.index0 }}. {{ s }}{{ 2 * loop.index }}{% if !loop.first %}{% else %} (first){% endif %}{% if loop.last %} (last){% endif %}
 {% endfor %}
diff --git a/testing/tests/loops.rs b/testing/tests/loops.rs
index b5fac7c..3ce6185 100644
--- a/testing/tests/loops.rs
+++ b/testing/tests/loops.rs
@@ -46,7 +46,10 @@ fn test_precedence_for() {
     let s = PrecedenceTemplate {
         strings: vec!["A", "alfa", "1"],
     };
-    assert_eq!(s.render().unwrap(), "0. A2 (first)\n1. alfa4\n2. 16\n");
+    assert_eq!(
+        s.render().unwrap(),
+        "0. A2 (first)\n1. alfa4\n2. 16 (last)\n"
+    );
 }
 
 #[derive(Template)]
@@ -59,5 +62,8 @@ struct ForRangeTemplate {
 #[test]
 fn test_for_range() {
     let s = ForRangeTemplate { init: -1, end: 1 };
-    assert_eq!(s.render().unwrap(), "foo\nfoo\nbar\nbar\nfoo\nbar\nbar\n");
+    assert_eq!(
+        s.render().unwrap(),
+        "foo (first)\nfoo (last)\nbar\nbar\nfoo\nbar\nbar\n"
+    );
 }