// 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" return result }