296 lines
6 KiB
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=\"..\"><<<</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)
|
|
}),
|
|
))
|
|
}
|