add path to slug (#37)
This commit is contained in:
parent
7e732f5212
commit
8a6543611e
11 changed files with 62 additions and 34 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
dendrite
Binary file not shown.
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(¬eCount))
|
require.NoError(t, Fixture.DB.QueryRow(`SELECT COUNT(*) FROM note WHERE slug = ?`, "subdir/templated-note").Scan(¬eCount))
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue