// 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)(.*)`)
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
/**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(" \n" + string(title) + "")
}
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)`)
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 = `"
}
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 + "\n"
for _, child := range this.Children {
var childSpace = strings.Repeat(" ", child.Depth)
result += childSpace + "- " + child.Title + "\n" + child.AsHTML() + childSpace + "
\n"
}
result += space + "
\n"
return result
}