diff --git a/service/main.go b/service/main.go index de0a94ca..63713348 100644 --- a/service/main.go +++ b/service/main.go @@ -29,6 +29,7 @@ import ( "github.com/movsb/taoblog/service/modules/comment_notify" "github.com/movsb/taoblog/service/modules/renderers/exif" "github.com/movsb/taoblog/service/modules/renderers/friends" + "github.com/movsb/taoblog/service/modules/renderers/reminders" "github.com/movsb/taoblog/service/modules/search" theme_fs "github.com/movsb/taoblog/theme/modules/fs" "github.com/movsb/taorm" @@ -87,6 +88,8 @@ type Service struct { exifTask *exif.Task // 朋友头像数据缓存任务 friendsTask *friends.Task + // 提醒事项任务 + remindersTask *reminders.Task // 文章内容缓存。 // NOTE:缓存 Key 是跟文章编号和内容选项相关的(因为内容选项不同内容也就不同), @@ -231,6 +234,11 @@ func newService(ctx context.Context, cancel context.CancelFunc, cfg *config.Conf s.updatePostMetadataTime(int64(postID), time.Now()) }) + s.remindersTask = reminders.NewTask(s.GetPluginStorage(`reminders`), func(pid int64) { + s.deletePostContentCacheFor(pid) + s.updatePostMetadataTime(pid, time.Now()) + }) + s.certDaysLeft.Store(-1) s.domainExpirationDaysLeft.Store(-1) s.exporter = _NewExporter(s) diff --git a/service/modules/renderers/highlight/highlight.go b/service/modules/renderers/highlight/highlight.go index 36faba01..cf3228e0 100644 --- a/service/modules/renderers/highlight/highlight.go +++ b/service/modules/renderers/highlight/highlight.go @@ -14,9 +14,9 @@ import ( "github.com/yuin/goldmark/util" ) -//go:generate sass --style compressed style.scss style.css +//go:generate sass --no-source-map --style compressed style.scss style.css -//go:embed style.css style.css.map script.js +//go:embed style.css script.js var _root embed.FS func init() { diff --git a/service/modules/renderers/reminders/README.md b/service/modules/renderers/reminders/README.md new file mode 100644 index 00000000..edbd70b9 --- /dev/null +++ b/service/modules/renderers/reminders/README.md @@ -0,0 +1,12 @@ +# 提醒事项 + +主要用于长期的提醒事项备忘📝;例如:生日、纪念日。 + +## 格式 + +```reminder +title: 纪念日! +description: 详细的描述。 +dates: + start: 2014-12-24 +``` diff --git a/service/modules/renderers/reminders/reminder.go b/service/modules/renderers/reminders/reminder.go new file mode 100644 index 00000000..16966ffe --- /dev/null +++ b/service/modules/renderers/reminders/reminder.go @@ -0,0 +1,171 @@ +package reminders + +import ( + "bytes" + "embed" + "html/template" + "time" + + "github.com/movsb/taoblog/modules/utils" + "github.com/movsb/taoblog/modules/utils/dir" + dynamic "github.com/movsb/taoblog/service/modules/renderers/_dynamic" + "github.com/movsb/taoblog/theme/modules/sass" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" + "gopkg.in/yaml.v2" +) + +//go:generate sass --no-source-map style.scss style.css + +//go:embed reminder.html style.css +var _root embed.FS + +func init() { + dynamic.RegisterInit(func() { + dynamic.Dynamic[`reminders`] = dynamic.Content{ + Styles: []string{ + string(utils.Must1(_root.ReadFile(`style.css`))), + }, + } + sass.WatchDefaultAsync(string(dir.SourceAbsoluteDir())) + }) +} + +type Reminders struct { + task *Task + pid int +} + +func New(task *Task, pid int) *Reminders { + f := &Reminders{ + task: task, + pid: pid, + } + + return f +} + +type UserDate time.Time + +var layouts = [...]string{ + `2006-01-02`, +} + +// TODO 需要修改成服务器时间。 +var fixedZone = time.FixedZone(`fixed`, 8*60*60) + +func (u *UserDate) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + var outErr error + for _, layout := range layouts { + // TODO 需要区分 Parse 与 ParseInLocation + t, err := time.ParseInLocation(layout, s, fixedZone) + if err != nil { + outErr = err + continue + } + *u = UserDate(t) + } + return outErr +} + +type Reminder struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Dates struct { + Start UserDate `yaml:"start"` + } `yaml:"dates"` +} + +func (r *Reminder) Days() int { + return int(time.Since(time.Time(r.Dates.Start)).Hours() / 24) +} + +func (r *Reminder) Start() string { + return time.Time(r.Dates.Start).Format(`2006-01-02`) +} + +var _ interface { + parser.ASTTransformer + goldmark.Extender + renderer.NodeRenderer +} = (*Reminders)(nil) + +var tmpl = template.Must(template.New(`reminder`).Parse(string(utils.Must1(_root.ReadFile(`reminder.html`))))) + +func (r *Reminders) Extend(m goldmark.Markdown) { + m.Parser().AddOptions(parser.WithASTTransformers(util.Prioritized(r, 100))) + m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(r, 100))) +} + +type _ReminderRendererBlock struct { + ast.BaseBlock + ref *ast.FencedCodeBlock +} + +var _reminderCodeBLockKind = ast.NewNodeKind(`reminder_code_block`) + +func (b *_ReminderRendererBlock) Kind() ast.NodeKind { + return _reminderCodeBLockKind +} +func (b *_ReminderRendererBlock) Dump(source []byte, level int) { + b.ref.Dump(source, level) +} + +func (r *Reminders) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(_reminderCodeBLockKind, r.renderCodeBlock) +} + +// Transform implements parser.ASTTransformer. +func (r *Reminders) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { + rCodeBlocks := []*ast.FencedCodeBlock{} + ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering && n.Kind() == ast.KindFencedCodeBlock { + cb := n.(*ast.FencedCodeBlock) + if cb.Info != nil { + info := string(cb.Info.Segment.Value(reader.Source())) + if info == `reminder` { + rCodeBlocks = append(rCodeBlocks, cb) + } + } + } + return ast.WalkContinue, nil + }) + for _, cb := range rCodeBlocks { + cb.Parent().ReplaceChild(cb.Parent(), cb, &_ReminderRendererBlock{ + ref: cb, + }) + } +} + +func (r *Reminders) renderCodeBlock(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + n = n.(*_ReminderRendererBlock).ref + b := bytes.NewBuffer(nil) + for i := 0; i < n.Lines().Len(); i++ { + line := n.Lines().At(i) + b.Write(line.Value(source)) + } + y := b.Bytes() + + rm := Reminder{} + if err := yaml.UnmarshalStrict(y, &rm); err != nil { + return ast.WalkStop, err + } + + if err := tmpl.Execute(writer, &rm); err != nil { + return ast.WalkStop, err + } + + return ast.WalkContinue, nil +} diff --git a/service/modules/renderers/reminders/reminder.html b/service/modules/renderers/reminders/reminder.html new file mode 100644 index 00000000..3f5e7f11 --- /dev/null +++ b/service/modules/renderers/reminders/reminder.html @@ -0,0 +1,11 @@ +
+
+ {{ .Title }} +
+
+ {{ .Days }} +
+
+ 开始时间:{{ .Start }} +
+
diff --git a/service/modules/renderers/reminders/style.scss b/service/modules/renderers/reminders/style.scss new file mode 100644 index 00000000..78d0c63a --- /dev/null +++ b/service/modules/renderers/reminders/style.scss @@ -0,0 +1,19 @@ +.reminder { + border: 1px solid var(--border-color); + display: inline-block; + padding: .5em 1em; + margin: 10px; + + .title { + font-size: 1.5em; + font-weight: bold; + border-bottom: 1px solid var(--border-color); + text-align: center; + padding: 0 2em; + } + .days { + font-size: 3em; + color: var(--accent-color); + text-align: center; + } +} diff --git a/service/modules/renderers/reminders/task.go b/service/modules/renderers/reminders/task.go new file mode 100644 index 00000000..8fed6c69 --- /dev/null +++ b/service/modules/renderers/reminders/task.go @@ -0,0 +1,35 @@ +package reminders + +import ( + "time" + + "github.com/movsb/taoblog/modules/utils" +) + +type Task struct { + invalidate func(pid int64) + posts map[int64]struct{} +} + +func NewTask(storage utils.PluginStorage, invalidate func(pid int64)) *Task { + t := &Task{ + invalidate: invalidate, + } + go t.invalidatePosts() + return t +} + +func (t *Task) AddReminder(pid int64) { + t.posts[pid] = struct{}{} +} + +func (t *Task) invalidatePosts() { + for now := range time.Tick(time.Second) { + // TODO 这个时间可能不精确。比如进程挂起后? + if now.Hour() == 23 && now.Minute() == 59 && now.Second() == 59 { + for id := range t.posts { + t.invalidate(id) + } + } + } +} diff --git a/service/post.go b/service/post.go index 60e4a3ad..1297c091 100644 --- a/service/post.go +++ b/service/post.go @@ -32,6 +32,7 @@ import ( katex "github.com/movsb/taoblog/service/modules/renderers/math" "github.com/movsb/taoblog/service/modules/renderers/media_size" "github.com/movsb/taoblog/service/modules/renderers/media_tags" + "github.com/movsb/taoblog/service/modules/renderers/reminders" "github.com/movsb/taoblog/service/modules/renderers/rooted_path" "github.com/movsb/taoblog/service/modules/renderers/scoped_css" task_list "github.com/movsb/taoblog/service/modules/renderers/tasklist" @@ -317,6 +318,7 @@ func (s *Service) renderMarkdown(secure bool, postId, commentId int64, sourceTyp wikitable.New(), extension.GFM, extension.NewFootnote(extension.WithFootnoteBacklinkHTML(`^`)), + reminders.New(s.remindersTask, int(postId)), // 所有人禁止贴无效协议的链接。 invalid_scheme.New(),