diff --git a/gnoland/website/main.go b/gnoland/website/main.go index c64eb4322e7..acb95d4d135 100644 --- a/gnoland/website/main.go +++ b/gnoland/website/main.go @@ -23,13 +23,15 @@ import ( "github.com/gorilla/mux" "github.com/gotuna/gotuna" + "github.com/gnolang/gno/gnoland/website/pkgs/doc" "github.com/gnolang/gno/gnoland/website/static" // for static files "github.com/gnolang/gno/pkgs/sdk/vm" // for error types // "github.com/gnolang/gno/pkgs/sdk" // for baseapp (info, status) ) const ( - qFileStr = "vm/qfile" + qFileStr = "vm/qfile" + qFilesStr = "vm/qfiles" ) var flags struct { @@ -309,7 +311,21 @@ func handlerRealmFile(app gotuna.App) http.Handler { vars := mux.Vars(r) diruri := "gno.land/r/" + vars["rlmname"] filename := vars["filename"] - renderPackageFile(app, w, r, diruri, filename) + if filename != "" { + renderPackageFile(app, w, r, diruri, filename) + return + } + res, err := makeRequest(qFileStr, []byte(diruri)) + if err != nil { + writeError(w, err) + return + } + files := strings.Split(string(res.Data), "\n") + tmpl := app.NewTemplatingEngine() + tmpl.Set("DirURI", diruri) + tmpl.Set("DirPath", pathOf(diruri)) + tmpl.Set("Files", files) + tmpl.Render(w, r, "realm_dir.html", "funcs.html") }) } @@ -318,50 +334,59 @@ func handlerPackageFile(app gotuna.App) http.Handler { vars := mux.Vars(r) pkgpath := "gno.land/p/" + vars["filepath"] diruri, filename := std.SplitFilepath(pkgpath) - if filename == "" && diruri == pkgpath { - // redirect to diruri + "/" - http.Redirect(w, r, "/p/"+vars["filepath"]+"/", http.StatusFound) + if filename != "" { + renderPackageFile(app, w, r, diruri, filename) return } - renderPackageFile(app, w, r, diruri, filename) + renderPackage(app, w, r, diruri) }) } +func renderPackage(app gotuna.App, w http.ResponseWriter, r *http.Request, diruri string) { + res, err := makeRequest(qFilesStr, []byte(diruri)) + if err != nil { + writeError(w, err) + return + } + + var files std.MemFileBodies + if err := json.Unmarshal(res.Data, &files); err != nil { + writeError(w, err) + return + } + + pkgdoc, err := doc.New(diruri, files) + if err != nil { + writeError(w, err) + return + } + + pkgContent, err := pkgdoc.Markdown() + if err != nil { + writeError(w, err) + return + } + + tmpl := app.NewTemplatingEngine() + tmpl.Set("DirPath", pathOf(diruri)) + tmpl.Set("PkgContent", string(pkgContent)) + tmpl.Render(w, r, "package_dir.html", "funcs.html") +} + func renderPackageFile(app gotuna.App, w http.ResponseWriter, r *http.Request, diruri string, filename string) { - if filename == "" { - // Request is for a folder. - qpath := qFileStr - data := []byte(diruri) - res, err := makeRequest(qpath, data) - if err != nil { - writeError(w, err) - return - } - files := strings.Split(string(res.Data), "\n") - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("Files", files) - tmpl.Render(w, r, "package_dir.html", "funcs.html") - } else { - // Request is for a file. - filepath := diruri + "/" + filename - qpath := qFileStr - data := []byte(filepath) - res, err := makeRequest(qpath, data) - if err != nil { - writeError(w, err) - return - } - // Render template. - tmpl := app.NewTemplatingEngine() - tmpl.Set("DirURI", diruri) - tmpl.Set("DirPath", pathOf(diruri)) - tmpl.Set("FileName", filename) - tmpl.Set("FileContents", string(res.Data)) - tmpl.Render(w, r, "package_file.html", "funcs.html") + // Request is for a file. + res, err := makeRequest(qFileStr, []byte(diruri+"/"+filename)) + if err != nil { + writeError(w, err) + return } + // Render template. + tmpl := app.NewTemplatingEngine() + tmpl.Set("DirURI", diruri) + tmpl.Set("DirPath", pathOf(diruri)) + tmpl.Set("FileName", filename) + tmpl.Set("FileContents", string(res.Data)) + tmpl.Render(w, r, "package_file.html", "funcs.html") } func makeRequest(qpath string, data []byte) (res *abci.ResponseQuery, err error) { diff --git a/gnoland/website/pkgs/doc/ast_to_string.go b/gnoland/website/pkgs/doc/ast_to_string.go new file mode 100644 index 00000000000..c7c6a1bf7c4 --- /dev/null +++ b/gnoland/website/pkgs/doc/ast_to_string.go @@ -0,0 +1,189 @@ +package doc + +import ( + "fmt" + "go/ast" + "strings" +) + +func generateFuncSignature(fn *ast.FuncDecl) string { + if fn == nil { + return "" + } + + var b strings.Builder + b.WriteString("func ") + + if fn.Recv != nil { + var receiverNames []string + for _, field := range fn.Recv.List { + var fieldName string + if len(field.Names) > 0 { + fieldName = field.Names[0].Name + } + receiverNames = append(receiverNames, fmt.Sprintf("%s %s", fieldName, typeString(field.Type))) + } + if len(receiverNames) > 0 { + b.WriteString(fmt.Sprintf("(%s) ", strings.Join(receiverNames, ", "))) + } + } + + fmt.Fprintf(&b, "%s(", fn.Name.Name) + + var params []string + if fn.Type.Params != nil { + for _, param := range fn.Type.Params.List { + paramType := typeString(param.Type) + if len(param.Names) == 0 { + params = append(params, paramType) + } else { + paramNames := make([]string, len(param.Names)) + for i, name := range param.Names { + paramNames[i] = name.Name + } + params = append(params, fmt.Sprintf("%s %s", strings.Join(paramNames, ", "), paramType)) + } + } + } + + fmt.Fprintf(&b, "%s)", strings.Join(params, ", ")) + + results := []string{} + if fn.Type.Results != nil { + hasNamedParams := false + for _, result := range fn.Type.Results.List { + if len(result.Names) == 0 { + results = append(results, typeString(result.Type)) + } else { + hasNamedParams = true + var resultNames []string + for _, id := range result.Names { + resultNames = append(resultNames, id.Name) + } + results = append(results, fmt.Sprintf("%s %s", strings.Join(resultNames, ", "), typeString(result.Type))) + } + } + + if len(results) > 0 { + b.WriteString(" ") + returnType := strings.Join(results, ", ") + + if hasNamedParams || len(results) > 1 { + returnType = fmt.Sprintf("(%s)", returnType) + } + + b.WriteString(returnType) + } + } + + return b.String() +} + +// This code is inspired by the code at https://cs.opensource.google/go/go/+/refs/tags/go1.20.1:src/go/doc/reader.go;drc=40ed3591829f67e7a116180aec543dd15bfcf5f9;bpv=1;bpt=1;l=124 +func typeString(expr ast.Expr) string { + if expr == nil { + return "" + } + + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.IndexExpr: + return typeString(t.X) + case *ast.IndexListExpr: + return typeString(t.X) + case *ast.SelectorExpr: + if _, ok := t.X.(*ast.Ident); ok { + return fmt.Sprintf("%s.%s", typeString(t.X), t.Sel.Name) + } + case *ast.ParenExpr: + return typeString(t.X) + case *ast.StarExpr: + return fmt.Sprintf("*%s", typeString(t.X)) + case *ast.BasicLit: + return t.Value + case *ast.Ellipsis: + return fmt.Sprintf("...%s", typeString(t.Elt)) + case *ast.FuncType: + var params []string + if t.Params != nil { + for _, field := range t.Params.List { + paramType := typeString(field.Type) + if len(field.Names) > 0 { + for _, name := range field.Names { + params = append(params, fmt.Sprintf("%s %s", name.Name, paramType)) + } + } else { + params = append(params, paramType) + } + } + } + var results []string + if t.Results != nil { + for _, field := range t.Results.List { + resultType := typeString(field.Type) + if len(field.Names) > 0 { + for _, name := range field.Names { + results = append(results, fmt.Sprintf("%s %s", name.Name, resultType)) + } + } else { + results = append(results, resultType) + } + } + } + + return strings.TrimSpace(fmt.Sprintf("func(%s) %s", strings.Join(params, ", "), strings.Join(results, ", "))) + case *ast.StructType: + var fields []string + for _, field := range t.Fields.List { + fieldType := typeString(field.Type) + if len(field.Names) > 0 { + for _, name := range field.Names { + fields = append(fields, fmt.Sprintf("%s %s", name.Name, fieldType)) + } + } else { + fields = append(fields, fieldType) + } + } + return fmt.Sprintf("struct{%s}", strings.Join(fields, "; ")) + case *ast.InterfaceType: + return "interface{}" + case *ast.MapType: + return fmt.Sprintf("map[%s]%s", typeString(t.Key), typeString(t.Value)) + case *ast.ChanType: + chanDir := "chan" + if t.Dir == ast.SEND { + chanDir = "chan<-" + } else if t.Dir == ast.RECV { + chanDir = "<-chan" + } + return fmt.Sprintf("%s %s", chanDir, typeString(t.Value)) + case *ast.ArrayType: + return fmt.Sprintf("[%s]%s", typeString(t.Len), typeString(t.Elt)) + case *ast.SliceExpr: + return fmt.Sprintf("[]%s", typeString(t.X)) + } + return "" +} + +func isFuncExported(fn *ast.FuncDecl) bool { + if !fn.Name.IsExported() { + return false + } + + if fn.Recv == nil { + return true + } + + for _, recv := range fn.Recv.List { + if ast.IsExported(removePointer(typeString(recv.Type))) { + return true + } + } + + return false +} + +func removePointer(name string) string { + return strings.TrimPrefix(name, "*") +} diff --git a/gnoland/website/pkgs/doc/ast_to_string_test.go b/gnoland/website/pkgs/doc/ast_to_string_test.go new file mode 100644 index 00000000000..fb5f1456ef3 --- /dev/null +++ b/gnoland/website/pkgs/doc/ast_to_string_test.go @@ -0,0 +1,314 @@ +package doc + +import ( + "go/ast" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateFuncSignature(t *testing.T) { + testcases := []struct { + name string + fn *ast.FuncDecl + want string + }{ + { + name: "NoParametersNoResults", + fn: &ast.FuncDecl{Name: ast.NewIdent("testFunc"), Type: &ast.FuncType{}}, + want: "func testFunc()", + }, + { + name: "ParametersNoResults", + fn: &ast.FuncDecl{ + Name: ast.NewIdent("testFunc"), + Type: &ast.FuncType{ + Params: &ast.FieldList{ + List: []*ast.Field{ + { + Names: []*ast.Ident{ast.NewIdent("param1")}, + Type: ast.NewIdent("string"), + }, + { + Names: []*ast.Ident{ast.NewIdent("param2")}, + Type: ast.NewIdent("int"), + }, + }, + }, + }, + }, + want: "func testFunc(param1 string, param2 int)", + }, + { + name: "NoParametersResults", + fn: &ast.FuncDecl{ + Name: ast.NewIdent("testFunc"), + Type: &ast.FuncType{ + Results: &ast.FieldList{ + List: []*ast.Field{ + { + Type: ast.NewIdent("string"), + }, + { + Type: ast.NewIdent("error"), + }, + }, + }, + }, + }, + want: "func testFunc() (string, error)", + }, + { + name: "OneNamedResult", + fn: &ast.FuncDecl{ + Name: &ast.Ident{ + Name: "testFunc", + }, + Type: &ast.FuncType{ + Results: &ast.FieldList{ + List: []*ast.Field{ + { + Type: ast.NewIdent("string"), + Names: []*ast.Ident{ + { + Name: "result", + }, + }, + }, + }, + }, + }, + }, + want: "func testFunc() (result string)", + }, + { + name: "TwoNamedResults", + fn: &ast.FuncDecl{ + Name: &ast.Ident{ + Name: "testFunc", + }, + Type: &ast.FuncType{ + Results: &ast.FieldList{ + List: []*ast.Field{ + { + Type: ast.NewIdent("string"), + Names: []*ast.Ident{ + { + Name: "result1", + }, + }, + }, + { + Type: ast.NewIdent("int"), + Names: []*ast.Ident{ + { + Name: "result2", + }, + }, + }, + }, + }, + }, + }, + want: "func testFunc() (result1 string, result2 int)", + }, + { + name: "FunctionParameter", + fn: &ast.FuncDecl{ + Name: &ast.Ident{Name: "testFunc"}, + Type: &ast.FuncType{ + Params: &ast.FieldList{ + List: []*ast.Field{ + { + Type: &ast.FuncType{ + Params: &ast.FieldList{ + List: []*ast.Field{ + { + Type: &ast.Ident{Name: "MyType"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: "func testFunc(func(MyType))", + }, + { + name: "InterfaceResult", + fn: &ast.FuncDecl{ + Name: &ast.Ident{ + Name: "testFunc", + }, + Type: &ast.FuncType{ + Results: &ast.FieldList{ + List: []*ast.Field{ + { + Type: &ast.InterfaceType{}, + }, + }, + }, + }, + }, + want: "func testFunc() interface{}", + }, + } + + for _, c := range testcases { + assert.Equal(t, c.want, generateFuncSignature(c.fn), c.name) + } +} + +func TestTypeString(t *testing.T) { + testcases := []struct { + expr ast.Expr + want string + }{ + {&ast.Ident{Name: "int"}, "int"}, + {&ast.Ident{Name: "string"}, "string"}, + {&ast.StarExpr{X: &ast.Ident{Name: "int"}}, "*int"}, + {&ast.ArrayType{Elt: &ast.Ident{Name: "string"}}, "[]string"}, + {&ast.ArrayType{Len: &ast.BasicLit{Value: "5"}, Elt: &ast.Ident{Name: "int"}}, "[5]int"}, + {&ast.MapType{Key: &ast.Ident{Name: "string"}, Value: &ast.Ident{Name: "int"}}, "map[string]int"}, + {&ast.ChanType{Value: &ast.Ident{Name: "string"}}, "chan string"}, + {&ast.ChanType{Dir: ast.SEND, Value: &ast.Ident{Name: "int"}}, "chan<- int"}, + {&ast.ChanType{Dir: ast.RECV, Value: &ast.Ident{Name: "float64"}}, "<-chan float64"}, + {&ast.StructType{ + Fields: &ast.FieldList{ + List: []*ast.Field{ + {Names: []*ast.Ident{{Name: "a"}}, Type: &ast.Ident{Name: "int"}}, + {Names: []*ast.Ident{{Name: "b"}}, Type: &ast.Ident{Name: "string"}}, + {Type: &ast.Ident{Name: "bool"}}, + }, + }, + }, "struct{a int; b string; bool}"}, + {&ast.FuncType{}, "func()"}, + {&ast.FuncType{ + Params: &ast.FieldList{ + List: []*ast.Field{ + {Names: []*ast.Ident{{Name: "x"}}, Type: &ast.Ident{Name: "int"}}, + {Names: []*ast.Ident{{Name: "y"}}, Type: &ast.Ident{Name: "string"}}, + }, + }, + Results: &ast.FieldList{ + List: []*ast.Field{ + {Type: &ast.Ident{Name: "float64"}}, + }, + }, + }, "func(x int, y string) float64"}, + {&ast.FuncType{ + Params: &ast.FieldList{ + List: []*ast.Field{ + {Names: []*ast.Ident{{Name: "f"}}, Type: &ast.FuncType{ + Params: &ast.FieldList{ + List: []*ast.Field{ + {Type: &ast.Ident{Name: "int"}}, + {Type: &ast.Ident{Name: "float64"}}, + }, + }, + Results: &ast.FieldList{ + List: []*ast.Field{}, + }, + }}, + }, + }, + Results: &ast.FieldList{}, + }, "func(f func(int, float64))"}, + {&ast.FuncType{ + Results: &ast.FieldList{ + List: []*ast.Field{ + { + Type: &ast.InterfaceType{}, + }, + }, + }, + }, "func() interface{}"}, + {&ast.SliceExpr{X: &ast.Ident{Name: "string"}}, "[]string"}, + {&ast.SliceExpr{X: &ast.Ident{Name: "int"}}, "[]int"}, + } + + for _, c := range testcases { + assert.Equal(t, c.want, typeString(c.expr)) + } +} + +func TestRemovePointer(t *testing.T) { + testcases := []struct { + name string + want string + }{ + {"MyType", "MyType"}, + {"*MyType", "MyType"}, + } + + for _, c := range testcases { + assert.Equal(t, c.want, removePointer(c.name)) + } +} + +func TestIsFuncExported(t *testing.T) { + testcases := []struct { + name string + fn *ast.FuncDecl + want bool + }{ + { + name: "exported function without receiver", + fn: &ast.FuncDecl{ + Name: ast.NewIdent("ExportedFunc"), + }, + want: true, + }, + { + name: "unexported function without receiver", + fn: &ast.FuncDecl{ + Name: ast.NewIdent("unexportedFunc"), + }, + want: false, + }, + { + name: "exported method with exported pointer receiver", + fn: &ast.FuncDecl{ + Name: ast.NewIdent("ExportedMethod"), + Recv: &ast.FieldList{ + List: []*ast.Field{ + { + Names: []*ast.Ident{ast.NewIdent("p")}, + Type: &ast.StarExpr{ + X: &ast.Ident{ + Name: "MyType", + }, + }, + }, + }, + }, + }, + want: true, + }, + { + name: "exported method with unexported pointer receiver", + fn: &ast.FuncDecl{ + Name: ast.NewIdent("ExportedMethod"), + Recv: &ast.FieldList{ + List: []*ast.Field{ + { + Names: []*ast.Ident{ast.NewIdent("p")}, + Type: &ast.StarExpr{ + X: &ast.Ident{ + Name: "myType", + }, + }, + }, + }, + }, + }, + want: false, + }, + } + + for _, c := range testcases { + assert.Equal(t, c.want, isFuncExported(c.fn), c.name) + } +} diff --git a/gnoland/website/pkgs/doc/doc.go b/gnoland/website/pkgs/doc/doc.go new file mode 100644 index 00000000000..bb0d8ace807 --- /dev/null +++ b/gnoland/website/pkgs/doc/doc.go @@ -0,0 +1,97 @@ +package doc + +import ( + "go/ast" + "go/parser" + "go/token" + "sort" + "strings" +) + +func New(pkgPath string, files map[string]string) (*Package, error) { + p := Package{ + ImportPath: pkgPath, + Path: strings.TrimPrefix(pkgPath, "gno.land"), + Filenames: make([]string, 0, len(files)), + } + + fset := token.NewFileSet() + gnoFiles := make(map[string]*ast.File, len(files)) + + for filename, fileContent := range files { + p.Filenames = append(p.Filenames, filename) + + if !strings.HasSuffix(filename, ".gno") || strings.HasSuffix(filename, "_test.gno") || strings.HasSuffix(filename, "_filetest.gno") { + continue + } + + f, err := parser.ParseFile(fset, filename, fileContent, parser.ParseComments) + if err != nil { + return nil, err + } + + ast.FileExports(f) + + gnoFiles[filename] = f + + if p.Name == "" { + p.Name = f.Name.Name + } + + if f.Doc != nil { + doc := f.Doc.Text() + if p.Doc != "" { + p.Doc += "\n" + } + p.Doc += doc + } + } + + for _, f := range gnoFiles { + for _, decl := range f.Decls { + switch x := decl.(type) { + case *ast.FuncDecl: + if isFuncExported(x) { + fn := extractFunc(x) + p.Funcs = append(p.Funcs, fn) + } + case *ast.GenDecl: + switch x.Tok { + case token.TYPE: + for _, spec := range x.Specs { + if ident, ok := spec.(*ast.TypeSpec); ok && ident.Name.IsExported() { + newType, _ := extractType(fset, ident) + if x.Doc != nil { + newType.Doc = x.Doc.Text() + newType.Doc + } + p.Types = append(p.Types, newType) + } + } + case token.VAR: + value, _ := extractValue(fset, x) + p.Vars = append(p.Vars, value) + case token.CONST: + value, _ := extractValue(fset, x) + p.Consts = append(p.Consts, value) + } + } + } + } + + for _, t := range p.Types { + t.Funcs, t.Methods = p.filterTypeFuncs(t.Name) + t.Vars, t.Consts = p.filterTypeValues(t.Name) + } + + sort.Slice(p.Types, func(i, j int) bool { + return p.Types[i].Name < p.Types[j].Name + }) + + sort.Slice(p.Funcs, func(i, j int) bool { + return p.Funcs[i].Name < p.Funcs[j].Name + }) + + sort.Strings(p.Filenames) + + return &p, nil +} diff --git a/gnoland/website/pkgs/doc/doc_test.go b/gnoland/website/pkgs/doc/doc_test.go new file mode 100644 index 00000000000..21fb5312635 --- /dev/null +++ b/gnoland/website/pkgs/doc/doc_test.go @@ -0,0 +1,148 @@ +package doc + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + files := map[string]string{ + "example.gno": ` +// Package example is an example package. +package example + +// A private variable. +var private string = "I'm private" + +// A public variable. +var Public string = "I'm public" + +// A public grouped variable. +var ( + Grouped1 string = "I'm Grouped1" + Grouped2 string = "I'm Grouped2" +) + +// A private constant. +const privateConst string = "I'm a private constant" + +// A public constant. +const PublicConst string = "I'm a public constant" + +// A private grouped constant. +const ( + groupedConst1 string = "I'm grouped const 1" + groupedConst2 string = "I'm grouped const 2" +) + +// A public type. +type MyType struct { + Name string // Name is a public field + age int // age is a private field +} + +// A method with a pointer. +func (mt *MyType) PointerMethod() {} + +// A method without a pointer. +func (mt MyType) NonPointerMethod() {} + +// A function that returns MyType. +func NewMyType(name string) *MyType { + return &MyType{Name: name} +} + +// A function that takes a MyType as a parameter. +func UseMyType(mt *MyType) {} + +// A private type. +type myPrivateType struct {} + +// A public method with a private type. +func (mPT *myPrivateType) PublicMethod() {} + +// A public interface. +type MyInterface interface { + MyMethod() string +} + +// A function that takes various types as parameters. +func ComplexFunction(s string, i int, f float64, b bool, a []string, fn func(), mt *MyType, iface MyInterface, mt2 MyType, fn2 func(string, int) (int, string)) {} + +// A function that returns multiple values. +func MultipleReturnValues() (string, int) { + return "gno", 42 +} + +// A function with named parameters and named return values. +func NamedParameters(firstParam int, secondParam string) (firstReturn string, secondReturn int) { + return "gno", 42 +} + +// A function with unnamed parameters and unnamed return values. +func UnnamedParameters(int, string) (string, int) { + return "gno", 42 +} +`, + } + pkgPath := "gno.land/p/demo/example" + pkg, err := New(pkgPath, files) + require.NoError(err) + require.NotNil(pkg) + + assert.Equal(pkgPath, pkg.ImportPath) + assert.Equal("example", pkg.Name) + assert.Equal("Package example is an example package.\n", pkg.Doc) + + assert.Len(pkg.Filenames, 1) + assert.Len(pkg.Consts, 1) + assert.Len(pkg.Vars, 2) + assert.Len(pkg.Funcs, 5) + require.Len(pkg.Types, 2) + + myInterfaceType := pkg.Types[0] + assert.Equal("MyInterface", myInterfaceType.Name) + assert.Equal("MyInterface", myInterfaceType.ID) + assert.Equal("A public interface.\n", myInterfaceType.Doc) + assert.Len(myInterfaceType.Vars, 0) + assert.Len(myInterfaceType.Consts, 0) + assert.Len(myInterfaceType.Funcs, 0) + assert.Len(myInterfaceType.Methods, 0) + assert.Equal("type MyInterface interface {\n\tMyMethod() string\n}", myInterfaceType.Definition) + + myTypeType := pkg.Types[1] + assert.Equal("MyType", myTypeType.Name) + assert.Equal("MyType", myTypeType.ID) + assert.Equal("A public type.\n", myTypeType.Doc) + assert.Len(myTypeType.Vars, 0) + assert.Len(myTypeType.Consts, 0) + + require.Len(myTypeType.Funcs, 1) + assert.Equal("NewMyType", myTypeType.Funcs[0].Name) + assert.Equal("NewMyType", myTypeType.Funcs[0].ID) + assert.Equal("A function that returns MyType.\n", myTypeType.Funcs[0].Doc) + assert.Len(myTypeType.Funcs[0].Params, 1) + assert.Len(myTypeType.Funcs[0].Returns, 1) + assert.Len(myTypeType.Funcs[0].Recv, 0) + + require.Len(myTypeType.Methods, 2) + assert.Equal("NonPointerMethod", myTypeType.Methods[0].Name) + assert.Equal("MyType.NonPointerMethod", myTypeType.Methods[0].ID) + assert.Equal("A method without a pointer.\n", myTypeType.Methods[0].Doc) + assert.Len(myTypeType.Methods[0].Params, 0) + assert.Len(myTypeType.Methods[0].Returns, 0) + assert.Len(myTypeType.Methods[0].Recv, 1) + + assert.Equal("PointerMethod", myTypeType.Methods[1].Name) + assert.Equal("*MyType.PointerMethod", myTypeType.Methods[1].ID) + assert.Equal("A method with a pointer.\n", myTypeType.Methods[1].Doc) + assert.Len(myTypeType.Methods[1].Params, 0) + assert.Len(myTypeType.Methods[1].Returns, 0) + assert.Len(myTypeType.Methods[1].Recv, 1) + + assert.Equal("type MyType struct {\n\tName string // Name is a public field\n\t// contains filtered or unexported fields\n}", myTypeType.Definition) +} diff --git a/gnoland/website/pkgs/doc/template.go b/gnoland/website/pkgs/doc/template.go new file mode 100644 index 00000000000..40cff3f7797 --- /dev/null +++ b/gnoland/website/pkgs/doc/template.go @@ -0,0 +1,115 @@ +package doc + +import ( + "fmt" + "go/doc/comment" +) + +func commentFmt(c string) string { + var p comment.Parser + pr := &comment.Printer{ + DocLinkBaseURL: "/p", + } + return string(pr.Markdown(p.Parse(c))) +} + +func codeFmt(code string) string { + return fmt.Sprintf("%s\n%s\n%s", "```go", code, "```") +} + +var TemplateMarkdown = ` +# Package {{ .Name }} + +import "{{ .ImportPath }}" + +## Overview + +{{ if .Doc }} +{{ comment .Doc }} +{{ else }} +This section is empty. +{{ end }} + +## Constants + +{{ range .Consts }} +{{ if .Doc }} +{{ comment .Doc }} +{{ end }} +{{ code .Signature }} +{{ else }} +This section is empty. +{{ end }} + +## Variables + +{{ range .Vars }} +{{ if .Doc }} +{{ comment .Doc }} +{{ end }} +{{ code .Signature }} +{{ else }} +This section is empty. +{{ end }} + +## Functions + +{{ range .Funcs }} +### func {{ . }} +{{ code .Signature }} +{{ if .Doc }} +{{ comment .Doc }} +{{ end }} +{{ else }} +This section is empty. +{{ end }} + +## Types + +{{ range .Types }} +### type {{ .Name }} +{{ if .Doc }} +{{ comment .Doc }} +{{ end }} +{{ code .Definition }} + +{{ range .Vars }} + +{{ if .Doc }} +{{ comment .Doc }} +{{ end }} +{{ code .Signature }} +{{ end }} + +{{ range .Consts }} + +{{ if .Doc }} +{{ comment .Doc }} +{{ end }} +{{ code .Signature }} +{{ end }} + +{{ range .Funcs }} +### func {{ . }} +{{ code .Signature }} +{{ if .Doc }} +{{ .Doc }} +{{ end }} +{{ end }} + +{{ range .Methods }} +### func {{ . }} +{{ code .Signature }} +{{ if .Doc }} +{{ comment .Doc }} +{{ end }} +{{ end }} +{{ else }} +This section is empty. +{{ end }} + +## Source Files +{{ range .Filenames }} +- [{{ . }}]({{ $.Path }}/{{ . }}) +{{ end }} +` diff --git a/gnoland/website/pkgs/doc/types.go b/gnoland/website/pkgs/doc/types.go new file mode 100644 index 00000000000..2d9ea4b3b76 --- /dev/null +++ b/gnoland/website/pkgs/doc/types.go @@ -0,0 +1,289 @@ +package doc + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/token" + "sort" + "strings" + "text/template" +) + +type Package struct { + ImportPath string + Path string + Name string + Doc string + Filenames []string + Funcs []*Func + Methods []*Func + Vars []*Value + Consts []*Value + Types []*Type +} + +func (p *Package) Markdown() ([]byte, error) { + var buf bytes.Buffer + + if err := template.Must(template.New("").Funcs(template.FuncMap{ + "code": codeFmt, + "comment": commentFmt, + }).Parse(TemplateMarkdown)).Execute(&buf, p); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (p *Package) filterTypeFuncs(typeName string) (funcs []*Func, methods []*Func) { + var remainingFuncs []*Func + + for _, fn := range p.Funcs { + if fn.Recv == nil { + matched := false + for _, r := range fn.Returns { + if removePointer(r.Type) == typeName { + funcs = append(funcs, fn) + matched = true + break + } + } + + if !matched { + remainingFuncs = append(remainingFuncs, fn) + } + continue + } + for _, n := range fn.Recv { + if removePointer(n) == typeName { + methods = append(methods, fn) + break + } else { + remainingFuncs = append(remainingFuncs, fn) + } + } + } + + sort.Slice(funcs, func(i, j int) bool { + return funcs[i].Name < funcs[j].Name + }) + + sort.Slice(methods, func(i, j int) bool { + return methods[i].Name < methods[j].Name + }) + + p.Funcs = remainingFuncs + + return +} + +func (p *Package) filterTypeValues(typeName string) (vars []*Value, consts []*Value) { + var ( + remainingVars []*Value + remainingConsts []*Value + ) + + for _, v := range p.Vars { + var matched bool + for _, item := range v.Items { + if removePointer(item.Type) == typeName { + vars = append(vars, v) + matched = true + break + } + } + if !matched { + remainingVars = append(remainingVars, v) + } + } + + for _, c := range p.Consts { + var matched bool + for _, item := range c.Items { + if item.Type == typeName { + consts = append(consts, c) + matched = true + break + } + } + if !matched { + remainingConsts = append(remainingConsts, c) + } + } + + p.Vars = remainingVars + p.Consts = remainingConsts + + return +} + +type Value struct { + ID string + Doc string + Names []string + Items []*ValueItem + Signature string +} + +type ValueItem struct { + ID string + Doc string + Type string + Name string + Value string +} + +type FuncParam struct { + Type string + Names []string +} + +type FuncReturn struct { + Type string + Names []string +} + +type Func struct { + ID string + Doc string + Name string + Params []*FuncParam + Returns []*FuncReturn + Recv []string + Signature string +} + +func (f Func) String() string { + var b strings.Builder + + if len(f.Recv) > 0 { + fmt.Fprintf(&b, "(%s) ", strings.Join(f.Recv, ", ")) + } + + b.WriteString(f.Name) + + return b.String() +} + +type Type struct { + ID string + Doc string + Name string + Definition string + Consts []*Value + Vars []*Value + Funcs []*Func + Methods []*Func +} + +func extractFunc(x *ast.FuncDecl) *Func { + fn := Func{ + ID: x.Name.String(), + Doc: x.Doc.Text(), + Name: x.Name.String(), + Signature: generateFuncSignature(x), + } + if x.Recv != nil { + for _, rcv := range x.Recv.List { + fn.Recv = append(fn.Recv, typeString(rcv.Type)) + } + } + + if len(fn.Recv) > 0 { + fn.ID = fmt.Sprintf("%s.%s", fn.Recv[0], fn.Name) + } + + for _, field := range x.Type.Params.List { + paramNames := []string{} + for _, name := range field.Names { + paramNames = append(paramNames, name.Name) + } + paramType := typeString(field.Type) + param := &FuncParam{ + Type: paramType, + Names: paramNames, + } + fn.Params = append(fn.Params, param) + } + + if x.Type.Results != nil { + for _, field := range x.Type.Results.List { + returnNames := []string{} + for _, name := range field.Names { + if name != nil { + continue + } + returnNames = append(returnNames, name.Name) + } + returnType := typeString(field.Type) + ret := &FuncReturn{ + Type: returnType, + Names: returnNames, + } + fn.Returns = append(fn.Returns, ret) + } + } + + return &fn +} + +func extractValue(fset *token.FileSet, x *ast.GenDecl) (*Value, error) { + value := Value{ + Doc: x.Doc.Text(), + } + x.Doc = nil + var buf bytes.Buffer + if err := format.Node(&buf, fset, x); err != nil { + return nil, err + } + value.Signature = buf.String() + for _, spec := range x.Specs { + if valueSpec, ok := spec.(*ast.ValueSpec); ok { + for i, name := range valueSpec.Names { + if !name.IsExported() { + continue + } + value.ID += name.String() + value.Names = append(value.Names, name.Name) + valueItem := ValueItem{ + ID: name.String(), + Type: typeString(valueSpec.Type), + Name: name.String(), + } + + if len(valueSpec.Values) > i { + if lit, ok := valueSpec.Values[i].(*ast.BasicLit); ok { + valueItem.Value = lit.Value + } else if ident, ok := valueSpec.Values[i].(*ast.Ident); ok { + valueItem.Value = ident.Name + } + } + if valueSpec.Doc != nil { + valueItem.Doc = valueSpec.Doc.Text() + } + value.Items = append(value.Items, &valueItem) + } + } + } + return &value, nil +} + +func extractType(fset *token.FileSet, x *ast.TypeSpec) (*Type, error) { + newType := Type{ + ID: x.Name.String(), + Name: x.Name.String(), + Doc: x.Doc.Text(), + } + + x.Doc = nil + var buf bytes.Buffer + buf.WriteString("type ") + if err := format.Node(&buf, fset, x); err != nil { + return nil, err + } + + newType.Definition = buf.String() + return &newType, nil +} diff --git a/gnoland/website/static/css/app.css b/gnoland/website/static/css/app.css index 2ca68b5d231..6d3039db5f7 100644 --- a/gnoland/website/static/css/app.css +++ b/gnoland/website/static/css/app.css @@ -355,3 +355,29 @@ code.hljs { #realm_help .func_name td { font-weight: bold; } + +/*** PKGS/DOC ***/ + +#pkg_doc { + padding: 0 22px; +} + +#pkg_doc h1, +#pkg_doc h2 { + border-bottom: 1px solid #ccc; + margin-bottom: 20px; + padding-bottom: 10px; +} + +#pkg_doc h2 { + margin-top: 60px; +} + +#pkg_doc h3 { + margin-top: 40px; +} + +#pkg_doc pre { + margin-top: 10px; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/gnoland/website/views/funcs.html b/gnoland/website/views/funcs.html index 94412ade18f..e06bb4de05c 100644 --- a/gnoland/website/views/funcs.html +++ b/gnoland/website/views/funcs.html @@ -115,18 +115,33 @@ {{ end }} +{{ define "hljs" }} + + +{{ end }} {{ define "subscribe" }}
diff --git a/gnoland/website/views/package_dir.html b/gnoland/website/views/package_dir.html index 7b4d62c00cc..a9402a20acd 100644 --- a/gnoland/website/views/package_dir.html +++ b/gnoland/website/views/package_dir.html @@ -5,7 +5,7 @@ Gno.land {{ template "html_head" }} - +
- -
- {{ template "dir_contents" . }} +
+
{{ .Data.PkgContent }}
{{ template "footer" }}
+ {{ template "hljs" }} {{ template "js" }} -{{- end -}} - -{{ define "dir_contents" }} -
- {{ $dirPath := .Data.DirPath }} -
    - {{ range .Data.Files }} -
  • - {{ . }} -
  • - {{ end }} -
-
-{{ end }} +{{- end -}} \ No newline at end of file diff --git a/gnoland/website/views/package_file.html b/gnoland/website/views/package_file.html index d30a98ddbac..f67db8ceb5b 100644 --- a/gnoland/website/views/package_file.html +++ b/gnoland/website/views/package_file.html @@ -21,14 +21,8 @@ {{ template "footer" }}
+ {{ template "hljs" }} {{ template "js" }} - - -{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/gnoland/website/views/realm_dir.html b/gnoland/website/views/realm_dir.html new file mode 100644 index 00000000000..bb2a462e1fd --- /dev/null +++ b/gnoland/website/views/realm_dir.html @@ -0,0 +1,40 @@ +{{- define "app" -}} + + + + Gno.land + {{ template "html_head" }} + + +
+ + +
+ {{ template "dir_contents" . }} +
+ + {{ template "footer" }} +
+ {{ template "js" }} + + +{{- end -}} + +{{ define "dir_contents" }} +
+ {{ $dirPath := .Data.DirPath }} + +
+{{ end }} \ No newline at end of file diff --git a/pkgs/sdk/vm/handler.go b/pkgs/sdk/vm/handler.go index 4a091e1850a..b7501e1c51e 100644 --- a/pkgs/sdk/vm/handler.go +++ b/pkgs/sdk/vm/handler.go @@ -1,6 +1,7 @@ package vm import ( + "encoding/json" "fmt" "strings" @@ -88,6 +89,7 @@ const ( QueryFuncs = "qfuncs" QueryEval = "qeval" QueryFile = "qfile" + QueryFiles = "qfiles" ) func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { @@ -104,6 +106,8 @@ func (vh vmHandler) Query(ctx sdk.Context, req abci.RequestQuery) (res abci.Resp return vh.queryEval(ctx, req) case QueryFile: return vh.queryFile(ctx, req) + case QueryFiles: + return vh.queryFiles(ctx, req) default: res = sdk.ABCIResponseQueryFromError( std.ErrUnknownRequest(fmt.Sprintf( @@ -193,6 +197,22 @@ func (vh vmHandler) queryFile(ctx sdk.Context, req abci.RequestQuery) (res abci. return } +func (vh vmHandler) queryFiles(ctx sdk.Context, req abci.RequestQuery) (res abci.ResponseQuery) { + dirpath := string(req.Data) + result, err := vh.vm.QueryFiles(ctx, dirpath) + if err != nil { + res = sdk.ABCIResponseQueryFromError(err) + return + } + b, err := json.Marshal(&result) + if err != nil { + res = sdk.ABCIResponseQueryFromError(err) + return + } + res.Data = b + return +} + //---------------------------------------- // misc diff --git a/pkgs/sdk/vm/keeper.go b/pkgs/sdk/vm/keeper.go index 1ecb1c0124e..072f3a04611 100644 --- a/pkgs/sdk/vm/keeper.go +++ b/pkgs/sdk/vm/keeper.go @@ -462,3 +462,13 @@ func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err return res, nil } } + +func (vm *VMKeeper) QueryFiles(ctx sdk.Context, path string) (res std.MemFileBodies, err error) { + dirpath, _ := std.SplitFilepath(path) + store := vm.getGnoStore(ctx) + if memPkg := store.GetMemPackage(dirpath); memPkg != nil { + return memPkg.GetFileBodies(), nil + } + + return std.MemFileBodies{}, nil +} diff --git a/pkgs/std/memfile.go b/pkgs/std/memfile.go index b0d7a41b7ef..464e75a5110 100644 --- a/pkgs/std/memfile.go +++ b/pkgs/std/memfile.go @@ -13,6 +13,8 @@ type MemFile struct { Body string } +type MemFileBodies map[string]string + // NOTE: in the future, a MemPackage may represent // updates/additional-files for an existing package. type MemPackage struct { @@ -30,6 +32,14 @@ func (mempkg *MemPackage) GetFile(name string) *MemFile { return nil } +func (mempkg *MemPackage) GetFileBodies() MemFileBodies { + fileBodies := MemFileBodies{} + for _, memFile := range mempkg.Files { + fileBodies[memFile.Name] = memFile.Body + } + return fileBodies +} + const ( reDomainPart = `gno\.land` rePathPart = `[a-z][a-z0-9_]*`