From 5fdcede6f868ca23adb8427a4208cf61577a277a Mon Sep 17 00:00:00 2001 From: adminoo Date: Mon, 23 Feb 2026 19:17:17 +0100 Subject: [PATCH] feat(release): v0.3.0 commit 533ac4e58256e6520a86af964fcf4c2f9a98d4ba Author: adminoo Date: Mon Feb 23 18:52:59 2026 +0100 feat: freebsd release tarball generator commit 874fb63fd053d3ec75972d23096c77fc9f2493a5 Author: adminoo Date: Mon Feb 23 14:05:24 2026 +0100 feat: bump changelog commit 46ab7e291132bcc63223730c356a04bc0e14da29 Author: adminoo Date: Mon Feb 23 13:58:14 2026 +0100 feat: margin and page breaks commit 44751a808a4b554fa918ccc02309518905cf4637 Author: adminoo Date: Mon Feb 23 13:57:56 2026 +0100 feat: picture are worth thousand words commit a5683428e04553cf91117b453408e0d68f23a246 Author: adminoo Date: Mon Feb 23 13:39:00 2026 +0100 feat: navigate individual sections commit 0d9b7c4e7bf8c67db303b9100d357a46e88ae9ab Author: adminoo Date: Mon Feb 23 13:38:19 2026 +0100 feat: make use of vendoring --- .gitignore | 3 +- CHANGELOG.md | 14 +++ Makefile | 24 ++++- README.md | 28 ++++++ dm_screen.jpg | Bin 0 -> 64529 bytes internal/note/sections.go | 103 ++++++++++++++++++++++ internal/note/sections_test.go | 69 +++++++++++++++ internal/web/handler.go | 102 +++++++++++++++++++++- internal/web/handler_test.go | 136 +++++++++++++++++++++++++++++ internal/web/static/css/main.css | 27 +++++- packaging/freebsd/release.Makefile | 12 +++ 11 files changed, 507 insertions(+), 11 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 dm_screen.jpg create mode 100644 internal/note/sections.go create mode 100644 internal/note/sections_test.go create mode 100644 packaging/freebsd/release.Makefile diff --git a/.gitignore b/.gitignore index 6a4f2bd..8feed97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ _bin -.#* \ No newline at end of file +.#* +vendor/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..113540b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog + +## 0.2.0 +- New section endpoints: `GET /notes/{id}/sections/{sectionID}` +- Level-two headings are now linkable and point to their section pages +- Print CSS improvements (page breaks before headings, avoid splitting blocks, reduced margins) +- Vendoring support for offline builds (`make vendor`) +- Makefile now defaults to `-mod=vendor` for build/test/run (breaking change) +- README now includes a UI screenshot + +## 0.1.0 +- Core web UI for browsing notes +- Tagging system (add/remove, filter, search) +- SQLite-backed storage with tests diff --git a/Makefile b/Makefile index a2c2a86..087cec0 100755 --- a/Makefile +++ b/Makefile @@ -1,18 +1,34 @@ build: mkdir -p _bin - go build -o _bin/donniemarko cmd/main.go + GOFLAGS=-mod=vendor go build -o _bin/donniemarko cmd/main.go install: cp bin/donniemarko ~/.local/bin/ +vendor: + go mod vendor + test: - go test -v -cover ./... + GOFLAGS=-mod=vendor go test -v -cover ./... run: - go run main.go + GOFLAGS=-mod=vendor go run main.go freebsd: mkdir -p _bin - GOOS=freebsd GOARCH=amd64 go build -o _bin/donniemarko-freebsd cmd/main.go + GOOS=freebsd GOARCH=amd64 GOFLAGS=-mod=vendor go build -o _bin/donniemarko-freebsd cmd/main.go + @stage_dir="_bin/freebsd-release"; \ + rm -rf "$$stage_dir"; \ + ver="$$(cat VERSION 2>/dev/null || echo 0.0.0)"; \ + archive_dir="donniemarko-freebsd-$${ver}"; \ + archive_root="$$stage_dir/$$archive_dir"; \ + mkdir -p "$$archive_root/usr/local/bin" \ + "$$archive_root/usr/local/etc/rc.d" \ + "$$archive_root/usr/local/etc/newsyslog.conf.d"; \ + cp _bin/donniemarko-freebsd "$$archive_root/usr/local/bin/donniemarko"; \ + cp packaging/freebsd/donniemarko "$$archive_root/usr/local/etc/rc.d/donniemarko"; \ + cp packaging/freebsd/newsyslog.conf.d/donniemarko "$$archive_root/usr/local/etc/newsyslog.conf.d/donniemarko"; \ + cp packaging/freebsd/release.Makefile "$$archive_root/Makefile"; \ + tar -C "$$stage_dir" -czf "_bin/$${archive_dir}.tar.gz" "$$archive_dir" all: build install diff --git a/README.md b/README.md index 739f8ef..7fafdcc 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,41 @@ Version: 0.2.0 Knowledge Management System over markdown notes. +![Donnie Marko interface screenshot](dm_screen.jpg) + +## Release 0.3.0 +- New section endpoints: `GET /notes/{id}/sections/{sectionID}` +- Level-two headings are now linkable and point to their section pages +- Print CSS improvements (page breaks before headings, avoid splitting blocks, reduced margins) +- Vendoring support for offline builds (`make vendor`) +- Makefile now defaults to `-mod=vendor` for build/test/run (breaking change) +- README now includes a UI screenshot + +## Release 0.2.0 +- Group notes by root folders +- Metadatas +- Mobile-friendly CSS +- FreeBSD-specific compile target and scripts +- Logging http requests and errors + ## Release 0.1.0 - Core web UI for browsing notes - Tagging system (add/remove, filter, search) - SQLite-backed storage with tests +## About donniemarko + `donniemarko` works as a read-only (for now) interface over a set of markdown notes. Its goals are: - Ensuring notes intented to be published online are correctly formatted - Rendering the notes in a printable-friendly format, taking advantage of HTML/CSS styling - Providing an interface to aggregate the content of those notes for quickly retrieving bits of information through searching and filtering - Providing an interface to cross-reference those notes through a tagging system, in the same fashion as a blog or a wiki + +## Development + +Vendoring is supported for offline builds. + +Common commands: +- `make vendor` to populate `vendor/` +- `make build` to build using vendored deps +- `make test` to run tests using vendored deps diff --git a/dm_screen.jpg b/dm_screen.jpg new file mode 100644 index 0000000000000000000000000000000000000000..68e25edfed00492d880288573dc0c01accbdf221 GIT binary patch literal 64529 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 b@PFgi?%QbS|Kj$4d2YH`k;{tr>)SsAW`Nj& literal 0 HcmV?d00001 diff --git a/internal/note/sections.go b/internal/note/sections.go new file mode 100644 index 0000000..58c36b0 --- /dev/null +++ b/internal/note/sections.go @@ -0,0 +1,103 @@ +package note + +import ( + "strconv" + "strings" + "unicode" +) + +type Section struct { + ID string + Heading string + Content string +} + +// ParseH2Heading returns the heading text for a level-two markdown heading. +func ParseH2Heading(line string) (string, bool) { + trimmed := strings.TrimLeft(line, " \t") + if !strings.HasPrefix(trimmed, "## ") { + return "", false + } + return strings.TrimSpace(strings.TrimPrefix(trimmed, "## ")), true +} + +// ParseSections splits markdown into level-two heading sections. +func ParseSections(content string) []Section { + if content == "" { + return nil + } + + lines := strings.Split(content, "\n") + var sections []Section + + var current *Section + var builder strings.Builder + counts := make(map[string]int) + + flush := func() { + if current == nil { + return + } + current.Content = builder.String() + sections = append(sections, *current) + current = nil + builder.Reset() + } + + for i, line := range lines { + heading, ok := ParseH2Heading(line) + if ok { + flush() + base := slugifyHeading(heading) + if base == "" { + base = "section" + } + counts[base]++ + id := base + if counts[base] > 1 { + id = base + "-" + strconv.Itoa(counts[base]) + } + current = &Section{ + ID: id, + Heading: heading, + } + } + + if current != nil { + builder.WriteString(line) + if i < len(lines)-1 { + builder.WriteString("\n") + } + } + } + + flush() + return sections +} + +func slugifyHeading(input string) string { + in := strings.TrimSpace(strings.ToLower(input)) + if in == "" { + return "" + } + + var b strings.Builder + prevDash := false + + for _, r := range in { + if unicode.IsLetter(r) || unicode.IsDigit(r) { + b.WriteRune(r) + prevDash = false + continue + } + + if !prevDash { + b.WriteByte('-') + prevDash = true + } + } + + out := b.String() + out = strings.Trim(out, "-") + return out +} diff --git a/internal/note/sections_test.go b/internal/note/sections_test.go new file mode 100644 index 0000000..cf4bf8b --- /dev/null +++ b/internal/note/sections_test.go @@ -0,0 +1,69 @@ +package note + +import "testing" + +func TestSlugifyHeading(t *testing.T) { + cases := []struct { + name string + input string + want string + }{ + {name: "simple", input: "Glossary", want: "glossary"}, + {name: "date and title", input: "2026-01-01 - First", want: "2026-01-01-first"}, + {name: "punctuation", input: "Hello, World!", want: "hello-world"}, + {name: "trim", input: " spaced out ", want: "spaced-out"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := slugifyHeading(tc.input); got != tc.want { + t.Fatalf("expected %q, got %q", tc.want, got) + } + }) + } +} + +func TestParseSections(t *testing.T) { + content := "# Title\nIntro\n## Alpha\nA1\nA2\n## Beta\nB1\n" + + sections := ParseSections(content) + if len(sections) != 2 { + t.Fatalf("expected 2 sections, got %d", len(sections)) + } + + if sections[0].Heading != "Alpha" { + t.Fatalf("expected first heading Alpha, got %q", sections[0].Heading) + } + if sections[0].ID != "alpha" { + t.Fatalf("expected first id alpha, got %q", sections[0].ID) + } + if want := "## Alpha\nA1\nA2\n"; sections[0].Content != want { + t.Fatalf("expected first content %q, got %q", want, sections[0].Content) + } + + if sections[1].Heading != "Beta" { + t.Fatalf("expected second heading Beta, got %q", sections[1].Heading) + } + if sections[1].ID != "beta" { + t.Fatalf("expected second id beta, got %q", sections[1].ID) + } + if want := "## Beta\nB1\n"; sections[1].Content != want { + t.Fatalf("expected second content %q, got %q", want, sections[1].Content) + } +} + +func TestParseSections_DuplicateHeadings(t *testing.T) { + content := "## Glossary\nTerm A\n## Glossary\nTerm B\n" + + sections := ParseSections(content) + if len(sections) != 2 { + t.Fatalf("expected 2 sections, got %d", len(sections)) + } + + if sections[0].ID != "glossary" { + t.Fatalf("expected first id glossary, got %q", sections[0].ID) + } + if sections[1].ID != "glossary-2" { + t.Fatalf("expected second id glossary-2, got %q", sections[1].ID) + } +} diff --git a/internal/web/handler.go b/internal/web/handler.go index ef3174f..e870ae6 100644 --- a/internal/web/handler.go +++ b/internal/web/handler.go @@ -43,6 +43,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.handleTags(w, r) return } + if strings.Contains(path, "/sections/") { + h.handleSections(w, r) + return + } h.handleNotes(w, r) return } @@ -185,8 +189,9 @@ func (h *Handler) handleNotes(w http.ResponseWriter, r *http.Request) { return } - // Convert markdown to HTML - htmlContent, err := render.RenderMarkdown([]byte(note.Content)) + // Convert markdown to HTML, linking section headings + basePath := basePathFromRequest(r) + htmlContent, err := renderNoteMarkdown(note.Content, note.ID, basePath) if err != nil { http.Error(w, "Failed to render markdown", http.StatusInternalServerError) return @@ -197,7 +202,7 @@ func (h *Handler) handleNotes(w http.ResponseWriter, r *http.Request) { state.RenderedNote = htmlContent state.LastActive = hash // Ensure note view carries proxy prefix for links/forms. - state.BasePath = basePathFromRequest(r) + state.BasePath = basePath if err := h.templates.Render(w, "index", state); err != nil { log.Printf("render error: %v", err) @@ -211,7 +216,7 @@ func (h *Handler) setActiveNote(state *ViewState, noteID string) error { return err } - htmlContent, err := render.RenderMarkdown([]byte(note.Content)) + htmlContent, err := renderNoteMarkdown(note.Content, note.ID, state.BasePath) if err != nil { return err } @@ -357,3 +362,92 @@ func parseTagRoute(path string) (noteID string, tag string, isRemove bool) { return noteID, "", false } + +func parseSectionRoute(path string) (noteID string, sectionID string) { + parts := strings.Split(strings.Trim(path, "/"), "/") + if len(parts) < 4 || parts[0] != "notes" || parts[2] != "sections" { + return "", "" + } + return parts[1], parts[3] +} + +func (h *Handler) handleSections(w http.ResponseWriter, r *http.Request) { + // Build base state + state, err := h.buildViewState(r) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + noteID, sectionID := parseSectionRoute(r.URL.Path) + if noteID == "" || sectionID == "" { + http.NotFound(w, r) + return + } + + n, err := h.notesService.GetNoteByHash(noteID) + if err != nil { + http.Error(w, "Note not found", http.StatusNotFound) + return + } + + var sectionContent string + for _, section := range note.ParseSections(n.Content) { + if section.ID == sectionID { + sectionContent = section.Content + break + } + } + + if sectionContent == "" { + http.Error(w, "Section not found", http.StatusNotFound) + return + } + + basePath := basePathFromRequest(r) + htmlContent, err := render.RenderMarkdown([]byte(sectionContent)) + if err != nil { + http.Error(w, "Failed to render markdown", http.StatusInternalServerError) + return + } + + state.Note = n + state.RenderedNote = htmlContent + state.LastActive = noteID + state.BasePath = basePath + + if err := h.templates.Render(w, "index", state); err != nil { + log.Printf("render error: %v", err) + http.Error(w, "Render error", http.StatusInternalServerError) + } +} + +func renderNoteMarkdown(content, noteID, basePath string) (template.HTML, error) { + linked := linkifySectionsMarkdown(content, noteID, basePath) + return render.RenderMarkdown([]byte(linked)) +} + +func linkifySectionsMarkdown(content, noteID, basePath string) string { + sections := note.ParseSections(content) + if len(sections) == 0 { + return content + } + + lines := strings.Split(content, "\n") + sectionIdx := 0 + + for i, line := range lines { + heading, ok := note.ParseH2Heading(line) + if !ok { + continue + } + if sectionIdx >= len(sections) { + break + } + link := basePath + "/notes/" + noteID + "/sections/" + sections[sectionIdx].ID + lines[i] = "## [" + heading + "](" + link + ")" + sectionIdx++ + } + + return strings.Join(lines, "\n") +} diff --git a/internal/web/handler_test.go b/internal/web/handler_test.go index af424d3..b8218f5 100644 --- a/internal/web/handler_test.go +++ b/internal/web/handler_test.go @@ -443,3 +443,139 @@ func TestGroupNotesByFolder(t *testing.T) { t.Fatalf("unexpected notes group: %+v", groups[1]) } } + +func TestHandlerNotes_SectionLinks(t *testing.T) { + env := newTestEnv(t) + + sectionNote := ¬e.Note{ + ID: "s1", + Title: "Sections", + Content: "# Sections\nIntro\n## 2026-01-01 - First\nFirst body\n## Glossary\nTerm A\n## Glossary\nTerm B\n", + Path: "notes/sections.md", + UpdatedAt: time.Date(2026, 1, 4, 10, 0, 0, 0, time.UTC), + } + + if err := env.storage.Create(sectionNote); err != nil { + t.Fatalf("create note: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "/notes/s1", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + body := rec.Body.String() + if !strings.Contains(body, `href="/notes/s1/sections/2026-01-01-first"`) { + t.Fatalf("expected link for first section") + } + if !strings.Contains(body, `href="/notes/s1/sections/glossary"`) { + t.Fatalf("expected link for glossary section") + } + if !strings.Contains(body, `href="/notes/s1/sections/glossary-2"`) { + t.Fatalf("expected link for duplicate glossary section") + } +} + +func TestHandlerNotes_SectionRoute(t *testing.T) { + env := newTestEnv(t) + + sectionNote := ¬e.Note{ + ID: "s1", + Title: "Sections", + Content: "# Sections\nIntro\n## 2026-01-01 - First\nFirst body\n## Glossary\nTerm A\n## Glossary\nTerm B\n", + Path: "notes/sections.md", + UpdatedAt: time.Date(2026, 1, 4, 10, 0, 0, 0, time.UTC), + } + + if err := env.storage.Create(sectionNote); err != nil { + t.Fatalf("create note: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "/notes/s1/sections/glossary", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + body := rec.Body.String() + if !strings.Contains(body, "

Glossary

") { + t.Fatalf("expected glossary heading to be rendered") + } + if !strings.Contains(body, "Term A") { + t.Fatalf("expected first glossary content") + } + if strings.Contains(body, "First body") { + t.Fatalf("expected other section content to be excluded") + } + if strings.Contains(body, "Term B") { + t.Fatalf("expected second glossary content to be excluded") + } +} + +func TestHandlerNotes_SectionRoute_Duplicate(t *testing.T) { + env := newTestEnv(t) + + sectionNote := ¬e.Note{ + ID: "s1", + Title: "Sections", + Content: "# Sections\nIntro\n## Glossary\nTerm A\n## Glossary\nTerm B\n", + Path: "notes/sections.md", + UpdatedAt: time.Date(2026, 1, 4, 10, 0, 0, 0, time.UTC), + } + + if err := env.storage.Create(sectionNote); err != nil { + t.Fatalf("create note: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "/notes/s1/sections/glossary-2", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + body := rec.Body.String() + if !strings.Contains(body, "

Glossary

") { + t.Fatalf("expected glossary heading to be rendered") + } + if !strings.Contains(body, "Term B") { + t.Fatalf("expected second glossary content") + } + if strings.Contains(body, "Term A") { + t.Fatalf("expected first glossary content to be excluded") + } +} + +func TestHandlerNotes_SectionRoute_NotFound(t *testing.T) { + env := newTestEnv(t) + + sectionNote := ¬e.Note{ + ID: "s1", + Title: "Sections", + Content: "# Sections\n## Alpha\nA\n", + Path: "notes/sections.md", + UpdatedAt: time.Date(2026, 1, 4, 10, 0, 0, 0, time.UTC), + } + + if err := env.storage.Create(sectionNote); err != nil { + t.Fatalf("create note: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "/notes/s1/sections/missing", nil) + rec := httptest.NewRecorder() + + env.handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("expected status 404, got %d", rec.Code) + } +} diff --git a/internal/web/static/css/main.css b/internal/web/static/css/main.css index c2d2896..e1e3814 100644 --- a/internal/web/static/css/main.css +++ b/internal/web/static/css/main.css @@ -29,7 +29,7 @@ /* PRINT MODE */ @media print { body { - margin: 2cm 1.5cm 2cm 1.5cm; + margin: 1cm; background: white; color: black; } @@ -40,7 +40,8 @@ } main { - margin-top: 0; + margin: 0; + padding: 0; break-after: always; color: black; } @@ -49,6 +50,28 @@ margin-top: 0; color: black; } + + h1, + h2, + h3 { + break-before: page; + page-break-before: always; + break-after: avoid; + page-break-after: avoid; + break-inside: avoid; + page-break-inside: avoid; + } + + p, + ul, + ol, + li, + pre, + blockquote, + table { + break-inside: avoid; + page-break-inside: avoid; + } } /* SCREEN MODE */ diff --git a/packaging/freebsd/release.Makefile b/packaging/freebsd/release.Makefile new file mode 100644 index 0000000..368f886 --- /dev/null +++ b/packaging/freebsd/release.Makefile @@ -0,0 +1,12 @@ +PREFIX ?= /usr/local + +BIN_DIR := $(PREFIX)/bin +ETC_DIR := $(PREFIX)/etc + +install: + install -d $(DESTDIR)$(BIN_DIR) + install -m 755 usr/local/bin/donniemarko $(DESTDIR)$(BIN_DIR)/donniemarko + install -d $(DESTDIR)$(ETC_DIR)/rc.d + install -m 755 usr/local/etc/rc.d/donniemarko $(DESTDIR)$(ETC_DIR)/rc.d/donniemarko + install -d $(DESTDIR)$(ETC_DIR)/newsyslog.conf.d + install -m 644 usr/local/etc/newsyslog.conf.d/donniemarko $(DESTDIR)$(ETC_DIR)/newsyslog.conf.d/donniemarko