diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..be2b82f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,95 @@
+# ---> Go
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+*.DS_Store
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
+*.sum
+
+# ---> JetBrains
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
+
+*.iml
+
+## Directory-based project format:
+.idea/
+# if you remove the above rule, at least ignore the following:
+
+# User-specific stuff:
+# .idea/workspace.xml
+# .idea/tasks.xml
+# .idea/dictionaries
+
+# Sensitive or high-churn files:
+# .idea/dataSources.ids
+# .idea/dataSources.xml
+# .idea/sqlDataSources.xml
+# .idea/dynamic.xml
+# .idea/uiDesigner.xml
+
+# Gradle:
+# .idea/gradle.xml
+# .idea/libraries
+
+# Mongo Explorer plugin:
+# .idea/mongoSettings.xml
+
+## File-based project format:
+*.ipr
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+
+*/log/
+
+bin/
+storage/
+coverage.out
+count.out
+*.out
+dist/
+
+static/dist
+
+cover.xml
+coverage.html
+coverage.txt
+coverage.xml
+report.html
+report.xml
+
+vendor/
\ No newline at end of file
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
new file mode 100644
index 0000000..f021ebb
--- /dev/null
+++ b/.goreleaser.yaml
@@ -0,0 +1,45 @@
+project_name: type2md
+builds:
+ - env:
+ - CGO_ENABLED=0
+ - GOPROXY="https://goproxy.cn,direct"
+ flags:
+ - -trimpath
+ ldflags:
+ - -s -w
+ - -X "main.Version={{ .Version }}"
+ - -X "main.CommitID={{ .ShortCommit }}"
+ - -X "main.BuildTime={{ .Date }}"
+ main: .
+
+checksum:
+ name_template: 'checksums.txt'
+
+changelog:
+ sort: asc
+ use: gitlab
+ groups:
+ - title: Features
+ regexp: "^.*feat[(\\w)]*:+.*$"
+ order: 100
+ - title: 'Bug fixes'
+ regexp: "^.*fix[(\\w)]*:+.*$"
+ order: 200
+ - title: 'Documentation updates'
+ regexp: "^.*docs[(\\w)]*:+.*$"
+ order: 400
+ - title: Others
+ order: 999
+ filters:
+ exclude:
+ - '^test:'
+ - '^chore'
+ - 'merge conflict'
+ - '^ci'
+ - '^style'
+ - Merge pull request
+ - Merge remote-tracking branch
+ - Merge branch
+
+release:
+ footer: "Thanks for your support!"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..933399d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+GO_BIN_PATH := $(if $(GOBIN),$(GOBIN),$(GOPATH)/bin)
+
+VERSION ?= dev
+BUILD_TIME ?= `date "+%Y-%m-%d %H:%M:%S"`
+COMMIT_ID ?= `git rev-parse --short HEAD`
+
+GO_BUILD := go build --trimpath --ldflags "-w -s \
+-X 'main.Version=$(VERSION)' \
+-X 'main.CommitID=$(COMMIT_ID)' \
+-X 'main.BuildTime=$(BUILD_TIME)'"
+
+all:
+ $(GO_BUILD) -o type2md ./*.go
+ mv type2md $(GO_BIN_PATH)/type2md
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7729bef
--- /dev/null
+++ b/README.md
@@ -0,0 +1,18 @@
+# golang type define to markdown
+
+
+## Usage
+
+```go
+package docs
+
+//go:generate type2md github.com/eleztian/test Config
+
+```
+
+```shell
+go install github.com/eleztian/type2md
+go generate ./...
+```
+![img.png](docs/img.png)
+
diff --git a/docs/doc_config.md b/docs/doc_config.md
new file mode 100644
index 0000000..58c3aae
--- /dev/null
+++ b/docs/doc_config.md
@@ -0,0 +1,33 @@
+# Config Doc
+Config doc.
+
+| key | 类型 | 必填 | 默认值 | 描述 |
+|----------|----------|-----|------------------|--------------|
+|Pre|[Hook](#ext.Hook)|false|||
+|Post|[Hook](#ext.Hook)|false|||
+|servers.{string}.host|string|false|||
+|servers.{string}.port|int|true||- `22`
- `65522`|
+
+## ext.Mode
+**Type:** int
+
+Mode mode define.
+
+| Value | 描述 |
+|----------|--------------|
+|1|mode q.|
+|2|mode a.|
+
+## ext.Hook
+Hook hook config.
+
+| key | 类型 | 必填 | 默认值 | 描述 |
+|----------|----------|-----|------------------|--------------|
+|name|string|true|example|hook name.|
+|commands.[].|string|false|||
+|envs.{string}.|string|false|||
+|mode|[Mode](#ext.Mode)|false|1|run mode.|
+
+---
+GENERATED BY THE COMMAND [type2md](https://github.com/eleztian/type2md)
+from github.com/eleztian/type2md/test.Config
diff --git a/docs/img.png b/docs/img.png
new file mode 100644
index 0000000..1953df6
Binary files /dev/null and b/docs/img.png differ
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..95346fb
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,14 @@
+module github.com/eleztian/type2md
+
+go 1.18
+
+require (
+ github.com/KyleBanks/depth v1.2.1
+ golang.org/x/tools v0.2.0
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ golang.org/x/mod v0.6.0 // indirect
+ golang.org/x/sys v0.1.0 // indirect
+)
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..04662d2
--- /dev/null
+++ b/main.go
@@ -0,0 +1,100 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "golang.org/x/mod/modfile"
+)
+
+var (
+ dstModPath string
+ dstTypes []string
+)
+var (
+ fileName = flag.String("f", "", "file path")
+ title = flag.String("t", "", "file title")
+ showVersion = flag.Bool("v", false, "show version")
+)
+
+func main() {
+ flag.Parse()
+
+ if *showVersion {
+ PrintVersion()
+ os.Exit(0)
+ }
+ dstModPath = os.Args[len(os.Args)-2]
+ dstTypes = strings.Split(os.Args[len(os.Args)-1], ",")
+
+ rootMod, _, _ := getModPath()
+ log.Println("Current Module:", rootMod)
+
+ parser, err := NewParser(dstModPath)
+ if err != nil {
+ log.Fatalf(err.Error())
+ }
+ for _, tp := range dstTypes {
+ log.Printf("start generate %s.%s\n", dstModPath, tp)
+ fs := parser.Parse(dstModPath, tp)
+
+ md := Markdown{
+ Title: *title,
+ MainStructName: dstModPath + "." + tp,
+ ObjTitleFunc: func(modPath string, typeName string) string {
+ modPath = strings.TrimPrefix(modPath, dstModPath)
+ modPath = strings.TrimPrefix(modPath, rootMod)
+ modPath = strings.TrimLeft(modPath, "./")
+ if modPath == "" {
+ return typeName
+ } else {
+ return fmt.Sprintf("%s.%s", modPath, typeName)
+ }
+ },
+ }
+ if md.Title == "" {
+ md.Title = tp + " Doc"
+ }
+
+ data := md.Generate(fs)
+
+ filename := *fileName
+ if filename == "" {
+ filename = tp + "_doc.md"
+ }
+ log.Printf("start to save to %s\n", filename)
+ _ = os.WriteFile(filename, data, 0655)
+ }
+}
+
+func getModPath() (string, string, error) {
+ current, _ := os.Getwd()
+ var path, _ = filepath.Abs(current)
+ for {
+ _, err := os.Stat(filepath.Join(path, "go.mod"))
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return "", "", err
+ }
+ path, _ = filepath.Abs(filepath.Join(path, "../"))
+ if path == "" {
+ return "", "", nil
+ }
+ continue
+ }
+ content, err := os.ReadFile(filepath.Join(path, "go.mod"))
+ if err != nil {
+ return "", "", err
+ }
+ f, err := modfile.Parse("go.mod", content, nil)
+ if err != nil {
+ return "", "", err
+ }
+
+ return f.Module.Mod.Path, strings.Replace(current, path, f.Module.Mod.Path, 1), nil
+ }
+}
diff --git a/struct_comment.go b/struct_comment.go
new file mode 100644
index 0000000..89af1a2
--- /dev/null
+++ b/struct_comment.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "go/ast"
+ "strings"
+)
+
+func TrimComment(src string) string {
+ res := strings.Trim(src, "\n ")
+ if len(res) != 0 && res[len(res)-1] != '.' {
+ res += "."
+ }
+ return res
+}
+
+func GetDescribeFromComment(doc *ast.CommentGroup, comment *ast.CommentGroup) string {
+ res := ""
+ if doc != nil {
+ res += TrimComment(doc.Text())
+ }
+ if comment != nil {
+ res += TrimComment(comment.Text())
+ }
+
+ return res
+}
diff --git a/struct_info.go b/struct_info.go
new file mode 100644
index 0000000..bf0c0d5
--- /dev/null
+++ b/struct_info.go
@@ -0,0 +1,33 @@
+package main
+
+type StructInfo struct {
+ Describe string `json:"describe"`
+ Fields []FieldInfo `json:"fields"`
+ Enums *EnumInfo `json:"enums"`
+}
+
+type FieldInfo struct {
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Default string `json:"default"`
+ Require bool `json:"require"`
+ Enums EnumInfo `json:"enums"`
+ Describe string `json:"describe"`
+ Reference string `json:"reference"`
+ skipNum int
+}
+
+func (fi FieldInfo) Copy() FieldInfo {
+ n := fi
+ enums := make([][2]string, 0, len(fi.Enums.Names))
+ for _, d := range enums {
+ enums = append(enums, d)
+ }
+ n.Enums.Names = enums
+ return n
+}
+
+type EnumInfo struct {
+ Type string
+ Names [][2]string // key: desc
+}
diff --git a/struct_info_test.go b/struct_info_test.go
new file mode 100644
index 0000000..1a48cd6
--- /dev/null
+++ b/struct_info_test.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ "fmt"
+ goparser "go/parser"
+ "go/token"
+ "testing"
+)
+
+var codeStr = `
+package main
+
+// hello
+type A struct {
+}
+`
+
+func TestName(t *testing.T) {
+ fset := token.NewFileSet()
+ f, err := goparser.ParseFile(fset, "", []byte(codeStr), goparser.ParseComments)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, c := range f.Comments {
+ fmt.Println(c.Text())
+ }
+ fmt.Println(f.Doc)
+ //for _, c := range f.Decls {
+ //fmt.Println(c.(*ast.TypeSpec).Doc.Text())
+ //}
+
+}
diff --git a/struct_markdown.go b/struct_markdown.go
new file mode 100644
index 0000000..6c6ce06
--- /dev/null
+++ b/struct_markdown.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+)
+
+type Markdown struct {
+ Title string
+ MainStructName string
+ Desc string
+ ObjTitleFunc func(modPath string, typeName string) string
+}
+
+func (mk *Markdown) Generate(data map[string]StructInfo) []byte {
+ buf := bytes.NewBuffer(nil)
+
+ buf.WriteString("# " + mk.Title + "\n")
+
+ genStruct := func(structInfo StructInfo) {
+ buf.WriteString(structInfo.Describe)
+ buf.WriteString("\n")
+
+ buf.WriteString(
+ "| key | 类型 | 必填 | 默认值 | 描述 |\n" +
+ "|----------|----------|-----|------------------|--------------|\n")
+ for _, item := range structInfo.Fields {
+ buf.WriteString("|")
+ buf.WriteString(item.Name)
+ buf.WriteString("|")
+ if item.Reference != "" {
+ buf.WriteString(fmt.Sprintf("[%s](#%s)", item.Type, mk.ObjTitleFunc(item.Reference, item.Type)))
+ } else {
+ buf.WriteString(item.Type)
+ }
+ buf.WriteString("|")
+ buf.WriteString(fmt.Sprintf("%v", item.Require))
+ buf.WriteString("|")
+ buf.WriteString(item.Default)
+ buf.WriteString("|")
+ buf.WriteString(item.Describe)
+ if len(item.Enums.Names) != 0 {
+ if item.Describe != "" {
+ buf.WriteString("
")
+ }
+ for idx, nd := range item.Enums.Names {
+ if nd[1] != "" {
+ buf.WriteString(fmt.Sprintf("- `%s`:%s
", nd[0], nd[1]))
+ } else {
+ buf.WriteString(fmt.Sprintf("- `%s`", nd[0]))
+ }
+ if idx != len(item.Enums.Names)-1 {
+ buf.WriteString("
")
+ }
+ }
+
+ }
+ buf.WriteString("|\n")
+ }
+ }
+
+ genEnums := func(enums *EnumInfo, desc string) {
+ buf.WriteString(fmt.Sprintf("**Type:** %s\n\n", enums.Type))
+ buf.WriteString(desc)
+ buf.WriteString("\n")
+
+ if len(enums.Names) != 0 {
+ buf.WriteString("| Value | 描述 |\n")
+ buf.WriteString("|----------|--------------|\n")
+ for _, info := range enums.Names {
+
+ buf.WriteString("|")
+ buf.WriteString(info[0])
+ buf.WriteString("|")
+ buf.WriteString(info[1])
+ buf.WriteString("|\n")
+ }
+ }
+
+ }
+ if mk.MainStructName != "" {
+ genStruct(data[mk.MainStructName])
+ delete(data, mk.MainStructName)
+ }
+
+ for name, structInfo := range data {
+ var (
+ modPath string
+ typeName string
+ )
+ lastIdx := strings.LastIndex(name, ".")
+ if lastIdx < 0 {
+ typeName = name
+ } else {
+ typeName = name[lastIdx+1:]
+ modPath = name[:lastIdx]
+ }
+
+ if structInfo.Enums != nil {
+ buf.WriteString(fmt.Sprintf("\n## %s\n", mk.ObjTitleFunc(modPath, typeName)))
+ genEnums(structInfo.Enums, structInfo.Describe)
+ } else {
+ buf.WriteString(fmt.Sprintf("\n## %s\n", mk.ObjTitleFunc(modPath, typeName)))
+ genStruct(structInfo)
+ }
+ }
+
+ buf.WriteString(fmt.Sprintf("\n---\nGENERATED BY THE COMMAND [type2md](https://github."+
+ "com/eleztian/type2md)\nfrom %s\n",
+ mk.MainStructName))
+
+ return buf.Bytes()
+}
diff --git a/struct_parser.go b/struct_parser.go
new file mode 100644
index 0000000..72e4ced
--- /dev/null
+++ b/struct_parser.go
@@ -0,0 +1,403 @@
+package main
+
+import (
+ "fmt"
+ "go/ast"
+ goparser "go/parser"
+ "go/token"
+ "go/types"
+ "strings"
+
+ "golang.org/x/tools/go/loader"
+)
+
+type Parser struct {
+ Tag string
+ RootModPath string
+ program *loader.Program
+ loaderCfg *loader.Config
+ fileImportNamed map[*ast.File]map[string]string
+ fileTypeDoc map[*ast.File]map[string]string
+}
+
+func NewParser(modPath string) (*Parser, error) {
+ cfg := &loader.Config{
+ ParserMode: goparser.ParseComments,
+ }
+ cfg.Import(modPath)
+
+ p, err := cfg.Load()
+ if err != nil {
+ return nil, err
+ }
+ res := &Parser{
+ Tag: "json",
+ RootModPath: "",
+ program: p,
+ loaderCfg: cfg,
+ fileImportNamed: map[*ast.File]map[string]string{},
+ fileTypeDoc: map[*ast.File]map[string]string{},
+ }
+
+ return res, nil
+}
+
+func (p *Parser) getFileImportNamedMap(file *ast.File) map[string]string {
+ namedImportMap := map[string]string{}
+ for _, imp := range file.Imports {
+ modPath := strings.Trim(imp.Path.Value, "\"")
+ if imp.Name == nil {
+ namedImportMap[p.program.Package(modPath).Pkg.Name()] = modPath
+ } else {
+ namedImportMap[imp.Name.Name] = modPath
+ }
+ }
+ return namedImportMap
+}
+
+func (p *Parser) getFileTypeDocMap(file *ast.File) map[string]string {
+ typeDocMap := map[string]string{}
+ for _, imp := range file.Decls {
+ genDecl, ok := imp.(*ast.GenDecl)
+ if !ok {
+ continue
+ }
+ if genDecl.Tok != token.TYPE {
+ continue
+ }
+ var comment = ""
+ if genDecl.Doc != nil {
+ comment = TrimComment(genDecl.Doc.Text())
+ if comment != "" {
+ comment += "\n"
+ }
+ }
+ for _, spec := range genDecl.Specs {
+ if specType, ok := spec.(*ast.TypeSpec); ok {
+ var subComment = GetDescribeFromComment(specType.Doc, specType.Comment)
+ typeDocMap[specType.Name.Name] = comment + subComment
+ }
+ }
+ }
+
+ return typeDocMap
+}
+
+func (p *Parser) parseFileStruct(
+ modPath string,
+ file *ast.File,
+ exParseMap map[string]struct{},
+ objName string,
+ objStructType *ast.StructType) map[string]StructInfo {
+
+ res := make(map[string]StructInfo, 0)
+ typeKey := TypeKey(modPath, objName)
+
+ namedImportMap := p.fileImportNamed[file]
+ typeDocMap := p.fileTypeDoc[file]
+
+ structInfo := p.parseStruct(objStructType, typeDocMap[objName])
+ res[typeKey] = structInfo
+ for idx, field := range structInfo.Fields {
+ if field.Reference == "" {
+ continue
+ }
+ subModPath := ""
+ if field.Reference == "." {
+ subModPath = modPath
+ } else {
+ subModPath = namedImportMap[field.Reference]
+ }
+ structInfo.Fields[idx].Reference = subModPath
+
+ key := TypeKey(subModPath, field.Type)
+ if _, ok := exParseMap[key]; !ok {
+ for typeName, fields := range p.Parse(subModPath, field.Type) {
+ res[typeName] = fields
+ }
+ exParseMap[key] = struct{}{}
+ }
+ }
+
+ return res
+}
+
+func (p *Parser) Parse(modPath string, typeName string) map[string]StructInfo {
+ pktInfo := p.program.Package(modPath)
+ if pktInfo == nil {
+ return map[string]StructInfo{}
+ }
+ res := make(map[string]StructInfo, 0)
+ exParseMap := make(map[string]struct{})
+
+ for _, file := range pktInfo.Files {
+ if _, ok := p.fileImportNamed[file]; !ok {
+ p.fileImportNamed[file] = p.getFileImportNamedMap(file)
+ }
+ if _, ok := p.fileTypeDoc[file]; !ok {
+ p.fileTypeDoc[file] = p.getFileTypeDocMap(file)
+ }
+ obj := file.Scope.Lookup(typeName)
+ if obj == nil {
+ continue
+ }
+ objTypeSpec := obj.Decl.(*ast.TypeSpec)
+ var objType = objTypeSpec.Type
+ if v, ok := objType.(*ast.StarExpr); ok {
+ objType = v.X
+ }
+ switch ost := objType.(type) {
+ case *ast.StructType:
+ structMaps := p.parseFileStruct(modPath, file, exParseMap, obj.Name, ost)
+ for name, info := range structMaps {
+ res[name] = info
+ }
+ case *ast.Ident:
+ if ost.Obj == nil { // 基本数据类型, 枚举值
+ enums := p.getEnumTypeValues(&pktInfo.Info, typeName, file.Decls)
+ res[TypeKey(modPath, typeName)] = StructInfo{
+ Describe: p.fileTypeDoc[file][typeName],
+ Enums: &EnumInfo{
+ Type: ost.Name,
+ Names: enums,
+ }}
+ }
+ case *ast.SelectorExpr:
+ subModPath := p.fileImportNamed[file][ost.X.(*ast.Ident).Name]
+ subTypeName := ost.Sel.Name
+ structMaps := p.Parse(subModPath, subTypeName)
+ oldKey := TypeKey(subModPath, subTypeName)
+ info := structMaps[oldKey]
+ info.Describe += fmt.Sprintf("alias `%s`", oldKey)
+
+ structMaps[TypeKey(modPath, typeName)] = info
+ delete(structMaps, oldKey)
+ return structMaps
+ }
+ }
+ return res
+}
+
+func (p *Parser) getEnumTypeValues(pktInfo *types.Info, name string, decls []ast.Decl) [][2]string {
+ res := make([][2]string, 0)
+
+EXIT:
+ for _, obj := range decls {
+
+ if genDecl, ok := obj.(*ast.GenDecl); ok {
+ if genDecl.Tok != token.CONST || len(genDecl.Specs) == 0 {
+ continue
+ }
+ firstSpec := genDecl.Specs[0]
+ if valueSpec, ok := firstSpec.(*ast.ValueSpec); ok {
+ t, ok := valueSpec.Type.(*ast.Ident)
+ if !ok || t.Name != name {
+ continue
+ }
+ } else {
+ continue
+ }
+
+ for _, s := range genDecl.Specs {
+ v := s.(*ast.ValueSpec) // safe because decl.Tok == token.CONST
+ for _, name := range v.Names {
+ c := pktInfo.ObjectOf(name).(*types.Const)
+ res = append(res, [2]string{
+ c.Val().ExactString(),
+ GetDescribeFromComment(v.Doc, v.Comment),
+ })
+ }
+ }
+ break EXIT
+ }
+ }
+
+ return res
+}
+
+func (p *Parser) parseTypeExpr(obj ast.Expr) []FieldInfo {
+ if v, ok := obj.(*ast.StarExpr); ok {
+ obj = v.X
+ }
+ var res []FieldInfo
+ switch ot := obj.(type) {
+ case *ast.SelectorExpr:
+ res = []FieldInfo{{Type: ot.Sel.Name, Reference: ot.X.(*ast.Ident).Name, skipNum: 1}}
+ case *ast.Ident:
+ field := FieldInfo{
+ Type: ot.Name,
+ }
+ if ot.Obj != nil {
+ field.Reference = "."
+ }
+ res = append(res, field)
+ case *ast.StructType:
+ res = p.parseStruct(ot, "").Fields
+ case *ast.MapType:
+ prefix := fmt.Sprintf("{%s}.", ot.Key)
+ res = p.parseTypeExpr(ot.Value)
+ for idx := range res {
+ res[idx].Name = prefix + res[idx].Name
+ }
+ case *ast.SliceExpr:
+ prefix := "[]."
+ res = p.parseTypeExpr(ot.X)
+ for idx := range res {
+ res[idx].Name = prefix + res[idx].Name
+ }
+ case *ast.ArrayType:
+ prefix := "[]"
+ t, ok := ot.Len.(*ast.BasicLit)
+ if ok {
+ prefix = fmt.Sprintf("[%s]", t.Value)
+ }
+ res = p.parseTypeExpr(ot.Elt)
+ for idx := range res {
+ res[idx].Name = prefix + res[idx].Name
+ }
+ }
+ return res
+}
+
+func (p *Parser) parseStruct(objStructType *ast.StructType, desc string) StructInfo {
+ res := StructInfo{
+ Describe: desc,
+ Fields: make([]FieldInfo, 0, len(objStructType.Fields.List)),
+ }
+
+ for _, f := range objStructType.Fields.List {
+ if f.Names[0].Name[0] <= 'Z' && f.Names[0].Name[0] >= 'A' {
+ res.Fields = append(res.Fields, p.parseStructField(f)...)
+ }
+ }
+
+ return res
+}
+
+func (p *Parser) parseStructField(f *ast.Field) []FieldInfo {
+ res := make([]FieldInfo, 0)
+
+ tagStr := ""
+ if f.Tag != nil {
+ tagStr = strings.Trim(f.Tag.Value, "`")
+ }
+ tagInfo := ParseStructTag(p.Tag, tagStr)
+ if tagInfo.Name == "-" {
+ return res
+ }
+
+ baseField := FieldInfo{
+ Name: tagInfo.Name,
+ Default: tagInfo.Default,
+ Require: tagInfo.Require,
+ Enums: EnumInfo{
+ Names: tagInfo.Enums,
+ },
+ }
+ if baseField.Name == "" {
+ baseField.Name = f.Names[0].Name
+ }
+
+ baseField.Describe += GetDescribeFromComment(f.Doc, f.Comment)
+
+ if s, ok := f.Type.(*ast.StarExpr); ok {
+ f.Type = s.X
+ }
+ switch tt := f.Type.(type) {
+ case *ast.Ident:
+ baseField.Type = tt.Name
+ if tt.Obj != nil {
+ baseField.Reference = "."
+ }
+ if tagInfo.Inline && baseField.Reference == "." {
+ for _, field := range p.parseTypeExpr(tt.Obj.Decl.(*ast.TypeSpec).Type) {
+ res = append(res, field)
+ }
+ } else {
+ res = append(res, baseField)
+ }
+ case *ast.StructType:
+ for _, f := range p.parseStruct(tt, baseField.Describe).Fields {
+ if tagInfo.Inline {
+ res = append(res, f)
+ } else {
+ if f.skipNum != 0 {
+ field := baseField.Copy()
+ field.Type = f.Type
+ if f.Name != "" {
+ field.Name += "." + f.Name
+ }
+ res = append(res, field)
+ } else {
+ f.Name = baseField.Name + "." + f.Name
+ res = append(res, f)
+ }
+ }
+ }
+ case *ast.MapType:
+ baseField.Name += fmt.Sprintf(".{%s}", tt.Key)
+
+ var subFields = p.parseTypeExpr(tt.Value)
+ for _, f := range subFields {
+ if f.skipNum != 0 {
+ field := baseField.Copy()
+ field.Type = f.Type
+ field.Reference = f.Reference
+ if f.Name != "" {
+ field.Name += "." + f.Name
+ }
+ res = append(res, field)
+ } else {
+ f.Name = baseField.Name + "." + f.Name
+ res = append(res, f)
+ }
+ }
+ case *ast.SliceExpr:
+ baseField.Name += ".[]"
+ for _, f := range p.parseTypeExpr(tt.X) {
+ if f.skipNum != 0 {
+ field := baseField.Copy()
+ field.Type = f.Type
+ field.Reference = f.Reference
+ if f.Name != "" {
+ field.Name += "." + f.Name
+ }
+ res = append(res, field)
+ } else {
+ f.Name = baseField.Name + "." + f.Name
+ res = append(res, f)
+ }
+ }
+ case *ast.ArrayType:
+ t, ok := tt.Len.(*ast.BasicLit)
+ if ok {
+ baseField.Name += fmt.Sprintf(".[%s]", t.Value)
+ } else {
+ baseField.Name += ".[]"
+ }
+ for _, f := range p.parseTypeExpr(tt.Elt) {
+ if f.skipNum != 0 {
+ field := baseField.Copy()
+ field.Type = f.Type
+ field.Reference = f.Reference
+ if f.Name != "" {
+ field.Name += "." + f.Name
+ }
+ res = append(res, field)
+ } else {
+ f.Name = baseField.Name + "." + f.Name
+ res = append(res, f)
+ }
+ }
+ case *ast.SelectorExpr:
+ baseField.Type = tt.Sel.Name
+ baseField.Reference = tt.X.(*ast.Ident).Name
+ res = append(res, baseField)
+ }
+
+ return res
+}
+
+func TypeKey(modPath string, typeName string) string {
+ return modPath + "." + typeName
+}
diff --git a/struct_tag.go b/struct_tag.go
new file mode 100644
index 0000000..2590f9a
--- /dev/null
+++ b/struct_tag.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "reflect"
+ "strings"
+)
+
+type TagInfo struct {
+ Name string
+ Default string
+ Require bool
+ Inline bool
+ Enums [][2]string
+}
+
+func ParseStructTag(tagType string, tagStr string) *TagInfo {
+ res := &TagInfo{}
+ tag := strings.TrimSpace(reflect.StructTag(tagStr).Get(tagType))
+ if tag != "" {
+ sp := strings.Split(tag, ",")
+ res.Name = strings.TrimSpace(sp[0])
+ for _, item := range sp[1:] {
+ item = strings.TrimSpace(item)
+ if item == "inline" {
+ res.Inline = true
+ }
+ }
+ }
+ res.Default, _ = reflect.StructTag(tagStr).Lookup("default")
+ _, res.Require = reflect.StructTag(tagStr).Lookup("require")
+ enumsStr, _ := reflect.StructTag(tagStr).Lookup("enums")
+
+ if enumsStr != "" {
+ res.Enums = make([][2]string, 0)
+ sp := strings.Split(enumsStr, ",")
+ for _, k := range sp {
+ var (
+ value string
+ desc string
+ )
+ idx := strings.Index(k, ":")
+ if idx >= 0 {
+ value = k[:idx]
+ desc = k[idx+1:]
+ } else {
+ value = k
+ }
+ res.Enums = append(res.Enums, [2]string{
+ value, desc,
+ })
+ }
+ }
+
+ return res
+}
diff --git a/test/Config_doc.md b/test/Config_doc.md
new file mode 100644
index 0000000..ae880e0
--- /dev/null
+++ b/test/Config_doc.md
@@ -0,0 +1,33 @@
+# Config Doc
+Config doc.
+
+| key | 类型 | 必填 | 默认值 | 描述 |
+|----------|----------|-----|------------------|--------------|
+|Pre|[Hook](#ext.Hook)|false|||
+|Post|[Hook](#ext.Hook)|false|||
+|servers.{string}.host|string|false|||
+|servers.{string}.port|int|true||- `22`
- `65522`|
+
+## ext.Hook
+Hook hook config.
+
+| key | 类型 | 必填 | 默认值 | 描述 |
+|----------|----------|-----|------------------|--------------|
+|name|string|true|example|hook name.|
+|commands.[].|string|false|||
+|envs.{string}.|string|false|||
+|mode|[Mode](#ext.Mode)|false|1|run mode.|
+
+## ext.Mode
+**Type:** int
+
+Mode mode define.
+
+| Value | 描述 |
+|----------|--------------|
+|1|mode q.|
+|2|mode a.|
+
+---
+GENERATED BY THE COMMAND [type2md](https://github.com/eleztian/type2md)
+from github.com/eleztian/type2md/test.Config
diff --git a/test/ext/hook.go b/test/ext/hook.go
new file mode 100644
index 0000000..b16e7f1
--- /dev/null
+++ b/test/ext/hook.go
@@ -0,0 +1,17 @@
+package ext
+
+// Hook hook config.
+type Hook struct {
+ Name string `json:"name" require:"" default:"example"` // hook name
+ Commands []string `json:"commands"` // command list
+ Envs map[string]string `json:"envs"` // env key map
+ Mode Mode `json:"mode" default:"1"` // run mode
+}
+
+// Mode mode define.
+type Mode int
+
+const (
+ Mode_Q Mode = iota + 1 // mode q
+ Mode_A // mode a
+)
diff --git a/test/test.go b/test/test.go
new file mode 100644
index 0000000..4e32a95
--- /dev/null
+++ b/test/test.go
@@ -0,0 +1,15 @@
+package test
+
+import "github.com/eleztian/type2md/test/ext"
+
+//go:generate type2md -f ../docs/doc_config.md github.com/eleztian/type2md/test Config
+
+// Config doc.
+type Config struct {
+ Pre ext.Hook
+ Post *ext.Hook
+ Servers map[string]struct {
+ Host string `json:"host"`
+ Port int `json:"port" enums:"22,65522" require:""`
+ } `json:"servers"` // server list
+}
diff --git a/version.go b/version.go
new file mode 100644
index 0000000..ab25ccf
--- /dev/null
+++ b/version.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+var (
+ Version = "v1.0.0"
+ CommitID = ""
+ BuildTime = ""
+)
+
+func PrintVersion() {
+ fmt.Printf(`%s
+---
+Parse the source code through the ast syntax tree,
+extract the specified structure definition and
+convert it into a markdown file.
+----
+Version : %s
+CommitID : %s
+BuildTime: %s
+Author : MoreSec CPF 中间件团队
+`, os.Args[0], Version, CommitID, BuildTime)
+}