bpdoc improvements
1. Extract module type documentation. 2. Support primary builder customization of factory function to use for documentation for each module type. 3. Change the ModuleType list order so that they are grouped by package. This is basically minor refactoring + readability improvement done on top of https://github.com/google/blueprint/pull/232. Change-Id: If7413e5ac23486b85f18d02fb3ba288a38730c32
This commit is contained in:
parent
3eeabc7991
commit
781f6b2896
7 changed files with 760 additions and 435 deletions
|
@ -119,6 +119,11 @@ bootstrap_go_package {
|
|||
pkgPath: "github.com/google/blueprint/bootstrap/bpdoc",
|
||||
srcs: [
|
||||
"bootstrap/bpdoc/bpdoc.go",
|
||||
"bootstrap/bpdoc/properties.go",
|
||||
"bootstrap/bpdoc/reader.go",
|
||||
],
|
||||
testSrcs: [
|
||||
"bootstrap/bpdoc/reader_test.go",
|
||||
],
|
||||
}
|
||||
|
||||
|
|
|
@ -2,92 +2,46 @@ package bpdoc
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/blueprint"
|
||||
"github.com/google/blueprint/proptools"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
pkgFiles map[string][]string // Map of package name to source files, provided by constructor
|
||||
// Package contains the information about a package relevant to generating documentation.
|
||||
type Package struct {
|
||||
// Name is the name of the package.
|
||||
Name string
|
||||
|
||||
mutex sync.Mutex
|
||||
pkgs map[string]*doc.Package // Map of package name to parsed Go AST, protected by mutex
|
||||
ps map[string]*PropertyStruct // Map of type name to property struct, protected by mutex
|
||||
// Path is the full package path of the package as used in the primary builder.
|
||||
Path string
|
||||
|
||||
// Text is the contents of the package comment documenting the module types in the package.
|
||||
Text string
|
||||
|
||||
// ModuleTypes is a list of ModuleType objects that contain information about each module type that is
|
||||
// defined by the package.
|
||||
ModuleTypes []*ModuleType
|
||||
}
|
||||
|
||||
func NewContext(pkgFiles map[string][]string) *Context {
|
||||
return &Context{
|
||||
pkgFiles: pkgFiles,
|
||||
pkgs: make(map[string]*doc.Package),
|
||||
ps: make(map[string]*PropertyStruct),
|
||||
}
|
||||
}
|
||||
// ModuleType contains the information about a module type that is relevant to generating documentation.
|
||||
type ModuleType struct {
|
||||
// Name is the string that will appear in Blueprints files when defining a new module of
|
||||
// this type.
|
||||
Name string
|
||||
|
||||
// Return the PropertyStruct associated with a property struct type. The type should be in the
|
||||
// format <package path>.<type name>
|
||||
func (c *Context) PropertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) {
|
||||
ps := c.getPropertyStruct(pkgPath, name)
|
||||
// PkgPath is the full package path of the package that contains the module type factory.
|
||||
PkgPath string
|
||||
|
||||
if ps == nil {
|
||||
pkg, err := c.pkg(pkgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Text is the contents of the comment documenting the module type.
|
||||
Text string
|
||||
|
||||
for _, t := range pkg.Types {
|
||||
if t.Name == name {
|
||||
ps, err = newPropertyStruct(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps = c.putPropertyStruct(pkgPath, name, ps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ps == nil {
|
||||
return nil, fmt.Errorf("package %q type %q not found", pkgPath, name)
|
||||
}
|
||||
|
||||
ps = ps.Clone()
|
||||
ps.SetDefaults(defaults)
|
||||
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func (c *Context) getPropertyStruct(pkgPath, name string) *PropertyStruct {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
name = pkgPath + "." + name
|
||||
|
||||
return c.ps[name]
|
||||
}
|
||||
|
||||
func (c *Context) putPropertyStruct(pkgPath, name string, ps *PropertyStruct) *PropertyStruct {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
name = pkgPath + "." + name
|
||||
|
||||
if c.ps[name] != nil {
|
||||
return c.ps[name]
|
||||
} else {
|
||||
c.ps[name] = ps
|
||||
return ps
|
||||
}
|
||||
// PropertyStructs is a list of PropertyStruct objects that contain information about each
|
||||
// property struct that is used by the module type, containing all properties that are valid
|
||||
// for the module type.
|
||||
PropertyStructs []*PropertyStruct
|
||||
}
|
||||
|
||||
type PropertyStruct struct {
|
||||
|
@ -107,338 +61,60 @@ type Property struct {
|
|||
Default string
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) Clone() *PropertyStruct {
|
||||
ret := *ps
|
||||
ret.Properties = append([]Property(nil), ret.Properties...)
|
||||
for i, prop := range ret.Properties {
|
||||
ret.Properties[i] = prop.Clone()
|
||||
}
|
||||
func AllPackages(pkgFiles map[string][]string, moduleTypeNameFactories map[string]reflect.Value,
|
||||
moduleTypeNamePropertyStructs map[string][]interface{}) ([]*Package, error) {
|
||||
// Read basic info from the files to construct a Reader instance.
|
||||
r := NewReader(pkgFiles)
|
||||
|
||||
return &ret
|
||||
}
|
||||
|
||||
func (p *Property) Clone() Property {
|
||||
ret := *p
|
||||
ret.Properties = append([]Property(nil), ret.Properties...)
|
||||
for i, prop := range ret.Properties {
|
||||
ret.Properties[i] = prop.Clone()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *Property) Equal(other Property) bool {
|
||||
return p.Name == other.Name && p.Type == other.Type && p.Tag == other.Tag &&
|
||||
p.Text == other.Text && p.Default == other.Default &&
|
||||
stringArrayEqual(p.OtherNames, other.OtherNames) &&
|
||||
htmlArrayEqual(p.OtherTexts, other.OtherTexts) &&
|
||||
p.SameSubProperties(other)
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) SetDefaults(defaults reflect.Value) {
|
||||
setDefaults(ps.Properties, defaults)
|
||||
}
|
||||
|
||||
func setDefaults(properties []Property, defaults reflect.Value) {
|
||||
for i := range properties {
|
||||
prop := &properties[i]
|
||||
fieldName := proptools.FieldNameForProperty(prop.Name)
|
||||
f := defaults.FieldByName(fieldName)
|
||||
if (f == reflect.Value{}) {
|
||||
panic(fmt.Errorf("property %q does not exist in %q", fieldName, defaults.Type()))
|
||||
pkgMap := map[string]*Package{}
|
||||
var pkgs []*Package
|
||||
// Scan through per-module-type property structs map.
|
||||
for mtName, propertyStructs := range moduleTypeNamePropertyStructs {
|
||||
// Construct ModuleType with the given info.
|
||||
mtInfo, err := assembleModuleTypeInfo(r, mtName, moduleTypeNameFactories[mtName], propertyStructs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Some pruning work
|
||||
removeEmptyPropertyStructs(mtInfo)
|
||||
collapseDuplicatePropertyStructs(mtInfo)
|
||||
collapseNestedPropertyStructs(mtInfo)
|
||||
combineDuplicateProperties(mtInfo)
|
||||
|
||||
if reflect.DeepEqual(f.Interface(), reflect.Zero(f.Type()).Interface()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Interface {
|
||||
f = f.Elem()
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Ptr {
|
||||
if f.IsNil() {
|
||||
continue
|
||||
// Add the ModuleInfo to the corresponding Package map/slice entries.
|
||||
pkg := pkgMap[mtInfo.PkgPath]
|
||||
if pkg == nil {
|
||||
var err error
|
||||
pkg, err = r.Package(mtInfo.PkgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f = f.Elem()
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Struct {
|
||||
setDefaults(prop.Properties, f)
|
||||
} else {
|
||||
prop.Default = fmt.Sprintf("%v", f.Interface())
|
||||
pkgMap[mtInfo.PkgPath] = pkg
|
||||
pkgs = append(pkgs, pkg)
|
||||
}
|
||||
pkg.ModuleTypes = append(pkg.ModuleTypes, mtInfo)
|
||||
}
|
||||
|
||||
// Sort ModuleTypes within each package.
|
||||
for _, pkg := range pkgs {
|
||||
sort.Slice(pkg.ModuleTypes, func(i, j int) bool { return pkg.ModuleTypes[i].Name < pkg.ModuleTypes[j].Name })
|
||||
}
|
||||
// Sort packages.
|
||||
sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Path < pkgs[j].Path })
|
||||
|
||||
return pkgs, nil
|
||||
}
|
||||
|
||||
func stringArrayEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
func assembleModuleTypeInfo(r *Reader, name string, factory reflect.Value,
|
||||
propertyStructs []interface{}) (*ModuleType, error) {
|
||||
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func htmlArrayEqual(a, b []template.HTML) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Property) SameSubProperties(other Property) bool {
|
||||
if len(p.Properties) != len(other.Properties) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range p.Properties {
|
||||
if !p.Properties[i].Equal(other.Properties[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) GetByName(name string) *Property {
|
||||
return getByName(name, "", &ps.Properties)
|
||||
}
|
||||
|
||||
func getByName(name string, prefix string, props *[]Property) *Property {
|
||||
for i := range *props {
|
||||
if prefix+(*props)[i].Name == name {
|
||||
return &(*props)[i]
|
||||
} else if strings.HasPrefix(name, prefix+(*props)[i].Name+".") {
|
||||
return getByName(name, prefix+(*props)[i].Name+".", &(*props)[i].Properties)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Property) Nest(nested *PropertyStruct) {
|
||||
//p.Name += "(" + nested.Name + ")"
|
||||
//p.Text += "(" + nested.Text + ")"
|
||||
p.Properties = append(p.Properties, nested.Properties...)
|
||||
}
|
||||
|
||||
func newPropertyStruct(t *doc.Type) (*PropertyStruct, error) {
|
||||
typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
|
||||
ps := PropertyStruct{
|
||||
Name: t.Name,
|
||||
Text: t.Doc,
|
||||
}
|
||||
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type of %q is not a struct", t.Name)
|
||||
}
|
||||
|
||||
var err error
|
||||
ps.Properties, err = structProperties(structType)
|
||||
mt, err := r.ModuleType(name, factory)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ps, nil
|
||||
}
|
||||
|
||||
func structProperties(structType *ast.StructType) (props []Property, err error) {
|
||||
for _, f := range structType.Fields.List {
|
||||
names := f.Names
|
||||
if names == nil {
|
||||
// Anonymous fields have no name, use the type as the name
|
||||
// TODO: hide the name and make the properties show up in the embedding struct
|
||||
if t, ok := f.Type.(*ast.Ident); ok {
|
||||
names = append(names, t)
|
||||
}
|
||||
}
|
||||
for _, n := range names {
|
||||
var name, typ, tag, text string
|
||||
var innerProps []Property
|
||||
if n != nil {
|
||||
name = proptools.PropertyNameForField(n.Name)
|
||||
}
|
||||
if f.Doc != nil {
|
||||
text = f.Doc.Text()
|
||||
}
|
||||
if f.Tag != nil {
|
||||
tag, err = strconv.Unquote(f.Tag.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
t := f.Type
|
||||
if star, ok := t.(*ast.StarExpr); ok {
|
||||
t = star.X
|
||||
}
|
||||
switch a := t.(type) {
|
||||
case *ast.ArrayType:
|
||||
typ = "list of strings"
|
||||
case *ast.InterfaceType:
|
||||
typ = "interface"
|
||||
case *ast.Ident:
|
||||
typ = a.Name
|
||||
case *ast.StructType:
|
||||
innerProps, err = structProperties(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
typ = fmt.Sprintf("%T", f.Type)
|
||||
}
|
||||
|
||||
var html template.HTML
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
preformatted := false
|
||||
for _, line := range lines {
|
||||
r, _ := utf8.DecodeRuneInString(line)
|
||||
indent := unicode.IsSpace(r)
|
||||
if indent && !preformatted {
|
||||
html += "<pre>\n"
|
||||
} else if !indent && preformatted {
|
||||
html += "</pre>\n"
|
||||
}
|
||||
preformatted = indent
|
||||
html += template.HTML(template.HTMLEscapeString(line)) + "\n"
|
||||
}
|
||||
if preformatted {
|
||||
html += "</pre>\n"
|
||||
}
|
||||
|
||||
props = append(props, Property{
|
||||
Name: name,
|
||||
Type: typ,
|
||||
Tag: reflect.StructTag(tag),
|
||||
Text: html,
|
||||
Properties: innerProps,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return props, nil
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) ExcludeByTag(key, value string) {
|
||||
filterPropsByTag(&ps.Properties, key, value, true)
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) IncludeByTag(key, value string) {
|
||||
filterPropsByTag(&ps.Properties, key, value, false)
|
||||
}
|
||||
|
||||
func filterPropsByTag(props *[]Property, key, value string, exclude bool) {
|
||||
// Create a slice that shares the storage of props but has 0 length. Appending up to
|
||||
// len(props) times to this slice will overwrite the original slice contents
|
||||
filtered := (*props)[:0]
|
||||
for _, x := range *props {
|
||||
tag := x.Tag.Get(key)
|
||||
for _, entry := range strings.Split(tag, ",") {
|
||||
if (entry == value) == !exclude {
|
||||
filtered = append(filtered, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*props = filtered
|
||||
}
|
||||
|
||||
// Package AST generation and storage
|
||||
func (c *Context) pkg(pkgPath string) (*doc.Package, error) {
|
||||
pkg := c.getPackage(pkgPath)
|
||||
if pkg == nil {
|
||||
if files, ok := c.pkgFiles[pkgPath]; ok {
|
||||
var err error
|
||||
pkgAST, err := NewPackageAST(files)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg = doc.New(pkgAST, pkgPath, doc.AllDecls)
|
||||
pkg = c.putPackage(pkgPath, pkg)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown package %q", pkgPath)
|
||||
}
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (c *Context) getPackage(pkgPath string) *doc.Package {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
return c.pkgs[pkgPath]
|
||||
}
|
||||
|
||||
func (c *Context) putPackage(pkgPath string, pkg *doc.Package) *doc.Package {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if c.pkgs[pkgPath] != nil {
|
||||
return c.pkgs[pkgPath]
|
||||
} else {
|
||||
c.pkgs[pkgPath] = pkg
|
||||
return pkg
|
||||
}
|
||||
}
|
||||
|
||||
func NewPackageAST(files []string) (*ast.Package, error) {
|
||||
asts := make(map[string]*ast.File)
|
||||
|
||||
fset := token.NewFileSet()
|
||||
for _, file := range files {
|
||||
ast, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
asts[file] = ast
|
||||
}
|
||||
|
||||
pkg, _ := ast.NewPackage(fset, asts, nil, nil)
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func ModuleTypes(pkgFiles map[string][]string, moduleTypePropertyStructs map[string][]interface{}) ([]*ModuleType, error) {
|
||||
c := NewContext(pkgFiles)
|
||||
|
||||
var moduleTypeList []*ModuleType
|
||||
for moduleType, propertyStructs := range moduleTypePropertyStructs {
|
||||
mt, err := getModuleType(c, moduleType, propertyStructs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
removeEmptyPropertyStructs(mt)
|
||||
collapseDuplicatePropertyStructs(mt)
|
||||
collapseNestedPropertyStructs(mt)
|
||||
combineDuplicateProperties(mt)
|
||||
moduleTypeList = append(moduleTypeList, mt)
|
||||
}
|
||||
|
||||
sort.Sort(moduleTypeByName(moduleTypeList))
|
||||
|
||||
return moduleTypeList, nil
|
||||
}
|
||||
|
||||
func getModuleType(c *Context, moduleTypeName string,
|
||||
propertyStructs []interface{}) (*ModuleType, error) {
|
||||
mt := &ModuleType{
|
||||
Name: moduleTypeName,
|
||||
//Text: c.ModuleTypeDocs(moduleType),
|
||||
}
|
||||
|
||||
// Reader.ModuleType only fills basic information such as name and package path. Collect more info
|
||||
// from property struct data.
|
||||
for _, s := range propertyStructs {
|
||||
v := reflect.ValueOf(s).Elem()
|
||||
t := v.Type()
|
||||
|
@ -447,7 +123,7 @@ func getModuleType(c *Context, moduleTypeName string,
|
|||
if t.PkgPath() == "" {
|
||||
continue
|
||||
}
|
||||
ps, err := c.PropertyStruct(t.PkgPath(), t.Name(), v)
|
||||
ps, err := r.PropertyStruct(t.PkgPath(), t.Name(), v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -460,7 +136,7 @@ func getModuleType(c *Context, moduleTypeName string,
|
|||
if nestedType.PkgPath() == "" {
|
||||
continue
|
||||
}
|
||||
nested, err := c.PropertyStruct(nestedType.PkgPath(), nestedType.Name(), nestedValue)
|
||||
nested, err := r.PropertyStruct(nestedType.PkgPath(), nestedType.Name(), nestedValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -528,7 +204,6 @@ func nestedPropertyStructs(s reflect.Value) map[string]reflect.Value {
|
|||
field.Name, fieldValue.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
walk(s, "")
|
||||
|
@ -633,27 +308,5 @@ propertyLoop:
|
|||
}
|
||||
n = append(n, child)
|
||||
}
|
||||
|
||||
*p = n
|
||||
}
|
||||
|
||||
type moduleTypeByName []*ModuleType
|
||||
|
||||
func (l moduleTypeByName) Len() int { return len(l) }
|
||||
func (l moduleTypeByName) Less(i, j int) bool { return l[i].Name < l[j].Name }
|
||||
func (l moduleTypeByName) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
|
||||
|
||||
// ModuleType contains the info about a module type that is relevant to generating documentation.
|
||||
type ModuleType struct {
|
||||
// Name is the string that will appear in Blueprints files when defining a new module of
|
||||
// this type.
|
||||
Name string
|
||||
|
||||
// Text is the contents of the comment documenting the module type
|
||||
Text string
|
||||
|
||||
// PropertyStructs is a list of PropertyStruct objects that contain information about each
|
||||
// property struct that is used by the module type, containing all properties that are valid
|
||||
// for the module type.
|
||||
PropertyStructs []*PropertyStruct
|
||||
}
|
||||
|
|
281
bootstrap/bpdoc/properties.go
Normal file
281
bootstrap/bpdoc/properties.go
Normal file
|
@ -0,0 +1,281 @@
|
|||
// Copyright 2019 Google Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bpdoc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/blueprint/proptools"
|
||||
)
|
||||
|
||||
//
|
||||
// Utility functions for PropertyStruct and Property
|
||||
//
|
||||
|
||||
func (ps *PropertyStruct) Clone() *PropertyStruct {
|
||||
ret := *ps
|
||||
ret.Properties = append([]Property(nil), ret.Properties...)
|
||||
for i, prop := range ret.Properties {
|
||||
ret.Properties[i] = prop.Clone()
|
||||
}
|
||||
|
||||
return &ret
|
||||
}
|
||||
|
||||
func (p *Property) Clone() Property {
|
||||
ret := *p
|
||||
ret.Properties = append([]Property(nil), ret.Properties...)
|
||||
for i, prop := range ret.Properties {
|
||||
ret.Properties[i] = prop.Clone()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p *Property) Equal(other Property) bool {
|
||||
return p.Name == other.Name && p.Type == other.Type && p.Tag == other.Tag &&
|
||||
p.Text == other.Text && p.Default == other.Default &&
|
||||
stringArrayEqual(p.OtherNames, other.OtherNames) &&
|
||||
htmlArrayEqual(p.OtherTexts, other.OtherTexts) &&
|
||||
p.SameSubProperties(other)
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) SetDefaults(defaults reflect.Value) {
|
||||
setDefaults(ps.Properties, defaults)
|
||||
}
|
||||
|
||||
func setDefaults(properties []Property, defaults reflect.Value) {
|
||||
for i := range properties {
|
||||
prop := &properties[i]
|
||||
fieldName := proptools.FieldNameForProperty(prop.Name)
|
||||
f := defaults.FieldByName(fieldName)
|
||||
if (f == reflect.Value{}) {
|
||||
panic(fmt.Errorf("property %q does not exist in %q", fieldName, defaults.Type()))
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(f.Interface(), reflect.Zero(f.Type()).Interface()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Interface {
|
||||
f = f.Elem()
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Ptr {
|
||||
if f.IsNil() {
|
||||
continue
|
||||
}
|
||||
f = f.Elem()
|
||||
}
|
||||
|
||||
if f.Kind() == reflect.Struct {
|
||||
setDefaults(prop.Properties, f)
|
||||
} else {
|
||||
prop.Default = fmt.Sprintf("%v", f.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stringArrayEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func htmlArrayEqual(a, b []template.HTML) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Property) SameSubProperties(other Property) bool {
|
||||
if len(p.Properties) != len(other.Properties) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range p.Properties {
|
||||
if !p.Properties[i].Equal(other.Properties[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) GetByName(name string) *Property {
|
||||
return getByName(name, "", &ps.Properties)
|
||||
}
|
||||
|
||||
func getByName(name string, prefix string, props *[]Property) *Property {
|
||||
for i := range *props {
|
||||
if prefix+(*props)[i].Name == name {
|
||||
return &(*props)[i]
|
||||
} else if strings.HasPrefix(name, prefix+(*props)[i].Name+".") {
|
||||
return getByName(name, prefix+(*props)[i].Name+".", &(*props)[i].Properties)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Property) Nest(nested *PropertyStruct) {
|
||||
p.Properties = append(p.Properties, nested.Properties...)
|
||||
}
|
||||
|
||||
func newPropertyStruct(t *doc.Type) (*PropertyStruct, error) {
|
||||
typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
|
||||
ps := PropertyStruct{
|
||||
Name: t.Name,
|
||||
Text: t.Doc,
|
||||
}
|
||||
|
||||
structType, ok := typeSpec.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type of %q is not a struct", t.Name)
|
||||
}
|
||||
|
||||
var err error
|
||||
ps.Properties, err = structProperties(structType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ps, nil
|
||||
}
|
||||
|
||||
func structProperties(structType *ast.StructType) (props []Property, err error) {
|
||||
for _, f := range structType.Fields.List {
|
||||
names := f.Names
|
||||
if names == nil {
|
||||
// Anonymous fields have no name, use the type as the name
|
||||
// TODO: hide the name and make the properties show up in the embedding struct
|
||||
if t, ok := f.Type.(*ast.Ident); ok {
|
||||
names = append(names, t)
|
||||
}
|
||||
}
|
||||
for _, n := range names {
|
||||
var name, typ, tag, text string
|
||||
var innerProps []Property
|
||||
if n != nil {
|
||||
name = proptools.PropertyNameForField(n.Name)
|
||||
}
|
||||
if f.Doc != nil {
|
||||
text = f.Doc.Text()
|
||||
}
|
||||
if f.Tag != nil {
|
||||
tag, err = strconv.Unquote(f.Tag.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
t := f.Type
|
||||
if star, ok := t.(*ast.StarExpr); ok {
|
||||
t = star.X
|
||||
}
|
||||
switch a := t.(type) {
|
||||
case *ast.ArrayType:
|
||||
typ = "list of strings"
|
||||
case *ast.InterfaceType:
|
||||
typ = "interface"
|
||||
case *ast.Ident:
|
||||
typ = a.Name
|
||||
case *ast.StructType:
|
||||
innerProps, err = structProperties(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
typ = fmt.Sprintf("%T", f.Type)
|
||||
}
|
||||
|
||||
var html template.HTML
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
preformatted := false
|
||||
for _, line := range lines {
|
||||
r, _ := utf8.DecodeRuneInString(line)
|
||||
indent := unicode.IsSpace(r)
|
||||
if indent && !preformatted {
|
||||
html += "<pre>\n"
|
||||
} else if !indent && preformatted {
|
||||
html += "</pre>\n"
|
||||
}
|
||||
preformatted = indent
|
||||
html += template.HTML(template.HTMLEscapeString(line)) + "\n"
|
||||
}
|
||||
if preformatted {
|
||||
html += "</pre>\n"
|
||||
}
|
||||
|
||||
props = append(props, Property{
|
||||
Name: name,
|
||||
Type: typ,
|
||||
Tag: reflect.StructTag(tag),
|
||||
Text: html,
|
||||
Properties: innerProps,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return props, nil
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) ExcludeByTag(key, value string) {
|
||||
filterPropsByTag(&ps.Properties, key, value, true)
|
||||
}
|
||||
|
||||
func (ps *PropertyStruct) IncludeByTag(key, value string) {
|
||||
filterPropsByTag(&ps.Properties, key, value, false)
|
||||
}
|
||||
|
||||
func filterPropsByTag(props *[]Property, key, value string, exclude bool) {
|
||||
// Create a slice that shares the storage of props but has 0 length. Appending up to
|
||||
// len(props) times to this slice will overwrite the original slice contents
|
||||
filtered := (*props)[:0]
|
||||
for _, x := range *props {
|
||||
tag := x.Tag.Get(key)
|
||||
for _, entry := range strings.Split(tag, ",") {
|
||||
if (entry == value) == !exclude {
|
||||
filtered = append(filtered, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*props = filtered
|
||||
}
|
227
bootstrap/bpdoc/reader.go
Normal file
227
bootstrap/bpdoc/reader.go
Normal file
|
@ -0,0 +1,227 @@
|
|||
// Copyright 2019 Google Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package bpdoc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/doc"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Handles parsing and low-level processing of Blueprint module source files. Note that most getter
|
||||
// functions associated with Reader only fill basic information that can be simply extracted from
|
||||
// AST parsing results. More sophisticated processing is performed in bpdoc.go
|
||||
type Reader struct {
|
||||
pkgFiles map[string][]string // Map of package name to source files, provided by constructor
|
||||
|
||||
mutex sync.Mutex
|
||||
goPkgs map[string]*doc.Package // Map of package name to parsed Go AST, protected by mutex
|
||||
ps map[string]*PropertyStruct // Map of module type name to property struct, protected by mutex
|
||||
}
|
||||
|
||||
func NewReader(pkgFiles map[string][]string) *Reader {
|
||||
return &Reader{
|
||||
pkgFiles: pkgFiles,
|
||||
goPkgs: make(map[string]*doc.Package),
|
||||
ps: make(map[string]*PropertyStruct),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) Package(path string) (*Package, error) {
|
||||
goPkg, err := r.goPkg(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Package{
|
||||
Name: goPkg.Name,
|
||||
Path: path,
|
||||
Text: goPkg.Doc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Reader) ModuleType(name string, factory reflect.Value) (*ModuleType, error) {
|
||||
f := runtime.FuncForPC(factory.Pointer())
|
||||
|
||||
pkgPath, err := funcNameToPkgPath(f.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
factoryName := strings.TrimPrefix(f.Name(), pkgPath+".")
|
||||
|
||||
text, err := r.getModuleTypeDoc(pkgPath, factoryName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ModuleType{
|
||||
Name: name,
|
||||
PkgPath: pkgPath,
|
||||
Text: text,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Return the PropertyStruct associated with a property struct type. The type should be in the
|
||||
// format <package path>.<type name>
|
||||
func (r *Reader) PropertyStruct(pkgPath, name string, defaults reflect.Value) (*PropertyStruct, error) {
|
||||
ps := r.getPropertyStruct(pkgPath, name)
|
||||
|
||||
if ps == nil {
|
||||
pkg, err := r.goPkg(pkgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range pkg.Types {
|
||||
if t.Name == name {
|
||||
ps, err = newPropertyStruct(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps = r.putPropertyStruct(pkgPath, name, ps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ps == nil {
|
||||
return nil, fmt.Errorf("package %q type %q not found", pkgPath, name)
|
||||
}
|
||||
|
||||
ps = ps.Clone()
|
||||
ps.SetDefaults(defaults)
|
||||
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func (r *Reader) getModuleTypeDoc(pkgPath, factoryFuncName string) (string, error) {
|
||||
goPkg, err := r.goPkg(pkgPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, fn := range goPkg.Funcs {
|
||||
if fn.Name == factoryFuncName {
|
||||
return fn.Doc, nil
|
||||
}
|
||||
}
|
||||
|
||||
// The doc package may associate the method with the type it returns, so iterate through those too
|
||||
for _, typ := range goPkg.Types {
|
||||
for _, fn := range typ.Funcs {
|
||||
if fn.Name == factoryFuncName {
|
||||
return fn.Doc, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (r *Reader) getPropertyStruct(pkgPath, name string) *PropertyStruct {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
name = pkgPath + "." + name
|
||||
|
||||
return r.ps[name]
|
||||
}
|
||||
|
||||
func (r *Reader) putPropertyStruct(pkgPath, name string, ps *PropertyStruct) *PropertyStruct {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
name = pkgPath + "." + name
|
||||
|
||||
if r.ps[name] != nil {
|
||||
return r.ps[name]
|
||||
} else {
|
||||
r.ps[name] = ps
|
||||
return ps
|
||||
}
|
||||
}
|
||||
|
||||
// Package AST generation and storage
|
||||
func (r *Reader) goPkg(pkgPath string) (*doc.Package, error) {
|
||||
pkg := r.getGoPkg(pkgPath)
|
||||
if pkg == nil {
|
||||
if files, ok := r.pkgFiles[pkgPath]; ok {
|
||||
var err error
|
||||
pkgAST, err := packageAST(files)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg = doc.New(pkgAST, pkgPath, doc.AllDecls)
|
||||
pkg = r.putGoPkg(pkgPath, pkg)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown package %q", pkgPath)
|
||||
}
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (r *Reader) getGoPkg(pkgPath string) *doc.Package {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
return r.goPkgs[pkgPath]
|
||||
}
|
||||
|
||||
func (r *Reader) putGoPkg(pkgPath string, pkg *doc.Package) *doc.Package {
|
||||
r.mutex.Lock()
|
||||
defer r.mutex.Unlock()
|
||||
|
||||
if r.goPkgs[pkgPath] != nil {
|
||||
return r.goPkgs[pkgPath]
|
||||
} else {
|
||||
r.goPkgs[pkgPath] = pkg
|
||||
return pkg
|
||||
}
|
||||
}
|
||||
|
||||
// A regex to find a package path within a function name. It finds the shortest string that is
|
||||
// followed by '.' and doesn't have any '/'s left.
|
||||
var pkgPathRe = regexp.MustCompile("^(.*?)\\.[^/]+$")
|
||||
|
||||
func funcNameToPkgPath(f string) (string, error) {
|
||||
s := pkgPathRe.FindStringSubmatch(f)
|
||||
if len(s) < 2 {
|
||||
return "", fmt.Errorf("failed to extract package path from %q", f)
|
||||
}
|
||||
return s[1], nil
|
||||
}
|
||||
|
||||
func packageAST(files []string) (*ast.Package, error) {
|
||||
asts := make(map[string]*ast.File)
|
||||
|
||||
fset := token.NewFileSet()
|
||||
for _, file := range files {
|
||||
ast, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
asts[file] = ast
|
||||
}
|
||||
|
||||
pkg, _ := ast.NewPackage(fset, asts, nil, nil)
|
||||
return pkg, nil
|
||||
}
|
136
bootstrap/bpdoc/reader_test.go
Normal file
136
bootstrap/bpdoc/reader_test.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2019 Google Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// bpdoc docs.
|
||||
package bpdoc
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/google/blueprint"
|
||||
)
|
||||
|
||||
// foo docs.
|
||||
func fooFactory() (blueprint.Module, []interface{}) {
|
||||
return nil, []interface{}{&props{}}
|
||||
}
|
||||
|
||||
// props docs.
|
||||
type props struct {
|
||||
// A docs.
|
||||
A string
|
||||
}
|
||||
|
||||
var pkgPath string
|
||||
var pkgFiles map[string][]string
|
||||
|
||||
func init() {
|
||||
pc, filename, _, _ := runtime.Caller(0)
|
||||
fn := runtime.FuncForPC(pc)
|
||||
|
||||
var err error
|
||||
pkgPath, err = funcNameToPkgPath(fn.Name())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
pkgFiles = map[string][]string{
|
||||
pkgPath: {filename},
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleTypeDocs(t *testing.T) {
|
||||
r := NewReader(pkgFiles)
|
||||
mt, err := r.ModuleType("foo_module", reflect.ValueOf(fooFactory))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if mt.Text != "foo docs.\n" {
|
||||
t.Errorf("unexpected docs %q", mt.Text)
|
||||
}
|
||||
|
||||
if mt.PkgPath != pkgPath {
|
||||
t.Errorf("expected pkgpath %q, got %q", pkgPath, mt.PkgPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropertyStruct(t *testing.T) {
|
||||
r := NewReader(pkgFiles)
|
||||
ps, err := r.PropertyStruct(pkgPath, "props", reflect.ValueOf(props{A: "B"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if ps.Text != "props docs.\n" {
|
||||
t.Errorf("unexpected docs %q", ps.Text)
|
||||
}
|
||||
if len(ps.Properties) != 1 {
|
||||
t.Fatalf("want 1 property, got %d", len(ps.Properties))
|
||||
}
|
||||
|
||||
if ps.Properties[0].Name != "a" || ps.Properties[0].Text != "A docs.\n\n" || ps.Properties[0].Default != "B" {
|
||||
t.Errorf("unexpected property docs %q %q %q",
|
||||
ps.Properties[0].Name, ps.Properties[0].Text, ps.Properties[0].Default)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackage(t *testing.T) {
|
||||
r := NewReader(pkgFiles)
|
||||
pkg, err := r.Package(pkgPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pkg.Text != "bpdoc docs.\n" {
|
||||
t.Errorf("unexpected docs %q", pkg.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuncToPkgPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
f string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
f: "github.com/google/blueprint/bootstrap.Main",
|
||||
want: "github.com/google/blueprint/bootstrap",
|
||||
},
|
||||
{
|
||||
f: "android/soong/android.GenruleFactory",
|
||||
want: "android/soong/android",
|
||||
},
|
||||
{
|
||||
f: "android/soong/android.ModuleFactoryAdapter.func1",
|
||||
want: "android/soong/android",
|
||||
},
|
||||
{
|
||||
f: "main.Main",
|
||||
want: "main",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.f, func(t *testing.T) {
|
||||
got, err := funcNameToPkgPath(tt.f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("funcNameToPkgPath(%v) = %v, want %v", tt.f, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"html/template"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/blueprint"
|
||||
"github.com/google/blueprint/bootstrap/bpdoc"
|
||||
|
@ -14,7 +15,7 @@ import (
|
|||
|
||||
// ModuleTypeDocs returns a list of bpdoc.ModuleType objects that contain information relevant
|
||||
// to generating documentation for module types supported by the primary builder.
|
||||
func ModuleTypeDocs(ctx *blueprint.Context) ([]*bpdoc.ModuleType, error) {
|
||||
func ModuleTypeDocs(ctx *blueprint.Context, factories map[string]reflect.Value) ([]*bpdoc.Package, error) {
|
||||
// Find the module that's marked as the "primary builder", which means it's
|
||||
// creating the binary that we'll use to generate the non-bootstrap
|
||||
// build.ninja file.
|
||||
|
@ -60,11 +61,22 @@ func ModuleTypeDocs(ctx *blueprint.Context) ([]*bpdoc.ModuleType, error) {
|
|||
}
|
||||
})
|
||||
|
||||
return bpdoc.ModuleTypes(pkgFiles, ctx.ModuleTypePropertyStructs())
|
||||
mergedFactories := make(map[string]reflect.Value)
|
||||
for moduleType, factory := range factories {
|
||||
mergedFactories[moduleType] = factory
|
||||
}
|
||||
|
||||
for moduleType, factory := range ctx.ModuleTypeFactories() {
|
||||
if _, exists := mergedFactories[moduleType]; !exists {
|
||||
mergedFactories[moduleType] = reflect.ValueOf(factory)
|
||||
}
|
||||
}
|
||||
|
||||
return bpdoc.AllPackages(pkgFiles, mergedFactories, ctx.ModuleTypePropertyStructs())
|
||||
}
|
||||
|
||||
func writeDocs(ctx *blueprint.Context, filename string) error {
|
||||
moduleTypeList, err := ModuleTypeDocs(ctx)
|
||||
moduleTypeList, err := ModuleTypeDocs(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -108,25 +120,28 @@ const (
|
|||
<h1>Build Docs</h1>
|
||||
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
|
||||
{{range .}}
|
||||
{{ $collapseIndex := unique }}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
|
||||
<h2 class="panel-title">
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapse{{$collapseIndex}}" aria-expanded="false" aria-controls="collapse{{$collapseIndex}}">
|
||||
{{.Name}}
|
||||
</a>
|
||||
</h2>
|
||||
<p>{{.Text}}</p>
|
||||
{{range .ModuleTypes}}
|
||||
{{ $collapseIndex := unique }}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
|
||||
<h2 class="panel-title">
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapse{{$collapseIndex}}" aria-expanded="false" aria-controls="collapse{{$collapseIndex}}">
|
||||
{{.Name}}
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
|
||||
<div class="panel-body">
|
||||
<p>{{.Text}}</p>
|
||||
{{range .PropertyStructs}}
|
||||
<div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
|
||||
<div class="panel-body">
|
||||
<p>{{.Text}}</p>
|
||||
{{template "properties" .Properties}}
|
||||
{{end}}
|
||||
{{range .PropertyStructs}}
|
||||
<p>{{.Text}}</p>
|
||||
{{template "properties" .Properties}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
|
|
|
@ -2829,6 +2829,14 @@ func (c *Context) ModuleTypePropertyStructs() map[string][]interface{} {
|
|||
return ret
|
||||
}
|
||||
|
||||
func (c *Context) ModuleTypeFactories() map[string]ModuleFactory {
|
||||
ret := make(map[string]ModuleFactory)
|
||||
for k, v := range c.moduleFactories {
|
||||
ret[k] = v
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *Context) ModuleName(logicModule Module) string {
|
||||
module := c.moduleInfo[logicModule]
|
||||
return module.Name()
|
||||
|
|
Loading…
Reference in a new issue