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}}