package main import ( "fmt" "slices" ) func truthy(val interface{}) bool { switch v := val.(type) { case string: return v != "" case bool: return v case int: return v != 0 } return false } // Templater for custom emails. // Slices "variables", "conditionals", and map "values" should NOT wrap names in { and }. // Variables should be written as {varName}. // If statements should be written as {if (!)varName}...{endif}. // Strings are true if != "", ints are true if != 0. // Errors returned are likely warnings only. func templateEmail(content string, variables []string, conditionals []string, values map[string]interface{}) (string, error) { // minimum length for templatable content (albeit just "{}" -> "") if len(content) < 2 { return content, nil } ifStart, ifEnd := -1, -1 ifTrue := false invalidIf := false previousEnd := -2 blockRawStart := -1 blockContentStart, blockContentEnd := -1, -1 varStart, varEnd := -1, -1 varName := "" out := "" var err error = nil oob := func(i int) bool { return i < 0 || i >= len(content) } for i, c := range content { if c == '{' { blockContentStart = i + 1 blockRawStart = i if content[i+1] == '{' { err = fmt.Errorf(`double braces ("{{") at position %d, use single brace only`, i) blockContentStart++ } for !oob(blockContentStart) && content[blockContentStart] == ' ' { blockContentStart++ } if oob(blockContentStart) { continue } if !oob(blockContentStart+3) && content[blockContentStart:blockContentStart+3] == "if " { varStart = blockContentStart + 3 for content[varStart] == ' ' { varStart++ } } if ifStart == -1 && (oob(i-1) || content[i-1] != '{') { out += content[previousEnd+2 : i] } if invalidIf || oob(blockContentStart+5) || content[blockContentStart:blockContentStart+5] != "endif" { continue } ifEnd = i - 1 if ifTrue { toAppend, subErr := templateEmail(content[ifStart:ifEnd+1], variables, conditionals, values) out += toAppend if subErr != nil { err = subErr } ifTrue = false } } else if c == '}' { doubleBraced := !oob(i+1) && content[i+1] == '}' if doubleBraced { err = fmt.Errorf(`double braces ("}}") at position %d, use single brace only`, i) } if !oob(i-1) && content[i-1] == '}' { continue } if varStart != -1 { ifStart = i + 1 varEnd = i - 1 for !oob(varEnd) && content[varEnd] == ' ' { varEnd-- } varName = content[varStart : varEnd+1] positive := true if varName[0] == '!' { positive = false varName = varName[1:] } validVar := slices.Contains(conditionals, varName) if validVar { ifTrue = positive == truthy(values[varName]) } else { invalidIf = true ifStart, ifEnd = -1, -1 } varStart, varEnd = -1, -1 } blockContentEnd = i - 1 for content[blockContentEnd] == ' ' { blockContentEnd-- } previousEnd = i - 1 // Skip the extra brace if doubleBraced { previousEnd++ } if !oob(blockContentEnd-4) && !oob(blockContentEnd+1) && content[blockContentEnd-4:blockContentEnd+1] == "endif" && !invalidIf { continue } varName = content[blockContentStart : blockContentEnd+1] blockContentStart, blockContentEnd = -1, -1 blockRawStart = -1 if ifStart != -1 { continue } validVar := slices.Contains(variables, varName) if !validVar { out += "{" + varName + "}" continue } out += fmt.Sprint(values[varName]) } } if blockContentStart != -1 && blockContentEnd == -1 { err = fmt.Errorf(`incomplete block (single "{") near position %d`, blockContentStart) // Include the brace, maybe the user wants it. previousEnd = blockRawStart - 2 } if previousEnd+1 != len(content)-1 { out += content[previousEnd+2:] } if out == "" { return content, err } return out, err }