github.com/coveo/gotemplate@v2.7.7+incompatible/template/template.go (about)

     1  package template
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"reflect"
     9  	"strings"
    10  	"sync"
    11  	"text/template"
    12  
    13  	"github.com/coveo/gotemplate/collections"
    14  	"github.com/coveo/gotemplate/utils"
    15  	logging "github.com/op/go-logging"
    16  )
    17  
    18  // String is an alias to collections.String
    19  type String = collections.String
    20  
    21  var templateMutex sync.Mutex
    22  
    23  // Template let us extend the functionalities of base go template library.
    24  type Template struct {
    25  	*template.Template
    26  	TempFolder     string
    27  	substitutes    []utils.RegexReplacer
    28  	context        interface{}
    29  	delimiters     []string
    30  	parent         *Template
    31  	folder         string
    32  	children       map[string]*Template
    33  	aliases        funcTableMap
    34  	functions      funcTableMap
    35  	options        OptionsSet
    36  	optionsEnabled OptionsSet
    37  }
    38  
    39  // Environment variables that could be defined to override default behaviors.
    40  const (
    41  	EnvAcceptNoValue    = "GOTEMPLATE_NO_VALUE"
    42  	EnvStrictErrorCheck = "GOTEMPLATE_STRICT_ERROR"
    43  	EnvSubstitutes      = "GOTEMPLATE_SUBSTITUTES"
    44  	EnvDebug            = "GOTEMPLATE_DEBUG"
    45  	EnvExtensionPath    = "GOTEMPLATE_PATH"
    46  	// TODO: Deprecated, to remove in future version
    47  	EnvDeprecatedAssign = "GOTEMPLATE_DEPRECATED_ASSIGN"
    48  )
    49  
    50  const (
    51  	noGoTemplate       = "no-gotemplate!"
    52  	noRazor            = "no-razor!"
    53  	explicitGoTemplate = "gotemplate!"
    54  )
    55  
    56  // Common variables
    57  var (
    58  	// ExtensionDepth the depth level of search of gotemplate extension from the current directory (default = 2).
    59  	ExtensionDepth = 2
    60  	toStrings      = collections.ToStrings
    61  	acceptNoValue  = String(os.Getenv(EnvAcceptNoValue)).ParseBool()
    62  	strictError    = String(os.Getenv(EnvStrictErrorCheck)).ParseBool()
    63  	Print          = utils.ColorPrint
    64  	Printf         = utils.ColorPrintf
    65  	Println        = utils.ColorPrintln
    66  	ErrPrintf      = utils.ColorErrorPrintf
    67  	ErrPrintln     = utils.ColorErrorPrintln
    68  	ErrPrint       = utils.ColorErrorPrint
    69  )
    70  
    71  // IsRazor determines if the supplied code appears to have Razor code (using default delimiters).
    72  func IsRazor(code string) bool { return strings.Contains(code, "@") }
    73  
    74  // IsCode determines if the supplied code appears to have gotemplate code (using default delimiters).
    75  func IsCode(code string) bool {
    76  	return IsRazor(code) || strings.Contains(code, "{{") || strings.Contains(code, "}}")
    77  }
    78  
    79  // NewTemplate creates an Template object with default initialization.
    80  func NewTemplate(folder string, context interface{}, delimiters string, options OptionsSet, substitutes ...string) (result *Template, err error) {
    81  	defer func() {
    82  		if rec := recover(); rec != nil {
    83  			result, err = nil, fmt.Errorf("%v", rec)
    84  		}
    85  	}()
    86  	t := Template{Template: template.New("Main")}
    87  	must(t.Parse(""))
    88  	t.options = iif(options != nil, options, DefaultOptions()).(OptionsSet)
    89  	if acceptNoValue {
    90  		t.options[AcceptNoValue] = true
    91  	}
    92  	if strictError {
    93  		t.options[StrictErrorCheck] = true
    94  	}
    95  	t.optionsEnabled = make(OptionsSet)
    96  	t.folder, _ = filepath.Abs(iif(folder != "", folder, utils.Pwd()).(string))
    97  	t.context = iif(context != nil, context, collections.CreateDictionary())
    98  	t.aliases = make(funcTableMap)
    99  	t.delimiters = []string{"{{", "}}", "@"}
   100  
   101  	// Set the regular expression replacements
   102  	baseSubstitutesRegex := []string{`/(?m)^\s*#!\s*$/`}
   103  	if substitutesFromEnv := os.Getenv(EnvSubstitutes); substitutesFromEnv != "" {
   104  		baseSubstitutesRegex = append(baseSubstitutesRegex, strings.Split(substitutesFromEnv, "\n")...)
   105  	}
   106  	t.substitutes = utils.InitReplacers(append(baseSubstitutesRegex, substitutes...)...)
   107  
   108  	if t.options[Extension] {
   109  		t.initExtension()
   110  	}
   111  
   112  	// Set the options supplied by caller
   113  	t.init("")
   114  	if delimiters != "" {
   115  		for i, delimiter := range strings.Split(delimiters, ",") {
   116  			if i == len(t.delimiters) {
   117  				return nil, fmt.Errorf("Invalid delimiters '%s', must be a maximum of three comma separated parts", delimiters)
   118  			}
   119  			if delimiter != "" {
   120  				t.delimiters[i] = delimiter
   121  			}
   122  		}
   123  	}
   124  	return &t, nil
   125  }
   126  
   127  // MustNewTemplate creates an Template object with default initialization.
   128  // It panics if an error occurs.
   129  func MustNewTemplate(folder string, context interface{}, delimiters string, options OptionsSet, substitutes ...string) *Template {
   130  	return must(NewTemplate(folder, context, delimiters, options, substitutes...)).(*Template)
   131  }
   132  
   133  // GetNewContext returns a distint context for each folder.
   134  func (t Template) GetNewContext(folder string, useCache bool) *Template {
   135  	folder = iif(folder != "", folder, t.folder).(string)
   136  	if context, found := t.children[folder]; useCache && found {
   137  		return context
   138  	}
   139  
   140  	newTemplate := Template(t)
   141  	newTemplate.Template = template.New(folder)
   142  	newTemplate.init(folder)
   143  	newTemplate.parent = &t
   144  	newTemplate.addFunctions(t.aliases)
   145  	newTemplate.importTemplates(t)
   146  	newTemplate.options = make(OptionsSet)
   147  	// We duplicate the options because the new context may alter them afterwhile and
   148  	// it should not modify the original values.
   149  	for k, v := range t.options {
   150  		newTemplate.options[k] = v
   151  	}
   152  
   153  	if !useCache {
   154  		return &newTemplate
   155  	}
   156  	// We register the new template as a child of the main template
   157  	t.children[folder] = &newTemplate
   158  	return t.children[folder]
   159  }
   160  
   161  // IsCode determines if the supplied code appears to have gotemplate code.
   162  func (t Template) IsCode(code string) bool {
   163  	return !strings.Contains(code, noGoTemplate) && (t.IsRazor(code) || strings.Contains(code, t.LeftDelim()) || strings.Contains(code, t.RightDelim()))
   164  }
   165  
   166  // IsRazor determines if the supplied code appears to have Razor code.
   167  func (t Template) IsRazor(code string) bool {
   168  	return strings.Contains(code, t.RazorDelim()) && !strings.Contains(code, noGoTemplate) && !strings.Contains(code, noRazor)
   169  }
   170  
   171  // LeftDelim returns the left delimiter.
   172  func (t Template) LeftDelim() string { return t.delimiters[0] }
   173  
   174  // RightDelim returns the right delimiter.
   175  func (t Template) RightDelim() string { return t.delimiters[1] }
   176  
   177  // RazorDelim returns the razor delimiter.
   178  func (t Template) RazorDelim() string { return t.delimiters[2] }
   179  
   180  // SetOption allows setting of template option after initialization.
   181  func (t *Template) SetOption(option Options, value bool) { t.options[option] = value }
   182  
   183  func (t Template) isTemplate(file string) bool {
   184  	for i := range templateExt {
   185  		if strings.HasSuffix(file, templateExt[i]) {
   186  			return true
   187  		}
   188  	}
   189  	return false
   190  }
   191  
   192  func (t *Template) initExtension() {
   193  	ext := t.GetNewContext("", false)
   194  	ext.options = DefaultOptions()
   195  
   196  	// We temporary set the logging level one grade lower
   197  	logLevel := logging.GetLevel(logger)
   198  	logging.SetLevel(logLevel-1, logger)
   199  	defer func() { logging.SetLevel(logLevel, logger) }()
   200  
   201  	var extensionfiles []string
   202  	if extensionFolders := strings.TrimSpace(os.Getenv(EnvExtensionPath)); extensionFolders != "" {
   203  		for _, path := range strings.Split(extensionFolders, string(os.PathListSeparator)) {
   204  			if path != "" {
   205  				files, _ := utils.FindFilesMaxDepth(path, ExtensionDepth, false, "*.gte")
   206  				extensionfiles = append(extensionfiles, files...)
   207  			}
   208  		}
   209  	}
   210  	extensionfiles = append(extensionfiles, utils.MustFindFilesMaxDepth(ext.folder, ExtensionDepth, false, "*.gte")...)
   211  
   212  	// Retrieve the template extension files
   213  	for _, file := range extensionfiles {
   214  		// We just load all the template files available to ensure that all template definition are loaded
   215  		// We do not use ParseFiles because it names the template with the base name of the file
   216  		// which result in overriding templates with the same base name in different folders.
   217  		content := string(must(ioutil.ReadFile(file)).([]byte))
   218  
   219  		// We execute the content, but we ignore errors. The goal is only to register the sub templates and aliases properly
   220  		// We also do not ask to clone the context as we wish to let extension to be able to alter the supplied context
   221  		if _, err := ext.processContentInternal(content, file, nil, 0, false); err != nil {
   222  			log.Error(err)
   223  		}
   224  	}
   225  
   226  	// Add the children contexts to the main context
   227  	for _, context := range ext.children {
   228  		t.importTemplates(*context)
   229  	}
   230  
   231  	// We reset the list of templates
   232  	t.children = make(map[string]*Template)
   233  }
   234  
   235  // Initialize a new template with same attributes as the current context.
   236  func (t *Template) init(folder string) {
   237  	if folder != "" {
   238  		t.folder, _ = filepath.Abs(folder)
   239  	}
   240  	t.addFuncs()
   241  	t.Parse("")
   242  	t.children = make(map[string]*Template)
   243  	t.Delims(t.delimiters[0], t.delimiters[1])
   244  	t.setConstant(false, "\n", "NL", "CR", "NEWLINE")
   245  	t.setConstant(false, true, "true")
   246  	t.setConstant(false, false, "false")
   247  	t.setConstant(false, nil, "null")
   248  }
   249  
   250  func (t *Template) setConstant(stopOnFirst bool, value interface{}, names ...string) {
   251  	c, err := collections.TryAsDictionary(t.context)
   252  	if err != nil {
   253  		return
   254  	}
   255  
   256  	context := c.AsMap()
   257  	for i := range names {
   258  		if val, isSet := context[names[i]]; !isSet {
   259  			context[names[i]] = value
   260  			if stopOnFirst {
   261  				return
   262  			}
   263  		} else if isSet && reflect.DeepEqual(value, val) {
   264  			return
   265  		}
   266  	}
   267  }
   268  
   269  // Import templates from another template.
   270  func (t *Template) importTemplates(source Template) {
   271  	for _, subTemplate := range source.Templates() {
   272  		if subTemplate.Name() != subTemplate.ParseName {
   273  			t.AddParseTree(subTemplate.Name(), subTemplate.Tree)
   274  		}
   275  	}
   276  }