The presets move from app.js to presets.json, built into the binary and served by /api/presets. PRESETS_FILE (default /data/presets.json) replaces the list when present; it is read again each time Settings opens and the built-in list is used if it is invalid. docs/presets.md (EN/FR) explains each preset and the file format. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
111 lines
3.0 KiB
Go
111 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// Built-in tag presets, used when no presets file is found (PRESETS_FILE,
|
|
// /data/presets.json by default). See docs/presets.md.
|
|
//
|
|
//go:embed presets.json
|
|
var builtinPresets []byte
|
|
|
|
// i18nText is a text per language ({"en": "...", "fr": "..."}) or one plain
|
|
// string for every language.
|
|
type i18nText map[string]string
|
|
|
|
func (t *i18nText) UnmarshalJSON(b []byte) error {
|
|
var s string
|
|
if json.Unmarshal(b, &s) == nil {
|
|
*t = i18nText{"en": s}
|
|
return nil
|
|
}
|
|
var m map[string]string
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
return errors.New("expected a string or an object of strings per language")
|
|
}
|
|
*t = m
|
|
return nil
|
|
}
|
|
|
|
type PresetTag struct {
|
|
Label i18nText `json:"label,omitempty"`
|
|
Pattern string `json:"pattern"`
|
|
Color string `json:"color"`
|
|
Regex *bool `json:"regex,omitempty"` // regular expression unless false
|
|
WholeWord bool `json:"wholeWord,omitempty"`
|
|
CaseSensitive bool `json:"caseSensitive,omitempty"`
|
|
}
|
|
|
|
type Preset struct {
|
|
ID string `json:"id"`
|
|
Name i18nText `json:"name"`
|
|
Tags []PresetTag `json:"tags"`
|
|
}
|
|
|
|
type PresetGroup struct {
|
|
Group i18nText `json:"group"`
|
|
Presets []Preset `json:"presets"`
|
|
}
|
|
|
|
// parsePresets decodes and checks a presets file with the same rules as the
|
|
// tags, so that every preset can be added as is.
|
|
func parsePresets(b []byte) ([]PresetGroup, error) {
|
|
var groups []PresetGroup
|
|
if err := json.Unmarshal(b, &groups); err != nil {
|
|
return nil, err
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, g := range groups {
|
|
for _, p := range g.Presets {
|
|
if p.ID == "" || seen[p.ID] {
|
|
return nil, fmt.Errorf("preset %q: missing or duplicate id", p.ID)
|
|
}
|
|
seen[p.ID] = true
|
|
if len(p.Tags) == 0 {
|
|
return nil, fmt.Errorf("preset %q: no tags", p.ID)
|
|
}
|
|
for i, pt := range p.Tags {
|
|
t := Tag{Pattern: pt.Pattern, Color: pt.Color, Regex: pt.Regex == nil || *pt.Regex}
|
|
if err := t.validate(); err != nil {
|
|
return nil, fmt.Errorf("preset %q, tag %d: %w", p.ID, i+1, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
// presetsResponse is what GET /api/presets returns.
|
|
type presetsResponse struct {
|
|
Source string `json:"source"` // "file" or "builtin"
|
|
File string `json:"file"`
|
|
Error string `json:"error,omitempty"` // the file is invalid: built-in presets are used
|
|
Groups []PresetGroup `json:"groups"`
|
|
}
|
|
|
|
// loadPresets reads the presets file on every call, so that edits apply
|
|
// without a restart, and falls back to the built-in presets.
|
|
func loadPresets(path string) presetsResponse {
|
|
res := presetsResponse{Source: "builtin", File: path}
|
|
if path != "" {
|
|
b, err := os.ReadFile(path)
|
|
switch {
|
|
case err == nil:
|
|
if res.Groups, err = parsePresets(b); err == nil {
|
|
res.Source = "file"
|
|
return res
|
|
}
|
|
res.Error = err.Error()
|
|
case !errors.Is(err, os.ErrNotExist):
|
|
res.Error = err.Error()
|
|
}
|
|
}
|
|
res.Groups, _ = parsePresets(builtinPresets) // checked by the tests
|
|
return res
|
|
}
|