741 lines
18 KiB
Go
741 lines
18 KiB
Go
package docfill
|
|
|
|
import (
|
|
"archive/zip"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
_ "modernc.org/sqlite"
|
|
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
type config struct {
|
|
Vars map[string]string `json:"vars"`
|
|
Data dataConfig `json:"data"`
|
|
Template templateConfig `json:"template"`
|
|
Output outputConfig `json:"output"`
|
|
}
|
|
|
|
type dataConfig struct {
|
|
Type string `json:"type"`
|
|
Path string `json:"path"`
|
|
Sheet string `json:"sheet"`
|
|
Query string `json:"query"`
|
|
Params []string `json:"params"`
|
|
HeaderRow int `json:"header_row"`
|
|
}
|
|
|
|
type templateConfig struct {
|
|
Path string `json:"path"`
|
|
}
|
|
|
|
type outputConfig struct {
|
|
Path string `json:"path"`
|
|
Overwrite bool `json:"overwrite"`
|
|
}
|
|
|
|
type RunOptions struct {
|
|
Vars map[string]string
|
|
}
|
|
|
|
type RunResult struct {
|
|
Files []string
|
|
}
|
|
|
|
var (
|
|
placeholderPattern = regexp.MustCompile(`\{\{([^{}<>]+)\}\}`)
|
|
docxParagraphPattern = regexp.MustCompile(`(?s)<w:p\b[^>]*>.*?</w:p>`)
|
|
docxTextNodePattern = regexp.MustCompile(`(?s)<w:t\b[^>]*>(.*?)</w:t>`)
|
|
docxGeneratedXMLTypes = map[string]struct{}{
|
|
".xml": {},
|
|
}
|
|
)
|
|
|
|
// Run executes one configured fill task.
|
|
func Run(configPath string) error {
|
|
_, err := RunWithOptions(configPath, RunOptions{})
|
|
return err
|
|
}
|
|
|
|
// RunWithOptions executes one configured fill task and returns generated files.
|
|
func RunWithOptions(configPath string, opts RunOptions) (RunResult, error) {
|
|
cfg, err := loadConfig(configPath)
|
|
if err != nil {
|
|
return RunResult{}, err
|
|
}
|
|
if err := validateConfig(cfg); err != nil {
|
|
return RunResult{}, err
|
|
}
|
|
|
|
vars := mergeVars(cfg.Vars, opts.Vars)
|
|
rows, err := readRowsWithVars(cfg.Data, vars)
|
|
if err != nil {
|
|
return RunResult{}, err
|
|
}
|
|
if len(rows) == 0 {
|
|
return RunResult{}, errors.New("data returned no rows")
|
|
}
|
|
|
|
result := RunResult{Files: make([]string, 0, len(rows))}
|
|
for i, row := range rows {
|
|
context := renderContext(vars, row, i+1)
|
|
if err := ensurePlaceholdersAvailable("output path", cfg.Output.Path, context); err != nil {
|
|
return RunResult{}, fmt.Errorf("render row %d: %w", i+1, err)
|
|
}
|
|
|
|
outputPath := replacePathPlaceholders(cfg.Output.Path, context)
|
|
if hasPlaceholder(outputPath) {
|
|
return RunResult{}, fmt.Errorf("render row %d: unresolved placeholder in output path %q", i+1, outputPath)
|
|
}
|
|
if !cfg.Output.Overwrite && fileExists(outputPath) {
|
|
return RunResult{}, fmt.Errorf("render row %d: output already exists: %s", i+1, outputPath)
|
|
}
|
|
if err := renderTemplate(cfg.Template.Path, outputPath, context); err != nil {
|
|
return RunResult{}, fmt.Errorf("render row %d: %w", i+1, err)
|
|
}
|
|
|
|
result.Files = append(result.Files, outputPath)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func loadConfig(path string) (config, error) {
|
|
if path == "" {
|
|
return config{}, errors.New("config path is required")
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return config{}, fmt.Errorf("read config: %w", err)
|
|
}
|
|
|
|
var cfg config
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return config{}, fmt.Errorf("parse config json: %w", err)
|
|
}
|
|
resolveConfigPaths(&cfg, filepath.Dir(absPath(path)))
|
|
return cfg, nil
|
|
}
|
|
|
|
func validateConfig(cfg config) error {
|
|
if cfg.Data.Type == "" {
|
|
return errors.New("data.type is required")
|
|
}
|
|
if cfg.Data.Path == "" {
|
|
return errors.New("data.path is required")
|
|
}
|
|
if cfg.Template.Path == "" {
|
|
return errors.New("template.path is required")
|
|
}
|
|
if cfg.Output.Path == "" {
|
|
return errors.New("output.path is required")
|
|
}
|
|
if cfg.Data.HeaderRow < 0 {
|
|
return errors.New("data.header_row must not be negative")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readRows(cfg dataConfig) ([]map[string]string, error) {
|
|
return readRowsWithVars(cfg, nil)
|
|
}
|
|
|
|
func readRowsWithVars(cfg dataConfig, vars map[string]string) ([]map[string]string, error) {
|
|
switch strings.ToLower(cfg.Type) {
|
|
case "excel", "xlsx":
|
|
return readExcelRows(cfg)
|
|
case "sqlite":
|
|
return readSQLiteRows(cfg, vars)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported data.type %q", cfg.Type)
|
|
}
|
|
}
|
|
|
|
func readExcelRows(cfg dataConfig) ([]map[string]string, error) {
|
|
f, err := excelize.OpenFile(cfg.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open excel data: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
sheet := cfg.Sheet
|
|
if sheet == "" {
|
|
sheet = f.GetSheetName(0)
|
|
}
|
|
if sheet == "" {
|
|
return nil, errors.New("excel data has no sheets")
|
|
}
|
|
|
|
rawRows, err := f.GetRows(sheet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read excel sheet %q: %w", sheet, err)
|
|
}
|
|
if len(rawRows) == 0 {
|
|
return nil, errors.New("excel data has no header row")
|
|
}
|
|
|
|
headerRow := cfg.HeaderRow
|
|
if headerRow == 0 {
|
|
headerRow = 1
|
|
}
|
|
if headerRow > len(rawRows) {
|
|
return nil, fmt.Errorf("excel header_row %d is outside sheet %q", headerRow, sheet)
|
|
}
|
|
|
|
headers := make([]string, len(rawRows[headerRow-1]))
|
|
for i, header := range rawRows[headerRow-1] {
|
|
headers[i] = strings.TrimSpace(header)
|
|
}
|
|
|
|
var rows []map[string]string
|
|
for _, raw := range rawRows[headerRow:] {
|
|
if rowIsEmpty(raw) {
|
|
continue
|
|
}
|
|
row := make(map[string]string)
|
|
for i, header := range headers {
|
|
if header == "" {
|
|
continue
|
|
}
|
|
if i < len(raw) {
|
|
row[header] = raw[i]
|
|
} else {
|
|
row[header] = ""
|
|
}
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
|
|
return rows, nil
|
|
}
|
|
|
|
func readSQLiteRows(cfg dataConfig, vars map[string]string) ([]map[string]string, error) {
|
|
if cfg.Query == "" {
|
|
return nil, errors.New("data.query is required for sqlite")
|
|
}
|
|
|
|
db, err := sql.Open("sqlite", cfg.Path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open sqlite data: %w", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
args, err := sqliteParams(cfg.Params, vars)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := db.Query(cfg.Query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query sqlite data: %w", err)
|
|
}
|
|
defer result.Close()
|
|
|
|
columns, err := result.Columns()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read sqlite columns: %w", err)
|
|
}
|
|
|
|
var rows []map[string]string
|
|
for result.Next() {
|
|
values := make([]any, len(columns))
|
|
scanTargets := make([]any, len(columns))
|
|
for i := range values {
|
|
scanTargets[i] = &values[i]
|
|
}
|
|
if err := result.Scan(scanTargets...); err != nil {
|
|
return nil, fmt.Errorf("scan sqlite row: %w", err)
|
|
}
|
|
|
|
row := make(map[string]string)
|
|
for i, column := range columns {
|
|
row[column] = stringify(values[i])
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
if err := result.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate sqlite rows: %w", err)
|
|
}
|
|
|
|
return rows, nil
|
|
}
|
|
|
|
func renderTemplate(templatePath, outputPath string, row map[string]string) error {
|
|
if err := validateTemplateFields(templatePath, row); err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
|
return fmt.Errorf("create output directory: %w", err)
|
|
}
|
|
|
|
switch strings.ToLower(filepath.Ext(templatePath)) {
|
|
case ".xlsx":
|
|
return renderXLSX(templatePath, outputPath, row)
|
|
case ".docx":
|
|
return renderDOCX(templatePath, outputPath, row)
|
|
default:
|
|
return fmt.Errorf("unsupported template extension %q", filepath.Ext(templatePath))
|
|
}
|
|
}
|
|
|
|
func renderXLSX(templatePath, outputPath string, row map[string]string) error {
|
|
f, err := excelize.OpenFile(templatePath)
|
|
if err != nil {
|
|
return fmt.Errorf("open xlsx template: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
for _, sheet := range f.GetSheetList() {
|
|
rows, err := f.GetRows(sheet)
|
|
if err != nil {
|
|
return fmt.Errorf("read template sheet %q: %w", sheet, err)
|
|
}
|
|
for r, values := range rows {
|
|
for c, value := range values {
|
|
if !strings.Contains(value, "{{") {
|
|
continue
|
|
}
|
|
cell, err := excelize.CoordinatesToCellName(c+1, r+1)
|
|
if err != nil {
|
|
return fmt.Errorf("cell name: %w", err)
|
|
}
|
|
rendered := replacePlaceholders(value, row)
|
|
if hasPlaceholder(rendered) {
|
|
return fmt.Errorf("unresolved placeholder in %s!%s", sheet, cell)
|
|
}
|
|
if err := f.SetCellValue(sheet, cell, rendered); err != nil {
|
|
return fmt.Errorf("set cell %s!%s: %w", sheet, cell, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := f.SaveAs(outputPath); err != nil {
|
|
return fmt.Errorf("save xlsx output: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func renderDOCX(templatePath, outputPath string, row map[string]string) error {
|
|
reader, err := zip.OpenReader(templatePath)
|
|
if err != nil {
|
|
return fmt.Errorf("open docx template: %w", err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
output, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("create docx output: %w", err)
|
|
}
|
|
|
|
writer := zip.NewWriter(output)
|
|
for _, source := range reader.File {
|
|
if err := copyDocxPart(writer, source, row); err != nil {
|
|
_ = writer.Close()
|
|
_ = output.Close()
|
|
return err
|
|
}
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
_ = output.Close()
|
|
return fmt.Errorf("close docx output: %w", err)
|
|
}
|
|
if err := output.Close(); err != nil {
|
|
return fmt.Errorf("close docx file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func copyDocxPart(writer *zip.Writer, source *zip.File, row map[string]string) error {
|
|
header := source.FileHeader
|
|
header.Name = source.Name
|
|
header.Method = source.Method
|
|
|
|
dest, err := writer.CreateHeader(&header)
|
|
if err != nil {
|
|
return fmt.Errorf("create docx part %s: %w", source.Name, err)
|
|
}
|
|
|
|
if source.FileInfo().IsDir() {
|
|
return nil
|
|
}
|
|
|
|
src, err := source.Open()
|
|
if err != nil {
|
|
return fmt.Errorf("open docx part %s: %w", source.Name, err)
|
|
}
|
|
defer src.Close()
|
|
|
|
data, err := io.ReadAll(src)
|
|
if err != nil {
|
|
return fmt.Errorf("read docx part %s: %w", source.Name, err)
|
|
}
|
|
if isDocxXMLPart(source.Name) {
|
|
rendered, err := renderDocxXML(string(data), row)
|
|
if err != nil {
|
|
return fmt.Errorf("render docx part %s: %w", source.Name, err)
|
|
}
|
|
if docxHasPlaceholder(rendered) {
|
|
return fmt.Errorf("unresolved placeholder in docx part %s", source.Name)
|
|
}
|
|
data = []byte(rendered)
|
|
}
|
|
|
|
if _, err := dest.Write(data); err != nil {
|
|
return fmt.Errorf("write docx part %s: %w", source.Name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func replacePlaceholders(text string, row map[string]string) string {
|
|
return replacePlaceholdersWith(text, row, func(value string) string {
|
|
return value
|
|
})
|
|
}
|
|
|
|
func replaceDocxPlaceholders(text string, row map[string]string) string {
|
|
return replacePlaceholdersWith(text, row, escapeXMLText)
|
|
}
|
|
|
|
func replacePathPlaceholders(text string, row map[string]string) string {
|
|
return replacePlaceholdersWith(text, row, sanitizePathValue)
|
|
}
|
|
|
|
func replacePlaceholdersWith(text string, row map[string]string, transform func(string) string) string {
|
|
return placeholderPattern.ReplaceAllStringFunc(text, func(match string) string {
|
|
key := placeholderKey(match)
|
|
value, ok := row[key]
|
|
if !ok {
|
|
return match
|
|
}
|
|
return transform(value)
|
|
})
|
|
}
|
|
|
|
func rowIsEmpty(row []string) bool {
|
|
for _, cell := range row {
|
|
if strings.TrimSpace(cell) != "" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func stringify(value any) string {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
return ""
|
|
case []byte:
|
|
return string(v)
|
|
case string:
|
|
return v
|
|
default:
|
|
return fmt.Sprint(v)
|
|
}
|
|
}
|
|
|
|
func mergeVars(configVars, optionVars map[string]string) map[string]string {
|
|
merged := make(map[string]string)
|
|
for key, value := range configVars {
|
|
merged[key] = value
|
|
}
|
|
for key, value := range optionVars {
|
|
merged[key] = value
|
|
}
|
|
return merged
|
|
}
|
|
|
|
func renderContext(vars, row map[string]string, rowNumber int) map[string]string {
|
|
context := make(map[string]string)
|
|
for key, value := range vars {
|
|
context[key] = value
|
|
}
|
|
for key, value := range row {
|
|
context[key] = value
|
|
}
|
|
context["_row"] = strconv.Itoa(rowNumber)
|
|
return context
|
|
}
|
|
|
|
func sqliteParams(params []string, vars map[string]string) ([]any, error) {
|
|
args := make([]any, 0, len(params))
|
|
for i, param := range params {
|
|
if err := ensurePlaceholdersAvailable(fmt.Sprintf("sqlite params[%d]", i), param, vars); err != nil {
|
|
return nil, err
|
|
}
|
|
rendered := replacePlaceholders(param, vars)
|
|
if hasPlaceholder(rendered) {
|
|
return nil, fmt.Errorf("unresolved placeholder in sqlite params[%d]", i)
|
|
}
|
|
args = append(args, rendered)
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
func validateTemplateFields(templatePath string, row map[string]string) error {
|
|
fields, err := templatePlaceholders(templatePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for field := range fields {
|
|
if _, ok := row[field]; !ok {
|
|
return fmt.Errorf("missing template field %q", field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func templatePlaceholders(templatePath string) (map[string]struct{}, error) {
|
|
switch strings.ToLower(filepath.Ext(templatePath)) {
|
|
case ".xlsx":
|
|
return xlsxPlaceholders(templatePath)
|
|
case ".docx":
|
|
return docxPlaceholders(templatePath)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported template extension %q", filepath.Ext(templatePath))
|
|
}
|
|
}
|
|
|
|
func xlsxPlaceholders(templatePath string) (map[string]struct{}, error) {
|
|
f, err := excelize.OpenFile(templatePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open xlsx template: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
fields := make(map[string]struct{})
|
|
for _, sheet := range f.GetSheetList() {
|
|
rows, err := f.GetRows(sheet)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read template sheet %q: %w", sheet, err)
|
|
}
|
|
for _, row := range rows {
|
|
for _, value := range row {
|
|
addPlaceholders(fields, value)
|
|
}
|
|
}
|
|
}
|
|
return fields, nil
|
|
}
|
|
|
|
func docxPlaceholders(templatePath string) (map[string]struct{}, error) {
|
|
reader, err := zip.OpenReader(templatePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open docx template: %w", err)
|
|
}
|
|
defer reader.Close()
|
|
|
|
fields := make(map[string]struct{})
|
|
for _, file := range reader.File {
|
|
if !strings.HasSuffix(strings.ToLower(file.Name), ".xml") {
|
|
continue
|
|
}
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open docx part %s: %w", file.Name, err)
|
|
}
|
|
data, readErr := io.ReadAll(rc)
|
|
closeErr := rc.Close()
|
|
if readErr != nil {
|
|
return nil, fmt.Errorf("read docx part %s: %w", file.Name, readErr)
|
|
}
|
|
if closeErr != nil {
|
|
return nil, fmt.Errorf("close docx part %s: %w", file.Name, closeErr)
|
|
}
|
|
xml := string(data)
|
|
addPlaceholders(fields, xml)
|
|
addDocxTextPlaceholders(fields, xml)
|
|
}
|
|
return fields, nil
|
|
}
|
|
|
|
func ensurePlaceholdersAvailable(label, text string, row map[string]string) error {
|
|
fields := make(map[string]struct{})
|
|
addPlaceholders(fields, text)
|
|
for field := range fields {
|
|
if _, ok := row[field]; !ok {
|
|
return fmt.Errorf("missing %s field %q", label, field)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addPlaceholders(fields map[string]struct{}, text string) {
|
|
matches := placeholderPattern.FindAllStringSubmatch(text, -1)
|
|
for _, match := range matches {
|
|
if len(match) < 2 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(match[1])
|
|
if key != "" {
|
|
fields[key] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
func placeholderKey(match string) string {
|
|
matches := placeholderPattern.FindStringSubmatch(match)
|
|
if len(matches) < 2 {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(matches[1])
|
|
}
|
|
|
|
func hasPlaceholder(text string) bool {
|
|
return placeholderPattern.MatchString(text)
|
|
}
|
|
|
|
func renderDocxXML(xml string, row map[string]string) (string, error) {
|
|
rendered := replaceDocxPlaceholders(xml, row)
|
|
return renderDocxSplitTextPlaceholders(rendered, row)
|
|
}
|
|
|
|
func renderDocxSplitTextPlaceholders(xml string, row map[string]string) (string, error) {
|
|
matches := docxParagraphPattern.FindAllStringIndex(xml, -1)
|
|
if len(matches) == 0 {
|
|
return renderDocxTextGroup(xml, row)
|
|
}
|
|
|
|
var out strings.Builder
|
|
cursor := 0
|
|
for _, match := range matches {
|
|
out.WriteString(xml[cursor:match[0]])
|
|
rendered, err := renderDocxTextGroup(xml[match[0]:match[1]], row)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
out.WriteString(rendered)
|
|
cursor = match[1]
|
|
}
|
|
out.WriteString(xml[cursor:])
|
|
return out.String(), nil
|
|
}
|
|
|
|
func renderDocxTextGroup(xml string, row map[string]string) (string, error) {
|
|
textMatches := docxTextNodePattern.FindAllStringSubmatchIndex(xml, -1)
|
|
if len(textMatches) == 0 {
|
|
return xml, nil
|
|
}
|
|
|
|
joined := joinedDocxText(xml, textMatches)
|
|
if !hasPlaceholder(joined) {
|
|
return xml, nil
|
|
}
|
|
if err := ensurePlaceholdersAvailable("docx text", joined, row); err != nil {
|
|
return "", err
|
|
}
|
|
rendered := replaceDocxPlaceholders(joined, row)
|
|
if hasPlaceholder(rendered) {
|
|
return "", errors.New("unresolved placeholder in docx text")
|
|
}
|
|
|
|
var out strings.Builder
|
|
cursor := 0
|
|
for i, match := range textMatches {
|
|
out.WriteString(xml[cursor:match[2]])
|
|
if i == 0 {
|
|
out.WriteString(rendered)
|
|
}
|
|
out.WriteString(xml[match[3]:match[1]])
|
|
cursor = match[1]
|
|
}
|
|
out.WriteString(xml[cursor:])
|
|
return out.String(), nil
|
|
}
|
|
|
|
func joinedDocxText(xml string, matches [][]int) string {
|
|
var joined strings.Builder
|
|
for _, match := range matches {
|
|
joined.WriteString(xml[match[2]:match[3]])
|
|
}
|
|
return joined.String()
|
|
}
|
|
|
|
func addDocxTextPlaceholders(fields map[string]struct{}, xml string) {
|
|
matches := docxParagraphPattern.FindAllStringIndex(xml, -1)
|
|
if len(matches) == 0 {
|
|
addPlaceholders(fields, joinedDocxText(xml, docxTextNodePattern.FindAllStringSubmatchIndex(xml, -1)))
|
|
return
|
|
}
|
|
for _, match := range matches {
|
|
paragraph := xml[match[0]:match[1]]
|
|
textMatches := docxTextNodePattern.FindAllStringSubmatchIndex(paragraph, -1)
|
|
addPlaceholders(fields, joinedDocxText(paragraph, textMatches))
|
|
}
|
|
}
|
|
|
|
func docxHasPlaceholder(xml string) bool {
|
|
if hasPlaceholder(xml) {
|
|
return true
|
|
}
|
|
fields := make(map[string]struct{})
|
|
addDocxTextPlaceholders(fields, xml)
|
|
return len(fields) > 0
|
|
}
|
|
|
|
func escapeXMLText(value string) string {
|
|
replacer := strings.NewReplacer(
|
|
"&", "&",
|
|
"<", "<",
|
|
">", ">",
|
|
)
|
|
return replacer.Replace(value)
|
|
}
|
|
|
|
func isDocxXMLPart(name string) bool {
|
|
_, ok := docxGeneratedXMLTypes[strings.ToLower(filepath.Ext(name))]
|
|
return ok
|
|
}
|
|
|
|
func sanitizePathValue(value string) string {
|
|
replacer := strings.NewReplacer(
|
|
"/", "_",
|
|
"\\", "_",
|
|
":", "_",
|
|
"*", "_",
|
|
"?", "_",
|
|
`"`, "_",
|
|
"<", "_",
|
|
">", "_",
|
|
"|", "_",
|
|
)
|
|
return strings.TrimSpace(replacer.Replace(value))
|
|
}
|
|
|
|
func fileExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
func resolveConfigPaths(cfg *config, baseDir string) {
|
|
cfg.Data.Path = resolvePath(baseDir, cfg.Data.Path)
|
|
cfg.Template.Path = resolvePath(baseDir, cfg.Template.Path)
|
|
cfg.Output.Path = resolvePath(baseDir, cfg.Output.Path)
|
|
}
|
|
|
|
func resolvePath(baseDir, path string) string {
|
|
if path == "" || filepath.IsAbs(path) {
|
|
return path
|
|
}
|
|
return filepath.Clean(filepath.Join(baseDir, path))
|
|
}
|
|
|
|
func absPath(path string) string {
|
|
abs, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return path
|
|
}
|
|
return abs
|
|
}
|