mark/includes/templates.go
Manuel Rüger fef39dc1e0 fix: preserve include directive in output on error in ProcessIncludes
All error paths in the ReplaceAllFunc callback returned nil, which
ReplaceAllFunc treats as an empty replacement, silently erasing the
guard (if err != nil) fired, every subsequent include in the file
was also erased. Return spec (the original directive bytes) instead
so failed includes are preserved and subsequent ones are not lost.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00

177 lines
3.2 KiB
Go

package includes
import (
"bytes"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"go.yaml.in/yaml/v3"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)
// <!-- Include: <template path>
//
// (Delims: (none | "<left>","<right>"))?
// <optional yaml data> -->
var reIncludeDirective = regexp.MustCompile(
`(?s)` +
`<!--\s*Include:\s*(?P<template>.+?)\s*` +
`(?:\n\s*Delims:\s*(?:(none|"(?P<left>.*?)"\s*,\s*"(?P<right>.*?)")))?\s*` +
`(?:\n(?P<config>.*?))?-->`,
)
func LoadTemplate(
base string,
includePath string,
path string,
left string,
right string,
templates *template.Template,
) (*template.Template, error) {
var (
name = strings.TrimSuffix(path, filepath.Ext(path))
facts = karma.Describe("name", name)
)
if template := templates.Lookup(name); template != nil {
return template, nil
}
var body []byte
body, err := os.ReadFile(filepath.Join(base, path))
if err != nil {
if includePath != "" {
body, err = os.ReadFile(filepath.Join(includePath, path))
}
if err != nil {
err = facts.Format(
err,
"unable to read template file",
)
return nil, err
}
}
body = bytes.ReplaceAll(
body,
[]byte("\r\n"),
[]byte("\n"),
)
templates, err = templates.New(name).Delims(left, right).Parse(string(body))
if err != nil {
err = facts.Format(
err,
"unable to parse template",
)
return nil, err
}
return templates, nil
}
func ProcessIncludes(
base string,
includePath string,
contents []byte,
templates *template.Template,
) (*template.Template, []byte, bool, error) {
vardump := func(
facts *karma.Context,
data map[string]interface{},
) *karma.Context {
for key, value := range data {
key = "var " + key
facts = facts.Describe(
key,
strings.ReplaceAll(
fmt.Sprint(value),
"\n",
"\n"+strings.Repeat(" ", len(key)+2),
),
)
}
return facts
}
var (
recurse bool
err error
)
contents = reIncludeDirective.ReplaceAllFunc(
contents,
func(spec []byte) []byte {
if err != nil {
return spec
}
groups := reIncludeDirective.FindSubmatch(spec)
var (
path = string(groups[1])
delimsNone = string(groups[2])
left = string(groups[3])
right = string(groups[4])
config = groups[5]
data = map[string]interface{}{}
facts = karma.Describe("path", path)
)
if delimsNone == "none" {
left = "\x00"
right = "\x01"
}
err = yaml.Unmarshal(config, &data)
if err != nil {
err = facts.
Describe("config", string(config)).
Format(
err,
"unable to unmarshal template data config",
)
return spec
}
log.Tracef(vardump(facts, data), "including template %q", path)
templates, err = LoadTemplate(base, includePath, path, left, right, templates)
if err != nil {
err = facts.Format(err, "unable to load template")
return spec
}
var buffer bytes.Buffer
err = templates.Execute(&buffer, data)
if err != nil {
err = vardump(facts, data).Format(
err,
"unable to execute template",
)
return spec
}
recurse = true
return buffer.Bytes()
},
)
return templates, contents, recurse, err
}