2025-07-29 12:44:01 +01:00
|
|
|
// Package notes implements a data structure for reasoning about and rendering
|
2025-01-26 22:23:42 +00:00
|
|
|
// markdown notes
|
|
|
|
|
package notes
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
2025-06-27 22:56:50 +01:00
|
|
|
"encoding/base64"
|
2025-07-29 12:44:01 +01:00
|
|
|
"errors"
|
2025-01-26 22:23:42 +00:00
|
|
|
"fmt"
|
2025-01-27 22:28:18 +00:00
|
|
|
"html/template"
|
2025-01-26 22:23:42 +00:00
|
|
|
"log"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
2025-06-01 21:27:08 +01:00
|
|
|
"forgejo.gwairfelin.com/max/gonotes/internal/conf"
|
2025-07-25 13:44:21 +01:00
|
|
|
markdown "github.com/teekennedy/goldmark-markdown"
|
2025-01-26 22:23:42 +00:00
|
|
|
"github.com/yuin/goldmark"
|
2025-07-25 13:44:21 +01:00
|
|
|
"github.com/yuin/goldmark/ast"
|
2025-01-27 22:28:18 +00:00
|
|
|
"github.com/yuin/goldmark/extension"
|
2025-07-25 13:44:21 +01:00
|
|
|
astext "github.com/yuin/goldmark/extension/ast"
|
|
|
|
|
"github.com/yuin/goldmark/parser"
|
|
|
|
|
"github.com/yuin/goldmark/text"
|
|
|
|
|
"github.com/yuin/goldmark/util"
|
2025-01-26 22:23:42 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Note is the central data structure. It can be Saved, Rendered and Loaded
|
|
|
|
|
// using the Save, Render and LoadNote functions.
|
|
|
|
|
type Note struct {
|
|
|
|
|
Title string
|
2025-01-27 22:28:18 +00:00
|
|
|
BodyRendered template.HTML
|
2025-01-26 22:23:42 +00:00
|
|
|
Body []byte
|
2025-07-30 13:41:03 +01:00
|
|
|
Owner string
|
2025-01-26 22:23:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fmtPath(path string) string {
|
|
|
|
|
return fmt.Sprintf("%s.%s", path, conf.Conf.Extension)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Save a note to a path derived from the title
|
|
|
|
|
func (n *Note) Save() error {
|
2025-07-30 13:41:03 +01:00
|
|
|
filename := filepath.Join(conf.Conf.NotesDir, n.Owner, fmtPath(n.EncodedTitle()))
|
2025-01-26 22:23:42 +00:00
|
|
|
return os.WriteFile(filename, n.Body, 0600)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render the markdown content of the note to HTML
|
|
|
|
|
func (n *Note) Render() {
|
|
|
|
|
var buf bytes.Buffer
|
2025-01-27 22:28:18 +00:00
|
|
|
md := goldmark.New(
|
|
|
|
|
goldmark.WithExtensions(
|
|
|
|
|
extension.TaskList,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
err := md.Convert(n.Body, &buf)
|
2025-01-26 22:23:42 +00:00
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
2025-01-27 22:28:18 +00:00
|
|
|
|
|
|
|
|
n.BodyRendered = template.HTML(buf.String())
|
2025-01-26 22:23:42 +00:00
|
|
|
}
|
|
|
|
|
|
2025-07-25 13:44:21 +01:00
|
|
|
// LoadNote from the disk. The path is derived from the title
|
2025-07-30 13:41:03 +01:00
|
|
|
func LoadNote(owner string, encodedTitle string) (*Note, error) {
|
|
|
|
|
filename := filepath.Join(conf.Conf.NotesDir, owner, fmtPath(encodedTitle))
|
2025-01-26 22:23:42 +00:00
|
|
|
body, err := os.ReadFile(filename)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2025-06-27 22:56:50 +01:00
|
|
|
title := DecodeTitle(encodedTitle)
|
2025-07-30 13:41:03 +01:00
|
|
|
return &Note{Title: title, Body: body, Owner: owner}, nil
|
2025-01-26 22:23:42 +00:00
|
|
|
}
|
2025-06-18 21:40:42 +01:00
|
|
|
|
2025-07-30 13:41:03 +01:00
|
|
|
func DeleteNote(owner string, title string) error {
|
|
|
|
|
filename := filepath.Join(conf.Conf.NotesDir, owner, fmtPath(title))
|
2025-06-18 21:40:42 +01:00
|
|
|
return os.Remove(filename)
|
|
|
|
|
}
|
2025-06-27 22:56:50 +01:00
|
|
|
|
|
|
|
|
func (n *Note) EncodedTitle() string {
|
|
|
|
|
return base64.StdEncoding.EncodeToString([]byte(n.Title))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 13:44:21 +01:00
|
|
|
type toggleCheckboxTransformer struct{ nthBox int }
|
|
|
|
|
|
|
|
|
|
func (t *toggleCheckboxTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
|
|
|
|
|
boxesFound := 0
|
|
|
|
|
|
|
|
|
|
// Walk to find checkbox, toggle checked status
|
|
|
|
|
ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
|
|
|
if !entering {
|
|
|
|
|
return ast.WalkContinue, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
box, ok := n.(*astext.TaskCheckBox)
|
|
|
|
|
if !ok {
|
|
|
|
|
return ast.WalkContinue, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if boxesFound < t.nthBox {
|
|
|
|
|
boxesFound += 1
|
|
|
|
|
return ast.WalkContinue, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
box.IsChecked = !box.IsChecked
|
|
|
|
|
return ast.WalkStop, nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 12:44:01 +01:00
|
|
|
func renderTaskCheckBox(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
|
|
|
var err error
|
|
|
|
|
var _ int
|
|
|
|
|
n, ok := node.(*astext.TaskCheckBox)
|
|
|
|
|
if !ok {
|
|
|
|
|
return ast.WalkContinue, errors.New("not a TaskCheckBox")
|
|
|
|
|
}
|
2025-07-25 13:44:21 +01:00
|
|
|
if entering {
|
2025-07-29 12:44:01 +01:00
|
|
|
// Render a box if necessary
|
|
|
|
|
if n.IsChecked {
|
|
|
|
|
_, err = writer.Write([]byte("[x] "))
|
|
|
|
|
} else {
|
|
|
|
|
_, err = writer.Write([]byte("[ ] "))
|
2025-07-25 13:44:21 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 12:44:01 +01:00
|
|
|
return ast.WalkContinue, err
|
2025-07-25 13:44:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (n *Note) ToggleBox(nthBox int) {
|
|
|
|
|
checkboxTransformer := toggleCheckboxTransformer{nthBox: nthBox}
|
|
|
|
|
transformer := util.Prioritized(&checkboxTransformer, 0)
|
|
|
|
|
|
|
|
|
|
renderer := markdown.NewRenderer()
|
2025-07-29 12:44:01 +01:00
|
|
|
renderer.Register(astext.KindTaskCheckBox, renderTaskCheckBox)
|
2025-07-25 13:44:21 +01:00
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
2025-07-29 12:44:01 +01:00
|
|
|
md := goldmark.New(goldmark.WithRenderer(renderer))
|
|
|
|
|
|
|
|
|
|
md.Parser().AddOptions(parser.WithInlineParsers(
|
|
|
|
|
util.Prioritized(extension.NewTaskCheckBoxParser(), 0),
|
|
|
|
|
), parser.WithASTTransformers(transformer))
|
2025-07-25 13:44:21 +01:00
|
|
|
|
|
|
|
|
md.Convert(n.Body, &buf)
|
|
|
|
|
n.Body = buf.Bytes()
|
|
|
|
|
n.Save()
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:56:50 +01:00
|
|
|
func DecodeTitle(encodedTitle string) string {
|
|
|
|
|
title, err := base64.StdEncoding.DecodeString(encodedTitle)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("Couldn't decode base64 string '%s': %s", encodedTitle, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string(title)
|
|
|
|
|
}
|