Skip to content

Commit

Permalink
docs: switch markdown renderer [wip]
Browse files Browse the repository at this point in the history
  • Loading branch information
dmke committed Jul 17, 2022
1 parent 726bab5 commit 1a7436b
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 89 deletions.
File renamed without changes.
231 changes: 159 additions & 72 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,125 +4,212 @@ import (
"bytes"
"embed"
"fmt"
"html/template"
"io"
"log"
"net/http"
"strings"

"github.com/Depado/bfchroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
bf "github.com/russross/blackfriday/v2"
toc "github.com/abhinav/goldmark-toc"
chromahtml "github.com/alecthomas/chroma/formatters/html"
"github.com/digineo/texd"
"github.com/microcosm-cc/bluemonday"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"gopkg.in/yaml.v3"
)

//go:embed docs.yml *.md **/*.md
//go:embed *.md **/*.md
var sources embed.FS

//go:embed docs.yml
var config []byte

//go:embed docs.html
var rawLayout string
var tplLayout = template.Must(template.New("layout").Parse(rawLayout))

type page struct {
Title string
Breadcrumbs []string
Body string
TOC *toc.TOC
CSS []byte
Body []byte
File string
Route string
Children []*page
}

var root = func() page {
structure, err := sources.Open("docs.yml")
if err != nil {
panic(err)
}
defer structure.Close()
type pageRoutes map[string]*page

func getRoutes(urlPrefix string) (pageRoutes, error) {
var menu page
dec := yaml.NewDecoder(structure)
dec := yaml.NewDecoder(bytes.NewReader(config))
dec.KnownFields(true)
if err := dec.Decode(&menu); err != nil {
panic(err)
return nil, err
}

menu.init()
return menu
}()
urlPrefix = strings.TrimSuffix(urlPrefix, "/")
return menu.init(urlPrefix, make(pageRoutes))
}

func (pg *page) init(crumbs ...string) {
func (pg *page) init(urlPrefix string, r pageRoutes, crumbs ...string) (pageRoutes, error) {
if pg.File != "" {
if r := strings.TrimSuffix(pg.File, ".md"); r == "index" {
pg.Route = ""
if r := strings.TrimSuffix(pg.File, ".md"); r == "README" {
pg.Route = urlPrefix
} else {
pg.Route = "/" + r
pg.Route = urlPrefix + "/" + r
}
r[pg.Route] = pg
err := pg.parseFile(urlPrefix)
if err != nil {
return nil, err
}

pg.parseFile()
}
if pg.Title != "" {
pg.Breadcrumbs = append(pg.Breadcrumbs, pg.Title)
pg.Breadcrumbs = append([]string{pg.Title}, crumbs...)
}
for _, child := range pg.Children {
child.init(pg.Breadcrumbs...)
_, err := child.init(urlPrefix, r, pg.Breadcrumbs...)
if err != nil {
return nil, err
}
}
return r, nil
}

func (pg *page) parseFile() {
body, err := sources.ReadFile(pg.File)
if err != nil {
panic(err)
}
type localLinkTransformer struct {
prefix string
}

r := bfchroma.NewRenderer(
bfchroma.WithoutAutodetect(),
bfchroma.ChromaOptions(
html.WithLineNumbers(true),
),
bfchroma.Extend(bf.NewHTMLRenderer(bf.HTMLRendererParameters{
Flags: bf.CommonHTMLFlags & ^bf.UseXHTML & ^bf.CompletePage,
})),
)
parser := bf.New(
bf.WithExtensions(bf.CommonExtensions),
bf.WithRenderer(r),
)
var _ parser.ASTTransformer = (*localLinkTransformer)(nil)

ast := parser.Parse(body)
var buf bytes.Buffer
var inH1 bool

ast.Walk(func(node *bf.Node, entering bool) bf.WalkStatus {
switch node.Type {
case bf.Heading:
inH1 = entering && node.HeadingData.Level == 1 && pg.Title == ""
case bf.Text:
if inH1 {
pg.Title = string(node.Literal)
}
case bf.Link:
if entering && bytes.HasPrefix(node.LinkData.Destination, []byte("./")) {
node.LinkData.Destination = bytes.TrimSuffix(node.LinkData.Destination, []byte(".md"))
func (link *localLinkTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering && n.Kind() == ast.KindLink {
if l, ok := n.(*ast.Link); ok {
link.rewrite(l)
}
}
return r.RenderNode(&buf, node, entering)
return ast.WalkContinue, nil
})
}

pg.Body = buf.String()
const (
localLinkPrefix = "./"
localLinkSuffix = ".md"
)

func (link *localLinkTransformer) rewrite(l *ast.Link) {
dst := string(l.Destination)
if strings.HasPrefix(dst, localLinkPrefix) && strings.HasSuffix(dst, localLinkSuffix) {
dst = strings.TrimPrefix(dst, localLinkPrefix)
dst = strings.TrimSuffix(dst, localLinkSuffix)
l.Destination = []byte(link.prefix + "/" + dst)
}
}

func (pg *page) Dump(w io.Writer) {
fmt.Fprintf(w, "- %s (%s)\n", pg.Title, pg.Route)
fmt.Fprintln(w, pg.Body)
fmt.Fprintln(w)
var sanitize = func() func(io.Reader) *bytes.Buffer {
p := bluemonday.UGCPolicy()
p.AllowAttrs("class").Globally()
return p.SanitizeReader
}()

for _, c := range pg.Children {
c.Dump(w)
func (pg *page) parseFile(urlPrefix string) error {
raw, err := sources.ReadFile(pg.File)
if err != nil {
return err
}

var css, body bytes.Buffer
md := goldmark.New(
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
parser.WithASTTransformers(util.PrioritizedValue{
Value: &localLinkTransformer{urlPrefix},
Priority: 999,
}),
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
),
goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithCSSWriter(&css),
highlighting.WithStyle("github"),
highlighting.WithFormatOptions(
chromahtml.WithLineNumbers(true),
chromahtml.WithClasses(true),
),
),
),
)

doc := md.Parser().Parse(text.NewReader(raw))
tree, err := toc.Inspect(doc, raw)
if err != nil {
return err
}
if pg.Title == "" {
if len(tree.Items) > 0 {
pg.Title = string(tree.Items[0].Title)
}
}
if err := md.Renderer().Render(&body, raw, doc); err != nil {
return err
}
pg.TOC = tree
pg.CSS = css.Bytes()
pg.Body = sanitize(&body).Bytes()
return nil
}

func Handler() http.Handler {
func Handler(prefix string) (http.Handler, error) {
type pageVars struct {
Version string
Title string
CSS template.CSS
Content template.HTML
}

routes, err := getRoutes(prefix)
if err != nil {
return nil, fmt.Errorf("failed to build docs: %w", err)
}

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
pg := routes[r.URL.Path]
if pg == nil {
http.NotFound(w, r)
return
}

fmt.Fprintf(w, "%#v\n\n", r.URL)
var buf bytes.Buffer
err := tplLayout.Execute(&buf, &pageVars{
Version: texd.Version(),
Title: strings.Join(pg.Breadcrumbs, " · "),
CSS: template.CSS(pg.CSS),
Content: template.HTML(pg.Body),
})

if err != nil {
log.Println(err)
code := http.StatusInternalServerError
http.Error(w, http.StatusText(code), code)
return
}

root.Dump(w)
})
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
_, _ = buf.WriteTo(w)
}), nil
}
39 changes: 39 additions & 0 deletions docs/docs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title>

<link rel="stylesheet" href="/assets/bootstrap-5.1.3.min.css">
<style>{{ .CSS }}</style>
</head>

<body>
<div id="app" class="pb-5">
<nav class="navbar navbar-light navbar-expand-sm bg-light">
<div class="container-fluid">
<a href="https://github.com/digineo/texd" class="navbar-brand">texd</a>

<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="/">Play</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/docs">Documentation</a>
</li>
</ul>

<span class="navbar-text">
{{ .Version }}
</span>
</div>
</nav>

<div class="container">
{{ .Content }}
</div>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion docs/docs.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
file: index.md
file: README.md
children:
- file: operation-modes.md
- file: cli-options.md
Expand Down
10 changes: 7 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,32 @@ module github.com/digineo/texd
go 1.18

require (
github.com/Depado/bfchroma/v2 v2.0.0
github.com/alecthomas/chroma/v2 v2.2.0
github.com/abhinav/goldmark-toc v0.2.1
github.com/alecthomas/chroma v0.10.0
github.com/bahlo/generic-list-go v0.2.0
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d
github.com/docker/docker v20.10.17+incompatible
github.com/docker/go-units v0.4.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/microcosm-cc/bluemonday v1.0.19
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/opencontainers/image-spec v1.0.2
github.com/prometheus/client_golang v1.12.2
github.com/russross/blackfriday/v2 v2.1.0
github.com/spf13/afero v1.8.2
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.0
github.com/thediveo/enumflag v0.10.1
github.com/yuin/goldmark v1.4.13
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594
go.uber.org/zap v1.21.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
Expand All @@ -36,6 +39,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/morikuni/aec v1.0.0 // indirect
Expand Down
Loading

0 comments on commit 1a7436b

Please sign in to comment.