add path to slug (#37)

This commit is contained in:
Kristian 2026-05-09 14:32:29 +02:00 committed by GitHub
parent 7e732f5212
commit 8a6543611e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 62 additions and 34 deletions

View file

@ -25,7 +25,7 @@ type ExtractedLink struct {
Col int Col int
} }
func ReadFile(path string) (*File, error) { func ReadFile(vaultRoot, path string) (*File, error) {
body, err := os.ReadFile(path) body, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, err
@ -47,13 +47,14 @@ func ReadFile(path string) (*File, error) {
return &File{ return &File{
Path: path, Path: path,
Title: fm.Title, Title: fm.Title,
Slug: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)), Slug: SlugRelative(vaultRoot, path),
FrontMatter: *fm, FrontMatter: *fm,
Content: strings.Split(string(body), "\n"), Content: strings.Split(string(body), "\n"),
ExtractedLinks: ExtractLinks(body), ExtractedLinks: ExtractLinks(body),
}, nil }, nil
} }
var linkRegex = regexp.MustCompile(`\[\[([^\]|]+)(?:\|([^\]]+))?\]\]`) var linkRegex = regexp.MustCompile(`\[\[([^\]|]+)(?:\|([^\]]+))?\]\]`)
func ExtractLinks(body []byte) []*ExtractedLink { func ExtractLinks(body []byte) []*ExtractedLink {

View file

@ -2,6 +2,8 @@ package filehandling
import ( import (
"bytes" "bytes"
"path/filepath"
"strings"
) )
func Slugify(s string) string { func Slugify(s string) string {
@ -17,3 +19,9 @@ func Slugify(s string) string {
} }
return slug.String() return slug.String()
} }
func SlugRelative(base, path string) string {
rel := strings.TrimPrefix(path, base+string(filepath.Separator))
rel = strings.TrimSuffix(rel, filepath.Ext(rel))
return rel
}

View file

@ -2,6 +2,7 @@ package filehandling
import ( import (
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
@ -12,16 +13,22 @@ type Template struct {
Content []byte Content []byte
Title string Title string
Slug string Slug string
Filename string
FrontMatter *FrontMatter FrontMatter *FrontMatter
} }
func NewTemplate(templateName string, title string) (*Template, error) { func NewTemplate(templateName, title, directory string) (*Template, error) {
var templatePath string var templatePath string
if templateName != "" { if templateName != "" {
templatePath = store.GetVaultStore().GetTemplatePath(templateName) templatePath = store.GetVaultStore().GetTemplatePath(templateName)
} }
slug := Slugify(title)
content, err := renderTemplate(templatePath, title, slug) fileNameSlug := Slugify(title)
pathSlug := filepath.ToSlash(filepath.Join(directory, fileNameSlug))
filename := fileNameSlug + ".md"
content, err := renderTemplate(templatePath, title, pathSlug, filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -34,12 +41,13 @@ func NewTemplate(templateName string, title string) (*Template, error) {
return &Template{ return &Template{
Content: content, Content: content,
Title: title, Title: title,
Slug: slug, Slug: pathSlug,
Filename: filename,
FrontMatter: fm, FrontMatter: fm,
}, nil }, nil
} }
func renderTemplate(templatePath string, title string, slug string) ([]byte, error) { func renderTemplate(templatePath, title, slug, filename string) ([]byte, error) {
template, err := readTemplate(templatePath, title) template, err := readTemplate(templatePath, title)
if err != nil { if err != nil {
return nil, err return nil, err
@ -49,7 +57,7 @@ func renderTemplate(templatePath string, title string, slug string) ([]byte, err
"{{title}}", title, "{{title}}", title,
"{{date}}", time.Now().Format("2006-01-02"), "{{date}}", time.Now().Format("2006-01-02"),
"{{slug}}", slug, "{{slug}}", slug,
"{{file}}", slug+".md", "{{file}}", filename,
) )
return []byte(r.Replace(string(template))), nil return []byte(r.Replace(string(template))), nil

View file

@ -39,12 +39,12 @@ func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (an
return nil, err return nil, err
} }
template, err := filehandling.NewTemplate(cmd.TemplateName, cmd.Title) template, err := filehandling.NewTemplate(cmd.TemplateName, cmd.Title, cmd.Directory)
if err != nil { if err != nil {
return nil, err return nil, err
} }
notePath := filepath.Join(store.GetVaultStore().Config.VaultPath(), cmd.Directory, template.Slug+".md") notePath := filepath.Join(store.GetVaultStore().Config.VaultPath(), cmd.Directory, template.Filename)
dbCtx, err := h.uow.Begin() dbCtx, err := h.uow.Begin()
if err != nil { if err != nil {
@ -53,6 +53,15 @@ func (h *CreateNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (an
defer h.uow.Rollback() defer h.uow.Rollback()
existingNote, err := h.noteRepo.GetBySlug(ctx, template.Slug)
if err != nil {
return nil, err
}
if existingNote != nil {
return notePath, nil
}
tagModels, err := h.tagService.CreateTags(ctx, dbCtx, template.FrontMatter.Tags) tagModels, err := h.tagService.CreateTags(ctx, dbCtx, template.FrontMatter.Tags)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -4,9 +4,10 @@ import (
"context" "context"
"encoding/json" "encoding/json"
filehandling "github.com/KristianJBorgwarth/dendrite.daemon/core/file_handling" "github.com/KristianJBorgwarth/dendrite.daemon/core/file_handling"
"github.com/KristianJBorgwarth/dendrite.daemon/core/services" "github.com/KristianJBorgwarth/dendrite.daemon/core/services"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories" "github.com/KristianJBorgwarth/dendrite.daemon/persistence/repositories"
"github.com/KristianJBorgwarth/dendrite.daemon/persistence/store"
) )
type saveNoteCommand struct { type saveNoteCommand struct {
@ -38,7 +39,9 @@ func (h *SaveNoteHandler) Handle(ctx context.Context, raw json.RawMessage) (any,
return nil, err return nil, err
} }
file, err := filehandling.ReadFile(cmd.Path) vaultRoot := store.GetVaultStore().Config.VaultPath()
file, err := filehandling.ReadFile(vaultRoot, cmd.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -119,7 +119,7 @@ func (r *indexRebuilder) readFiles(vault string) ([]*filehandling.File, error) {
return nil return nil
} }
pendingFile, err := filehandling.ReadFile(path) pendingFile, err := filehandling.ReadFile(vault, path)
if err != nil { if err != nil {
return err return err
} }

BIN
dendrite

Binary file not shown.

View file

@ -1,6 +1,6 @@
CREATE TABLE IF NOT EXISTS note ( CREATE TABLE IF NOT EXISTS note (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
path TEXT UNIQUE, path TEXT UNIQUE NOT NULL,
title TEXT, title TEXT,
slug TEXT UNIQUE NOT NULL, slug TEXT UNIQUE NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,

View file

@ -33,9 +33,7 @@ func (r *noteRepository) Insert(ctx context.Context, dbContext persistence.IDbCo
query := ` query := `
INSERT INTO note (id, title, path, slug, created_at, updated_at) INSERT INTO note (id, title, path, slug, created_at, updated_at)
VALUES (?, ?, ?, ?, datetime('now'), datetime('now')) VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))
ON CONFLICT (slug) DO UPDATE ON CONFLICT DO NOTHING;
SET title = EXCLUDED.title,
path = EXCLUDED.path;
` `
_, err := dbContext.ExecContext(ctx, query, note.ID(), note.Title(), note.Path(), note.Slug()) _, err := dbContext.ExecContext(ctx, query, note.ID(), note.Title(), note.Path(), note.Slug())
return err return err

View file

@ -79,7 +79,7 @@ func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t
assert.Equal(t, notePath, result) assert.Equal(t, notePath, result)
var noteCount int var noteCount int
require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "templated-note").Scan(&noteCount)) require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "subdir/templated-note").Scan(&noteCount))
assert.Equal(t, 1, noteCount) assert.Equal(t, 1, noteCount)
rows, err := Fixture.DB.Query(`SELECT name FROM tag WHERE name IN ('go', 'testing')`) rows, err := Fixture.DB.Query(`SELECT name FROM tag WHERE name IN ('go', 'testing')`)
@ -98,7 +98,7 @@ func TestCreateNoteHandler_WithTemplate_CreatesNoteFileWithTagsAndReturnsPath(t
assert.NoError(t, statErr) assert.NoError(t, statErr)
} }
func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) { func TestCreateNoteHandler_DuplicateSlug_ReturnsExistingPath(t *testing.T) {
// Arrange // Arrange
handler := newCreateNoteHandler() handler := newCreateNoteHandler()
@ -107,29 +107,22 @@ func TestCreateNoteHandler_DuplicateSlug_Upserts(t *testing.T) {
require.NoError(t, os.MkdirAll(filepath.Join(vaultPath, subDir), 0o755)) require.NoError(t, os.MkdirAll(filepath.Join(vaultPath, subDir), 0o755))
defer os.RemoveAll(filepath.Join(vaultPath, subDir)) defer os.RemoveAll(filepath.Join(vaultPath, subDir))
params1, _ := json.Marshal(map[string]any{"title": "Dup Note", "templateName": "", "directory": subDir}) params, _ := json.Marshal(map[string]any{"title": "Dup Note", "templateName": "", "directory": subDir})
_, err := handler.Handle(Fixture.TestContext, params1) _, err := handler.Handle(Fixture.TestContext, params)
require.NoError(t, err) require.NoError(t, err)
subDir2 := "dup-dir-moved" // Act — same title and directory produces the same slug, handler returns early
require.NoError(t, os.MkdirAll(filepath.Join(vaultPath, subDir2), 0o755)) result, err := handler.Handle(Fixture.TestContext, params)
defer os.RemoveAll(filepath.Join(vaultPath, subDir2))
params2, _ := json.Marshal(map[string]any{"title": "Dup Note", "templateName": "", "directory": subDir2})
// Act
_, err = handler.Handle(Fixture.TestContext, params2)
// Assert // Assert
require.NoError(t, err) require.NoError(t, err)
var count int var count int
require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "dup-note").Scan(&count)) require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "dup-dir/dup-note").Scan(&count))
assert.Equal(t, 1, count) assert.Equal(t, 1, count)
expectedPath := filepath.Join(vaultPath, subDir2, "dup-note.md") expectedPath := filepath.Join(vaultPath, subDir, "dup-note.md")
var path string assert.Equal(t, expectedPath, result)
require.NoError(t, Fixture.DB.QueryRow(`SELECT path FROM note WHERE slug = ?`, "dup-note").Scan(&path))
assert.Equal(t, expectedPath, path)
} }
func TestCreateNoteHandler_InvalidJSON_ReturnsError(t *testing.T) { func TestCreateNoteHandler_InvalidJSON_ReturnsError(t *testing.T) {

View file

@ -30,7 +30,15 @@ func NewDBFixture() *DBFixture {
panic(err) panic(err)
} }
dbPath := filepath.Join(os.TempDir(), ".index", "index.db") xdg := os.Getenv("XDG_DATA_HOME")
if xdg == "" {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
xdg = filepath.Join(home, ".local", "share")
}
dbPath := filepath.Join(xdg, "dendrite", vaultPath, "index.db")
dbContext := persistence.GetDBContext() dbContext := persistence.GetDBContext()