go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/text/stringtemplate/template.go (about)

     1  // Copyright 2017 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package stringtemplate implements Python string.Template-like substitution.
    16  package stringtemplate
    17  
    18  import (
    19  	"regexp"
    20  	"strings"
    21  
    22  	"go.chromium.org/luci/common/errors"
    23  )
    24  
    25  const idPattern = "[_a-z][_a-z0-9]*"
    26  
    27  // We're looking for:
    28  //
    29  // Submatch indices:
    30  // [0:1] Full match
    31  // [2:3] $$ (escaped)
    32  // [4:5] $key (without braces)
    33  // [6:7] ${key} (with braces)
    34  // [8:9] $... (Invalid)
    35  var namedFormatMatcher = regexp.MustCompile(
    36  	`\$(?:` +
    37  		`(\$)|` + // Escaped ($$)
    38  		`(` + idPattern + `)|` + // Without braces: $key
    39  		`(?:\{(` + idPattern + `)\})|` + // With braces: ${key}
    40  		`(.*)` + // Invalid
    41  		`)`)
    42  
    43  // Resolve resolves substitutions in v using the supplied substitution map,
    44  // subst.
    45  //
    46  // A substitution can have the form:
    47  //
    48  //	$key
    49  //	${key}
    50  //
    51  // The substitution can also be escaped using a second "$", such as "$$".
    52  //
    53  // If the string includes an erroneous substitution, or if a referenced
    54  // template variable isn't included in the "substitutions" map, Resolve will
    55  // return an error.
    56  func Resolve(v string, subst map[string]string) (string, error) {
    57  	smi := namedFormatMatcher.FindAllStringSubmatchIndex(v, -1)
    58  	if len(smi) == 0 {
    59  		// No substitutions.
    60  		return v, nil
    61  	}
    62  
    63  	var (
    64  		parts = make([]string, 0, (len(smi)*2)+1)
    65  		pos   = 0
    66  	)
    67  
    68  	for _, match := range smi {
    69  		key := ""
    70  		switch {
    71  		case match[8] >= 0:
    72  			// Invalid.
    73  			return "", errors.Reason("invalid template: %q", v).Err()
    74  
    75  		case match[2] >= 0:
    76  			// Escaped.
    77  			parts = append(parts, v[pos:match[2]])
    78  			pos = match[3]
    79  			continue
    80  
    81  		case match[4] >= 0:
    82  			// Key (without braces)
    83  			key = v[match[4]:match[5]]
    84  		case match[6] >= 0:
    85  			// Key (with braces)
    86  			key = v[match[6]:match[7]]
    87  
    88  		default:
    89  			panic("impossible")
    90  		}
    91  
    92  		// Add anything in between the previous match and the current. If our match
    93  		// includes a non-escape character, add that too.
    94  		if pos < match[0] {
    95  			parts = append(parts, v[pos:match[0]])
    96  		}
    97  		pos = match[1]
    98  
    99  		subst, ok := subst[key]
   100  		if !ok {
   101  			return "", errors.Reason("no substitution for %q", key).Err()
   102  		}
   103  		parts = append(parts, subst)
   104  	}
   105  
   106  	// Append any trailing string.
   107  	parts = append(parts, v[pos:])
   108  
   109  	// Join the parts.
   110  	return strings.Join(parts, ""), nil
   111  }