Skip to content

Commit

Permalink
Enhance doc gen tool: support custom template (#147)
Browse files Browse the repository at this point in the history
* update doc render template: avoid multiple index.md with same name; remove redundant schema prefix

* fix doc test bug: continue comparing

* enhance: doc gen support index without directory path

* doc gen: ignore file extension when render index content

* doc gen: support custom template files
  • Loading branch information
amyXia1994 committed Aug 30, 2023
1 parent 69e4008 commit bc9e918
Show file tree
Hide file tree
Showing 23 changed files with 298 additions and 127 deletions.
5 changes: 5 additions & 0 deletions cmds/kcl-go/command/cmd_doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ kcl-go doc generate --file-path <package path> --target <target directory>`,
Name: "escape-html",
Usage: "whether to escape html symbols when the output format is markdown. Always scape when the output format is html. Default to false",
},
&cli.StringFlag{
Name: "template",
Usage: "The template directory based on the KCL package root. If not specified, the built-in templates will be used.",
},
},
Action: func(context *cli.Context) error {
opts := gen.GenOpts{
Expand All @@ -70,6 +74,7 @@ kcl-go doc generate --file-path <package path> --target <target directory>`,
Format: context.String("format"),
Target: context.String("target"),
EscapeHtml: context.Bool("escape-html"),
TemplateDir: context.String("template"),
}

genContext, err := opts.ValidateComplete()
Expand Down
127 changes: 100 additions & 27 deletions pkg/tools/gen/gendoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,10 @@ var schemaDocTmpl string
//go:embed templates/doc/packageDoc.gotmpl
var packageDocTmpl string

var tmpl *template.Template

func init() {
var err error
tmpl = template.New("").Funcs(funcMap())
_, err = tmpl.Parse(schemaDocTmpl)
if err != nil {
panic(err)
}
_, err = tmpl.Parse(packageDocTmpl)
if err != nil {
panic(err)
}
}
const (
schemaDocTmplFile = "schemaDoc.gotmpl"
packageDocTmplFile = "packageDoc.gotmpl"
)

// GenContext defines the context during the generation
type GenContext struct {
Expand All @@ -48,6 +38,12 @@ type GenContext struct {
IgnoreDeprecated bool
// EscapeHtml defines whether to escape html symbols when the output format is markdown
EscapeHtml bool
// SchemaDocTmpl defines the content of the schemaDoc template
SchemaDocTmpl string
// PackageDocTmpl defines the content of the packageDoc template
PackageDocTmpl string
// Template is the doc render template
Template *template.Template
}

// GenOpts is the user interface defines the doc generate options
Expand All @@ -62,6 +58,8 @@ type GenOpts struct {
IgnoreDeprecated bool
// EscapeHtml defines whether to escape html symbols when the output format is markdown
EscapeHtml bool
// TemplateDir defines the relative path from the package root to the template directory
TemplateDir string
}

type Format string
Expand Down Expand Up @@ -196,37 +194,50 @@ func funcMap() template.FuncMap {
return filepath.Join(tpe.GetSchemaPkgDir(""), tpe.KclExtensions.XKclModelType.Import.Alias)
},
"indexContent": func(pkg *KclPackage) string {
return pkg.getIndexContent(0, " ", "")
return pkg.getIndexContent(0, " ", "", false)
},
"indexContentIgnoreDirPath": func(pkg *KclPackage) string {
return pkg.getIndexContent(0, " ", "", true)
},
}
}

func (pkg *KclPackage) getPackageIndexContent(level int, indentation string, pkgPath string) string {
func (pkg *KclPackage) getPackageIndexContent(level int, indentation string, pkgPath string, ignoreDir bool) string {
currentPkgPath := filepath.Join(pkgPath, pkg.Name)
currentDocPath := filepath.Join(currentPkgPath, "index.md")
currentDocPath := pkg.Name
if !ignoreDir {
// get the full directory path
currentDocPath = filepath.Join(currentPkgPath, fmt.Sprintf("%s.md", pkg.Name))
}
return fmt.Sprintf(`%s- [%s](%s)
%s`, strings.Repeat(indentation, level), pkg.Name, currentDocPath, pkg.getIndexContent(level+1, indentation, currentPkgPath))
%s`, strings.Repeat(indentation, level), pkg.Name, currentDocPath, pkg.getIndexContent(level+1, indentation, currentPkgPath, ignoreDir))
}

func (tpe *KclOpenAPIType) getSchemaIndexContent(level int, indentation string, pkgPath string) string {
docPath := filepath.Join(pkgPath, "index.md")
func (tpe *KclOpenAPIType) getSchemaIndexContent(level int, indentation string, pkgPath string, pkgName string, ignoreDir bool) string {
docPath := pkgName
if !ignoreDir {
// get the full directory path
docPath = filepath.Join(pkgPath, fmt.Sprintf("%s.md", pkgName))
}
if level == 0 {
// the schema is defined in current package
docPath = ""
}
return fmt.Sprintf(`%s- [%s](%s#schema-%s)
`, strings.Repeat(indentation, level), tpe.KclExtensions.XKclModelType.Type, docPath, tpe.KclExtensions.XKclModelType.Type)

return fmt.Sprintf(`%s- [%s](%s#%s)
`, strings.Repeat(indentation, level), tpe.KclExtensions.XKclModelType.Type, docPath, strings.ToLower(tpe.KclExtensions.XKclModelType.Type))
}

func (pkg *KclPackage) getIndexContent(level int, indentation string, pkgPath string) string {
func (pkg *KclPackage) getIndexContent(level int, indentation string, pkgPath string, ignoreDir bool) string {
var content string
if len(pkg.SchemaList) > 0 {
for _, sch := range pkg.SchemaList {
content += sch.getSchemaIndexContent(level, indentation, pkgPath)
content += sch.getSchemaIndexContent(level, indentation, pkgPath, pkg.Name, ignoreDir)
}
}
if len(pkg.SubPackageList) > 0 {
for _, pkg := range pkg.SubPackageList {
content += pkg.getPackageIndexContent(level, indentation, pkgPath)
content += pkg.getPackageIndexContent(level, indentation, pkgPath, ignoreDir)
}
}
return content
Expand All @@ -235,9 +246,13 @@ func (pkg *KclPackage) getIndexContent(level int, indentation string, pkgPath st
func (g *GenContext) renderPackage(pkg *KclPackage, parentDir string) error {
// render the package's index.md page
//fmt.Println(fmt.Sprintf("creating %s/index.md", parentDir))
indexFileName := fmt.Sprintf("%s.%s", "index", g.Format)
pkgName := pkg.Name
if pkg.Name == "" {
pkgName = "main"
}
indexFileName := fmt.Sprintf("%s.%s", pkgName, g.Format)
var contentBuf bytes.Buffer
err := tmpl.ExecuteTemplate(&contentBuf, "packageDoc", struct {
err := g.Template.ExecuteTemplate(&contentBuf, "packageDoc", struct {
EscapeHtml bool
Data *KclPackage
}{
Expand Down Expand Up @@ -293,6 +308,63 @@ func (opts *GenOpts) ValidateComplete() (*GenContext, error) {
}
g.PackagePath = absPath

// --- template directory ---
g.SchemaDocTmpl = schemaDocTmpl
g.PackageDocTmpl = packageDocTmpl
if opts.TemplateDir != "" {
tmplAbsPath := filepath.Join(g.PackagePath, opts.TemplateDir)
templatesDirInfo, err := os.Stat(tmplAbsPath)
if err != nil {
return nil, fmt.Errorf("invalid template directory path: %s. error: %s", opts.TemplateDir, err.Error())
}
if !templatesDirInfo.IsDir() {
return nil, fmt.Errorf("template path is not a directory: %s", opts.TemplateDir)
}
err = filepath.Walk(tmplAbsPath, func(path string, info os.FileInfo, _ error) error {
if info.IsDir() {
// skip directories
return nil
}
rel, err := filepath.Rel(tmplAbsPath, path)
if err != nil {
return err
}
switch rel {
case schemaDocTmplFile:
// use custom schema Doc Template file
content, err := os.ReadFile(path)
if err != nil {
return err
}
g.SchemaDocTmpl = string(content)
return nil
case packageDocTmplFile:
// use custom package Doc Template file
content, err := os.ReadFile(path)
if err != nil {
return err
}
g.PackageDocTmpl = string(content)
return nil
default:
return fmt.Errorf("unexpected template file: %s", path)
}
})
if err != nil {
return nil, err
}
}
// parse template
g.Template = template.New("").Funcs(funcMap())
_, err = g.Template.Parse(g.SchemaDocTmpl)
if err != nil {
return nil, err
}
_, err = g.Template.Parse(g.PackageDocTmpl)
if err != nil {
return nil, err
}

// --- target ---
if opts.Target == "" {
// complete target output directory
Expand All @@ -310,6 +382,7 @@ func (opts *GenOpts) ValidateComplete() (*GenContext, error) {
if !file.IsDir() {
return nil, fmt.Errorf("invalid target directory(%s) to output the doc files: not a directory", opts.Target)
}
g.Target = opts.Target
}
g.Target = path.Join(g.Target, "docs")
if _, err := os.Stat(g.Target); err == nil {
Expand Down
117 changes: 105 additions & 12 deletions pkg/tools/gen/gendoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,121 @@ import (
"bufio"
"errors"
"fmt"
assert2 "github.com/stretchr/testify/assert"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)

func TestIndexContent(t *testing.T) {
if runtime.GOOS == "windows" {
return
}
rootPkg := KclPackage{
Name: "test",
SubPackageList: []*KclPackage{
{
Name: "sub1",
SubPackageList: []*KclPackage{
{
Name: "sub2",
SchemaList: []*KclOpenAPIType{
{
KclExtensions: &KclExtensions{
XKclModelType: &XKclModelType{
Type: "C",
},
},
},
},
},
},
SchemaList: []*KclOpenAPIType{
{
KclExtensions: &KclExtensions{
XKclModelType: &XKclModelType{
Type: "B",
},
},
},
},
},
},
SchemaList: []*KclOpenAPIType{
{
KclExtensions: &KclExtensions{
XKclModelType: &XKclModelType{
Type: "A",
},
},
},
},
}
tCases := []struct {
root KclPackage
ignoreDir bool
expect string
}{
{
root: rootPkg,
ignoreDir: false,
expect: `- [A](#a)
- [sub1](sub1/sub1.md)
- [B](sub1/sub1.md#b)
- [sub2](sub1/sub2/sub2.md)
- [C](sub1/sub2/sub2.md#c)
`,
},
{
root: rootPkg,
ignoreDir: true,
expect: `- [A](#a)
- [sub1](sub1)
- [B](sub1#b)
- [sub2](sub2)
- [C](sub2#c)
`,
},
}
for _, tCase := range tCases {
got := tCase.root.getIndexContent(0, " ", "", tCase.ignoreDir)
assert2.Equal(t, tCase.expect, got)
}
}

func TestDocGenerate(t *testing.T) {
tCases := initTestCases(t)
for _, tCase := range tCases {
genContext := GenContext{
PackagePath: tCase.PackagePath,
Format: Markdown,
IgnoreDeprecated: false,
// create target directory
err := os.MkdirAll(tCase.GotMd, 0755)
if err != nil {
t.Fatal(err)
}
genOpts := GenOpts{
Path: tCase.PackagePath,
Format: string(Markdown),
Target: tCase.GotMd,
IgnoreDeprecated: false,
EscapeHtml: true,
TemplateDir: tCase.TmplPath,
}
err := genContext.GenDoc()
genContext, err := genOpts.ValidateComplete()
if err != nil {
t.Fatal(err)
}
err = genContext.GenDoc()
if err != nil {
t.Fatalf("generate failed: %s", err)
}
// check the content of expected and actual
err = CompareDir(tCase.ExpectMd, tCase.GotMd)
err = CompareDir(tCase.ExpectMd, filepath.Join(tCase.GotMd, "docs"))
if err != nil {
t.Fatal(err)
}
// if test failed, keep generate files for checking
os.RemoveAll(genContext.Target)
os.RemoveAll(tCase.GotMd)
}
}

Expand All @@ -50,14 +137,15 @@ func initTestCases(t *testing.T) []*TestCase {
tcases := make([]*TestCase, len(sourcePkgs))

for i, p := range sourcePkgs {
resultDir := filepath.Join(cwd, testdataDir, p)
packageDir := filepath.Join(cwd, testdataDir, p)
var resultDir string
if runtime.GOOS == "windows" {
resultDir = filepath.Join(resultDir, "windows")
resultDir = filepath.Join(packageDir, "windows")
} else {
resultDir = filepath.Join(resultDir, "unixlike")
resultDir = filepath.Join(packageDir, "unixlike")
}
tcases[i] = &TestCase{
PackagePath: filepath.Join(testdataDir, p),
PackagePath: packageDir,
ExpectMd: filepath.Join(resultDir, "md"),
ExpectHtml: filepath.Join(resultDir, "html"),
GotMd: filepath.Join(resultDir, "md_got"),
Expand All @@ -73,6 +161,7 @@ type TestCase struct {
ExpectHtml string
GotMd string
GotHtml string
TmplPath string
}

func CompareDir(a string, b string) error {
Expand All @@ -99,7 +188,11 @@ func CompareDir(a string, b string) error {
return fmt.Errorf("open file failed when compare, file path: %s", bPath)
}
if fA.IsDir() {
return CompareDir(aPath, bPath)
err := CompareDir(aPath, bPath)
if err != nil {
return err
}
continue
}
linesA, err := readLines(aPath)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/tools/gen/templates/doc/packageDoc.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{{- $Data := .Data -}}
{{- $EscapeHtml := .EscapeHtml -}}
# Package {{if ne $Data.Name ""}}{{$Data.Name}}{{else}}main{{end}}{{/* the package name should not be empty, issue: https://github.com/kcl-lang/kpm/issues/171 */}}
# {{if ne $Data.Name ""}}{{$Data.Name}}{{else}}main{{end}}{{/* the package name should not be empty, issue: https://github.com/kcl-lang/kpm/issues/171 */}}
{{if ne $Data.Description ""}}
## Overview

Expand Down
Loading

0 comments on commit bc9e918

Please sign in to comment.