package collaboration import ( "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "path" "strings" "time" ) func NormalizeFolderPath(raw string) (string, error) { raw = strings.TrimSpace(strings.ReplaceAll(raw, "\\", "/")) if raw == "" || raw == "/" { return "", nil } clean := path.Clean(strings.Trim(raw, "/")) if clean == "." { return "", nil } if clean == ".." || strings.HasPrefix(clean, "../") { return "", fmt.Errorf("folder path must remain inside the content root") } return clean, nil } func randomID(prefix string) string { buf := make([]byte, 12) if _, err := rand.Read(buf); err != nil { panic(err) } return prefix + ":" + hex.EncodeToString(buf) } func hashAnchor(text string) string { normalized := strings.TrimSpace(text) sum := sha256.Sum256([]byte(normalized)) return hex.EncodeToString(sum[:8]) } func parseTime(raw string) (time.Time, error) { if raw == "" { return time.Time{}, nil } return time.Parse(time.RFC3339, raw) } func formatTime(t time.Time) string { return t.UTC().Format(time.RFC3339) } func ptr[T any](v T) *T { return &v } func nullString(s string) *string { if s == "" { return nil } return &s } func scanNullString(ns *string) string { if ns == nil { return "" } return *ns } func coalesceString(values ...string) string { for _, v := range values { if v != "" { return v } } return "" } func formatDocumentID(path string) string { clean := strings.TrimPrefix(path, "/") clean = strings.TrimSuffix(clean, ".md") return "doc:" + clean } func documentPathFromID(id string) string { return strings.TrimPrefix(id, "doc:") }