1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
package transcoder
import (
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
"github.com/SayaAndy/saya-today-article-metadata-add/config"
"github.com/SayaAndy/saya-today-article-metadata-add/internal/draft"
"github.com/SayaAndy/saya-today-article-metadata-add/internal/storage"
)
type SayauzTranscoder struct {
category string
storage storage.StorageClient
}
func NewSayauzTranscoder(cfg any) (Transcoder, error) {
sayauzCfg, ok := cfg.(*config.SayauzConfig)
if !ok {
return nil, fmt.Errorf("invalid config for sayauz transcoder")
}
newClient, ok := storage.NewStorageClientMap[sayauzCfg.Storage.Type]
if !ok {
return nil, fmt.Errorf("unsupported storage type %q for sayauz transcoder", sayauzCfg.Storage.Type)
}
storageClient, err := newClient(&sayauzCfg.Storage)
if err != nil {
return nil, fmt.Errorf("init storage client: %w", err)
}
return &SayauzTranscoder{
category: sayauzCfg.Category,
storage: storageClient,
}, nil
}
func (t *SayauzTranscoder) Name() string { return "sayauz" }
func (t *SayauzTranscoder) Transcode(doc *draft.Document) error {
if doc.Metadata == nil {
return fmt.Errorf("draft %q has no frontmatter metadata", doc.SourcePath)
}
prodKey := t.category + "/" + doc.Codename + ".md"
// publishedTime is the first-publish time: reuse an existing value (from the
// draft frontmatter, else from the already-published object), otherwise stamp now.
inject := doc.Metadata.PublishedTime.IsZero()
if inject {
published := time.Time{}
if meta, err := t.storage.GetMetadata(prodKey); err == nil {
if v := meta["published-time"]; v != "" {
if parsed, err := time.Parse(time.RFC3339, v); err == nil {
published = parsed
}
}
} else {
slog.Warn("could not read existing metadata for publishedTime, stamping now",
slog.String("key", prodKey), slog.String("error", err.Error()))
}
if published.IsZero() {
published = time.Now()
}
doc.Metadata.PublishedTime = published
}
formatted := formatContent(doc.RawContent, doc.Metadata.PublishedTime, inject)
localPath := filepath.Join(filepath.Dir(doc.SourcePath), doc.Codename+".md")
if err := os.WriteFile(localPath, formatted, 0o644); err != nil {
return fmt.Errorf("write formatted file %q: %w", localPath, err)
}
if err := t.storage.Put(prodKey, formatted, doc.Metadata); err != nil {
return fmt.Errorf("upload %q: %w", prodKey, err)
}
slog.Info("sayauz published page",
slog.String("key", prodKey),
slog.String("local", localPath))
return nil
}
func (t *SayauzTranscoder) Finalize() error {
return t.storage.BuildIndex()
}
// formatContent renders a draft into its published form: standalone "..."
// separator lines become blank lines (trailing ones are dropped), and when
// inject is true a "publishedTime:" line is added after "actionDate:" in the
// frontmatter. Frontmatter, galleries and "---" rules are otherwise untouched.
func formatContent(raw []byte, published time.Time, inject bool) []byte {
lines := strings.Split(string(raw), "\n")
out := make([]string, 0, len(lines)+1)
inFrontmatter := false
frontmatterDone := false
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if !frontmatterDone {
if i == 0 && trimmed == "---" {
inFrontmatter = true
out = append(out, line)
continue
}
if inFrontmatter {
out = append(out, line)
switch {
case trimmed == "---":
inFrontmatter = false
frontmatterDone = true
case inject && strings.HasPrefix(trimmed, "actionDate:"):
out = append(out, "publishedTime: "+published.Format(time.RFC3339))
}
continue
}
}
if trimmed == "..." {
out = append(out, "")
continue
}
out = append(out, line)
}
for len(out) > 0 && out[len(out)-1] == "" {
out = out[:len(out)-1]
}
return []byte(strings.Join(out, "\n"))
}
|