diff --git a/README.md b/README.md
index 999d940..135ba3a 100644
--- a/README.md
+++ b/README.md
@@ -246,6 +246,29 @@ The value of the `loop` attribute is not a pure expressions evaluation, and it d
So you don't need to declare all the available variables (in this case, the index is skipped), and the expressions after `in` doesn't need to be a local variable, it can be any expressions.
+#### Loop meta
+
+Inside a loop, you have access to a special `loop` object, which contains information about the loop currently being executed:
+
+- `loop.index` - the current iteration of the loop (0 indexed)
+- `loop.remaining` - number of iterations until the end (0 indexed)
+- `loop.first` - boolean indicating if it's the first iteration
+- `loop.last` - boolean indicating if it's the last iteration
+- `loop.length` - total number of items
+
+Example:
+
+```html
+
+ Item value: {{ item }}
+ Current iteration of the loop: {{ loop.index }}
+ Number of iterations until the end: {{ loop.remaining }}
+ This {{ loop.first ? 'is' : 'is not' }} the first iteration
+ This {{ loop.last ? 'is' : 'is not' }} the last iteration
+ Total number of items: {{ loop.length }}
+
+```
+
### Scopes
You can replace locals inside certain area wrapped in a `` tag. For example you can use it after [posthtml-include](https://github.com/posthtml/posthtml-include)
diff --git a/lib/index.js b/lib/index.js
index 15c4eb7..ca42e15 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -55,6 +55,29 @@ function executeScope (scope, locals, node) {
return walk({ locals: scope }, node.content)
}
+/**
+ * @description Returns an object containing loop metadata
+ *
+ * @method getLoopMeta
+ *
+ * @param {Integer|Object} index Current iteration
+ * @param {Object} target Object being iterated
+ *
+ * @return {Object} Object containing loop metadata
+ */
+function getLoopMeta (index, target) {
+ index = Array.isArray(target) ? index : Object.keys(target).indexOf(index)
+ const arr = Array.isArray(target) ? target : Object.keys(target)
+
+ return {
+ index: index,
+ remaining: arr.length - index - 1,
+ first: arr.indexOf(arr[index]) === 0,
+ last: index + 1 == arr.length,
+ length: arr.length
+ }
+}
+
/**
* @author Jeff Escalante Denis (@jescalan),
* Denis Malinochkin (mrmlnc),
@@ -324,10 +347,12 @@ function walk (opts, nodes) {
// run the loop, different types of loops for arrays and objects
if (Array.isArray(target)) {
for (let index = 0; index < target.length; index++) {
+ opts.locals.loop = getLoopMeta(index, target)
m.push(executeLoop(keys, target[index], index, opts.locals, treeString))
}
} else {
for (let key in target) {
+ opts.locals.loop = getLoopMeta(key, target)
m.push(executeLoop(keys, target[key], key, opts.locals, treeString))
}
}
diff --git a/test/expect/loop_metadata.html b/test/expect/loop_metadata.html
new file mode 100644
index 0000000..7bcea1d
--- /dev/null
+++ b/test/expect/loop_metadata.html
@@ -0,0 +1,24 @@
+
+
+ - Item: 1
+ - Current iteration of the loop: 0
+ - Number of iterations until the end: 2
+ - This is the first iteration
+ - This is not the last iteration
+ - Total number of items: 3
+
+ - Item: 2
+ - Current iteration of the loop: 1
+ - Number of iterations until the end: 1
+ - This is not the first iteration
+ - This is not the last iteration
+ - Total number of items: 3
+
+ - Item: 3
+ - Current iteration of the loop: 2
+ - Number of iterations until the end: 0
+ - This is not the first iteration
+ - This is the last iteration
+ - Total number of items: 3
+
+
diff --git a/test/expect/loop_nested_metadata.html b/test/expect/loop_nested_metadata.html
new file mode 100644
index 0000000..025c2e8
--- /dev/null
+++ b/test/expect/loop_nested_metadata.html
@@ -0,0 +1,47 @@
+
+
+ - foo: [1,2]
+ - There are 1 iterations remaining on the foo object
+
+
+ - Nested item value: 1
+ - Current iteration of the loop: 0
+ - Number of iterations until the end: 1
+ - This is the first iteration
+ - This is not the last iteration
+ - Total number of items: 2
+
+
+
+ - Nested item value: 2
+ - Current iteration of the loop: 1
+ - Number of iterations until the end: 0
+ - This is not the first iteration
+ - This is the last iteration
+ - Total number of items: 2
+
+
+
+ - bar: [3,4]
+ - There are 0 iterations remaining on the bar object
+
+
+ - Nested item value: 3
+ - Current iteration of the loop: 0
+ - Number of iterations until the end: 1
+ - This is the first iteration
+ - This is not the last iteration
+ - Total number of items: 2
+
+
+
+ - Nested item value: 4
+ - Current iteration of the loop: 1
+ - Number of iterations until the end: 0
+ - This is not the first iteration
+ - This is the last iteration
+ - Total number of items: 2
+
+
+
+
diff --git a/test/fixtures/loop_metadata.html b/test/fixtures/loop_metadata.html
new file mode 100644
index 0000000..0ed8021
--- /dev/null
+++ b/test/fixtures/loop_metadata.html
@@ -0,0 +1,10 @@
+
+
+ - Item: {{ item }}
+ - Current iteration of the loop: {{ loop.index }}
+ - Number of iterations until the end: {{ loop.remaining }}
+ - This {{ loop.first ? 'is' : 'is not' }} the first iteration
+ - This {{ loop.last ? 'is' : 'is not' }} the last iteration
+ - Total number of items: {{ loop.length }}
+
+
diff --git a/test/fixtures/loop_nested_metadata.html b/test/fixtures/loop_nested_metadata.html
new file mode 100644
index 0000000..c4be33b
--- /dev/null
+++ b/test/fixtures/loop_nested_metadata.html
@@ -0,0 +1,16 @@
+
+
+ - {{ key }}: {{ JSON.stringify(value) }}
+ - There are {{ loop.remaining }} iterations remaining on the {{ key }} object
+
+
+ - Nested item value: {{ item }}
+ - Current iteration of the loop: {{ loop.index }}
+ - Number of iterations until the end: {{ loop.remaining }}
+ - This {{ loop.first ? 'is' : 'is not' }} the first iteration
+ - This {{ loop.last ? 'is' : 'is not' }} the last iteration
+ - Total number of items: {{ loop.length }}
+
+
+
+
diff --git a/test/test-loops.js b/test/test-loops.js
index 22c8b19..952ca0c 100644
--- a/test/test-loops.js
+++ b/test/test-loops.js
@@ -120,3 +120,15 @@ test('Loops - expression error', (t) => {
t.is(err.message, 'Invalid or unexpected token')
})
})
+
+test('Loops - metadata', (t) => {
+ return process(t, 'loop_metadata', {
+ locals: { items: [1, 2, 3] }
+ })
+})
+
+test('Loops - nested metadata', (t) => {
+ return process(t, 'loop_nested_metadata', {
+ locals: { items: { foo: [1, 2], bar: [3, 4] } }
+ })
+})