Initial commit (code only without large binaries)

This commit is contained in:
robin
2026-02-15 18:58:44 +08:00
commit 35df75498f
9442 changed files with 1495866 additions and 0 deletions

View 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
}

View 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) + "\">&nbsp;</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
}

View 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()
})
}