Initial commit (code only without large binaries)
This commit is contained in:
114
EdgeUser/internal/web/actions/default/docs/docutils/cache.go
Normal file
114
EdgeUser/internal/web/actions/default/docs/docutils/cache.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package docutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
|
||||
"github.com/yuin/goldmark"
|
||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type cacheItem struct {
|
||||
ModifiedAt int64
|
||||
Data []byte
|
||||
}
|
||||
|
||||
var cacheMap = map[string]*cacheItem{} // path => *cacheItem
|
||||
var cacheLocker = &sync.RWMutex{}
|
||||
|
||||
func ReadMarkdownFile(path string, productName string, rootURL string) ([]byte, error) {
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
var modifiedAt = stat.ModTime().Unix()
|
||||
|
||||
cacheLocker.RLock()
|
||||
item, ok := cacheMap[path]
|
||||
if ok && item.ModifiedAt == modifiedAt {
|
||||
cacheLocker.RUnlock()
|
||||
return item.Data, nil
|
||||
}
|
||||
cacheLocker.RUnlock()
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// keywords
|
||||
data = bytes.ReplaceAll(data, []byte("GoEdge"), []byte(productName))
|
||||
data = bytes.ReplaceAll(data, []byte("GOEDGE"), []byte(productName))
|
||||
data = bytes.ReplaceAll(data, []byte("http://goedge.cn"), []byte("http://example.com"))
|
||||
data = bytes.ReplaceAll(data, []byte("https://goedge.cn"), []byte("https://example.com"))
|
||||
|
||||
var markdown = goldmark.New(
|
||||
goldmark.WithParserOptions(
|
||||
parser.WithAutoHeadingID(), // read note
|
||||
),
|
||||
goldmark.WithExtensions(extension.Table,
|
||||
extension.Strikethrough,
|
||||
highlighting.NewHighlighting(highlighting.WithStyle("github"), highlighting.WithFormatOptions(chromahtml.TabWidth(4))),
|
||||
),
|
||||
goldmark.WithRendererOptions(html.WithHardWraps()),
|
||||
)
|
||||
|
||||
var buf = &bytes.Buffer{}
|
||||
err = markdown.Convert(data, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = buf.Bytes()
|
||||
|
||||
// convert links
|
||||
{
|
||||
var reg = regexp.MustCompile(`(?U)<a href="(.+\.md)">`)
|
||||
data = reg.ReplaceAllFunc(data, func(link []byte) []byte {
|
||||
var pieces = reg.FindSubmatch(link)
|
||||
if len(pieces) > 1 {
|
||||
var newLink = append([]byte(rootURL), bytes.TrimSuffix(pieces[1], []byte(".md"))...)
|
||||
newLink = append(newLink, []byte(".html")...)
|
||||
newLink = []byte(filepath.Clean(string(newLink)))
|
||||
return bytes.ReplaceAll(link, pieces[1], newLink)
|
||||
}
|
||||
return link
|
||||
})
|
||||
}
|
||||
|
||||
// convert images
|
||||
{
|
||||
var reg = regexp.MustCompile(`(?U)<img src="(.+)"[^>]*>`)
|
||||
data = reg.ReplaceAllFunc(data, func(link []byte) []byte {
|
||||
var pieces = reg.FindSubmatch(link)
|
||||
if len(pieces) > 1 {
|
||||
var newLink = append([]byte(rootURL), pieces[1]...)
|
||||
newLink = []byte(filepath.Clean(string(newLink)))
|
||||
return []byte("<a href=\"" + string(newLink) + "\" target=\"_blank\">" + string(bytes.ReplaceAll(link, pieces[1], newLink)) + "</a>")
|
||||
}
|
||||
return link
|
||||
})
|
||||
}
|
||||
|
||||
// put into cache
|
||||
item = &cacheItem{
|
||||
ModifiedAt: modifiedAt,
|
||||
Data: data,
|
||||
}
|
||||
cacheLocker.Lock()
|
||||
cacheMap[path] = item
|
||||
cacheLocker.Unlock()
|
||||
|
||||
return data, nil
|
||||
}
|
||||
304
EdgeUser/internal/web/actions/default/docs/index.go
Normal file
304
EdgeUser/internal/web/actions/default/docs/index.go
Normal file
@@ -0,0 +1,304 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package docs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/TeaOSLab/EdgeUser/internal/web/actions/actionutils"
|
||||
"github.com/TeaOSLab/EdgeUser/internal/web/actions/default/docs/docutils"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"github.com/yuin/goldmark"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const URLPrefix = "/docs"
|
||||
|
||||
type IndexAction struct {
|
||||
actionutils.ParentAction
|
||||
}
|
||||
|
||||
func (this *IndexAction) Init() {
|
||||
this.Nav("", "", "")
|
||||
}
|
||||
|
||||
func (this *IndexAction) RunGet(params struct {
|
||||
Page string
|
||||
}) {
|
||||
var docRoot = Tea.Root
|
||||
if Tea.IsTesting() {
|
||||
docRoot += "/../web"
|
||||
} else {
|
||||
docRoot += "/web"
|
||||
}
|
||||
docRoot += "/docs"
|
||||
|
||||
docRoot = filepath.Clean(docRoot)
|
||||
|
||||
var pageName = params.Page
|
||||
if strings.Contains(pageName, "..") { // prevent path traveling
|
||||
this.ResponseWriter.WriteHeader(http.StatusNotFound)
|
||||
this.WriteString("not found '" + params.Page + "'")
|
||||
return
|
||||
}
|
||||
if len(pageName) == 0 {
|
||||
pageName = "index.html"
|
||||
} else if strings.HasSuffix(pageName, "/") {
|
||||
pageName += "index.html"
|
||||
}
|
||||
|
||||
// extension
|
||||
var ext = filepath.Ext(pageName)
|
||||
switch ext {
|
||||
case ".html":
|
||||
this.doHTML(docRoot, pageName)
|
||||
case ".jpg", ".jpeg", ".png", "gif", ".webp":
|
||||
this.doImage(docRoot, pageName, ext)
|
||||
default:
|
||||
this.ResponseWriter.WriteHeader(http.StatusNotFound)
|
||||
this.WriteString("not found '" + params.Page + "'")
|
||||
}
|
||||
}
|
||||
|
||||
func (this *IndexAction) doHTML(docRoot string, pageName string) {
|
||||
pageName = strings.TrimSuffix(pageName, ".html")
|
||||
var path = filepath.Clean(docRoot + "/" + pageName + ".md")
|
||||
if !strings.HasPrefix(path, docRoot) {
|
||||
this.ResponseWriter.WriteHeader(http.StatusNotFound)
|
||||
this.WriteString("not found '" + pageName + "'")
|
||||
return
|
||||
}
|
||||
|
||||
// convert links
|
||||
var rootURL = URLPrefix + "/"
|
||||
var pageDir = filepath.Dir(pageName)
|
||||
if len(pageDir) > 0 && pageDir != "." {
|
||||
rootURL += pageDir + "/"
|
||||
}
|
||||
|
||||
resultData, err := docutils.ReadMarkdownFile(path, this.Data.GetString("teaName"), rootURL)
|
||||
if err != nil {
|
||||
this.ResponseWriter.WriteHeader(http.StatusNotFound)
|
||||
this.WriteString("not found '" + pageName + "'")
|
||||
return
|
||||
}
|
||||
|
||||
// toc
|
||||
{
|
||||
var reg = regexp.MustCompile(`(?U)<h(\d)\sid="(.+)">(.*)</h\d>`)
|
||||
var subMatches = reg.FindAllStringSubmatch(string(resultData), -1)
|
||||
var tocItem = &TOCItem{
|
||||
Depth: 0,
|
||||
}
|
||||
var lastItem *TOCItem
|
||||
for _, subMatch := range subMatches {
|
||||
var depth = types.Int(subMatch[1])
|
||||
var id = subMatch[2]
|
||||
var title = subMatch[3]
|
||||
var item = &TOCItem{
|
||||
Depth: depth,
|
||||
Id: id,
|
||||
Title: title,
|
||||
Children: []*TOCItem{},
|
||||
Parent: nil,
|
||||
}
|
||||
|
||||
if lastItem == nil {
|
||||
item.Parent = tocItem
|
||||
tocItem.Children = append(tocItem.Children, item)
|
||||
} else if lastItem.Depth == item.Depth {
|
||||
if lastItem.Parent != nil {
|
||||
item.Parent = lastItem.Parent
|
||||
lastItem.Parent.Children = append(lastItem.Parent.Children, item)
|
||||
}
|
||||
} else if lastItem.Depth < item.Depth {
|
||||
item.Parent = lastItem
|
||||
lastItem.Children = append(lastItem.Children, item)
|
||||
} else {
|
||||
var parent = lastItem.Parent
|
||||
for parent != nil {
|
||||
if parent.Depth == item.Depth {
|
||||
if parent.Parent != nil {
|
||||
item.Parent = parent.Parent
|
||||
parent.Parent.Children = append(parent.Parent.Children, item)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
parent = parent.Parent
|
||||
}
|
||||
}
|
||||
|
||||
lastItem = item
|
||||
}
|
||||
|
||||
// reset parent
|
||||
tocItem.UnsetParent()
|
||||
|
||||
if len(tocItem.Children) > 0 {
|
||||
this.Data["toc"] = tocItem.AsHTML()
|
||||
} else {
|
||||
this.Data["toc"] = ""
|
||||
}
|
||||
|
||||
// format <h1...6>
|
||||
/**resultData = reg.ReplaceAllFunc(resultData, func(i []byte) []byte {
|
||||
var subMatch = reg.FindSubmatch(i)
|
||||
if len(subMatch) > 1 {
|
||||
var depth = subMatch[1]
|
||||
var idData = subMatch[2]
|
||||
var title = subMatch[3]
|
||||
return []byte("<a class=\"anchor\" id=\"" + string(idData) + "\"> </a>\n<h" + string(depth) + ">" + string(title) + "</h" + string(depth) + ">")
|
||||
}
|
||||
return i
|
||||
})**/
|
||||
}
|
||||
|
||||
this.Data["content"] = string(resultData)
|
||||
|
||||
// 整体菜单
|
||||
this.readMenu(docRoot)
|
||||
|
||||
this.Show()
|
||||
}
|
||||
|
||||
func (this *IndexAction) doImage(docRoot string, pageName string, ext string) {
|
||||
var path = filepath.Clean(docRoot + "/" + pageName)
|
||||
if !strings.HasPrefix(path, docRoot) {
|
||||
this.ResponseWriter.WriteHeader(http.StatusNotFound)
|
||||
this.WriteString("not found '" + pageName + "'")
|
||||
return
|
||||
}
|
||||
|
||||
var mimeType = ""
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
mimeType = "image/jpeg"
|
||||
case ".png":
|
||||
mimeType = "image/png"
|
||||
case ".webp":
|
||||
mimeType = "image/webp"
|
||||
case ".gif":
|
||||
mimeType = "image/gif"
|
||||
}
|
||||
|
||||
if len(mimeType) == 0 {
|
||||
this.ResponseWriter.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
this.ResponseWriter.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
this.ResponseWriter.Header().Set("Content-Length", types.String(stat.Size()))
|
||||
this.ResponseWriter.Header().Set("Content-Type", mimeType)
|
||||
|
||||
fp, err := os.Open(path)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = fp.Close()
|
||||
}()
|
||||
_, err = io.Copy(this.ResponseWriter, fp)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (this *IndexAction) readMenu(docRoot string) {
|
||||
var path = filepath.Clean(docRoot + "/@toc.md")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
var markdown = goldmark.New()
|
||||
|
||||
var buf = &bytes.Buffer{}
|
||||
err = markdown.Convert(data, buf)
|
||||
if err != nil {
|
||||
this.ErrorPage(err)
|
||||
return
|
||||
}
|
||||
|
||||
var resultString = buf.String()
|
||||
var reg = regexp.MustCompile(`(?U)<a href="(.+)">`)
|
||||
resultString = reg.ReplaceAllStringFunc(resultString, func(s string) string {
|
||||
var match = reg.FindStringSubmatch(s)
|
||||
if len(match) > 1 {
|
||||
var link = match[1]
|
||||
var isExternal = strings.HasPrefix(link, "http://") || strings.HasPrefix(link, "https://")
|
||||
if !isExternal && !strings.HasPrefix(link, "/") {
|
||||
link = URLPrefix + "/" + link
|
||||
}
|
||||
if !isExternal && strings.HasSuffix(link, ".md") {
|
||||
link = strings.TrimSuffix(link, ".md") + ".html"
|
||||
}
|
||||
var a = `<a href="` + link + `"`
|
||||
|
||||
// open external url in new window
|
||||
if isExternal {
|
||||
a += ` target="_blank"`
|
||||
}
|
||||
|
||||
// active
|
||||
if link == this.Request.URL.Path {
|
||||
a += ` class="active"`
|
||||
}
|
||||
|
||||
return a + ">"
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
this.Data["rootTOC"] = resultString
|
||||
}
|
||||
|
||||
type TOCItem struct {
|
||||
Depth int `json:"depth"`
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Children []*TOCItem `json:"children"`
|
||||
Parent *TOCItem `json:"-"`
|
||||
}
|
||||
|
||||
func (this *TOCItem) UnsetParent() {
|
||||
this.Parent = nil
|
||||
for _, child := range this.Children {
|
||||
child.UnsetParent()
|
||||
}
|
||||
}
|
||||
|
||||
func (this *TOCItem) AsHTML() string {
|
||||
if len(this.Children) == 0 || this.Depth >= 2 /** only 2 levels **/ {
|
||||
return "\n"
|
||||
}
|
||||
|
||||
var space = strings.Repeat(" ", this.Depth)
|
||||
var result = space + "<ul>\n"
|
||||
|
||||
for _, child := range this.Children {
|
||||
var childSpace = strings.Repeat(" ", child.Depth)
|
||||
result += childSpace + "<li><a href=\"#" + child.Id + "\">" + child.Title + "</a>\n" + child.AsHTML() + childSpace + "</li>\n"
|
||||
}
|
||||
|
||||
result += space + "</ul>\n"
|
||||
|
||||
return result
|
||||
}
|
||||
21
EdgeUser/internal/web/actions/default/docs/init.go
Normal file
21
EdgeUser/internal/web/actions/default/docs/init.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
|
||||
package docs
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeUser/internal/web/helpers"
|
||||
"github.com/iwind/TeaGo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||
server.
|
||||
Prefix("/docs").
|
||||
Helper(helpers.NewUserMustAuth("")).
|
||||
Data("teaMenu", "docs").
|
||||
Get("", new(IndexAction)).
|
||||
Get("/", new(IndexAction)).
|
||||
Get("/:page(.+)", new(IndexAction)).
|
||||
EndAll()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user