diff --git a/mo b/mo index 3ed5af9..4b98512 100755 --- a/mo +++ b/mo @@ -1,61 +1,5 @@ #!/bin/bash -# Return 0 if the passed name is a function. Function names are captured -# at the start of the program and are stored in the $MUSTACHE_FUNCTIONS array. -# -# Parameters: -# $1: Name to check if it's a function -# -# Return code: -# 0 if the name is a function, 1 otherwise -mustache-is-function() { - local NAME - - for NAME in ${MUSTACHE_FUNCTIONS[@]}; do - if [[ "$NAME" == "$1" ]]; then - return 0 - fi - done - - return 1 -} - - -# Process a chunk of content some number of times. -# -# Parameters: -# $1: Content to parse and reparse and reparse -# $2: Tag prefix (context name) -# $3-*: Names to insert into the parsed content -mustache-loop() { - local CONTENT CONTEXT CONTEXT_BASE IGNORE - - CONTENT=$1 - CONTEXT_BASE=$2 - shift 2 - - while [[ "${#@}" -gt 0 ]]; do - mustache-full-tag-name CONTEXT "$CONTEXT_BASE" "$1" - mustache-parse IGNORE "$CONTENT" "$CONTEXT" false - shift - done -} - - -# Return a dotted name based on current context and target name -# -# Parameters: -# $1: Target variable to store results -# $2: Context name -# $3: Desired variable name -mustache-full-tag-name() { - if [[ -z "$2" ]]; then - local "$1" && mustache-indirect "$1" "$3" - else - local "$1" && mustache-indirect "$1" "${2}.${3}" - fi -} - # Eat content until the right end tag is found. Returns an array with the # following members: @@ -130,6 +74,259 @@ mustache-find-end-tag() { } +# Find the first index of a substring +# +# Parameters: +# $1: Destination variable +# $2: Haystack +# $3: Needle +mustache-find-string() { + local POS STRING + + STRING=${2%%$3*} + [[ "$STRING" == "$2" ]] && POS=-1 || POS=${#STRING} + local "$1" && mustache-indirect "$1" $POS +} + + +# Return a dotted name based on current context and target name +# +# Parameters: +# $1: Target variable to store results +# $2: Context name +# $3: Desired variable name +mustache-full-tag-name() { + if [[ -z "$2" ]]; then + local "$1" && mustache-indirect "$1" "$3" + else + local "$1" && mustache-indirect "$1" "${2}.${3}" + fi +} + + +# Return the content to parse. Can be a list of partials for files or +# the content from stdin. +# +# Parameters: +# $1: Variable name to assign this content back as +# $2-*: File names (optional) +mustache-get-content() { + local CONTENT FILENAME TARGET + + TARGET=$1 + shift + if [[ "${#@}" -gt 0 ]]; then + CONTENT="" + + for FILENAME in ${1+"$@"}; do + # This is so relative paths work from inside template files + CONTENT="$CONTENT"'{{>'"$FILENAME"'}}' + done + else + mustache-load-file CONTENT /dev/stdin + fi + + local "$TARGET" && mustache-indirect "$TARGET" "$CONTENT" +} + + +# Indent a string, placing the indent at the beginning of every +# line that has any content. +# +# Parameters: +# $1: Name of destination variable to get an array of lines +# $2: The indent string +# $3: The string to reindent +mustache-indent-lines() { + local CONTENT FRAGMENT POS_N POS_R RESULT TRIMMED + + RESULT="" + CONTENT="${3:0: -1}" # Remove newline and dot from workaround - in mustache-partial + + if [ -z "$2" ]; then + local "$1" && mustache-indirect "$1" "$CONTENT" + return 0 + fi + + mustache-find-string POS_N "$CONTENT" $'\n' + mustache-find-string POS_R "$CONTENT" $'\r' + + while [[ "$POS_N" -gt -1 ]] || [[ "$POS_R" -gt -1 ]]; do + if [[ "$POS_N" -gt -1 ]]; then + FRAGMENT="${CONTENT:0:$POS_N + 1}" + CONTENT=${CONTENT:$POS_N + 1} + else + FRAGMENT="${CONTENT:0:$POS_R + 1}" + CONTENT=${CONTENT:$POS_R + 1} + fi + + mustache-trim-chars TRIMMED "$FRAGMENT" false true " " "$'\t'" "$'\n'" "$'\r'" + + if [ ! -z "$TRIMMED" ]; then + FRAGMENT="$2$FRAGMENT" + fi + + RESULT="$RESULT$FRAGMENT" + mustache-find-string POS_N "$CONTENT" $'\n' + mustache-find-string POS_R "$CONTENT" $'\r' + done + + mustache-trim-chars TRIMMED "$CONTENT" false true " " "$'\t'" + + if [ ! -z "$TRIMMED" ]; then + CONTENT="$2$CONTENT" + fi + + RESULT="$RESULT$CONTENT" + + local "$1" && mustache-indirect "$1" "$RESULT" +} + + +# Send a variable up to caller of a function +# +# Parameters: +# $1: Variable name +# $2: Value +mustache-indirect() { + unset -v "$1" + printf -v "$1" '%s' "$2" +} + + +# Send an array up to caller of a function +# +# Parameters: +# $1: Variable name +# $2-*: Array elements +mustache-indirect-array() { + unset -v "$1" + eval $1=\(\"\${@:2}\"\) +} + + +# Determine if a given environment variable exists and if it is an array. +# +# Parameters: +# $1: Name of environment variable +# +# Return code: +# 0 if the name is not empty, 1 otherwise +mustache-is-array() { + local MUSTACHE_TEST + + MUSTACHE_TEST=$(declare -p "$1" 2>/dev/null) || return 1 + [[ "${MUSTACHE_TEST:0:10}" == "declare -a" ]] && return 0 + [[ "${MUSTACHE_TEST:0:10}" == "declare -A" ]] && return 0 + + return 1 +} + + +# Return 0 if the passed name is a function. Function names are captured +# at the start of the program and are stored in the $MUSTACHE_FUNCTIONS array. +# +# Parameters: +# $1: Name to check if it's a function +# +# Return code: +# 0 if the name is a function, 1 otherwise +mustache-is-function() { + local NAME + + for NAME in ${MUSTACHE_FUNCTIONS[@]}; do + if [[ "$NAME" == "$1" ]]; then + return 0 + fi + done + + return 1 +} + + +# Determine if the tag is a standalone tag based on whitespace before and +# after the tag. +# +# Passes back a string containing two numbers in the format "BEFORE AFTER" +# like "27 10". It indicates the number of bytes remaining in the "before" +# string (27) and the number of bytes to trim in the "after" string (10). +# Useful for string manipulation: +# +# mustache-is-standalone RESULT "$before" "$after" false || return 0 +# RESULT_ARRAY=( $RESULT ) +# echo "${before:0:${RESULT_ARRAY[0]}}...${after:${RESULT_ARRAY[1]}}" +# +# Parameters: +# $1: Variable to pass data back +# $2: Content before the tag +# $3: Content after the tag +# $4: true/false: is this the beginning of the content? +mustache-is-standalone() { + local AFTER_TRIMMED BEFORE_TRIMMED CHAR + + mustache-trim-chars BEFORE_TRIMMED "$2" false true " " "$'\t'" + mustache-trim-chars AFTER_TRIMMED "$3" true false " " "$'\t'" + CHAR=${BEFORE_TRIMMED: -1} + + if [[ "$CHAR" != $'\n' ]] && [[ "$CHAR" != $'\r' ]]; then + if [[ ! -z "$CHAR" ]] || ! $4; then + return 1; + fi + fi + + CHAR=${AFTER_TRIMMED:0:1} + + if [[ "$CHAR" != $'\n' ]] && [[ "$CHAR" != $'\r' ]] && [[ ! -z "$CHAR" ]]; then + return 2; + fi + + if [[ "$CHAR" == $'\r' ]] && [[ "${AFTER_TRIMMED:1:1}" == $'\n' ]]; then + CHAR="$CHAR"$'\n' + fi + + local "$1" && mustache-indirect "$1" "$((${#BEFORE_TRIMMED})) $((${#3} + ${#CHAR} - ${#AFTER_TRIMMED}))" +} + + +# Read a file +# +# Parameters: +# $1: Variable name to receive the file's content +# $2: Filename to load +mustache-load-file() { + local CONTENT + + # 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? + CONTENT=$(cat $2; echo '.') + CONTENT=${CONTENT:0: -1} # Remove last dot + + local "$1" && mustache-indirect "$1" "$CONTENT" +} + + +# Process a chunk of content some number of times. +# +# Parameters: +# $1: Content to parse and reparse and reparse +# $2: Tag prefix (context name) +# $3-*: Names to insert into the parsed content +mustache-loop() { + local CONTENT CONTEXT CONTEXT_BASE IGNORE + + CONTENT=$1 + CONTEXT_BASE=$2 + shift 2 + + while [[ "${#@}" -gt 0 ]]; do + mustache-full-tag-name CONTEXT "$CONTEXT_BASE" "$1" + mustache-parse IGNORE "$CONTENT" "$CONTEXT" false + shift + done +} + + # Parse a block of text # # Parameters: @@ -309,141 +506,6 @@ mustache-partial() { } -# Indent a string, placing the indent at the beginning of every -# line that has any content. -# -# Parameters: -# $1: Name of destination variable to get an array of lines -# $2: The indent string -# $3: The string to reindent -mustache-indent-lines() { - local CONTENT FRAGMENT POS_N POS_R RESULT TRIMMED - - RESULT="" - CONTENT="${3:0: -1}" # Remove newline and dot from workaround - in mustache-partial - - if [ -z "$2" ]; then - local "$1" && mustache-indirect "$1" "$CONTENT" - return 0 - fi - - mustache-find-string POS_N "$CONTENT" $'\n' - mustache-find-string POS_R "$CONTENT" $'\r' - - while [[ "$POS_N" -gt -1 ]] || [[ "$POS_R" -gt -1 ]]; do - if [[ "$POS_N" -gt -1 ]]; then - FRAGMENT="${CONTENT:0:$POS_N + 1}" - CONTENT=${CONTENT:$POS_N + 1} - else - FRAGMENT="${CONTENT:0:$POS_R + 1}" - CONTENT=${CONTENT:$POS_R + 1} - fi - - mustache-trim-chars TRIMMED "$FRAGMENT" false true " " "$'\t'" "$'\n'" "$'\r'" - - if [ ! -z "$TRIMMED" ]; then - FRAGMENT="$2$FRAGMENT" - fi - - RESULT="$RESULT$FRAGMENT" - mustache-find-string POS_N "$CONTENT" $'\n' - mustache-find-string POS_R "$CONTENT" $'\r' - done - - mustache-trim-chars TRIMMED "$CONTENT" false true " " "$'\t'" - - if [ ! -z "$TRIMMED" ]; then - CONTENT="$2$CONTENT" - fi - - RESULT="$RESULT$CONTENT" - - local "$1" && mustache-indirect "$1" "$RESULT" -} - - -# Handle the content for a standalone tag. This means removing whitespace -# (not newlines) before a tag and whitespace and a newline after a tag. -# That is, assuming, that the line is otherwise empty. -# -# Parameters: -# $1: Name of destination "content" variable. -# $2: Content before the tag that was not yet written -# $3: Tag content (not used) -# $4: Content after the tag -# $5: true/false: is this the beginning of the content? -mustache-standalone-allowed() { - local STANDALONE_BYTES - - if mustache-is-standalone STANDALONE_BYTES "$2" "$4" $5; then - STANDALONE_BYTES=( $STANDALONE_BYTES ) - echo -n "${2:0:${STANDALONE_BYTES[0]}}" - local "$1" && mustache-indirect "$1" "${4:${STANDALONE_BYTES[1]}}" - else - echo -n "$2" - local "$1" && mustache-indirect "$1" "$4" - fi -} - - -# Determine if the tag is a standalone tag based on whitespace before and -# after the tag. -# -# Passes back a string containing two numbers in the format "BEFORE AFTER" -# like "27 10". It indicates the number of bytes remaining in the "before" -# string (27) and the number of bytes to trim in the "after" string (10). -# Useful for string manipulation: -# -# mustache-is-standalone RESULT "$before" "$after" false || return 0 -# RESULT_ARRAY=( $RESULT ) -# echo "${before:0:${RESULT_ARRAY[0]}}...${after:${RESULT_ARRAY[1]}}" -# -# Parameters: -# $1: Variable to pass data back -# $2: Content before the tag -# $3: Content after the tag -# $4: true/false: is this the beginning of the content? -mustache-is-standalone() { - local AFTER_TRIMMED BEFORE_TRIMMED CHAR - - mustache-trim-chars BEFORE_TRIMMED "$2" false true " " "$'\t'" - mustache-trim-chars AFTER_TRIMMED "$3" true false " " "$'\t'" - CHAR=${BEFORE_TRIMMED: -1} - - if [[ "$CHAR" != $'\n' ]] && [[ "$CHAR" != $'\r' ]]; then - if [[ ! -z "$CHAR" ]] || ! $4; then - return 1; - fi - fi - - CHAR=${AFTER_TRIMMED:0:1} - - if [[ "$CHAR" != $'\n' ]] && [[ "$CHAR" != $'\r' ]] && [[ ! -z "$CHAR" ]]; then - return 2; - fi - - if [[ "$CHAR" == $'\r' ]] && [[ "${AFTER_TRIMMED:1:1}" == $'\n' ]]; then - CHAR="$CHAR"$'\n' - fi - - local "$1" && mustache-indirect "$1" "$((${#BEFORE_TRIMMED})) $((${#3} + ${#CHAR} - ${#AFTER_TRIMMED}))" -} - - -# Handle the content for a tag that is never "standalone". No adjustments -# are made for newlines and whitespace. -# -# Parameters: -# $1: Name of destination "content" variable. -# $2: Content before the tag that was not yet written -# $3: Tag content (not used) -# $4: Content after the tag -mustache-standalone-denied() { - echo -n "$2" - local "$1" && mustache-indirect "$1" "$4" -} - - # Show an environment variable or the output of a function. # # Limit/prefix any variables used @@ -471,6 +533,77 @@ mustache-show() { } +# Split a larger string into an array +# +# Parameters: +# $1: Destination variable +# $2: String to split +# $3: Starting delimiter +# $4: Ending delimiter (optional) +mustache-split() { + local POS RESULT + + RESULT=( "$2" ) + mustache-find-string POS "${RESULT[0]}" "$3" + + if [[ "$POS" -ne -1 ]]; then + # The first delimiter was found + RESULT[1]=${RESULT[0]:$POS + ${#3}} + RESULT[0]=${RESULT[0]:0:$POS} + + if [[ ! -z "$4" ]]; then + mustache-find-string POS "${RESULT[1]}" "$4" + + if [[ "$POS" -ne -1 ]]; then + # The second delimiter was found + RESULT[2]="${RESULT[1]:$POS + ${#4}}" + RESULT[1]="${RESULT[1]:0:$POS}" + fi + fi + fi + + local "$1" && mustache-indirect-array "$1" "${RESULT[@]}" +} + + +# Handle the content for a standalone tag. This means removing whitespace +# (not newlines) before a tag and whitespace and a newline after a tag. +# That is, assuming, that the line is otherwise empty. +# +# Parameters: +# $1: Name of destination "content" variable. +# $2: Content before the tag that was not yet written +# $3: Tag content (not used) +# $4: Content after the tag +# $5: true/false: is this the beginning of the content? +mustache-standalone-allowed() { + local STANDALONE_BYTES + + if mustache-is-standalone STANDALONE_BYTES "$2" "$4" $5; then + STANDALONE_BYTES=( $STANDALONE_BYTES ) + echo -n "${2:0:${STANDALONE_BYTES[0]}}" + local "$1" && mustache-indirect "$1" "${4:${STANDALONE_BYTES[1]}}" + else + echo -n "$2" + local "$1" && mustache-indirect "$1" "$4" + fi +} + + +# Handle the content for a tag that is never "standalone". No adjustments +# are made for newlines and whitespace. +# +# Parameters: +# $1: Name of destination "content" variable. +# $2: Content before the tag that was not yet written +# $3: Tag content (not used) +# $4: Content after the tag +mustache-standalone-denied() { + echo -n "$2" + local "$1" && mustache-indirect "$1" "$4" +} + + # Returns 0 (success) if the named thing is a function or if it is a non-empty # environment variable. # @@ -499,24 +632,6 @@ mustache-test() { } -# Determine if a given environment variable exists and if it is an array. -# -# Parameters: -# $1: Name of environment variable -# -# Return code: -# 0 if the name is not empty, 1 otherwise -mustache-is-array() { - local MUSTACHE_TEST - - MUSTACHE_TEST=$(declare -p "$1" 2>/dev/null) || return 1 - [[ "${MUSTACHE_TEST:0:10}" == "declare -a" ]] && return 0 - [[ "${MUSTACHE_TEST:0:10}" == "declare -A" ]] && return 0 - - return 1 -} - - # Trim the leading whitespace only # # Parameters: @@ -564,119 +679,6 @@ mustache-trim-whitespace() { } -# Split a larger string into an array -# -# Parameters: -# $1: Destination variable -# $2: String to split -# $3: Starting delimiter -# $4: Ending delimiter (optional) -mustache-split() { - local POS RESULT - - RESULT=( "$2" ) - mustache-find-string POS "${RESULT[0]}" "$3" - - if [[ "$POS" -ne -1 ]]; then - # The first delimiter was found - RESULT[1]=${RESULT[0]:$POS + ${#3}} - RESULT[0]=${RESULT[0]:0:$POS} - - if [[ ! -z "$4" ]]; then - mustache-find-string POS "${RESULT[1]}" "$4" - - if [[ "$POS" -ne -1 ]]; then - # The second delimiter was found - RESULT[2]="${RESULT[1]:$POS + ${#4}}" - RESULT[1]="${RESULT[1]:0:$POS}" - fi - fi - fi - - local "$1" && mustache-indirect-array "$1" "${RESULT[@]}" -} - -# Find the first index of a substring -# -# Parameters: -# $1: Destination variable -# $2: Haystack -# $3: Needle -mustache-find-string() { - local POS STRING - - STRING=${2%%$3*} - [[ "$STRING" == "$2" ]] && POS=-1 || POS=${#STRING} - local "$1" && mustache-indirect "$1" $POS -} - - -# Send a variable up to caller of a function -# -# Parameters: -# $1: Variable name -# $2: Value -mustache-indirect() { - unset -v "$1" - printf -v "$1" '%s' "$2" -} - - -# Send an array up to caller of a function -# -# Parameters: -# $1: Variable name -# $2-*: Array elements -mustache-indirect-array() { - unset -v "$1" - eval $1=\(\"\${@:2}\"\) -} - - -# Return the content to parse. Can be a list of partials for files or -# the content from stdin. -# -# Parameters: -# $1: Variable name to assign this content back as -# $2-*: File names (optional) -mustache-get-content() { - local CONTENT FILENAME TARGET - - TARGET=$1 - shift - if [[ "${#@}" -gt 0 ]]; then - CONTENT="" - - for FILENAME in ${1+"$@"}; do - # This is so relative paths work from inside template files - CONTENT="$CONTENT"'{{>'"$FILENAME"'}}' - done - else - mustache-load-file CONTENT /dev/stdin - fi - - local "$TARGET" && mustache-indirect "$TARGET" "$CONTENT" -} - - -# Read a file -# -# Parameters: -# $1: Variable name to receive the file's content -# $2: Filename to load -mustache-load-file() { - local CONTENT - - # 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? - CONTENT=$(cat $2; echo '.') - CONTENT=${CONTENT:0: -1} # Remove last dot - - local "$1" && mustache-indirect "$1" "$CONTENT" -} - - # Save the list of functions as an array MUSTACHE_FUNCTIONS=$(declare -F) MUSTACHE_FUNCTIONS=( ${MUSTACHE_FUNCTIONS//declare -f /} )