@@ -24,7 +24,6 @@ import (
2424 "bytes"
2525 "fmt"
2626 "io"
27- "maps"
2827 "path/filepath"
2928 "regexp"
3029 "strings"
@@ -221,7 +220,7 @@ type Header struct {
221220 matcher * regexp.Regexp
222221
223222 author string
224- variables map [string ]any
223+ variables map [string ]Var
225224 yearMode YearMode
226225 commentStyle CommentStyle
227226}
@@ -230,14 +229,24 @@ var tmplFuncMap = template.FuncMap{
230229 "basename" : filepath .Base ,
231230}
232231
232+ // Var is a template variable.
233+ type Var struct {
234+ // Value is the variable value.
235+ Value string
236+
237+ // Regexp is a regexp used to match the variable value.
238+ // If empty, the regexp-escaped value of Value will be used.
239+ Regexp string
240+ }
241+
233242// HeaderOpts are the options for creating a license header.
234243type HeaderOpts struct {
235244 Template string
236245 Matcher string
237246 MatcherEscape bool
238247 Author string
239248 AuthorRegexp string
240- Variables map [string ]any
249+ Variables map [string ]Var
241250 YearMode YearMode
242251 CommentStyle CommentStyle
243252}
@@ -264,11 +273,23 @@ func NewHeader(opts HeaderOpts) (*Header, error) {
264273 "filename" : "test" ,
265274 "year" : "2025" ,
266275 }
267- maps . Copy (m , opts .Variables )
276+ addVariables (m , opts .Variables )
268277 if err = t .Execute (io .Discard , m ); err != nil {
269278 return nil , fmt .Errorf ("execute template: %w" , err )
270279 }
271280
281+ // Test compiling variable regexps.
282+ for name , v := range opts .Variables {
283+ switch v .Regexp {
284+ case "" :
285+ v .Regexp = regexp .QuoteMeta (v .Value )
286+ default :
287+ if _ , err = regexp .Compile (v .Regexp ); err != nil {
288+ return nil , fmt .Errorf ("compile %q regexp: %w" , name , err )
289+ }
290+ }
291+ }
292+
272293 // Create author regexp
273294 authorRegexpStr := opts .AuthorRegexp
274295 if authorRegexpStr == "" {
@@ -284,15 +305,15 @@ func NewHeader(opts HeaderOpts) (*Header, error) {
284305 mt , err := template .New ("" ).Funcs (tmplFuncMap ).
285306 Option ("missingkey=error" ).Parse (opts .Matcher )
286307 if err != nil {
287- return nil , fmt .Errorf ("new match template: %w" , err )
308+ return nil , fmt .Errorf ("new matcher template: %w" , err )
288309 }
289310 matcher , err = headerMatcher (mt , opts .MatcherEscape , authorRegexp , opts .Variables )
290311 if err != nil {
291- return nil , fmt .Errorf ("create header matcher (with match template) : %w" , err )
312+ return nil , fmt .Errorf ("create header matcher: %w" , err )
292313 }
293314 } else {
294- // If a match template wasn't provided, create a matcher using the
295- // header template (regexp-escaped).
315+ // If a matcher wasn't provided, create a matcher using the header
316+ // template (regexp-escaped).
296317 matcher , err = headerMatcher (t , true , authorRegexp , opts .Variables )
297318 if err != nil {
298319 return nil , fmt .Errorf ("create header matcher: %w" , err )
@@ -399,7 +420,7 @@ func (h *Header) render(filename, year string) (string, error) {
399420 "filename" : filename ,
400421 "year" : year ,
401422 }
402- maps . Copy (m , h .variables )
423+ addVariables (m , h .variables )
403424
404425 var b bytes.Buffer
405426 if err := h .tmpl .Execute (& b , m ); err != nil {
@@ -408,32 +429,47 @@ func (h *Header) render(filename, year string) (string, error) {
408429 return b .String (), nil
409430}
410431
411- func headerMatcher (tmpl * template.Template , escapeTmpl bool , authorRegexp * regexp.Regexp , variables map [string ]any ) (* regexp.Regexp , error ) {
412- m := map [string ]any {
413- "author" : "__AUTHOR__" ,
414- "filename" : "__FILENAME__" ,
415- "year" : "__YEAR__" ,
432+ func headerMatcher (tmpl * template.Template , escapeTmpl bool , authorRegexp * regexp.Regexp , variables map [string ]Var ) (* regexp.Regexp , error ) {
433+ m := map [string ]string {
434+ "author" : "__VAR_author__" ,
435+ "filename" : "__VAR_filename__" ,
436+ "year" : "__VAR_year__" ,
437+ }
438+ regexps := map [string ]string {
439+ "author" : authorRegexp .String (),
440+ "filename" : "(?P<filename>.+)" ,
441+ "year" : regexpYears .String (),
442+ }
443+ for k , v := range variables {
444+ m [k ] = "__VAR_" + k + "__"
445+ regexps [k ] = v .Regexp
416446 }
417- maps .Copy (m , variables )
418447
419448 // Execute matcher template.
420449 var b bytes.Buffer
421450 if err := tmpl .Execute (& b , m ); err != nil {
422451 return nil , fmt .Errorf ("execute template: %w" , err )
423452 }
424453 headerExpr := b .String ()
454+
455+ // Optionally escape the rendered template.
425456 if escapeTmpl {
426457 headerExpr = regexp .QuoteMeta (b .String ())
427458 }
428459
429- // Insert regexp matchers here, as there is a chance we escape the
430- // headerExpr above.
431- expr := strings .NewReplacer (
432- "__AUTHOR__" , authorRegexp .String (),
433- "__FILENAME__" , "(?P<filename>.+)" ,
434- "__YEAR__" , regexpYears .String (),
435- ).Replace (headerExpr )
460+ // Replace variable placeholders with regexp expressions.
461+ replacements := make ([]string , 0 , len (m )* 2 )
462+ for k , v := range m {
463+ replacements = append (replacements , v , regexps [k ])
464+ }
465+ headerExpr = strings .NewReplacer (replacements ... ).Replace (headerExpr )
466+
467+ // Compile header matcher regexp.
468+ return regexp .Compile (headerExpr )
469+ }
436470
437- // Compile header matching regexp.
438- return regexp .Compile (expr )
471+ func addVariables (m map [string ]any , vars map [string ]Var ) {
472+ for k , v := range vars {
473+ m [k ] = v .Value
474+ }
439475}
0 commit comments