All tests pass.

Specs: 181 total, 111 pass (with 7 overridden), 63 fail, 7 skip.
This commit is contained in:
Tyler Akins 2023-04-23 09:24:12 -05:00
parent 0f150ccb19
commit 1d4e186486
No known key found for this signature in database
GPG Key ID: 8F3B8C432F4393BD
7 changed files with 188 additions and 44 deletions

126
mo
View File

@ -514,19 +514,19 @@ mo::parseBlock() {
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER"
MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"} MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"}
mo::tokensToString moTokensString "${moTokens[@]}" mo::tokensToString moTokensString "${moTokens[@]:1}"
mo::debug "Parsing block: $moTokensString" mo::debug "Parsing block: $moTokensString"
if mo::standaloneCheck "$MO_STANDALONE_CONTENT"; then if mo::standaloneCheck "$MO_STANDALONE_CONTENT"; then
mo::standaloneProcess mo::standaloneProcess
fi fi
if [[ "${moTokens[0]}" == "NAME" ]] && mo::isFunction "${moTokens[1]}"; then if [[ "${moTokens[1]}" == "NAME" ]] && mo::isFunction "${moTokens[2]}"; then
mo::parseBlockFunction "$moInvertBlock" "$moTokensString" "${moTokens[@]}" mo::parseBlockFunction "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}"
elif [[ "${moTokens[0]}" == "NAME" ]] && mo::isArray "${moTokens[1]}"; then elif [[ "${moTokens[1]}" == "NAME" ]] && mo::isArray "${moTokens[2]}"; then
mo::parseBlockArray "$moInvertBlock" "$moTokensString" "${moTokens[@]}" mo::parseBlockArray "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}"
else else
mo::parseBlockValue "$moInvertBlock" "$moTokensString" "${moTokens[@]}" mo::parseBlockValue "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}"
fi fi
} }
@ -549,8 +549,9 @@ mo::parseBlockFunction() {
# Pass unparsed content to the function. # Pass unparsed content to the function.
# Keep the updated delimiters if they changed. # Keep the updated delimiters if they changed.
if [[ "$moInvertBlock" == "true" ]]; then if [[ "$moInvertBlock" != "true" ]]; then
mo::evaluateFunction moResult "$moTemp" "${moTokens[@]:1}" mo::evaluateFunction moResult "$moTemp" "${moTokens[@]:1}"
MO_PARSED="$MO_PARSED$moResult"
fi fi
mo::debug "Done parsing block function: $moTokensString" mo::debug "Done parsing block function: $moTokensString"
@ -564,7 +565,7 @@ mo::parseBlockFunction() {
# #
# Returns nothing # Returns nothing
mo::parseBlockArray() { 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 moInvertBlock=$1
moTokensString=$2 moTokensString=$2
@ -585,7 +586,10 @@ mo::parseBlockArray() {
# Restore the delimiter before parsing # Restore the delimiter before parsing
MO_OPEN_DELIMITER=$moOpenDelimiterBefore MO_OPEN_DELIMITER=$moOpenDelimiterBefore
MO_CLOSE_DELIMITER=$moCloseDelimiterBefore MO_CLOSE_DELIMITER=$moCloseDelimiterBefore
moCurrent=$MO_CURRENT
MO_CURRENT=$moArrayName
mo::parse moParsed "$moTemp" mo::parse moParsed "$moTemp"
MO_CURRENT=$moCurrent
MO_PARSED="$MO_PARSED$moParsed" MO_PARSED="$MO_PARSED$moParsed"
fi fi
else else
@ -597,8 +601,11 @@ mo::parseBlockArray() {
# Restore the delimiter before parsing # Restore the delimiter before parsing
MO_OPEN_DELIMITER=$moOpenDelimiterBefore MO_OPEN_DELIMITER=$moOpenDelimiterBefore
MO_CLOSE_DELIMITER=$moCloseDelimiterBefore 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::parse moParsed "$moTemp" "$moArrayName"
MO_CURRENT=$moCurrent
MO_PARSED="$MO_PARSED$moParsed" MO_PARSED="$MO_PARSED$moParsed"
done done
@ -694,7 +701,7 @@ mo::parsePartial() {
exit 1 exit 1
fi fi
mo::indentLines "$moIndentation" mo::indentLines moPartialContent "$moIndentation" "$moPartialContent"
# Delimiters are reset when loading a new partial # Delimiters are reset when loading a new partial
MO_OPEN_DELIMITER="{{" MO_OPEN_DELIMITER="{{"
@ -765,7 +772,7 @@ mo::parseValue() {
moUnparsedOriginal=$MO_UNPARSED moUnparsedOriginal=$MO_UNPARSED
mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER"
mo::evaluate moResult "${moTokens[@]}" mo::evaluate moResult "${moTokens[@]:1}"
MO_PARSED="$MO_PARSED$moResult" MO_PARSED="$MO_PARSED$moResult"
if [[ "${MO_UNPARSED:0:${#MO_CLOSE_DELIMITER}}" != "$MO_CLOSE_DELIMITER" ]]; then 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 if [[ "${moStack[0]:-}" == "NAME" ]] && mo::isFunction "${moStack[1]}"; then
# Special case - if the first argument is a function, then the rest are # Special case - if the first argument is a function, then the rest are
# passed to the function. # passed to the function.
moFunction=$2 mo::debug "Evaluating function: ${moStack[1]}"
mo::debug "Evaluating function: $moFunction" mo::evaluateFunction moResult "" "${moStack[@]:1}"
mo::evaluateFunction moResult "" "${moStack[@]:2}"
else else
# Concatenate # Concatenate
mo::debug "Concatenating ${#moStack[@]} stack items" mo::debug "Concatenating ${#moStack[@]} stack items"
@ -1323,22 +1329,30 @@ mo::standaloneProcess() {
# Internal: Apply indentation before any line that has content in MO_UNPARSED. # 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. # Returns nothing.
mo::indentLines() { mo::indentLines() {
local moContent moIndentation moResult moN moR moChunk local moContent moIndentation moResult moN moR moChunk
moIndentation=$1 moIndentation=$2
moContent=$3
if [[ -z "$moIndentation" ]] || [[ -z "$MO_UNPARSED" ]]; then if [[ -z "$moIndentation" ]]; then
mo::debug "Not applying indentation, indentation ${#moIndentation} bytes, content ${#MO_UNPARSED} bytes" mo::debug "Not applying indentation, empty indentation"
return local "$1" && mo::indirect "$1" "$moContent"
fi fi
moContent=$MO_UNPARSED if [[ -z "$moContent" ]]; then
MO_UNPARSED= mo::debug "Not applying indentation, empty contents"
local "$1" && mo::indirect "$1" "$moContent"
fi
moResult=
moN=$'\n' moN=$'\n'
moR=$'\r' moR=$'\r'
@ -1350,12 +1364,14 @@ mo::indentLines() {
moContent=${moContent:${#moChunk}} moContent=${moContent:${#moChunk}}
if [[ -n "$moChunk" ]]; then if [[ -n "$moChunk" ]]; then
MO_UNPARSED="$MO_UNPARSED$moIndentation$moChunk" moResult="$moResult$moIndentation$moChunk"
fi fi
MO_UNPARSED="$MO_UNPARSED${moContent:0:1}" moResult="$moResult${moContent:0:1}"
moContent=${moContent:1} moContent=${moContent:1}
done done
local "$1" && mo::indirect "$1" "$moResult"
} }
@ -1608,7 +1624,7 @@ mo::getContentWithinTag() {
moUnparsed=${MO_UNPARSED} moUnparsed=${MO_UNPARSED}
mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER"
MO_UNPARSED=${MO_UNPARSED#"$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}))} moParsed=${moUnparsed:0:$((${#moUnparsed} - ${#MO_UNPARSED}))}
local "$1" && mo::indirectArray "$1" "$moParsed" "$moTokensString" local "$1" && mo::indirectArray "$1" "$moParsed" "$moTokensString"
@ -1621,20 +1637,22 @@ mo::getContentWithinTag() {
# $1 - Destination variable for the array of contents. # $1 - Destination variable for the array of contents.
# $2 - Stop processing when this content is found. # $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'}) # Given: a 'bc' "de\"\n" (f {g 'h'})
# Result: ([0]=NAME [1]=a [2]=VALUE [3]=bc [4]=VALUE [5]=$'de\"\n' # Result: ([0]=4 [1]=NAME [2]=a [3]=VALUE [4]=bc [5]=VALUE [6]=$'de\"\n'
# [6]=NAME [7]=f [8]=NAME [9]=g [10]=VALUE [11]=h # [7]=NAME [8]=f [9]=NAME [10]=g [11]=VALUE [12]=h
# [12]=BRACE [13]=2 [14]=PAREN [15]=2 # [13]=BRACE [14]=2 [15]=PAREN [16]=2
# #
# Returns nothing # Returns nothing
mo::tokenizeTagContents() { mo::tokenizeTagContents() {
local moResult moTerminator moTemp moUnparsedOriginal local moResult moTerminator moTemp moUnparsedOriginal moTokenCount
moTerminator=$2 moTerminator=$2
moResult=() moResult=()
moUnparsedOriginal=$MO_UNPARSED moUnparsedOriginal=$MO_UNPARSED
moTokenCount=0
mo::debug "Tokenizing tag contents until terminator: $moTerminator" mo::debug "Tokenizing tag contents until terminator: $moTerminator"
while true; do while true; do
@ -1647,7 +1665,7 @@ mo::tokenizeTagContents() {
"$moTerminator"*) "$moTerminator"*)
mo::debug "Found terminator" mo::debug "Found terminator"
local "$1" && mo::indirectArray "$1" "${moResult[@]}" local "$1" && mo::indirectArray "$1" "$moTokenCount" "${moResult[@]}"
return return
;; ;;
@ -1655,7 +1673,7 @@ mo::tokenizeTagContents() {
# Do not tokenize the open paren - treat this as RPL # Do not tokenize the open paren - treat this as RPL
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
mo::tokenizeTagContents moTemp ')' mo::tokenizeTagContents moTemp ')'
moResult=("${moResult[@]}" "${moTemp[@]}" PAREN "$((${#moTemp[@]} / 2))") moResult=("${moResult[@]}" "${moTemp[@]:1}" PAREN "${moTemp[0]}")
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
;; ;;
@ -1663,7 +1681,7 @@ mo::tokenizeTagContents() {
# Do not tokenize the open brace - treat this as RPL # Do not tokenize the open brace - treat this as RPL
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
mo::tokenizeTagContents moTemp '}' mo::tokenizeTagContents moTemp '}'
moResult=("${moResult[@]}" "${moTemp[@]}" BRACE "$((${#moTemp[@]} / 2))") moResult=("${moResult[@]}" "${moTemp[@]:1}" BRACE "${moTemp[0]}")
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
;; ;;
@ -1688,6 +1706,7 @@ mo::tokenizeTagContents() {
esac esac
mo::debug "Got chunk: ${moTemp[0]} ${moTemp[1]}" mo::debug "Got chunk: ${moTemp[0]} ${moTemp[1]}"
moTokenCount=$((moTokenCount + 1))
done done
} }
@ -1701,10 +1720,10 @@ mo::tokenizeTagContentsName() {
local moTemp local moTemp
mo::chomp moTemp "${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*}" 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_UNPARSED=${MO_UNPARSED:${#moTemp}}
mo::trimUnparsed mo::trimUnparsed
mo::debug "Parsed default token: $moTemp" mo::debug "Parsed default token: $moTemp"
@ -1724,6 +1743,7 @@ mo::tokenizeTagContentsDoubleQuote() {
moUnparsedOriginal=$MO_UNPARSED moUnparsedOriginal=$MO_UNPARSED
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
moResult=
mo::debug "Getting double quoted tag contents" mo::debug "Getting double quoted tag contents"
while true; do while true; do
@ -1734,32 +1754,53 @@ mo::tokenizeTagContentsDoubleQuote() {
case "$MO_UNPARSED" in case "$MO_UNPARSED" in
'"'*) '"'*)
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
local "$1" && mo::indirect "$1" "VALUE" "$moResult" local "$1" && mo::indirectArray "$1" "VALUE" "$moResult"
return 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' moResult="$moResult"$'\n'
MO_UNPARSED=${MO_UNPARSED:2} MO_UNPARSED=${MO_UNPARSED:2}
;; ;;
\\r) \\r*)
moResult="$moResult"$'\r' moResult="$moResult"$'\r'
MO_UNPARSED=${MO_UNPARSED:2} MO_UNPARSED=${MO_UNPARSED:2}
;; ;;
\\t) \\t*)
moResult="$moResult"$'\t' moResult="$moResult"$'\t'
MO_UNPARSED=${MO_UNPARSED:2} MO_UNPARSED=${MO_UNPARSED:2}
;; ;;
\\v*)
moResult="$moResult"$'\v'
MO_UNPARSED=${MO_UNPARSED:2}
;;
\\*) \\*)
moResult="$moResult${MO_UNPARSED:1:1}" moResult="$moResult${MO_UNPARSED:1:1}"
MO_UNPARSED=${MO_UNPARSED:2} MO_UNPARSED=${MO_UNPARSED:2}
;; ;;
*) *)
moResult=${MO_UNPARSED:0:1} moResult="$moResult${MO_UNPARSED:0:1}"
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
;; ;;
esac esac
@ -1778,6 +1819,7 @@ mo::tokenizeTagContentsSingleQuote() {
moUnparsedOriginal=$MO_UNPARSED moUnparsedOriginal=$MO_UNPARSED
MO_UNPARSED=${MO_UNPARSED:1} MO_UNPARSED=${MO_UNPARSED:1}
moResult=
mo::debug "Getting single quoted tag contents" mo::debug "Getting single quoted tag contents"
while true; do 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} MO_UNPARSED=${MO_UNPARSED:1}
;; ;;
esac esac

View File

@ -16,6 +16,10 @@ template() {
... this is the last line. ... this is the last line.
EOF EOF
} }
export expected=$'<b> Willy is awesome.</b>\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=$'<b> {{name}} is awesome.</b>\n... this is the last line.\n'
runTest runTest

View File

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

View File

@ -20,7 +20,7 @@ template() {
No args: {{testArgs}} - done No args: {{testArgs}} - done
One arg: {{testArgs 'one'}} - done One arg: {{testArgs 'one'}} - done
Getting name in a string: {{testArgs {"The name is " name}}} - 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 EOF
} }
expected() { expected() {

View File

@ -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="<b>$(cat)</b>"
# Parse the content using mustache
mo::parse content "$content"
# The echo adds a newline, which is preserved.
echo "$content"
}
template() {
cat <<EOF
{{#wrapped}}
{{name}} is awesome.
{{/wrapped}}
... this is the last line.
EOF
}
export expected=$'<b> Willy is awesome.</b>\n... this is the last line.\n'
runTest

View File

@ -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 functions referenced in templates to receive additional options and
arguments. arguments.
MO_CLOSE_DELIMITER - The string used when closing a tag. Defaults to "}}". 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 MO_DEBUG - When set to a non-empty value, additional debug information is
written to stderr. written to stderr.
MO_FUNCTION_ARGS - Arguments passed to the function. MO_FUNCTION_ARGS - Arguments passed to the function.

View File

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