176 lines
No EOL
5.1 KiB
Go
176 lines
No EOL
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
FILES_DIR = "files"
|
|
TEMPLATES_DIR = "templates"
|
|
TIME_FORMAT = "Jan 2 2006 15:04:05"
|
|
)
|
|
|
|
const (
|
|
POST_ICON = "/static/post.gif"
|
|
FILE_ICON = "/static/file.gif"
|
|
)
|
|
|
|
var (
|
|
headerPattern = regexp.MustCompile(`(?m)^# (.+)$`)
|
|
subHeaderPattern = regexp.MustCompile(`(?m)^## (.+)$`)
|
|
newlinePattern = regexp.MustCompile(`(?m)^([^#<].+)`)
|
|
boldPattern = regexp.MustCompile(`\*\*(.*?)\*\*|__(.*?)__`)
|
|
italicPattern = regexp.MustCompile(`\*(.*?)\*|_(.*?)_`)
|
|
inlineCodePattern = regexp.MustCompile("`([^`]+)`")
|
|
codeBlockPattern = regexp.MustCompile("(?s)```(.*?)```")
|
|
blockquotePattern = regexp.MustCompile(`(?m)^> (.+)$`)
|
|
ulPattern = regexp.MustCompile(`(?m)^(?:-|\*) (.+)$`)
|
|
olPattern = regexp.MustCompile(`(?m)^\d+\. (.+)$`)
|
|
linkPattern = regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
|
|
)
|
|
|
|
type FileEntry struct {
|
|
Name string
|
|
Timestamp string
|
|
ParentPath string
|
|
Icon string
|
|
}
|
|
|
|
type Page struct {
|
|
Title string
|
|
Content template.HTML
|
|
Files []FileEntry
|
|
Folders []string
|
|
BasePath string
|
|
ParentPath string
|
|
TimeStamp string
|
|
}
|
|
|
|
func listEntries(basePath string) ([]FileEntry, []string) {
|
|
entries, err := os.ReadDir(filepath.Join(FILES_DIR, strings.TrimPrefix(basePath, "/")))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
var files []FileEntry
|
|
var folders []string
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
folders = append(folders, entry.Name())
|
|
} else if strings.HasSuffix(entry.Name(), ".md") {
|
|
info, _ := entry.Info()
|
|
files = append(files, FileEntry{Name: entry.Name(), Timestamp: info.ModTime().Format(TIME_FORMAT), ParentPath: basePath, Icon: POST_ICON})
|
|
}else
|
|
{
|
|
info, _ := entry.Info()
|
|
files = append(files, FileEntry{Name: entry.Name(), Timestamp: info.ModTime().Format(TIME_FORMAT), ParentPath: basePath, Icon: FILE_ICON})
|
|
}
|
|
}
|
|
sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
|
|
sort.Strings(folders)
|
|
return files, folders
|
|
}
|
|
|
|
func readMarkdown(filename string) string {
|
|
content, err := os.ReadFile(filepath.Join(FILES_DIR, filename))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return string(content)
|
|
}
|
|
|
|
|
|
func renderMarkdown(content string) template.HTML {
|
|
content = headerPattern.ReplaceAllString(content, "<h1>$1</h1>")
|
|
content = subHeaderPattern.ReplaceAllString(content, "<h2>$1</h2>")
|
|
content = boldPattern.ReplaceAllString(content, "<b>$1$2</b>")
|
|
content = italicPattern.ReplaceAllString(content, "<i>$1$2</i>")
|
|
content = inlineCodePattern.ReplaceAllString(content, "<code>$1</code>")
|
|
content = codeBlockPattern.ReplaceAllString(content, "<pre><code>$1</code></pre>")
|
|
content = blockquotePattern.ReplaceAllString(content, "<blockquote>$1</blockquote>")
|
|
content = ulPattern.ReplaceAllString(content, "<li>$1</li>")
|
|
content = olPattern.ReplaceAllString(content, "<li>$1</li>")
|
|
content = linkPattern.ReplaceAllString(content, `<a href="$2">$1</a>`)
|
|
|
|
// wrap list items in <ul> or <ol>
|
|
content = regexp.MustCompile(`(<li>.+?</li>)`).ReplaceAllString(content, "<ul>$1</ul>")
|
|
|
|
// convert newlines to paragraphs
|
|
content = newlinePattern.ReplaceAllString(content, "<p>$1</p>")
|
|
|
|
return template.HTML(content)
|
|
}
|
|
|
|
|
|
func loadTemplate(name string) *template.Template {
|
|
tmplPath := filepath.Join(TEMPLATES_DIR, name)
|
|
tmpl, err := template.ParseFiles(tmplPath)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return tmpl
|
|
}
|
|
|
|
func indexHandler(w http.ResponseWriter, r *http.Request) {
|
|
basePath := strings.TrimPrefix(r.URL.Path, "/")
|
|
files, folders := listEntries(r.URL.Path)
|
|
page := Page{Files: files, Folders: folders, BasePath: basePath}
|
|
tmpl := loadTemplate("index.html")
|
|
tmpl.Execute(w, page)
|
|
}
|
|
|
|
func postHandler(w http.ResponseWriter, r *http.Request) {
|
|
filename := strings.TrimPrefix(r.URL.Path, "/")
|
|
|
|
parentPath := filepath.ToSlash(filepath.Dir(filename) + "/")
|
|
if parentPath == "." {
|
|
parentPath = ""
|
|
}
|
|
|
|
info, err := os.Stat(filepath.Join(FILES_DIR, filename))
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
timeStamp := info.ModTime().Format(TIME_FORMAT)
|
|
|
|
content := readMarkdown(filename)
|
|
page := Page{Title: filename, Content: renderMarkdown(content), ParentPath: parentPath, TimeStamp: timeStamp}
|
|
tmpl := loadTemplate("post.html")
|
|
tmpl.Execute(w, page)
|
|
|
|
println("handling request " + r.URL.Path + " with parentdir " + parentPath)
|
|
}
|
|
|
|
func main() {
|
|
if _, err := os.Stat(FILES_DIR); os.IsNotExist(err) {
|
|
os.Mkdir(FILES_DIR, os.ModePerm)
|
|
}
|
|
|
|
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
if strings.HasSuffix(path, ".md") {
|
|
postHandler(w, r)
|
|
} else if strings.Contains(path, ".") { // detect non-md files
|
|
http.ServeFile(w, r, filepath.Join(FILES_DIR, path)) // serve raw file
|
|
} else {
|
|
indexHandler(w, r)
|
|
}
|
|
})
|
|
|
|
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
|
http.ServeFile(w, r, "static/post.gif") // or just `w.WriteHeader(http.StatusNoContent)`
|
|
})
|
|
|
|
|
|
log.Println("Serving on :8080")
|
|
http.ListenAndServe(":8080", nil)
|
|
} |