Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions cmd/leafwiki/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ func printUsage() {
leafwiki --jwt-secret <SECRET> --admin-password <PASSWORD> [--host <HOST>] [--port <PORT>] [--data-dir <DIR>]
leafwiki --disable-auth [--host <HOST>] [--port <PORT>] [--data-dir <DIR>]
leafwiki reset-admin-password
leafwiki [--data-dir <DIR>] reconstruct-tree
leafwiki --help

Options:
Expand Down Expand Up @@ -130,22 +129,6 @@ func main() {
fmt.Println("Admin password reset successfully.")
fmt.Printf("New password for user %s: %s\n", user.Username, user.Password)
return
case "reconstruct-tree":
// Ensure data directory exists before reconstruction
if _, err := os.Stat(dataDir); err != nil {
if os.IsNotExist(err) {
if err := os.MkdirAll(dataDir, 0755); err != nil {
fail("Failed to create data directory", "error", err)
}
} else {
fail("Failed to access data directory", "error", err)
}
}
if err := tools.ReconstructTreeFromFS(dataDir); err != nil {
fail("Tree reconstruction failed", "error", err)
}
fmt.Println("Tree reconstructed successfully from filesystem.")
return
case "--help", "-h", "help":
printUsage()
return
Expand Down
80 changes: 46 additions & 34 deletions internal/core/markdown/frontmatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,55 @@ func invalidYAMLKeyRune(r rune) bool {
type Frontmatter struct {
LeafWikiID string `yaml:"leafwiki_id,omitempty" json:"id,omitempty"`
LeafWikiTitle string `yaml:"leafwiki_title,omitempty" json:"title,omitempty"`
CreatedAt string `yaml:"created_at,omitempty" json:"created_at,omitempty"`
CreatorID string `yaml:"creator_id,omitempty" json:"creator_id,omitempty"`
UpdatedAt string `yaml:"updated_at,omitempty" json:"updated_at,omitempty"`
LastAuthorID string `yaml:"last_author_id,omitempty" json:"last_author_id,omitempty"`
}

func (fm *Frontmatter) stripSingleAndDoubleQuotes(s string) string {
s = strings.Trim(s, `"`)
s = strings.Trim(s, `'`)
return s
}

func (fm *Frontmatter) Normalize() {
fm.LeafWikiID = fm.stripSingleAndDoubleQuotes(strings.TrimSpace(fm.LeafWikiID))
fm.LeafWikiTitle = fm.stripSingleAndDoubleQuotes(strings.TrimSpace(fm.LeafWikiTitle))
fm.CreatedAt = fm.stripSingleAndDoubleQuotes(strings.TrimSpace(fm.CreatedAt))
fm.CreatorID = fm.stripSingleAndDoubleQuotes(strings.TrimSpace(fm.CreatorID))
fm.UpdatedAt = fm.stripSingleAndDoubleQuotes(strings.TrimSpace(fm.UpdatedAt))
fm.LastAuthorID = fm.stripSingleAndDoubleQuotes(strings.TrimSpace(fm.LastAuthorID))
}

func (fm Frontmatter) IsZero() bool {
return fm.LeafWikiID == "" &&
fm.LeafWikiTitle == "" &&
fm.CreatedAt == "" &&
fm.CreatorID == "" &&
fm.UpdatedAt == "" &&
fm.LastAuthorID == ""
}

func (fm *Frontmatter) LoadFrontMatterFromContent(yamlPart string) (has bool, err error) {
if err := yaml.Unmarshal([]byte(yamlPart), fm); err != nil {
return true, errors.Join(ErrFrontmatterParse, err)
}

/** Check for title also in frontmatter **/
type titleOnlyStruct struct {
Title string `yaml:"title,omitempty"`
}
var tos titleOnlyStruct
if err := yaml.Unmarshal([]byte(yamlPart), &tos); err == nil {
if tos.Title != "" {
if tos.Title != "" && fm.LeafWikiTitle == "" {
fm.LeafWikiTitle = tos.Title
}
}

fm.LeafWikiID = fm.stripSingleAndDoubleQuotes(fm.LeafWikiID)
fm.LeafWikiTitle = fm.stripSingleAndDoubleQuotes(fm.LeafWikiTitle)

fm.Normalize()
return true, nil
}

func (fm *Frontmatter) stripSingleAndDoubleQuotes(s string) string {
s = strings.Trim(s, `"`)
s = strings.Trim(s, `'`)
return s
}

func (fm *Frontmatter) LoadFrontMatterFromFile(mdFilePath string) (has bool, err error) {
content, err := os.ReadFile(mdFilePath)
if err != nil {
Expand All @@ -57,43 +76,34 @@ func (fm *Frontmatter) LoadFrontMatterFromFile(mdFilePath string) (has bool, err
}

func splitFrontmatter(md string) (yamlPart string, body string, has bool) {
// BOM-safe + normalize newlines
s := strings.TrimPrefix(md, "\ufeff")
s = strings.ReplaceAll(s, "\r\n", "\n")
s = strings.ReplaceAll(s, "\r", "\n")

// Must start with '---' on the very first line
if s != "---" && !strings.HasPrefix(s, "---\n") {
return "", md, false
}

// Find end of first line
firstNL := strings.IndexByte(s, '\n')
if firstNL == -1 {
// it's exactly "---" (or a single-line file)
return "", md, false
}
if strings.TrimSpace(s[:firstNL]) != "---" {
return "", md, false
}

// Find closing delimiter on its own line: "\n---\n" or "\n---" at EOF
// We'll scan line-by-line using indices.
pos := firstNL + 1
yamlStart := pos

endDelimLineStart := -1
endDelimLineEnd := -1

looksLikeYAML := false

for pos <= len(s) {
// find end of current line
nextNL := strings.IndexByte(s[pos:], '\n')
var line string
var lineEnd int
if nextNL == -1 {
// last line
lineEnd = len(s)
line = s[pos:lineEnd]
} else {
Expand All @@ -108,8 +118,6 @@ func splitFrontmatter(md string) (yamlPart string, body string, has bool) {
break
}

// Heuristic: at least one "key:" line => treat as YAML frontmatter
// Skip blanks/comments
if trim != "" && !strings.HasPrefix(trim, "#") {
if idx := strings.IndexByte(trim, ':'); idx > 0 {
key := strings.TrimSpace(trim[:idx])
Expand All @@ -119,29 +127,23 @@ func splitFrontmatter(md string) (yamlPart string, body string, has bool) {
}
}

// advance to next line
if nextNL == -1 {
pos = len(s) + 1
} else {
pos = lineEnd + 1
}
}

// No closing delimiter found => treat as no frontmatter
if endDelimLineStart == -1 {
return "", md, false
}

// If it doesn't look like YAML, treat as plain markdown (separator use-case)
if !looksLikeYAML {
return "", md, false
}

// YAML is between yamlStart and the start of the closing delimiter line
yamlPart = s[yamlStart:endDelimLineStart]
yamlPart = strings.TrimSuffix(yamlPart, "\n") // nice-to-have
yamlPart = strings.TrimSuffix(yamlPart, "\n")

// Body starts after the closing delimiter line (+ its trailing newline if present)
bodyStart := endDelimLineEnd
if bodyStart < len(s) && s[bodyStart:bodyStart+1] == "\n" {
bodyStart++
Expand All @@ -161,14 +163,24 @@ func ParseFrontmatter(md string) (fm Frontmatter, body string, has bool, err err
return Frontmatter{}, md, true, errors.Join(ErrFrontmatterParse, err)
}

fm.LeafWikiID = fm.stripSingleAndDoubleQuotes(fm.LeafWikiID)
fm.LeafWikiTitle = fm.stripSingleAndDoubleQuotes(fm.LeafWikiTitle)
type titleOnlyStruct struct {
Title string `yaml:"title,omitempty"`
}
var tos titleOnlyStruct
if err := yaml.Unmarshal([]byte(yamlPart), &tos); err == nil {
if tos.Title != "" && fm.LeafWikiTitle == "" {
fm.LeafWikiTitle = tos.Title
}
}

fm.Normalize()
return fm, body, true, nil
}

func BuildMarkdownWithFrontmatter(fm Frontmatter, body string) (string, error) {
// Avoid emitting empty frontmatter like "{}"
if strings.TrimSpace(fm.LeafWikiID) == "" {
fm.Normalize()

if fm.IsZero() {
return body, nil
}

Expand All @@ -179,7 +191,7 @@ func BuildMarkdownWithFrontmatter(fm Frontmatter, body string) (string, error) {

var out bytes.Buffer
out.WriteString("---\n")
out.Write(b) // yaml.v3 usually ends with \n, which is fine
out.Write(b)
out.WriteString("---\n")
out.WriteString(body)
return out.String(), nil
Expand Down
17 changes: 16 additions & 1 deletion internal/core/markdown/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,20 @@ func (mf *MarkdownFile) GetFrontmatter() Frontmatter {
}

func (mf *MarkdownFile) SetFrontmatterID(id string) {
mf.fm.LeafWikiID = id
mf.fm.LeafWikiID = strings.TrimSpace(id)
}

func (mf *MarkdownFile) SetFrontmatterTitle(title string) {
mf.fm.LeafWikiTitle = strings.TrimSpace(title)
}

func (mf *MarkdownFile) SetFrontmatterMetadata(createdAt, creatorID, updatedAt, lastAuthorID string) {
mf.fm.CreatedAt = strings.TrimSpace(createdAt)
mf.fm.CreatorID = strings.TrimSpace(creatorID)
mf.fm.UpdatedAt = strings.TrimSpace(updatedAt)
mf.fm.LastAuthorID = strings.TrimSpace(lastAuthorID)
}

func (mf *MarkdownFile) SetContent(content string) {
mf.content = content
}
10 changes: 0 additions & 10 deletions internal/core/tools/reconstruct_tree.go

This file was deleted.

8 changes: 8 additions & 0 deletions internal/core/tree/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path"
"time"
)

func GeneratePathFromPageNode(entry *PageNode) string {
Expand Down Expand Up @@ -77,3 +78,10 @@ func FoldPageFolderIfEmpty(storageDir string, pagePath string) error {

return nil
}

func formatRFC3339(t time.Time) string {
if t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC3339)
}
20 changes: 20 additions & 0 deletions internal/core/tree/legacy_tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tree

import "time"

type legacyPageMetadata struct {
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
CreatorID string `json:"creatorId"`
LastAuthorID string `json:"lastAuthorId"`
}

type legacyPageNode struct {
ID string `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Children []*legacyPageNode `json:"children"`
Position int `json:"position"`
Kind NodeKind `json:"kind"`
Metadata legacyPageMetadata `json:"metadata"`
}
Loading
Loading