feat(release): v0.3.0
commit 533ac4e58256e6520a86af964fcf4c2f9a98d4ba Author: adminoo <git@kadath.corp> Date: Mon Feb 23 18:52:59 2026 +0100 feat: freebsd release tarball generator commit874fb63fd0Author: adminoo <git@kadath.corp> Date: Mon Feb 23 14:05:24 2026 +0100 feat: bump changelog commit46ab7e2911Author: adminoo <git@kadath.corp> Date: Mon Feb 23 13:58:14 2026 +0100 feat: margin and page breaks commit44751a808aAuthor: adminoo <git@kadath.corp> Date: Mon Feb 23 13:57:56 2026 +0100 feat: picture are worth thousand words commita5683428e0Author: adminoo <git@kadath.corp> Date: Mon Feb 23 13:39:00 2026 +0100 feat: navigate individual sections commit0d9b7c4e7bAuthor: adminoo <git@kadath.corp> Date: Mon Feb 23 13:38:19 2026 +0100 feat: make use of vendoring
This commit is contained in:
103
internal/note/sections.go
Normal file
103
internal/note/sections.go
Normal file
@ -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
|
||||
}
|
||||
69
internal/note/sections_test.go
Normal file
69
internal/note/sections_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user