rwiki/server/main.go

296 lines
6 KiB
Go

package server
import (
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/parser"
"vultras.su/core/bond"
"vultras.su/core/bond/contents"
"vultras.su/util/pp"
"path/filepath"
"path"
"os"
"fmt"
"strings"
"bytes"
)
const htmlHead = `
<!doctype html>
<html><head>
<title>Title</title>
<link rel="stylesheet" href="/web/main.css">
</head><body>
`
const htmlFooter = `
</body></html>
`
type ServerOptions struct {
WikiPath string
WikiExt string
WebPath string
AddFileNavigation bool
AddDocNavigation bool
}
type Server struct {
handler bond.Handler
options ServerOptions
prep *pp.Preprocessor
}
func (srv *Server) makeMdParser() *parser.Parser {
return parser.NewWithExtensions(
parser.CommonExtensions | parser.AutoHeadingIDs |
parser.NoEmptyLineBeforeBlock | parser.Attributes |
parser.Tables,
)
}
func (srv *Server) makeHtmlRenderer() *html.Renderer {
return html.NewRenderer(
html.RendererOptions{
Flags: html.CommonFlags | html.HrefTargetBlank,
},
)
}
func New(opts ServerOptions) *Server {
srv := &Server{}
srv.handler = makeRootHandler(opts)
srv.options = opts
srv.prep = pp.NewPp(pp.NewTengo())
return srv
}
func (srv *Server) Handle(c *bond.Context) {
c.Data = ContextData{
Server: srv,
}
srv.handler.Handle(c)
}
func (srv *Server) ProcessPmd(pth string, bts []byte) ([]byte, error) {
prep, err := srv.prep.Process(pth, string(bts))
if err != nil {
return nil, err
}
return []byte(prep), nil
}
func (srv *Server) pageHead() string {
return htmlHead
}
func (srv *Server) pageFooter() string {
return htmlFooter
}
type Heading struct {
Id string
Level int
Text string
}
func getDocumentHeadings(doc ast.Node) []Heading {
ret := []Heading{}
ast.WalkFunc(doc, func(node ast.Node, entering bool) ast.WalkStatus {
heading, ok := node.(*ast.Heading)
if !ok {
return ast.GoToNext
}
children := node.GetChildren()
if len(children) > 0 && !entering {
leaf := children[0].AsLeaf()
if leaf == nil {
return ast.GoToNext
}
ret = append(ret, Heading{
Id: heading.HeadingID,
Level: heading.Level,
Text: string(leaf.Literal),
})
return ast.SkipChildren
}
return ast.GoToNext
})
return ret
}
type HeadingTree struct {
Heading Heading
Children HeadingTrees
}
type HeadingTrees []*HeadingTree
func makeHeadingTrees(hs []Heading) HeadingTrees {
fmt.Println("fooooooooooooooouuuuuuuuund", len(hs), hs)
if len(hs) == 0 {
return nil
}
if len(hs) == 1 {
return HeadingTrees{
&HeadingTree{
Heading: hs[0],
},
}
}
var (
lasti int
found bool
)
first := hs[0]
rest := hs[1:]
for i, h := range rest {
if first.Level >= h.Level {
fmt.Println("thefound: ", first, h)
lasti = i+1
found = true
break
}
}
if !found {
fmt.Println("in not found")
return HeadingTrees{
&HeadingTree{
Heading: first,
Children: makeHeadingTrees(rest),
},
}
}
fmt.Println("through", lasti)
return append(
makeHeadingTrees(hs[:lasti]),
makeHeadingTrees(hs[lasti:])... ,
)
}
func RenderHeadingTrees(trees HeadingTrees, first bool) string {
var b strings.Builder
fmt.Fprint(&b, "<nav")
if first {
fmt.Fprint(&b, " class=\"document\"")
}
fmt.Fprint(&b, ">")
for _, tree := range trees {
fmt.Fprintf(
&b, "<a href=\"#%s\">%s</a>",
tree.Heading.Id, tree.Heading.Text,
)
if len(tree.Children) > 0 {
fmt.Fprint(&b, RenderHeadingTrees(tree.Children, false))
}
}
fmt.Fprint(&b, "</nav>")
return b.String()
}
func (srv *Server) ProcessToHtml(urlPath, filePath string, bts []byte) ([]byte, error) {
var b bytes.Buffer
doc := srv.makeMdParser().Parse(bts)
main_section := markdown.Render(doc, srv.makeHtmlRenderer())
fmt.Fprint(&b, srv.pageHead())
if srv.options.AddFileNavigation {
fileDirPath := filepath.Dir(filePath)
urlDirPath := path.Dir(urlPath)
entries, err := os.ReadDir(fileDirPath)
if err != nil {
fmt.Println("leaving", fileDirPath)
return nil, err
}
fmt.Fprint(&b, "<header><nav class=\"base\">")
fmt.Fprintf(
&b, "<a href=\"..\">&lt&lt&lt</a><a href=\".\">%s</a>",
srv.makeFileName(fileDirPath, "index"+srv.options.WikiExt),
)
fmt.Fprint(&b, "</nav><nav class=\"dirs\">")
for _, entry := range entries {
if !entry.IsDir() {
continue
}
fmt.Fprintf(
&b, "<a href=%s>%s</a>",
srv.makeFileLink(urlDirPath, entry.Name()) + "/",
srv.makeFileName(fileDirPath, entry.Name()),
)
}
fmt.Fprint(&b, "</nav><nav class=\"files\">")
for _, entry := range entries {
if entry.IsDir() || entry.Name() == "index" + srv.options.WikiExt{
continue
}
fmt.Fprintf(
&b, "<a href=\"%s\">%s</a>",
srv.makeFileLink(urlDirPath, entry.Name()) ,
srv.makeFileName(fileDirPath, entry.Name()),
)
}
fmt.Fprint(&b, "</nav></header>")
}
fmt.Fprint(&b, "<div class=\"content\">")
if srv.options.AddDocNavigation {
headings := getDocumentHeadings(doc)
trees := makeHeadingTrees(headings)
docNav := RenderHeadingTrees(trees, true)
fmt.Fprint(&b, docNav)
}
fmt.Fprint(&b, "<main>")
fmt.Fprintf(&b, string(main_section))
fmt.Fprintf(&b, "</main>")
fmt.Fprintf(&b, "</div>")
fmt.Fprint(&b, srv.pageFooter())
return b.Bytes(), nil
}
var makeRootHandler = func(opts ServerOptions) bond.Handler {
return bond.Root(bond.Path().
Case(
"web",
bond.StaticDir(opts.WebPath),
).Case(
"api",
nil,
).Default(
Func(func(c *Context){
pth := c.wikiFilePath(c.Path())
bts, err := os.ReadFile(pth)
if err != nil {
pth = c.wikiFilePath(c.Path()+"/")
bts, err = os.ReadFile(pth)
if err != nil {
c.NotFound()
return
}
}
prep, err := c.ProcessPmd(pth, bts)
if err != nil {
c.InternalServerError(err)
return
}
fmt.Println(c.Path(), pth)
bts, err = c.ProcessToHtml(c.Path(), pth, prep)
if err != nil {
c.InternalServerError(err)
return
}
c.SetContentType(contents.Html, contents.Utf8)
c.W.Write(bts)
}),
))
}