From 6376a9b817a519d6d9e119c4dd3e2f7e791685c5 Mon Sep 17 00:00:00 2001
From: Tyler Akins <fidian@rumkin.com>
Date: Tue, 20 Jun 2017 14:19:44 -0500
Subject: [PATCH] Fixing indentation of multiple partials

This fixes #16.
---
 API.md                           | 40 ++++++++++++++++++++++------
 mo                               | 45 +++++++++++++++++++++++++-------
 tests/indented-partials.env      |  0
 tests/indented-partials.expected | 19 ++++++++++++++
 tests/indented-partials.partial  |  2 ++
 tests/indented-partials.template | 13 +++++++++
 6 files changed, 101 insertions(+), 18 deletions(-)
 create mode 100644 tests/indented-partials.env
 create mode 100644 tests/indented-partials.expected
 create mode 100644 tests/indented-partials.partial
 create mode 100644 tests/indented-partials.template

diff --git a/API.md b/API.md
index 2084dec..5d6ef61 100644
--- a/API.md
+++ b/API.md
@@ -9,15 +9,17 @@ mo()
 
 Public: Template parser function.  Writes templates to stdout.
 
-* $0            - Name of the mo file, used for getting the help message.
-* --false       - Treat "false" as an empty value.  You may set the MO_FALSE_IS_EMPTY environment variable instead to a non-empty value to enable this behavior.
-* --help        - Display a help message.
-* --source=FILE - Source a file into the environment before processint template files.
-* --            - Used to indicate the end of options.  You may optionally use this when filenames may start with two hyphens.
-* $@            - Filenames to parse.
+* $0             - Name of the mo file, used for getting the help message.
+* --fail-not-set - Fail upon expansion of an unset variable.  Default behavior is to silently ignore and expand into empty string.
+* --false        - Treat "false" as an empty value.  You may set the MO_FALSE_IS_EMPTY environment variable instead to a non-empty value to enable this behavior.
+* --help         - Display a help message.
+* --source=FILE  - Source a file into the environment before processint template files.
+* --             - Used to indicate the end of options.  You may optionally use this when filenames may start with two hyphens.
+* $@             - Filenames to parse.
 
 Mo uses the following environment variables:
 
+* MO_FAIL_ON_UNSET    - When set to a non-empty value, expansion of an unset env variable will be aborted with an error.
 * MO_FALSE_IS_EMPTY   - When set to a non-empty value, the string "false" will be treated as an empty value for the purposes of conditionals.
 * MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate a help message.
 
@@ -30,6 +32,12 @@ files
 After we encounter two hyphens together, all the rest of the arguments are files.
 
 
+MO_FAIL_ON_UNSET
+----------------
+
+shellcheck disable=SC2030
+
+
 MO_FALSE_IS_EMPTY
 -----------------
 
@@ -160,6 +168,8 @@ Internal: Determine if a given environment variable exists and if it is an array
 
 * $1 - Name of environment variable
 
+Be extremely careful.  Even if strict mode is enabled, it is not honored in newer versions of Bash.  Any errors that crop up here will not be caught automatically.
+
 Examples
 
     var=(abc)
@@ -178,6 +188,8 @@ Internal: Determine if the given name is a defined function.
 
 * $1 - Function name to check
 
+Be extremely careful.  Even if strict mode is enabled, it is not honored in newer versions of Bash.  Any errors that crop up here will not be caught automatically.
+
 Examples
 
     moo () {
@@ -263,11 +275,13 @@ moPartial()
 
 Internal: Process a partial.
 
-Indentation should be applied to the entire partial
+Indentation should be applied to the entire partial.
+
+This sends back the "is beginning" flag because the newline after a standalone partial is consumed. That newline is very important in the middle of content. We send back this flag to reset the processing loop's `moIsBeginning` variable, so the software thinks we are back at the beginning of a file and standalone processing continues to work.
 
 Prefix all variables.
 
-* $1 - Name of destination "content" variable.
+* $1 - Name of destination variable. Element [0] is the content, [1] is the true/false flag indicating if we are at the beginning of content.
 * $2 - Content before the tag that was not yet written
 * $3 - Tag content
 * $4 - Content after the tag
@@ -344,6 +358,16 @@ Do not use variables without prefixes here if possible as this needs to check if
 Returns 0 if the name is not empty, 1 otherwise.  When MO_FALSE_IS_EMPTY is set, this returns 1 if the name is "false".
 
 
+moTestVarSet()
+--------------
+
+Internal: Determine if a variable is assigned, even if it is assigned an empty value.
+
+* $1 - Variable name to check.
+
+Returns true (0) if the variable is set, 1 if the variable is unset.
+
+
 moTrimChars()
 -------------
 
diff --git a/mo b/mo
index 57f0fba..cc66240 100755
--- a/mo
+++ b/mo
@@ -464,15 +464,20 @@ moIsStandalone() {
     char=$((${#beforeTrimmed} - 1))
     char=${beforeTrimmed:$char}
 
+    # If the content before didn't end in a newline
     if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]]; then
+        # and there was content or this didn't start the file
         if [[ -n "$char" ]] || ! $4; then
+            # then this is not a standalone tag.
             return 1
         fi
     fi
 
     char=${afterTrimmed:0:1}
 
+    # If the content after doesn't start with a newline and it is something
     if [[ "$char" != $'\n' ]] && [[ "$char" != $'\r' ]] && [[ -n "$char" ]]; then
+        # then this is not a standalone tag.
         return 2
     fi
 
@@ -518,7 +523,8 @@ moLoadFile() {
 
     # The subshell removes any trailing newlines.  We forcibly add
     # a dot to the content to preserve all newlines.
-    # TODO: remove cat and replace with read loop?
+    # As a future optimization, it would be worth considering removing
+    # cat and replacing this with a read loop.
 
     content=$(cat -- "$2"; echo '.')
     len=$((${#content} - 1))
@@ -561,7 +567,7 @@ moLoop() {
 moParse() {
     # Keep naming variables mo* here to not overwrite needed variables
     # used in the string replacements
-    local moBlock moContent moCurrent moIsBeginning moTag
+    local moBlock moContent moCurrent moIsBeginning moNextIsBeginning moTag
 
     moCurrent=$2
     moIsBeginning=$3
@@ -571,6 +577,7 @@ moParse() {
 
     while [[ "${#moContent[@]}" -gt 1 ]]; do
         moTrimWhitespace moTag "${moContent[1]}"
+        moNextIsBeginning=false
 
         case $moTag in
             '#'*)
@@ -584,7 +591,7 @@ moParse() {
                 if moTest "$moTag"; then
                     # Show / loop / pass through function
                     if moIsFunction "$moTag"; then
-                        #: TODO: Consider piping the output to moGetContent
+                        #: Consider piping the output to moGetContent
                         #: so the lambda does not execute in a subshell?
                         moContent=$($moTag "${moBlock[0]}")
                         moParse "$moContent" "$moCurrent" false
@@ -602,6 +609,8 @@ moParse() {
             '>'*)
                 # Load partial - get name of file relative to cwd
                 moPartial moContent "${moContent[@]}" "$moIsBeginning" "$moCurrent"
+                moNextIsBeginning=${moContent[1]}
+                moContent=${moContent[0]}
                 ;;
 
             '/'*)
@@ -639,7 +648,7 @@ moParse() {
             '=')
                 # Change delimiters
                 # Any two non-whitespace sequences separated by whitespace.
-                # TODO
+                # This tag is ignored.
                 moStandaloneAllowed moContent "${moContent[@]}" "$moIsBeginning"
                 ;;
 
@@ -672,7 +681,7 @@ moParse() {
                 ;;
         esac
 
-        moIsBeginning=false
+        moIsBeginning=$moNextIsBeginning
         moSplit moContent "$moContent" '{{' '}}'
     done
 
@@ -682,11 +691,18 @@ moParse() {
 
 # Internal: Process a partial.
 #
-# Indentation should be applied to the entire partial
+# Indentation should be applied to the entire partial.
+#
+# This sends back the "is beginning" flag because the newline after a
+# standalone partial is consumed. That newline is very important in the middle
+# of content. We send back this flag to reset the processing loop's
+# `moIsBeginning` variable, so the software thinks we are back at the
+# beginning of a file and standalone processing continues to work.
 #
 # Prefix all variables.
 #
-# $1 - Name of destination "content" variable.
+# $1 - Name of destination variable. Element [0] is the content, [1] is the
+#      true/false flag indicating if we are at the beginning of content.
 # $2 - Content before the tag that was not yet written
 # $3 - Tag content
 # $4 - Content after the tag
@@ -696,24 +712,27 @@ moParse() {
 # Returns nothing.
 moPartial() {
     # Namespace variables here to prevent conflicts.
-    local moContent moFilename moIndent moPartial moStandalone moUnindented
+    local moContent moFilename moIndent moIsBeginning moPartial moStandalone moUnindented
 
     if moIsStandalone moStandalone "$2" "$4" "$5"; then
         moStandalone=( $moStandalone )
         echo -n "${2:0:${moStandalone[0]}}"
         moIndent=${2:${moStandalone[0]}}
         moContent=${4:${moStandalone[1]}}
+        moIsBeginning=true
     else
         moIndent=""
         echo -n "$2"
         moContent=$4
+        moIsBeginning=$5
     fi
 
     moTrimWhitespace moFilename "${3:1}"
 
     # Execute in subshell to preserve current cwd and environment
     (
-        # TODO:  Remove dirname and use a function instead
+        # It would be nice to remove `dirname` and use a function instead,
+        # but that's difficult when you're only given filenames.
         cd "$(dirname -- "$moFilename")" || exit 1
         moUnindented="$(
             moLoadFile moPartial "${moFilename##*/}"
@@ -727,7 +746,13 @@ moPartial() {
         echo -n "$moPartial"
     ) || exit 1
 
-    local "$1" && moIndirect "$1" "$moContent"
+    # If this is a standalone tag, the trailing newline after the tag is
+    # removed and the contents of the partial are added, which typically
+    # contain a newline. We need to send a signal back to the processing
+    # loop that the moIsBeginning flag needs to be turned on again.
+    #
+    # [0] is the content, [1] is that flag.
+    local "$1" && moIndirectArray "$1" "$moContent" "$moIsBeginning"
 }
 
 
diff --git a/tests/indented-partials.env b/tests/indented-partials.env
new file mode 100644
index 0000000..e69de29
diff --git a/tests/indented-partials.expected b/tests/indented-partials.expected
new file mode 100644
index 0000000..73888d4
--- /dev/null
+++ b/tests/indented-partials.expected
@@ -0,0 +1,19 @@
+With spacing
+    first line
+    second line
+
+    first line
+    second line
+
+Without spacing
+    first line
+    second line
+    first line
+    second line
+
+With text
+    first line
+    second line
+    text
+    first line
+    second line
diff --git a/tests/indented-partials.partial b/tests/indented-partials.partial
new file mode 100644
index 0000000..06fcdd7
--- /dev/null
+++ b/tests/indented-partials.partial
@@ -0,0 +1,2 @@
+first line
+second line
diff --git a/tests/indented-partials.template b/tests/indented-partials.template
new file mode 100644
index 0000000..8cc08f0
--- /dev/null
+++ b/tests/indented-partials.template
@@ -0,0 +1,13 @@
+With spacing
+    {{> indented-partials.partial}}
+
+    {{> indented-partials.partial}}
+
+Without spacing
+    {{> indented-partials.partial}}
+    {{> indented-partials.partial}}
+
+With text
+    {{> indented-partials.partial}}
+    text
+    {{> indented-partials.partial}}