feat(release): v0.1.0
commit06ed2c3cbeAuthor: adminoo <git@kadath.corp> Date: Tue Feb 3 11:34:24 2026 +0100 fix: changed detected by scanner but no updated by render layer commit01dcaf882aAuthor: adminoo <git@kadath.corp> Date: Tue Feb 3 10:19:05 2026 +0100 feat: VERSION bumb commit229223f77aAuthor: adminoo <git@kadath.corp> Date: Tue Feb 3 09:53:08 2026 +0100 feat: filter and search by tag commitcb11e34798Author: adminoo <git@kadath.corp> Date: Tue Feb 3 09:41:03 2026 +0100 feat: tag system commit3f5cf0d673Author: adminoo <git@kadath.corp> Date: Tue Feb 3 09:15:29 2026 +0100 feat: sqlite storage draft commitd6617cec02Author: adminoo <git@kadath.corp> Date: Tue Feb 3 09:04:11 2026 +0100 feat: metadata draft commit7238d02a13Author: adminoo <git@kadath.corp> Date: Mon Feb 2 10:18:42 2026 +0100 fix: body overflowing commit16ff836274Author: adminoo <git@kadath.corp> Date: Mon Feb 2 10:09:01 2026 +0100 feat: tests for http handlers and render package commit36ac3f03aaAuthor: adminoo <git@kadath.corp> Date: Mon Feb 2 09:45:29 2026 +0100 feat: Dark theme, placeholder metadata panel commite6923fa4f5Author: adminoo <git@kadath.corp> Date: Sun Feb 1 18:26:59 2026 +0100 fix: uneeded func + uneeded bogus note creation logic commit4458ba2d15Author: adminoo <git@kadath.corp> Date: Sun Feb 1 18:26:21 2026 +0100 feat: log when changing note states commit92a6f84540Author: adminoo <git@kadath.corp> Date: Sun Feb 1 16:55:40 2026 +0100 possibly first working draft commite27aadc603Author: adminoo <git@kadath.corp> Date: Sun Feb 1 11:55:16 2026 +0100 draft shits
This commit is contained in:
114
internal/render/render.go
Normal file
114
internal/render/render.go
Normal file
@ -0,0 +1,114 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
type TemplateData struct {
|
||||
Name string
|
||||
FileNameSet []string
|
||||
}
|
||||
|
||||
type TemplateManager struct {
|
||||
templates map[string]*template.Template
|
||||
mu sync.RWMutex
|
||||
basePath string
|
||||
devMode bool
|
||||
}
|
||||
|
||||
func NewTemplateManager(basePath string) *TemplateManager {
|
||||
return &TemplateManager{
|
||||
templates: make(map[string]*template.Template),
|
||||
basePath: basePath,
|
||||
devMode: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *TemplateManager) buildTemplatePath(name string) string {
|
||||
return filepath.Join(tm.basePath, name+".tmpl")
|
||||
}
|
||||
|
||||
func (tm *TemplateManager) GetTemplate(td *TemplateData) (*template.Template, error) {
|
||||
// Skip cache in dev mode
|
||||
if !tm.devMode {
|
||||
tm.mu.RLock()
|
||||
if tmpl, exists := tm.templates[td.Name]; exists {
|
||||
tm.mu.RUnlock()
|
||||
return tmpl, nil
|
||||
}
|
||||
tm.mu.RUnlock()
|
||||
}
|
||||
|
||||
// Build file paths
|
||||
var files []string
|
||||
for _, file := range td.FileNameSet {
|
||||
files = append(files, tm.buildTemplatePath(file))
|
||||
}
|
||||
|
||||
// Parse template
|
||||
tmpl, err := template.ParseFiles(files...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse template %s: %w", td.Name, err)
|
||||
}
|
||||
|
||||
// Cache it (unless in dev mode)
|
||||
if !tm.devMode {
|
||||
tm.mu.Lock()
|
||||
tm.templates[td.Name] = tmpl
|
||||
tm.mu.Unlock()
|
||||
}
|
||||
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
func (tm *TemplateManager) Render(w http.ResponseWriter, name string, data any) error {
|
||||
// Build the template files - include all necessary templates
|
||||
var files []string
|
||||
|
||||
// Always include base template
|
||||
files = append(files, tm.buildTemplatePath("base"))
|
||||
|
||||
// Include noteList template (used by index)
|
||||
files = append(files, tm.buildTemplatePath("noteList"))
|
||||
|
||||
// Include metadata template (used by index)
|
||||
files = append(files, tm.buildTemplatePath("metadata"))
|
||||
|
||||
// Include metadata template
|
||||
files = append(files, tm.buildTemplatePath("metadata"))
|
||||
|
||||
// Add the specific template
|
||||
files = append(files, tm.buildTemplatePath(name))
|
||||
|
||||
// Parse templates
|
||||
tmpl, err := template.ParseFiles(files...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse template %s: %w", name, err)
|
||||
}
|
||||
|
||||
// Set content type
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "base", data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("execute template %s: %w", name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Render markdown to HTML with target="_blank" on links
|
||||
func RenderMarkdown(content []byte) (template.HTML, error) {
|
||||
renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
|
||||
Flags: blackfriday.CommonHTMLFlags | blackfriday.HrefTargetBlank,
|
||||
})
|
||||
|
||||
html := blackfriday.Run(content, blackfriday.WithRenderer(renderer))
|
||||
return template.HTML(html), nil
|
||||
}
|
||||
149
internal/render/render_test.go
Normal file
149
internal/render/render_test.go
Normal file
@ -0,0 +1,149 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenderMarkdown(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
markdown string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Markdown, no link",
|
||||
markdown: `# Test
|
||||
|
||||
## 01/24/26 09:14:20 - some entry
|
||||
check this out`,
|
||||
want: `<h1>Test</h1><h2>01/24/26 09:14:20 - some entry</h2><p>check this out</p>`,
|
||||
},
|
||||
{
|
||||
name: "Markdown, some link",
|
||||
markdown: `# Test 2
|
||||
## 01/24/26 09:14:20 - some entry (bare link)
|
||||
Check this out http://tatata.toto here
|
||||
`,
|
||||
want: `<h1>Test 2</h1><h2>01/24/26 09:14:20 - some entry (bare link)</h2><p>Check this out <a href="http://tatata.toto" target="_blank">http://tatata.toto</a> here</p>`,
|
||||
},
|
||||
{
|
||||
name: "Markdown, some link with description",
|
||||
markdown: `# Test 2
|
||||
## 01/24/26 09:14:20 - some entry (bare link)
|
||||
Check this out [here](http://tatata.toto)
|
||||
`,
|
||||
want: `<h1>Test 2</h1><h2>01/24/26 09:14:20 - some entry (bare link)</h2><p>Check this out <a href="http://tatata.toto" target="_blank">here</a></p>`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
got, err := RenderMarkdown([]byte(test.markdown))
|
||||
if err != nil {
|
||||
t.Errorf("Error rendering markdown: '%s'\n", err)
|
||||
}
|
||||
strip := strings.ReplaceAll(string(got), "\n", "")
|
||||
strip = strings.Trim(strip, " ")
|
||||
|
||||
if strip != test.want {
|
||||
t.Errorf("Rendering markdown: Wanted '%s', got '%s'.\n", test.want, strip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateManagerGetTemplate_Caches(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
writeTemplate(t, baseDir, "base.tmpl", `{{ define "base" }}base{{ end }}`)
|
||||
writeTemplate(t, baseDir, "index.tmpl", `{{ define "content" }}index{{ end }}`)
|
||||
|
||||
tm := NewTemplateManager(baseDir)
|
||||
td := &TemplateData{
|
||||
Name: "index",
|
||||
FileNameSet: []string{"base", "index"},
|
||||
}
|
||||
|
||||
first, err := tm.GetTemplate(td)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
second, err := tm.GetTemplate(td)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if first != second {
|
||||
t.Fatalf("expected cached template instance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateManagerGetTemplate_Missing(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
tm := NewTemplateManager(baseDir)
|
||||
|
||||
td := &TemplateData{
|
||||
Name: "missing",
|
||||
FileNameSet: []string{"missing"},
|
||||
}
|
||||
|
||||
if _, err := tm.GetTemplate(td); err == nil {
|
||||
t.Fatalf("expected error for missing template")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateManagerRender(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
writeTemplate(t, baseDir, "base.tmpl", `{{ define "base" }}<html>{{ template "content" . }}</html>{{ end }}`)
|
||||
writeTemplate(t, baseDir, "noteList.tmpl", `{{ define "noteList" }}notes{{ end }}`)
|
||||
writeTemplate(t, baseDir, "metadata.tmpl", `{{ define "metadata" }}meta{{ end }}`)
|
||||
writeTemplate(t, baseDir, "index.tmpl", `{{ define "content" }}hello{{ end }}`)
|
||||
writeTemplate(t, baseDir, "metadata.tmpl", `{{ define "noteList" }}notes{{ end }}`)
|
||||
|
||||
tm := NewTemplateManager(baseDir)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
err := tm.Render(rec, "index", map[string]string{"msg": "hi"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if ct := rec.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" {
|
||||
t.Fatalf("expected content-type text/html; charset=utf-8, got %q", ct)
|
||||
}
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
|
||||
if !strings.Contains(rec.Body.String(), "hello") {
|
||||
t.Fatalf("expected rendered template content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateManagerRender_MissingTemplate(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
writeTemplate(t, baseDir, "base.tmpl", `{{ define "base" }}<html>{{ template "content" . }}</html>{{ end }}`)
|
||||
writeTemplate(t, baseDir, "noteList.tmpl", `{{ define "noteList" }}notes{{ end }}`)
|
||||
writeTemplate(t, baseDir, "metadata.tmpl", `{{ define "metadata" }}meta{{ end }}`)
|
||||
writeTemplate(t, baseDir, "metadata.tmpl", `{{ define "noteList" }}notes{{ end }}`)
|
||||
|
||||
tm := NewTemplateManager(baseDir)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
if err := tm.Render(rec, "index", nil); err == nil {
|
||||
t.Fatalf("expected error for missing template")
|
||||
}
|
||||
}
|
||||
|
||||
func writeTemplate(t *testing.T, dir, name, contents string) {
|
||||
t.Helper()
|
||||
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
||||
t.Fatalf("write template %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user