5a9f4d1db2
Embedded anonymous structs have no name, use the type as the name for now. Eventually we should hide the name completely and put the properties in the embedding struct.
709 lines
18 KiB
Go
709 lines
18 KiB
Go
package bpdoc
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/doc"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/ioutil"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"text/template"
|
|
|
|
"github.com/google/blueprint"
|
|
"github.com/google/blueprint/proptools"
|
|
)
|
|
|
|
type DocCollector struct {
|
|
pkgFiles map[string][]string // Map of package name to source files, provided by constructor
|
|
|
|
mutex sync.Mutex
|
|
pkgDocs map[string]*doc.Package // Map of package name to parsed Go AST, protected by mutex
|
|
docs map[string]*PropertyStructDocs // Map of type name to docs, protected by mutex
|
|
}
|
|
|
|
func NewDocCollector(pkgFiles map[string][]string) *DocCollector {
|
|
return &DocCollector{
|
|
pkgFiles: pkgFiles,
|
|
pkgDocs: make(map[string]*doc.Package),
|
|
docs: make(map[string]*PropertyStructDocs),
|
|
}
|
|
}
|
|
|
|
// Return the PropertyStructDocs associated with a property struct type. The type should be in the
|
|
// format <package path>.<type name>
|
|
func (dc *DocCollector) Docs(pkg, name string, defaults reflect.Value) (*PropertyStructDocs, error) {
|
|
docs := dc.getDocs(pkg, name)
|
|
|
|
if docs == nil {
|
|
pkgDocs, err := dc.packageDocs(pkg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, t := range pkgDocs.Types {
|
|
if t.Name == name {
|
|
docs, err = newDocs(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
docs = dc.putDocs(pkg, name, docs)
|
|
}
|
|
}
|
|
}
|
|
|
|
if docs == nil {
|
|
return nil, fmt.Errorf("package %q type %q not found", pkg, name)
|
|
}
|
|
|
|
docs = docs.Clone()
|
|
docs.SetDefaults(defaults)
|
|
|
|
return docs, nil
|
|
}
|
|
|
|
func (dc *DocCollector) getDocs(pkg, name string) *PropertyStructDocs {
|
|
dc.mutex.Lock()
|
|
defer dc.mutex.Unlock()
|
|
|
|
name = pkg + "." + name
|
|
|
|
return dc.docs[name]
|
|
}
|
|
|
|
func (dc *DocCollector) putDocs(pkg, name string, docs *PropertyStructDocs) *PropertyStructDocs {
|
|
dc.mutex.Lock()
|
|
defer dc.mutex.Unlock()
|
|
|
|
name = pkg + "." + name
|
|
|
|
if dc.docs[name] != nil {
|
|
return dc.docs[name]
|
|
} else {
|
|
dc.docs[name] = docs
|
|
return docs
|
|
}
|
|
}
|
|
|
|
type PropertyStructDocs struct {
|
|
Name string
|
|
Text string
|
|
Properties []PropertyDocs
|
|
}
|
|
|
|
type PropertyDocs struct {
|
|
Name string
|
|
OtherNames []string
|
|
Type string
|
|
Tag reflect.StructTag
|
|
Text string
|
|
OtherTexts []string
|
|
Properties []PropertyDocs
|
|
Default string
|
|
}
|
|
|
|
func (docs *PropertyStructDocs) Clone() *PropertyStructDocs {
|
|
ret := *docs
|
|
ret.Properties = append([]PropertyDocs(nil), ret.Properties...)
|
|
for i, prop := range ret.Properties {
|
|
ret.Properties[i] = prop.Clone()
|
|
}
|
|
|
|
return &ret
|
|
}
|
|
|
|
func (docs *PropertyDocs) Clone() PropertyDocs {
|
|
ret := *docs
|
|
ret.Properties = append([]PropertyDocs(nil), ret.Properties...)
|
|
for i, prop := range ret.Properties {
|
|
ret.Properties[i] = prop.Clone()
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func (docs *PropertyDocs) Equal(other PropertyDocs) bool {
|
|
return docs.Name == other.Name && docs.Type == other.Type && docs.Tag == other.Tag &&
|
|
docs.Text == other.Text && docs.Default == other.Default &&
|
|
stringArrayEqual(docs.OtherNames, other.OtherNames) &&
|
|
stringArrayEqual(docs.OtherTexts, other.OtherTexts) &&
|
|
docs.SameSubProperties(other)
|
|
}
|
|
|
|
func (docs *PropertyStructDocs) SetDefaults(defaults reflect.Value) {
|
|
setDefaults(docs.Properties, defaults)
|
|
}
|
|
|
|
func setDefaults(properties []PropertyDocs, 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.Type().Kind() == reflect.Interface {
|
|
f = f.Elem()
|
|
}
|
|
|
|
if f.Type().Kind() == reflect.Ptr {
|
|
f = f.Elem()
|
|
}
|
|
|
|
if f.Type().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 (docs *PropertyDocs) SameSubProperties(other PropertyDocs) bool {
|
|
if len(docs.Properties) != len(other.Properties) {
|
|
return false
|
|
}
|
|
|
|
for i := range docs.Properties {
|
|
if !docs.Properties[i].Equal(other.Properties[i]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (docs *PropertyStructDocs) GetByName(name string) *PropertyDocs {
|
|
return getByName(name, "", &docs.Properties)
|
|
}
|
|
|
|
func getByName(name string, prefix string, props *[]PropertyDocs) *PropertyDocs {
|
|
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 (prop *PropertyDocs) Nest(nested *PropertyStructDocs) {
|
|
//prop.Name += "(" + nested.Name + ")"
|
|
//prop.Text += "(" + nested.Text + ")"
|
|
prop.Properties = append(prop.Properties, nested.Properties...)
|
|
}
|
|
|
|
func newDocs(t *doc.Type) (*PropertyStructDocs, error) {
|
|
typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
|
|
docs := PropertyStructDocs{
|
|
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
|
|
docs.Properties, err = structProperties(structType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &docs, nil
|
|
}
|
|
|
|
func structProperties(structType *ast.StructType) (props []PropertyDocs, 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 []PropertyDocs
|
|
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
|
|
}
|
|
}
|
|
switch a := f.Type.(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)
|
|
}
|
|
|
|
props = append(props, PropertyDocs{
|
|
Name: name,
|
|
Type: typ,
|
|
Tag: reflect.StructTag(tag),
|
|
Text: text,
|
|
Properties: innerProps,
|
|
})
|
|
}
|
|
}
|
|
|
|
return props, nil
|
|
}
|
|
|
|
func (docs *PropertyStructDocs) ExcludeByTag(key, value string) {
|
|
filterPropsByTag(&docs.Properties, key, value, true)
|
|
}
|
|
|
|
func (docs *PropertyStructDocs) IncludeByTag(key, value string) {
|
|
filterPropsByTag(&docs.Properties, key, value, false)
|
|
}
|
|
|
|
func filterPropsByTag(props *[]PropertyDocs, 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 (dc *DocCollector) packageDocs(pkg string) (*doc.Package, error) {
|
|
pkgDocs := dc.getPackageDocs(pkg)
|
|
if pkgDocs == nil {
|
|
if files, ok := dc.pkgFiles[pkg]; ok {
|
|
var err error
|
|
pkgAST, err := NewPackageAST(files)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkgDocs = doc.New(pkgAST, pkg, doc.AllDecls)
|
|
pkgDocs = dc.putPackageDocs(pkg, pkgDocs)
|
|
} else {
|
|
return nil, fmt.Errorf("unknown package %q", pkg)
|
|
}
|
|
}
|
|
return pkgDocs, nil
|
|
}
|
|
|
|
func (dc *DocCollector) getPackageDocs(pkg string) *doc.Package {
|
|
dc.mutex.Lock()
|
|
defer dc.mutex.Unlock()
|
|
|
|
return dc.pkgDocs[pkg]
|
|
}
|
|
|
|
func (dc *DocCollector) putPackageDocs(pkg string, pkgDocs *doc.Package) *doc.Package {
|
|
dc.mutex.Lock()
|
|
defer dc.mutex.Unlock()
|
|
|
|
if dc.pkgDocs[pkg] != nil {
|
|
return dc.pkgDocs[pkg]
|
|
} else {
|
|
dc.pkgDocs[pkg] = pkgDocs
|
|
return pkgDocs
|
|
}
|
|
}
|
|
|
|
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 Write(filename string, pkgFiles map[string][]string,
|
|
moduleTypePropertyStructs map[string][]interface{}) error {
|
|
|
|
docSet := NewDocCollector(pkgFiles)
|
|
|
|
var moduleTypeList []*moduleTypeDoc
|
|
for moduleType, propertyStructs := range moduleTypePropertyStructs {
|
|
mtDoc, err := getModuleTypeDoc(docSet, moduleType, propertyStructs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
removeEmptyPropertyStructs(mtDoc)
|
|
collapseDuplicatePropertyStructs(mtDoc)
|
|
collapseNestedPropertyStructs(mtDoc)
|
|
combineDuplicateProperties(mtDoc)
|
|
moduleTypeList = append(moduleTypeList, mtDoc)
|
|
}
|
|
|
|
sort.Sort(moduleTypeByName(moduleTypeList))
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
unique := 0
|
|
|
|
tmpl, err := template.New("file").Funcs(map[string]interface{}{
|
|
"unique": func() int {
|
|
unique++
|
|
return unique
|
|
}}).Parse(fileTemplate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = tmpl.Execute(buf, moduleTypeList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = ioutil.WriteFile(filename, buf.Bytes(), 0666)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getModuleTypeDoc(docSet *DocCollector, moduleType string,
|
|
propertyStructs []interface{}) (*moduleTypeDoc, error) {
|
|
mtDoc := &moduleTypeDoc{
|
|
Name: moduleType,
|
|
//Text: docSet.ModuleTypeDocs(moduleType),
|
|
}
|
|
|
|
for _, s := range propertyStructs {
|
|
v := reflect.ValueOf(s).Elem()
|
|
t := v.Type()
|
|
|
|
// Ignore property structs with unexported or unnamed types
|
|
if t.PkgPath() == "" {
|
|
continue
|
|
}
|
|
psDoc, err := docSet.Docs(t.PkgPath(), t.Name(), v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
psDoc.ExcludeByTag("blueprint", "mutated")
|
|
|
|
for nested, nestedValue := range nestedPropertyStructs(v) {
|
|
nestedType := nestedValue.Type()
|
|
|
|
// Ignore property structs with unexported or unnamed types
|
|
if nestedType.PkgPath() == "" {
|
|
continue
|
|
}
|
|
nestedDoc, err := docSet.Docs(nestedType.PkgPath(), nestedType.Name(), nestedValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
nestedDoc.ExcludeByTag("blueprint", "mutated")
|
|
nestPoint := psDoc.GetByName(nested)
|
|
if nestPoint == nil {
|
|
return nil, fmt.Errorf("nesting point %q not found", nested)
|
|
}
|
|
|
|
key, value, err := blueprint.HasFilter(nestPoint.Tag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if key != "" {
|
|
nestedDoc.IncludeByTag(key, value)
|
|
}
|
|
|
|
nestPoint.Nest(nestedDoc)
|
|
}
|
|
mtDoc.PropertyStructs = append(mtDoc.PropertyStructs, psDoc)
|
|
}
|
|
|
|
return mtDoc, nil
|
|
}
|
|
|
|
func nestedPropertyStructs(s reflect.Value) map[string]reflect.Value {
|
|
ret := make(map[string]reflect.Value)
|
|
var walk func(structValue reflect.Value, prefix string)
|
|
walk = func(structValue reflect.Value, prefix string) {
|
|
typ := structValue.Type()
|
|
for i := 0; i < structValue.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
if field.PkgPath != "" {
|
|
// The field is not exported so just skip it.
|
|
continue
|
|
}
|
|
|
|
fieldValue := structValue.Field(i)
|
|
|
|
switch fieldValue.Kind() {
|
|
case reflect.Bool, reflect.String, reflect.Slice, reflect.Int, reflect.Uint:
|
|
// Nothing
|
|
case reflect.Struct:
|
|
walk(fieldValue, prefix+proptools.PropertyNameForField(field.Name)+".")
|
|
case reflect.Ptr, reflect.Interface:
|
|
if !fieldValue.IsNil() {
|
|
// We leave the pointer intact and zero out the struct that's
|
|
// pointed to.
|
|
elem := fieldValue.Elem()
|
|
if fieldValue.Kind() == reflect.Interface {
|
|
if elem.Kind() != reflect.Ptr {
|
|
panic(fmt.Errorf("can't get type of field %q: interface "+
|
|
"refers to a non-pointer", field.Name))
|
|
}
|
|
elem = elem.Elem()
|
|
}
|
|
if elem.Kind() != reflect.Struct {
|
|
panic(fmt.Errorf("can't get type of field %q: points to a "+
|
|
"non-struct", field.Name))
|
|
}
|
|
nestPoint := prefix + proptools.PropertyNameForField(field.Name)
|
|
ret[nestPoint] = elem
|
|
walk(elem, nestPoint+".")
|
|
}
|
|
default:
|
|
panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
|
|
field.Name, fieldValue.Kind()))
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
walk(s, "")
|
|
return ret
|
|
}
|
|
|
|
// Remove any property structs that have no exported fields
|
|
func removeEmptyPropertyStructs(mtDoc *moduleTypeDoc) {
|
|
for i := 0; i < len(mtDoc.PropertyStructs); i++ {
|
|
if len(mtDoc.PropertyStructs[i].Properties) == 0 {
|
|
mtDoc.PropertyStructs = append(mtDoc.PropertyStructs[:i], mtDoc.PropertyStructs[i+1:]...)
|
|
i--
|
|
}
|
|
}
|
|
}
|
|
|
|
// Squashes duplicates of the same property struct into single entries
|
|
func collapseDuplicatePropertyStructs(mtDoc *moduleTypeDoc) {
|
|
var collapsedDocs []*PropertyStructDocs
|
|
|
|
propertyStructLoop:
|
|
for _, from := range mtDoc.PropertyStructs {
|
|
for _, to := range collapsedDocs {
|
|
if from.Name == to.Name {
|
|
collapseDuplicateProperties(&to.Properties, &from.Properties)
|
|
continue propertyStructLoop
|
|
}
|
|
}
|
|
collapsedDocs = append(collapsedDocs, from)
|
|
}
|
|
mtDoc.PropertyStructs = collapsedDocs
|
|
}
|
|
|
|
func collapseDuplicateProperties(to, from *[]PropertyDocs) {
|
|
propertyLoop:
|
|
for _, f := range *from {
|
|
for i := range *to {
|
|
t := &(*to)[i]
|
|
if f.Name == t.Name {
|
|
collapseDuplicateProperties(&t.Properties, &f.Properties)
|
|
continue propertyLoop
|
|
}
|
|
}
|
|
*to = append(*to, f)
|
|
}
|
|
}
|
|
|
|
// Find all property structs that only contain structs, and move their children up one with
|
|
// a prefixed name
|
|
func collapseNestedPropertyStructs(mtDoc *moduleTypeDoc) {
|
|
for _, ps := range mtDoc.PropertyStructs {
|
|
collapseNestedProperties(&ps.Properties)
|
|
}
|
|
}
|
|
|
|
func collapseNestedProperties(p *[]PropertyDocs) {
|
|
var n []PropertyDocs
|
|
|
|
for _, parent := range *p {
|
|
var containsProperty bool
|
|
for j := range parent.Properties {
|
|
child := &parent.Properties[j]
|
|
if len(child.Properties) > 0 {
|
|
collapseNestedProperties(&child.Properties)
|
|
} else {
|
|
containsProperty = true
|
|
}
|
|
}
|
|
if containsProperty || len(parent.Properties) == 0 {
|
|
n = append(n, parent)
|
|
} else {
|
|
for j := range parent.Properties {
|
|
child := parent.Properties[j]
|
|
child.Name = parent.Name + "." + child.Name
|
|
n = append(n, child)
|
|
}
|
|
}
|
|
}
|
|
*p = n
|
|
}
|
|
|
|
func combineDuplicateProperties(mtDoc *moduleTypeDoc) {
|
|
for _, ps := range mtDoc.PropertyStructs {
|
|
combineDuplicateSubProperties(&ps.Properties)
|
|
}
|
|
}
|
|
|
|
func combineDuplicateSubProperties(p *[]PropertyDocs) {
|
|
var n []PropertyDocs
|
|
propertyLoop:
|
|
for _, child := range *p {
|
|
if len(child.Properties) > 0 {
|
|
combineDuplicateSubProperties(&child.Properties)
|
|
for i := range n {
|
|
s := &n[i]
|
|
if s.SameSubProperties(child) {
|
|
s.OtherNames = append(s.OtherNames, child.Name)
|
|
s.OtherTexts = append(s.OtherTexts, child.Text)
|
|
continue propertyLoop
|
|
}
|
|
}
|
|
}
|
|
n = append(n, child)
|
|
}
|
|
|
|
*p = n
|
|
}
|
|
|
|
type moduleTypeByName []*moduleTypeDoc
|
|
|
|
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] }
|
|
|
|
type moduleTypeDoc struct {
|
|
Name string
|
|
Text string
|
|
PropertyStructs []*PropertyStructDocs
|
|
}
|
|
|
|
var (
|
|
fileTemplate = `
|
|
<html>
|
|
<head>
|
|
<title>Build Docs</title>
|
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<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>
|
|
</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}}
|
|
<p>{{.Text}}</p>
|
|
{{template "properties" .Properties}}
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
|
|
{{define "properties"}}
|
|
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
|
|
{{range .}}
|
|
{{$collapseIndex := unique}}
|
|
{{if .Properties}}
|
|
<div class="panel panel-default">
|
|
<div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
|
|
<h4 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}}{{range .OtherNames}}, {{.}}{{end}}
|
|
</a>
|
|
</h4>
|
|
</div>
|
|
</div>
|
|
<div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
|
|
<div class="panel-body">
|
|
<p>{{.Text}}</p>
|
|
{{range .OtherTexts}}<p>{{.}}</p>{{end}}
|
|
{{template "properties" .Properties}}
|
|
</div>
|
|
</div>
|
|
{{else}}
|
|
<div>
|
|
<h4>{{.Name}}{{range .OtherNames}}, {{.}}{{end}}</h4>
|
|
<p>{{.Text}}</p>
|
|
{{range .OtherTexts}}<p>{{.}}</p>{{end}}
|
|
<p><i>Type: {{.Type}}</i></p>
|
|
{{if .Default}}<p><i>Default: {{.Default}}</i></p>{{end}}
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
`
|
|
)
|