internal/tree/tree.go

package tree

import (
	"os"
	"path"
	"path/filepath"
	"sort"
	"static-repo/internal/models"
	"static-repo/internal/utils"
	"strings"
)

func BuildFileTree(root, currentRel, urlPrefix string) (*models.FileNode, error) {
	absPath := filepath.Join(root, currentRel)
	entries, err := os.ReadDir(absPath)
	if err != nil {
		return nil, err
	}

	node := &models.FileNode{
		Name:  filepath.Base(absPath),
		Path:  currentRel,
		IsDir: true,
	}
	if currentRel == "" {
		node.Name = "Root"
	}

	for _, entry := range entries {
		rel := filepath.Join(currentRel, entry.Name())
		if entry.IsDir() {
			child, err := BuildFileTree(root, rel, urlPrefix)
			if err != nil {
				return nil, err
			}
			node.Children = append(node.Children, child)
		} else {
			ctype := utils.GetContentType(entry.Name())
			linkName := rel
			if strings.ToLower(filepath.Base(rel)) == "readme.md" {
				linkName = filepath.Join(filepath.Dir(rel), "index")
			}
			link := path.Join("/", urlPrefix, filepath.ToSlash(linkName)) + ".html"
			node.Children = append(node.Children, &models.FileNode{
				Name:        entry.Name(),
				Path:        filepath.ToSlash(rel),
				Link:        link,
				IsDir:       false,
				ContentType: ctype,
				Icon:        utils.GetIconClass(ctype),
			})
		}
	}

	sort.Slice(node.Children, func(i, j int) bool {
		a, b := node.Children[i], node.Children[j]

		// 1. Folders first
		if a.IsDir != b.IsDir {
			return a.IsDir
		}

		// Both are folders OR both are files
		if !a.IsDir {
			// both are files
			isAMd := strings.HasSuffix(strings.ToLower(a.Name), ".md")
			isBMd := strings.HasSuffix(strings.ToLower(b.Name), ".md")

			if isAMd != isBMd {
				return isAMd
			}

			if isAMd && isBMd {
				isAReadme := strings.ToLower(a.Name) == "readme.md"
				isBReadme := strings.ToLower(b.Name) == "readme.md"
				if isAReadme != isBReadme {
					return isAReadme
				}
			}
		}

		// Alphabetical within groups (folders, markdown, or other files)
		return strings.ToLower(a.Name) < strings.ToLower(b.Name)
	})

	return node, nil
}