diff --git a/mo b/mo index bdfd9a4..cc4f9dd 100755 --- a/mo +++ b/mo @@ -786,7 +786,7 @@ mo::parsePartial() { moR=$'\r' moIndentation="$moN${moPrevious//$moR/$moN}" moIndentation=${moIndentation##*$moN} - mo::debug "Adding indentation: '$moIndentation'" + mo::debug "Adding indentation to partial: '$moIndentation'" mo::standaloneProcessBefore moPrevious "$moPrevious" mo::standaloneProcessAfter moContent "$moContent" moStandaloneContent=$'\n' @@ -810,6 +810,8 @@ mo::parsePartial() { exit 1 fi + mo::indentLines moResult "$moResult" "$moIndentation" + # Delimiters are reset when loading a new partial mo::parse moResult "$moResult" "$moCurrent" "" "{{" "}}" "" $'\n' @@ -823,7 +825,7 @@ mo::parsePartial() { exit 1 fi - mo::indentLines moResult "${moResult%.}" "$moIndentation" + moResult=${moResult%.} fi local "$1" && mo::indirectArray "$1" "$moPrevious$moResult" "$moContent" "$moStandaloneContent" @@ -1317,6 +1319,42 @@ mo::isArray() { } +# Internal: Determine if an array index exists. +# +# $1 - Variable name to check +# $2 - The index to check +# +# Has to check if the variable is an array and if the index is valid for that +# type of array. +# +# Returns true (0) if everything was ok, 1 if there's any condition that fails. +mo::isArrayIndexValid() { + local moDeclare moTest + + moDeclare=$(declare -p "$1") + moTest="" + + if [[ "${moDeclare:0:10}" == "declare -a" ]]; then + # Numerically indexed array - must check if the index looks like a + # number because using a string to index a numerically indexed array + # will appear like it worked. + if [[ "$2" == "0" ]] || [[ "$2" =~ ^[1-9][0-9]*$ ]]; then + # Index looks like a number + eval "moTest=\"\${$1[$2]+ok}\"" + fi + elif [[ "${moDeclare:0:10}" == "declare -A" ]]; then + # Associative array + eval "moTest=\"\${$1[$2]+ok}\"" + fi + + if [[ -n "$moTest" ]]; then + return 0; + fi + + return 1 +} + + # Internal: Determine if a variable is assigned, even if it is assigned an empty # value. # @@ -1442,24 +1480,24 @@ mo::evaluateListOfSingles() { # # Returns nothing mo::evaluateSingle() { - local moResult moCurrent moVarNameParts moType moArg + local moResult moCurrent moType moArg moCurrent=$2 moType=$3 moArg=$4 - mo::debug "Evaluating $moType: $moArg" + + mo::debug "Evaluating $moType: $moArg ($moCurrent)" if [[ "$moType" == "VALUE" ]]; then moResult=$moArg elif [[ "$moArg" == "." ]]; then - mo::evaluateVariable moResult "$moCurrent" + mo::evaluateVariable moResult "$moCurrent" "" elif [[ "$moArg" == "@key" ]]; then mo::evaluateKey moResult "$moCurrent" elif mo::isFunction "$moArg"; then mo::evaluateFunction moResult "" "$moArg" else - mo::split moVarNameParts "$moArg" . - mo::evaluateVariable moResult "$moArg" + mo::evaluateVariable moResult "$moArg" "$moCurrent" fi local "$1" && mo::indirect "$1" "$moResult" @@ -1491,16 +1529,19 @@ mo::evaluateKey() { # # $1 - Destination variable name # $2 - Variable name +# $3 - Current value # # Returns nothing. mo::evaluateVariable() { local moResult moCurrent moArg moNameParts moJoined moKey moValue moArg=$2 + moCurrent=$3 moResult="" - mo::split moNameParts "$moArg" . + mo::findVariableName moNameParts "$moArg" "$moCurrent" + mo::debug "Evaluate variable ($moArg + $moCurrent): ${moNameParts[*]}" - if [[ -z "${moNameParts[1]-}" ]]; then + if [[ -z "${moNameParts[1]}" ]]; then if mo::isArray "$moArg"; then eval mo::join moResult "," "\${$moArg[@]}" else @@ -1523,6 +1564,58 @@ mo::evaluateVariable() { } +# Internal: Find the name of a variable to use +# +# $1 - Destination variable name, receives an array +# $2 - Variable name from the template +# $3 - The name of the "current value", from block parsing +# +# The array contains the following values +# [0] - Variable name +# [1] - Array index, or empty string +# +# Example variables +# a="a" +# b="b" +# c=("c.0" "c.1") +# d=([b]="d.b" [d]="d.d") +# +# Given these inputs, produce these outputs +# a c => a +# a c.0 => a +# b d => d.b +# b d.d => d.b +# a d => d.a +# a d.d => d.a +# c.0 d => c.0 +# d.b d => d.b +# Returns nothing. +mo::findVariableName() { + local moVar moCurrent moNameParts moResultBase moResultIndex + + moVar=$2 + moCurrent=$3 + moResultBase=$moVar + moResultIndex="" + + if [[ "$moVar" == *.* ]]; then + mo::debug "Find variable name; name has dot: $moVar" + moResultBase=${moVar%%.*} + moResultIndex=${moVar#*.} + elif [[ -n "$moCurrent" ]]; then + moCurrent=${moCurrent%%.*} + mo::debug "Find variable name; look in array: $moCurrent" + + if mo::isArrayIndexValid "$moCurrent" "$moVar"; then + moResultBase=$moCurrent + moResultIndex=$moVar + fi + fi + + local "$1" && mo::indirectArray "$1" "$moResultBase" "$moResultIndex" +} + + # Internal: Join / implode an array # # $1 - Variable name to receive the joined content diff --git a/run-spec.js b/run-spec.js index 8e50ae1..ad41931 100644 --- a/run-spec.js +++ b/run-spec.js @@ -11,28 +11,45 @@ const fsPromises = require("fs").promises; // // To override any test property, just define that property. const testOverrides = { - 'Interpolation -> HTML Escaping': { - skip: 'HTML escaping is not supported' + "Interpolation -> HTML Escaping": { + skip: "HTML escaping is not supported" }, - 'Interpolation -> Implicit Iterators - HTML Escaping': { - skip: 'HTML escaping is not supported' + "Interpolation -> Implicit Iterators - HTML Escaping": { + skip: "HTML escaping is not supported" }, - 'Lambdas -> Escaping': { - skip: 'HTML escaping is not supported' + "Lambdas -> Escaping": { + skip: "HTML escaping is not supported" }, - 'Sections -> Dotted Names - Broken Chains': { + "Sections -> Deeply Nested Contexts": { + skip: "Nested objects are not supported" + }, + "Sections -> Dotted Names - Broken Chains": { // Complex objects are not supported template: `"{{#a.b}}Here{{/a.b}}" == ""` }, - 'Sections -> Dotted Names - Falsey': { + "Sections -> Dotted Names - Falsey": { // Complex objects are not supported data: { a: { b: false } }, template: `"{{#a.b}}Here{{/a.b}}" == ""` }, - 'Sections -> Dotted Names - Truthy': { + "Sections -> Dotted Names - Truthy": { // Complex objects are not supported data: { a: { b: true } }, template: `"{{#a.b}}Here{{/a.b}}" == "Here"` + }, + "Sections -> Implicit Iterator - Array": { + skip: "Nested arrays are not supported" + }, + "Sections -> List": { + // Arrays of objects are not supported + data: { list: [1, 2, 3] }, + template: `"{{#list}}{{.}}{{/list}}"` + }, + "Sections -> List Context": { + skip: "Deeply nested objects are not supported" + }, + "Sections -> List Contexts": { + skip: "Deeply nested objects are not supported" } }; @@ -110,7 +127,7 @@ function addToEnvironmentObjectConvertedToAssociativeArray(name, value) { const values = []; for (const [k, v] of Object.entries(value)) { - if (typeof v === 'object') { + if (typeof v === "object") { if (v) { // An object - abort return `# ${name}.${k} is an object that can not be converted to an associative array`; @@ -123,7 +140,7 @@ function addToEnvironmentObjectConvertedToAssociativeArray(name, value) { } } - return `declare -A ${name}\n${name}=(${values.join(' ')})`; + return `declare -A ${name}\n${name}=(${values.join(" ")})`; } function addToEnvironmentObject(name, value) { @@ -134,10 +151,7 @@ function addToEnvironmentObject(name, value) { // Sometimes the __tag__ property of the code in the lambdas may be // missing. Compensate by detecting commonly defined languages. - if ( - (value.__tag__ === "code") || - (value.ruby && value.php && value.perl) - ) { + if (value.__tag__ === "code" || (value.ruby && value.php && value.perl)) { if (value.bash) { return `${name}() { ${value.bash}; }`; } @@ -145,7 +159,6 @@ function addToEnvironmentObject(name, value) { return `${name}() { perl -e 'print ((${value.perl})->("'"$1"'"))'; }`; } - return addToEnvironmentObjectConvertedToAssociativeArray(name, value); } @@ -201,16 +214,20 @@ function setupEnvironment(test) { function executeScript(test) { return new Promise((resolve) => { - exec("bash spec-runner/spec-script 2>&1", { - timeout: 2000 - }, (err, stdout) => { - if (err) { - test.scriptError = err.toString(); - } + exec( + "bash spec-runner/spec-script 2>&1", + { + timeout: 2000 + }, + (err, stdout) => { + if (err) { + test.scriptError = err.toString(); + } - test.output = stdout; - resolve(); - }); + test.output = stdout; + resolve(); + } + ); }); } @@ -232,9 +249,9 @@ function detectFailure(test) { function showFailureDetails(test) { console.log(`FAILURE: ${test.fullName}`); - console.log(''); + console.log(""); console.log(test.desc); - console.log(''); + console.log(""); console.log(JSON.stringify(test, null, 4)); } @@ -261,12 +278,12 @@ function runTest(testSet, test) { test.script = buildScript(test); if (test.skip) { - debug('Skipping test:', testSet.fullName, `$(${test.skip})`); + debug("Skipping test:", testSet.fullName, `$(${test.skip})`); return Promise.resolve(); } - debug('Running test:', testSet.fullName); + debug("Running test:", testSet.fullName); return setupEnvironment(test) .then(() => executeScript(test)) @@ -303,7 +320,9 @@ function processSpecFile(filename) { testSet.pass += 1; } } - console.log(`### ${testSet.name} Results = ${testSet.pass} passed, ${testSet.fail} failed, ${testSet.skip} skipped`); + console.log( + `### ${testSet.name} Results = ${testSet.pass} passed, ${testSet.fail} failed, ${testSet.skip} skipped` + ); return testSet; }); @@ -318,11 +337,14 @@ if (process.argv.length < 3) { processArraySequentially(process.argv.slice(2), processSpecFile).then( (result) => { - console.log('========================================='); - console.log(''); - console.log('Failed Test Summary'); - console.log(''); - let pass = 0, fail = 0, skip = 0, total = 0; + console.log("========================================="); + console.log(""); + console.log("Failed Test Summary"); + console.log(""); + let pass = 0, + fail = 0, + skip = 0, + total = 0; for (const testSet of result) { pass += testSet.pass; @@ -330,7 +352,9 @@ processArraySequentially(process.argv.slice(2), processSpecFile).then( skip += testSet.skip; total += testSet.tests.length; - console.log(`* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass, ${testSet.fail} fail, ${testSet.skip} skip`); + console.log( + `* ${testSet.name}: ${testSet.tests.length} total, ${testSet.pass} pass, ${testSet.fail} fail, ${testSet.skip} skip` + ); for (const test of testSet.tests) { if (test.isFailure) { @@ -339,8 +363,10 @@ processArraySequentially(process.argv.slice(2), processSpecFile).then( } } - console.log(''); - console.log(`Final result: ${total} total, ${pass} pass, ${fail} fail, ${skip} skip`); + console.log(""); + console.log( + `Final result: ${total} total, ${pass} pass, ${fail} fail, ${skip} skip` + ); if (fail) { process.exit(1); diff --git a/run-tests b/run-tests index be45782..84b0212 100755 --- a/run-tests +++ b/run-tests @@ -50,7 +50,7 @@ runTest() ( echo "Actual:" echo "$testActual" - if [[ -n "${MO_DEBUG-}" ]]; then + if [[ -n "${MO_DEBUG_TEST-}" ]]; then declare -p testExpected declare -p testActual fi diff --git a/tests/fixtures/standalone-indentation.partial b/tests/fixtures/standalone-indentation.partial new file mode 100644 index 0000000..96d7a30 --- /dev/null +++ b/tests/fixtures/standalone-indentation.partial @@ -0,0 +1,3 @@ +| +{{content}} +| diff --git a/tests/list-contexts b/tests/list-contexts new file mode 100755 index 0000000..97c9a70 --- /dev/null +++ b/tests/list-contexts @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +cd "${0%/*}" || exit 1 +. ../run-tests + +a=foo +b=wrong +declare -A sec +sec=([b]="bar") +declare -A c +c=([d]="baz") +template="{{#sec}}{{a}} {{b}} {{c.d}}{{/sec}}" +expected="foo bar baz" + +runTest diff --git a/tests/multi-line-partial b/tests/multi-line-partial index 95772b0..7137f5b 100755 --- a/tests/multi-line-partial +++ b/tests/multi-line-partial @@ -24,8 +24,9 @@ line 2 Indented: line 1 - line 2 +line 2 EOF + # This one looks odd, but if you check the spec spec/specs/partials.yaml, name "Standalone Indentation" (mirrors "standalone-indentation" in tests/), then the spec clearly shows that the indentation is applied before rendering. } runTest diff --git a/tests/standalone-indentation b/tests/standalone-indentation new file mode 100755 index 0000000..d3b51c0 --- /dev/null +++ b/tests/standalone-indentation @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +cd "${0%/*}" || exit 1 +. ../run-tests + +content=$'<\n->' +template() { + cat <fixtures/standalone-indentation.partial}} +/ +EOF +} +expected() { + cat < + | +/ +EOF +} + +runTest