From 1d4e18648619dc95ccb444691862fc54ab484258 Mon Sep 17 00:00:00 2001 From: Tyler Akins Date: Sun, 23 Apr 2023 09:24:12 -0500 Subject: [PATCH] All tests pass. Specs: 181 total, 111 pass (with 7 overridden), 63 fail, 7 skip. --- mo | 126 +++++++++++++++++--------- tests/function | 6 +- tests/function-alternate-delimiters | 16 ++++ tests/function-args | 2 +- tests/function-with-more-parsing | 28 ++++++ tests/help | 1 + tests/variable-braces-and-parenthesis | 53 +++++++++++ 7 files changed, 188 insertions(+), 44 deletions(-) create mode 100755 tests/function-alternate-delimiters create mode 100755 tests/function-with-more-parsing create mode 100755 tests/variable-braces-and-parenthesis diff --git a/mo b/mo index 49068d7..a79270d 100755 --- a/mo +++ b/mo @@ -514,19 +514,19 @@ mo::parseBlock() { MO_UNPARSED=${MO_UNPARSED:1} mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"} - mo::tokensToString moTokensString "${moTokens[@]}" + mo::tokensToString moTokensString "${moTokens[@]:1}" mo::debug "Parsing block: $moTokensString" if mo::standaloneCheck "$MO_STANDALONE_CONTENT"; then mo::standaloneProcess fi - if [[ "${moTokens[0]}" == "NAME" ]] && mo::isFunction "${moTokens[1]}"; then - mo::parseBlockFunction "$moInvertBlock" "$moTokensString" "${moTokens[@]}" - elif [[ "${moTokens[0]}" == "NAME" ]] && mo::isArray "${moTokens[1]}"; then - mo::parseBlockArray "$moInvertBlock" "$moTokensString" "${moTokens[@]}" + if [[ "${moTokens[1]}" == "NAME" ]] && mo::isFunction "${moTokens[2]}"; then + mo::parseBlockFunction "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}" + elif [[ "${moTokens[1]}" == "NAME" ]] && mo::isArray "${moTokens[2]}"; then + mo::parseBlockArray "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}" else - mo::parseBlockValue "$moInvertBlock" "$moTokensString" "${moTokens[@]}" + mo::parseBlockValue "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}" fi } @@ -549,8 +549,9 @@ mo::parseBlockFunction() { # Pass unparsed content to the function. # Keep the updated delimiters if they changed. - if [[ "$moInvertBlock" == "true" ]]; then + if [[ "$moInvertBlock" != "true" ]]; then mo::evaluateFunction moResult "$moTemp" "${moTokens[@]:1}" + MO_PARSED="$MO_PARSED$moResult" fi mo::debug "Done parsing block function: $moTokensString" @@ -564,7 +565,7 @@ mo::parseBlockFunction() { # # Returns nothing mo::parseBlockArray() { - local moInvertBlock moTokens moResult moArrayName moArrayIndexes moArrayIndex moTemp moUnparsed moOpenDelimiterBefore moCloseDelimiterBefore moOpenDelimiterAfter moCloseDelimiterAfter moParsed moTokensString + local moInvertBlock moTokens moResult moArrayName moArrayIndexes moArrayIndex moTemp moUnparsed moOpenDelimiterBefore moCloseDelimiterBefore moOpenDelimiterAfter moCloseDelimiterAfter moParsed moTokensString moCurrent moInvertBlock=$1 moTokensString=$2 @@ -585,7 +586,10 @@ mo::parseBlockArray() { # Restore the delimiter before parsing MO_OPEN_DELIMITER=$moOpenDelimiterBefore MO_CLOSE_DELIMITER=$moCloseDelimiterBefore + moCurrent=$MO_CURRENT + MO_CURRENT=$moArrayName mo::parse moParsed "$moTemp" + MO_CURRENT=$moCurrent MO_PARSED="$MO_PARSED$moParsed" fi else @@ -597,8 +601,11 @@ mo::parseBlockArray() { # Restore the delimiter before parsing MO_OPEN_DELIMITER=$moOpenDelimiterBefore MO_CLOSE_DELIMITER=$moCloseDelimiterBefore - mo::debug "Iterate over array using element: $moArrayName.$moArrayIndex" + moCurrent=$MO_CURRENT + MO_CURRENT=$moArrayName.$moArrayIndex + mo::debug "Iterate over array using element: $MO_CURRENT" mo::parse moParsed "$moTemp" "$moArrayName" + MO_CURRENT=$moCurrent MO_PARSED="$MO_PARSED$moParsed" done @@ -694,7 +701,7 @@ mo::parsePartial() { exit 1 fi - mo::indentLines "$moIndentation" + mo::indentLines moPartialContent "$moIndentation" "$moPartialContent" # Delimiters are reset when loading a new partial MO_OPEN_DELIMITER="{{" @@ -765,7 +772,7 @@ mo::parseValue() { moUnparsedOriginal=$MO_UNPARSED mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" - mo::evaluate moResult "${moTokens[@]}" + mo::evaluate moResult "${moTokens[@]:1}" MO_PARSED="$MO_PARSED$moResult" if [[ "${MO_UNPARSED:0:${#MO_CLOSE_DELIMITER}}" != "$MO_CLOSE_DELIMITER" ]]; then @@ -967,9 +974,8 @@ mo::evaluate() { if [[ "${moStack[0]:-}" == "NAME" ]] && mo::isFunction "${moStack[1]}"; then # Special case - if the first argument is a function, then the rest are # passed to the function. - moFunction=$2 - mo::debug "Evaluating function: $moFunction" - mo::evaluateFunction moResult "" "${moStack[@]:2}" + mo::debug "Evaluating function: ${moStack[1]}" + mo::evaluateFunction moResult "" "${moStack[@]:1}" else # Concatenate mo::debug "Concatenating ${#moStack[@]} stack items" @@ -1323,22 +1329,30 @@ mo::standaloneProcess() { # Internal: Apply indentation before any line that has content in MO_UNPARSED. # -# $1 - The indentation string +# $1 - Destination variable name. +# $2 - The indentation string. +# $3 - The content that needs the indentation string prepended on each line. # # Returns nothing. mo::indentLines() { local moContent moIndentation moResult moN moR moChunk - moIndentation=$1 + moIndentation=$2 + moContent=$3 - if [[ -z "$moIndentation" ]] || [[ -z "$MO_UNPARSED" ]]; then - mo::debug "Not applying indentation, indentation ${#moIndentation} bytes, content ${#MO_UNPARSED} bytes" + if [[ -z "$moIndentation" ]]; then + mo::debug "Not applying indentation, empty indentation" - return + local "$1" && mo::indirect "$1" "$moContent" fi - moContent=$MO_UNPARSED - MO_UNPARSED= + if [[ -z "$moContent" ]]; then + mo::debug "Not applying indentation, empty contents" + + local "$1" && mo::indirect "$1" "$moContent" + fi + + moResult= moN=$'\n' moR=$'\r' @@ -1350,12 +1364,14 @@ mo::indentLines() { moContent=${moContent:${#moChunk}} if [[ -n "$moChunk" ]]; then - MO_UNPARSED="$MO_UNPARSED$moIndentation$moChunk" + moResult="$moResult$moIndentation$moChunk" fi - MO_UNPARSED="$MO_UNPARSED${moContent:0:1}" + moResult="$moResult${moContent:0:1}" moContent=${moContent:1} done + + local "$1" && mo::indirect "$1" "$moResult" } @@ -1608,7 +1624,7 @@ mo::getContentWithinTag() { moUnparsed=${MO_UNPARSED} mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"} - mo::tokensToString moTokensString "${moTokens[@]}" + mo::tokensToString moTokensString "${moTokens[@]:1}" moParsed=${moUnparsed:0:$((${#moUnparsed} - ${#MO_UNPARSED}))} local "$1" && mo::indirectArray "$1" "$moParsed" "$moTokensString" @@ -1621,20 +1637,22 @@ mo::getContentWithinTag() { # $1 - Destination variable for the array of contents. # $2 - Stop processing when this content is found. # -# The list of tokens are in RPN form +# The list of tokens are in RPN form. The first item in the resulting array is +# the number of actual tokens (after combining command tokens) in the list. # # Given: a 'bc' "de\"\n" (f {g 'h'}) -# Result: ([0]=NAME [1]=a [2]=VALUE [3]=bc [4]=VALUE [5]=$'de\"\n' -# [6]=NAME [7]=f [8]=NAME [9]=g [10]=VALUE [11]=h -# [12]=BRACE [13]=2 [14]=PAREN [15]=2 +# Result: ([0]=4 [1]=NAME [2]=a [3]=VALUE [4]=bc [5]=VALUE [6]=$'de\"\n' +# [7]=NAME [8]=f [9]=NAME [10]=g [11]=VALUE [12]=h +# [13]=BRACE [14]=2 [15]=PAREN [16]=2 # # Returns nothing mo::tokenizeTagContents() { - local moResult moTerminator moTemp moUnparsedOriginal + local moResult moTerminator moTemp moUnparsedOriginal moTokenCount moTerminator=$2 moResult=() moUnparsedOriginal=$MO_UNPARSED + moTokenCount=0 mo::debug "Tokenizing tag contents until terminator: $moTerminator" while true; do @@ -1647,7 +1665,7 @@ mo::tokenizeTagContents() { "$moTerminator"*) mo::debug "Found terminator" - local "$1" && mo::indirectArray "$1" "${moResult[@]}" + local "$1" && mo::indirectArray "$1" "$moTokenCount" "${moResult[@]}" return ;; @@ -1655,7 +1673,7 @@ mo::tokenizeTagContents() { # Do not tokenize the open paren - treat this as RPL MO_UNPARSED=${MO_UNPARSED:1} mo::tokenizeTagContents moTemp ')' - moResult=("${moResult[@]}" "${moTemp[@]}" PAREN "$((${#moTemp[@]} / 2))") + moResult=("${moResult[@]}" "${moTemp[@]:1}" PAREN "${moTemp[0]}") MO_UNPARSED=${MO_UNPARSED:1} ;; @@ -1663,7 +1681,7 @@ mo::tokenizeTagContents() { # Do not tokenize the open brace - treat this as RPL MO_UNPARSED=${MO_UNPARSED:1} mo::tokenizeTagContents moTemp '}' - moResult=("${moResult[@]}" "${moTemp[@]}" BRACE "$((${#moTemp[@]} / 2))") + moResult=("${moResult[@]}" "${moTemp[@]:1}" BRACE "${moTemp[0]}") MO_UNPARSED=${MO_UNPARSED:1} ;; @@ -1688,6 +1706,7 @@ mo::tokenizeTagContents() { esac mo::debug "Got chunk: ${moTemp[0]} ${moTemp[1]}" + moTokenCount=$((moTokenCount + 1)) done } @@ -1701,10 +1720,10 @@ mo::tokenizeTagContentsName() { local moTemp mo::chomp moTemp "${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*}" - moTemp=${moTemp##(*} - moTemp=${moTemp##)*} - moTemp=${moTemp##\{*} - moTemp=${moTemp##\}*} + moTemp=${moTemp%%(*} + moTemp=${moTemp%%)*} + moTemp=${moTemp%%\{*} + moTemp=${moTemp%%\}*} MO_UNPARSED=${MO_UNPARSED:${#moTemp}} mo::trimUnparsed mo::debug "Parsed default token: $moTemp" @@ -1724,6 +1743,7 @@ mo::tokenizeTagContentsDoubleQuote() { moUnparsedOriginal=$MO_UNPARSED MO_UNPARSED=${MO_UNPARSED:1} + moResult= mo::debug "Getting double quoted tag contents" while true; do @@ -1734,32 +1754,53 @@ mo::tokenizeTagContentsDoubleQuote() { case "$MO_UNPARSED" in '"'*) MO_UNPARSED=${MO_UNPARSED:1} - local "$1" && mo::indirect "$1" "VALUE" "$moResult" + local "$1" && mo::indirectArray "$1" "VALUE" "$moResult" return ;; - \\n) + \\b*) + moResult="$moResult"$'\b' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\e*) + # Note, \e is ESC, but in Bash $'\E' is ESC. + moResult="$moResult"$'\E' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\f*) + moResult="$moResult"$'\f' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\n*) moResult="$moResult"$'\n' MO_UNPARSED=${MO_UNPARSED:2} ;; - \\r) + \\r*) moResult="$moResult"$'\r' MO_UNPARSED=${MO_UNPARSED:2} ;; - \\t) + \\t*) moResult="$moResult"$'\t' MO_UNPARSED=${MO_UNPARSED:2} ;; + \\v*) + moResult="$moResult"$'\v' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + \\*) moResult="$moResult${MO_UNPARSED:1:1}" MO_UNPARSED=${MO_UNPARSED:2} ;; *) - moResult=${MO_UNPARSED:0:1} + moResult="$moResult${MO_UNPARSED:0:1}" MO_UNPARSED=${MO_UNPARSED:1} ;; esac @@ -1778,6 +1819,7 @@ mo::tokenizeTagContentsSingleQuote() { moUnparsedOriginal=$MO_UNPARSED MO_UNPARSED=${MO_UNPARSED:1} + moResult= mo::debug "Getting single quoted tag contents" while true; do @@ -1793,7 +1835,7 @@ mo::tokenizeTagContentsSingleQuote() { ;; *) - moResult=${MO_UNPARSED:0:1} + moResult="$moResult${MO_UNPARSED:0:1}" MO_UNPARSED=${MO_UNPARSED:1} ;; esac diff --git a/tests/function b/tests/function index 83253bd..967a128 100755 --- a/tests/function +++ b/tests/function @@ -16,6 +16,10 @@ template() { ... this is the last line. EOF } -export expected=$' Willy is awesome.\n... this is the last line.\n' + +# We don't expect {{name}} to be changed. The function returns whatever content +# that should be the result. There is a separate test where the function handles +# parsing mustache tags. +export expected=$' {{name}} is awesome.\n... this is the last line.\n' runTest diff --git a/tests/function-alternate-delimiters b/tests/function-alternate-delimiters new file mode 100755 index 0000000..a4fa6a8 --- /dev/null +++ b/tests/function-alternate-delimiters @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +cd "${0%/*}" || exit 1 +. ../run-tests + +export planet=Earth +lambda() { + local content + + content=$(cat) + mo::parse content "$content{{planet}} => |planet|$content" + echo -n "$content" +} +export template="{{= | | =}}<|#lambda|-|/lambda|>" +export expected="<-{{planet}} => Earth->" + +runTest diff --git a/tests/function-args b/tests/function-args index 94cb55d..a7958d6 100755 --- a/tests/function-args +++ b/tests/function-args @@ -20,7 +20,7 @@ template() { No args: {{testArgs}} - done One arg: {{testArgs 'one'}} - done Getting name in a string: {{testArgs {"The name is " name}}} - done -Reverse this: {{#pipeTo "rev"}}abcde{{/pipeTo}} +Reverse this: {{#pipeTo "rev"}}abcde{{/pipeTo "rev"}} EOF } expected() { diff --git a/tests/function-with-more-parsing b/tests/function-with-more-parsing new file mode 100755 index 0000000..825e655 --- /dev/null +++ b/tests/function-with-more-parsing @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +cd "${0%/*}" || exit 1 +. ../run-tests + +export name=Willy +wrapped() { + local content + + # Wrapping 'cat' in a subshell eats the trailing whitespace + content="$(cat)" + + # Parse the content using mustache + mo::parse content "$content" + + # The echo adds a newline, which is preserved. + echo "$content" +} +template() { + cat < Willy is awesome.\n... this is the last line.\n' + +runTest diff --git a/tests/help b/tests/help index 77ae670..6f7fcbc 100755 --- a/tests/help +++ b/tests/help @@ -54,6 +54,7 @@ MO_ALLOW_FUNCTION_ARGUMENTS - When set to a non-empty value, this allows functions referenced in templates to receive additional options and arguments. MO_CLOSE_DELIMITER - The string used when closing a tag. Defaults to "}}". +MO_CURRENT - Variable name to use for ".". MO_DEBUG - When set to a non-empty value, additional debug information is written to stderr. MO_FUNCTION_ARGS - Arguments passed to the function. diff --git a/tests/variable-braces-and-parenthesis b/tests/variable-braces-and-parenthesis new file mode 100755 index 0000000..2e2f939 --- /dev/null +++ b/tests/variable-braces-and-parenthesis @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +cd "${0%/*}" || exit 1 +. ../run-tests + +export array=("wrong0" "item1" "wrong2") +export index=1 + +# Example #8 is weird because of how the variable name is parsed. Considering +# it's an edge case when a user probably has a bug in a template, I think we're +# good leaving it as-is until a bug report is filed. + +template() { + cat <<'EOF' +Starting point: +1 "{{ array.1 }}" = "item1" + +Whole expression: +2 "{{ {'array.' index} }}" = "array.1" +3 "{{ ('array.' index) }}" = "item1" + +Partial expression: +4 "{{ 'array.' {index} }}" = "array.1" +5 "{{ 'array.' (index) }}" = "array." + +Combined: +6 "{{ {'array.' {index}} }}" = "array.1" +7 "{{ {'array.' (index)} }}" = "array." +8 "{{ ('array.' (index)) }}" = "wrong0,item1,wrong2" +9 "{{ ('array.' {index}) }}" = "item1" +EOF +} + +expected() { + cat <<'EOF' +Starting point: +1 "item1" = "item1" + +Whole expression: +2 "array.1" = "array.1" +3 "item1" = "item1" + +Partial expression: +4 "array.1" = "array.1" +5 "array." = "array." + +Combined: +6 "array.1" = "array.1" +7 "array." = "array." +8 "wrong0,item1,wrong2" = "wrong0,item1,wrong2" +9 "item1" = "item1" +EOF +} +runTest