github.com/colincross/blueprint@v0.0.0-20150626231830-9c067caf2eb5/bootstrap/bpdoc/bpdoc.go (about)

     1  package bpdoc
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/ast"
     7  	"go/doc"
     8  	"go/parser"
     9  	"go/token"
    10  	"io/ioutil"
    11  	"reflect"
    12  	"sort"
    13  	"strconv"
    14  	"strings"
    15  	"sync"
    16  	"text/template"
    17  
    18  	"github.com/google/blueprint"
    19  	"github.com/google/blueprint/proptools"
    20  )
    21  
    22  type DocCollector struct {
    23  	pkgFiles map[string][]string // Map of package name to source files, provided by constructor
    24  
    25  	mutex   sync.Mutex
    26  	pkgDocs map[string]*doc.Package        // Map of package name to parsed Go AST, protected by mutex
    27  	docs    map[string]*PropertyStructDocs // Map of type name to docs, protected by mutex
    28  }
    29  
    30  func NewDocCollector(pkgFiles map[string][]string) *DocCollector {
    31  	return &DocCollector{
    32  		pkgFiles: pkgFiles,
    33  		pkgDocs:  make(map[string]*doc.Package),
    34  		docs:     make(map[string]*PropertyStructDocs),
    35  	}
    36  }
    37  
    38  // Return the PropertyStructDocs associated with a property struct type.  The type should be in the
    39  // format <package path>.<type name>
    40  func (dc *DocCollector) Docs(pkg, name string, defaults reflect.Value) (*PropertyStructDocs, error) {
    41  	docs := dc.getDocs(name)
    42  
    43  	if docs == nil {
    44  		pkgDocs, err := dc.packageDocs(pkg)
    45  		if err != nil {
    46  			return nil, err
    47  		}
    48  
    49  		for _, t := range pkgDocs.Types {
    50  			if t.Name == name {
    51  				docs, err = newDocs(t)
    52  				if err != nil {
    53  					return nil, err
    54  				}
    55  				docs = dc.putDocs(name, docs)
    56  			}
    57  		}
    58  	}
    59  
    60  	if docs == nil {
    61  		return nil, fmt.Errorf("package %q type %q not found", pkg, name)
    62  	}
    63  
    64  	docs = docs.Clone()
    65  	docs.SetDefaults(defaults)
    66  
    67  	return docs, nil
    68  }
    69  
    70  func (dc *DocCollector) getDocs(name string) *PropertyStructDocs {
    71  	dc.mutex.Lock()
    72  	defer dc.mutex.Unlock()
    73  
    74  	return dc.docs[name]
    75  }
    76  
    77  func (dc *DocCollector) putDocs(name string, docs *PropertyStructDocs) *PropertyStructDocs {
    78  	dc.mutex.Lock()
    79  	defer dc.mutex.Unlock()
    80  
    81  	if dc.docs[name] != nil {
    82  		return dc.docs[name]
    83  	} else {
    84  		dc.docs[name] = docs
    85  		return docs
    86  	}
    87  }
    88  
    89  type PropertyStructDocs struct {
    90  	Name       string
    91  	Text       string
    92  	Properties []PropertyDocs
    93  }
    94  
    95  type PropertyDocs struct {
    96  	Name       string
    97  	OtherNames []string
    98  	Type       string
    99  	Tag        reflect.StructTag
   100  	Text       string
   101  	OtherTexts []string
   102  	Properties []PropertyDocs
   103  	Default    string
   104  }
   105  
   106  func (docs *PropertyStructDocs) Clone() *PropertyStructDocs {
   107  	ret := *docs
   108  	ret.Properties = append([]PropertyDocs(nil), ret.Properties...)
   109  	for i, prop := range ret.Properties {
   110  		ret.Properties[i] = prop.Clone()
   111  	}
   112  
   113  	return &ret
   114  }
   115  
   116  func (docs *PropertyDocs) Clone() PropertyDocs {
   117  	ret := *docs
   118  	ret.Properties = append([]PropertyDocs(nil), ret.Properties...)
   119  	for i, prop := range ret.Properties {
   120  		ret.Properties[i] = prop.Clone()
   121  	}
   122  
   123  	return ret
   124  }
   125  
   126  func (docs *PropertyDocs) Equal(other PropertyDocs) bool {
   127  	return docs.Name == other.Name && docs.Type == other.Type && docs.Tag == other.Tag &&
   128  		docs.Text == other.Text && docs.Default == other.Default &&
   129  		stringArrayEqual(docs.OtherNames, other.OtherNames) &&
   130  		stringArrayEqual(docs.OtherTexts, other.OtherTexts) &&
   131  		docs.SameSubProperties(other)
   132  }
   133  
   134  func (docs *PropertyStructDocs) SetDefaults(defaults reflect.Value) {
   135  	setDefaults(docs.Properties, defaults)
   136  }
   137  
   138  func setDefaults(properties []PropertyDocs, defaults reflect.Value) {
   139  	for i := range properties {
   140  		prop := &properties[i]
   141  		fieldName := proptools.FieldNameForProperty(prop.Name)
   142  		f := defaults.FieldByName(fieldName)
   143  		if (f == reflect.Value{}) {
   144  			panic(fmt.Errorf("property %q does not exist in %q", fieldName, defaults.Type()))
   145  		}
   146  
   147  		if reflect.DeepEqual(f.Interface(), reflect.Zero(f.Type()).Interface()) {
   148  			continue
   149  		}
   150  
   151  		if f.Type().Kind() == reflect.Interface {
   152  			f = f.Elem()
   153  		}
   154  
   155  		if f.Type().Kind() == reflect.Ptr {
   156  			f = f.Elem()
   157  		}
   158  
   159  		if f.Type().Kind() == reflect.Struct {
   160  			setDefaults(prop.Properties, f)
   161  		} else {
   162  			prop.Default = fmt.Sprintf("%v", f.Interface())
   163  		}
   164  	}
   165  }
   166  
   167  func stringArrayEqual(a, b []string) bool {
   168  	if len(a) != len(b) {
   169  		return false
   170  	}
   171  
   172  	for i := range a {
   173  		if a[i] != b[i] {
   174  			return false
   175  		}
   176  	}
   177  
   178  	return true
   179  }
   180  
   181  func (docs *PropertyDocs) SameSubProperties(other PropertyDocs) bool {
   182  	if len(docs.Properties) != len(other.Properties) {
   183  		return false
   184  	}
   185  
   186  	for i := range docs.Properties {
   187  		if !docs.Properties[i].Equal(other.Properties[i]) {
   188  			return false
   189  		}
   190  	}
   191  
   192  	return true
   193  }
   194  
   195  func (docs *PropertyStructDocs) GetByName(name string) *PropertyDocs {
   196  	return getByName(name, "", &docs.Properties)
   197  }
   198  
   199  func getByName(name string, prefix string, props *[]PropertyDocs) *PropertyDocs {
   200  	for i := range *props {
   201  		if prefix+(*props)[i].Name == name {
   202  			return &(*props)[i]
   203  		} else if strings.HasPrefix(name, prefix+(*props)[i].Name+".") {
   204  			return getByName(name, prefix+(*props)[i].Name+".", &(*props)[i].Properties)
   205  		}
   206  	}
   207  	return nil
   208  }
   209  
   210  func (prop *PropertyDocs) Nest(nested *PropertyStructDocs) {
   211  	//prop.Name += "(" + nested.Name + ")"
   212  	//prop.Text += "(" + nested.Text + ")"
   213  	prop.Properties = append(prop.Properties, nested.Properties...)
   214  }
   215  
   216  func newDocs(t *doc.Type) (*PropertyStructDocs, error) {
   217  	typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
   218  	docs := PropertyStructDocs{
   219  		Name: t.Name,
   220  		Text: t.Doc,
   221  	}
   222  
   223  	structType, ok := typeSpec.Type.(*ast.StructType)
   224  	if !ok {
   225  		return nil, fmt.Errorf("type of %q is not a struct", t.Name)
   226  	}
   227  
   228  	var err error
   229  	docs.Properties, err = structProperties(structType)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	return &docs, nil
   235  }
   236  
   237  func structProperties(structType *ast.StructType) (props []PropertyDocs, err error) {
   238  	for _, f := range structType.Fields.List {
   239  		//fmt.Printf("%T %#v\n", f, f)
   240  		for _, n := range f.Names {
   241  			var name, typ, tag, text string
   242  			var innerProps []PropertyDocs
   243  			if n != nil {
   244  				name = proptools.PropertyNameForField(n.Name)
   245  			}
   246  			if f.Doc != nil {
   247  				text = f.Doc.Text()
   248  			}
   249  			if f.Tag != nil {
   250  				tag, err = strconv.Unquote(f.Tag.Value)
   251  				if err != nil {
   252  					return nil, err
   253  				}
   254  			}
   255  			switch a := f.Type.(type) {
   256  			case *ast.ArrayType:
   257  				typ = "list of strings"
   258  			case *ast.InterfaceType:
   259  				typ = "interface"
   260  			case *ast.Ident:
   261  				typ = a.Name
   262  			case *ast.StructType:
   263  				innerProps, err = structProperties(a)
   264  				if err != nil {
   265  					return nil, err
   266  				}
   267  			default:
   268  				typ = fmt.Sprintf("%T", f.Type)
   269  			}
   270  
   271  			props = append(props, PropertyDocs{
   272  				Name:       name,
   273  				Type:       typ,
   274  				Tag:        reflect.StructTag(tag),
   275  				Text:       text,
   276  				Properties: innerProps,
   277  			})
   278  		}
   279  	}
   280  
   281  	return props, nil
   282  }
   283  
   284  func (docs *PropertyStructDocs) ExcludeByTag(key, value string) {
   285  	filterPropsByTag(&docs.Properties, key, value, true)
   286  }
   287  
   288  func (docs *PropertyStructDocs) IncludeByTag(key, value string) {
   289  	filterPropsByTag(&docs.Properties, key, value, false)
   290  }
   291  
   292  func filterPropsByTag(props *[]PropertyDocs, key, value string, exclude bool) {
   293  	// Create a slice that shares the storage of props but has 0 length.  Appending up to
   294  	// len(props) times to this slice will overwrite the original slice contents
   295  	filtered := (*props)[:0]
   296  	for _, x := range *props {
   297  		tag := x.Tag.Get(key)
   298  		for _, entry := range strings.Split(tag, ",") {
   299  			if (entry == value) == !exclude {
   300  				filtered = append(filtered, x)
   301  			}
   302  		}
   303  	}
   304  
   305  	*props = filtered
   306  }
   307  
   308  // Package AST generation and storage
   309  func (dc *DocCollector) packageDocs(pkg string) (*doc.Package, error) {
   310  	pkgDocs := dc.getPackageDocs(pkg)
   311  	if pkgDocs == nil {
   312  		if files, ok := dc.pkgFiles[pkg]; ok {
   313  			var err error
   314  			pkgAST, err := NewPackageAST(files)
   315  			if err != nil {
   316  				return nil, err
   317  			}
   318  			pkgDocs = doc.New(pkgAST, pkg, doc.AllDecls)
   319  			pkgDocs = dc.putPackageDocs(pkg, pkgDocs)
   320  		} else {
   321  			return nil, fmt.Errorf("unknown package %q", pkg)
   322  		}
   323  	}
   324  	return pkgDocs, nil
   325  }
   326  
   327  func (dc *DocCollector) getPackageDocs(pkg string) *doc.Package {
   328  	dc.mutex.Lock()
   329  	defer dc.mutex.Unlock()
   330  
   331  	return dc.pkgDocs[pkg]
   332  }
   333  
   334  func (dc *DocCollector) putPackageDocs(pkg string, pkgDocs *doc.Package) *doc.Package {
   335  	dc.mutex.Lock()
   336  	defer dc.mutex.Unlock()
   337  
   338  	if dc.pkgDocs[pkg] != nil {
   339  		return dc.pkgDocs[pkg]
   340  	} else {
   341  		dc.pkgDocs[pkg] = pkgDocs
   342  		return pkgDocs
   343  	}
   344  }
   345  
   346  func NewPackageAST(files []string) (*ast.Package, error) {
   347  	asts := make(map[string]*ast.File)
   348  
   349  	fset := token.NewFileSet()
   350  	for _, file := range files {
   351  		ast, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
   352  		if err != nil {
   353  			return nil, err
   354  		}
   355  		asts[file] = ast
   356  	}
   357  
   358  	pkg, _ := ast.NewPackage(fset, asts, nil, nil)
   359  	return pkg, nil
   360  }
   361  
   362  func Write(filename string, pkgFiles map[string][]string,
   363  	moduleTypePropertyStructs map[string][]interface{}) error {
   364  
   365  	docSet := NewDocCollector(pkgFiles)
   366  
   367  	var moduleTypeList []*moduleTypeDoc
   368  	for moduleType, propertyStructs := range moduleTypePropertyStructs {
   369  		mtDoc, err := getModuleTypeDoc(docSet, moduleType, propertyStructs)
   370  		if err != nil {
   371  			return err
   372  		}
   373  		removeEmptyPropertyStructs(mtDoc)
   374  		collapseDuplicatePropertyStructs(mtDoc)
   375  		collapseNestedPropertyStructs(mtDoc)
   376  		combineDuplicateProperties(mtDoc)
   377  		moduleTypeList = append(moduleTypeList, mtDoc)
   378  	}
   379  
   380  	sort.Sort(moduleTypeByName(moduleTypeList))
   381  
   382  	buf := &bytes.Buffer{}
   383  
   384  	unique := 0
   385  
   386  	tmpl, err := template.New("file").Funcs(map[string]interface{}{
   387  		"unique": func() int {
   388  			unique++
   389  			return unique
   390  		}}).Parse(fileTemplate)
   391  	if err != nil {
   392  		return err
   393  	}
   394  
   395  	err = tmpl.Execute(buf, moduleTypeList)
   396  	if err != nil {
   397  		return err
   398  	}
   399  
   400  	err = ioutil.WriteFile(filename, buf.Bytes(), 0666)
   401  	if err != nil {
   402  		return err
   403  	}
   404  
   405  	return nil
   406  }
   407  
   408  func getModuleTypeDoc(docSet *DocCollector, moduleType string,
   409  	propertyStructs []interface{}) (*moduleTypeDoc, error) {
   410  	mtDoc := &moduleTypeDoc{
   411  		Name: moduleType,
   412  		//Text: docSet.ModuleTypeDocs(moduleType),
   413  	}
   414  
   415  	for _, s := range propertyStructs {
   416  		v := reflect.ValueOf(s).Elem()
   417  		t := v.Type()
   418  
   419  		// Ignore property structs with unexported or unnamed types
   420  		if t.PkgPath() == "" {
   421  			continue
   422  		}
   423  		psDoc, err := docSet.Docs(t.PkgPath(), t.Name(), v)
   424  		if err != nil {
   425  			return nil, err
   426  		}
   427  		psDoc.ExcludeByTag("blueprint", "mutated")
   428  
   429  		for nested, nestedValue := range nestedPropertyStructs(v) {
   430  			nestedType := nestedValue.Type()
   431  
   432  			// Ignore property structs with unexported or unnamed types
   433  			if nestedType.PkgPath() == "" {
   434  				continue
   435  			}
   436  			nestedDoc, err := docSet.Docs(nestedType.PkgPath(), nestedType.Name(), nestedValue)
   437  			if err != nil {
   438  				return nil, err
   439  			}
   440  			nestedDoc.ExcludeByTag("blueprint", "mutated")
   441  			nestPoint := psDoc.GetByName(nested)
   442  			if nestPoint == nil {
   443  				return nil, fmt.Errorf("nesting point %q not found", nested)
   444  			}
   445  
   446  			key, value, err := blueprint.HasFilter(nestPoint.Tag)
   447  			if err != nil {
   448  				return nil, err
   449  			}
   450  			if key != "" {
   451  				nestedDoc.IncludeByTag(key, value)
   452  			}
   453  
   454  			nestPoint.Nest(nestedDoc)
   455  		}
   456  		mtDoc.PropertyStructs = append(mtDoc.PropertyStructs, psDoc)
   457  	}
   458  
   459  	return mtDoc, nil
   460  }
   461  
   462  func nestedPropertyStructs(s reflect.Value) map[string]reflect.Value {
   463  	ret := make(map[string]reflect.Value)
   464  	var walk func(structValue reflect.Value, prefix string)
   465  	walk = func(structValue reflect.Value, prefix string) {
   466  		typ := structValue.Type()
   467  		for i := 0; i < structValue.NumField(); i++ {
   468  			field := typ.Field(i)
   469  			if field.PkgPath != "" {
   470  				// The field is not exported so just skip it.
   471  				continue
   472  			}
   473  
   474  			fieldValue := structValue.Field(i)
   475  
   476  			switch fieldValue.Kind() {
   477  			case reflect.Bool, reflect.String, reflect.Slice, reflect.Int, reflect.Uint:
   478  				// Nothing
   479  			case reflect.Struct:
   480  				walk(fieldValue, prefix+proptools.PropertyNameForField(field.Name)+".")
   481  			case reflect.Ptr, reflect.Interface:
   482  				if !fieldValue.IsNil() {
   483  					// We leave the pointer intact and zero out the struct that's
   484  					// pointed to.
   485  					elem := fieldValue.Elem()
   486  					if fieldValue.Kind() == reflect.Interface {
   487  						if elem.Kind() != reflect.Ptr {
   488  							panic(fmt.Errorf("can't get type of field %q: interface "+
   489  								"refers to a non-pointer", field.Name))
   490  						}
   491  						elem = elem.Elem()
   492  					}
   493  					if elem.Kind() != reflect.Struct {
   494  						panic(fmt.Errorf("can't get type of field %q: points to a "+
   495  							"non-struct", field.Name))
   496  					}
   497  					nestPoint := prefix + proptools.PropertyNameForField(field.Name)
   498  					ret[nestPoint] = elem
   499  					walk(elem, nestPoint+".")
   500  				}
   501  			default:
   502  				panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
   503  					field.Name, fieldValue.Kind()))
   504  			}
   505  		}
   506  
   507  	}
   508  
   509  	walk(s, "")
   510  	return ret
   511  }
   512  
   513  // Remove any property structs that have no exported fields
   514  func removeEmptyPropertyStructs(mtDoc *moduleTypeDoc) {
   515  	for i := 0; i < len(mtDoc.PropertyStructs); i++ {
   516  		if len(mtDoc.PropertyStructs[i].Properties) == 0 {
   517  			mtDoc.PropertyStructs = append(mtDoc.PropertyStructs[:i], mtDoc.PropertyStructs[i+1:]...)
   518  			i--
   519  		}
   520  	}
   521  }
   522  
   523  // Squashes duplicates of the same property struct into single entries
   524  func collapseDuplicatePropertyStructs(mtDoc *moduleTypeDoc) {
   525  	var collapsedDocs []*PropertyStructDocs
   526  
   527  propertyStructLoop:
   528  	for _, from := range mtDoc.PropertyStructs {
   529  		for _, to := range collapsedDocs {
   530  			if from.Name == to.Name {
   531  				collapseDuplicateProperties(&to.Properties, &from.Properties)
   532  				continue propertyStructLoop
   533  			}
   534  		}
   535  		collapsedDocs = append(collapsedDocs, from)
   536  	}
   537  	mtDoc.PropertyStructs = collapsedDocs
   538  }
   539  
   540  func collapseDuplicateProperties(to, from *[]PropertyDocs) {
   541  propertyLoop:
   542  	for _, f := range *from {
   543  		for i := range *to {
   544  			t := &(*to)[i]
   545  			if f.Name == t.Name {
   546  				collapseDuplicateProperties(&t.Properties, &f.Properties)
   547  				continue propertyLoop
   548  			}
   549  		}
   550  		*to = append(*to, f)
   551  	}
   552  }
   553  
   554  // Find all property structs that only contain structs, and move their children up one with
   555  // a prefixed name
   556  func collapseNestedPropertyStructs(mtDoc *moduleTypeDoc) {
   557  	for _, ps := range mtDoc.PropertyStructs {
   558  		collapseNestedProperties(&ps.Properties)
   559  	}
   560  }
   561  
   562  func collapseNestedProperties(p *[]PropertyDocs) {
   563  	var n []PropertyDocs
   564  
   565  	for _, parent := range *p {
   566  		var containsProperty bool
   567  		for j := range parent.Properties {
   568  			child := &parent.Properties[j]
   569  			if len(child.Properties) > 0 {
   570  				collapseNestedProperties(&child.Properties)
   571  			} else {
   572  				containsProperty = true
   573  			}
   574  		}
   575  		if containsProperty || len(parent.Properties) == 0 {
   576  			n = append(n, parent)
   577  		} else {
   578  			for j := range parent.Properties {
   579  				child := parent.Properties[j]
   580  				child.Name = parent.Name + "." + child.Name
   581  				n = append(n, child)
   582  			}
   583  		}
   584  	}
   585  	*p = n
   586  }
   587  
   588  func combineDuplicateProperties(mtDoc *moduleTypeDoc) {
   589  	for _, ps := range mtDoc.PropertyStructs {
   590  		combineDuplicateSubProperties(&ps.Properties)
   591  	}
   592  }
   593  
   594  func combineDuplicateSubProperties(p *[]PropertyDocs) {
   595  	var n []PropertyDocs
   596  propertyLoop:
   597  	for _, child := range *p {
   598  		if len(child.Properties) > 0 {
   599  			combineDuplicateSubProperties(&child.Properties)
   600  			for i := range n {
   601  				s := &n[i]
   602  				if s.SameSubProperties(child) {
   603  					s.OtherNames = append(s.OtherNames, child.Name)
   604  					s.OtherTexts = append(s.OtherTexts, child.Text)
   605  					continue propertyLoop
   606  				}
   607  			}
   608  		}
   609  		n = append(n, child)
   610  	}
   611  
   612  	*p = n
   613  }
   614  
   615  type moduleTypeByName []*moduleTypeDoc
   616  
   617  func (l moduleTypeByName) Len() int           { return len(l) }
   618  func (l moduleTypeByName) Less(i, j int) bool { return l[i].Name < l[j].Name }
   619  func (l moduleTypeByName) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
   620  
   621  type moduleTypeDoc struct {
   622  	Name            string
   623  	Text            string
   624  	PropertyStructs []*PropertyStructDocs
   625  }
   626  
   627  var (
   628  	fileTemplate = `
   629  <html>
   630  <head>
   631  <title>Build Docs</title>
   632  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
   633  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
   634  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
   635  </head>
   636  <body>
   637  <h1>Build Docs</h1>
   638  <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
   639    {{range .}}
   640      {{ $collapseIndex := unique }}
   641      <div class="panel panel-default">
   642        <div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
   643          <h2 class="panel-title">
   644            <a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapse{{$collapseIndex}}" aria-expanded="false" aria-controls="collapse{{$collapseIndex}}">
   645               {{.Name}}
   646            </a>
   647          </h2>
   648        </div>
   649      </div>
   650      <div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
   651        <div class="panel-body">
   652          <p>{{.Text}}</p>
   653          {{range .PropertyStructs}}
   654            <p>{{.Text}}</p>
   655            {{template "properties" .Properties}}
   656          {{end}}
   657        </div>
   658      </div>
   659    {{end}}
   660  </div>
   661  </body>
   662  </html>
   663  
   664  {{define "properties"}}
   665    <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
   666      {{range .}}
   667        {{$collapseIndex := unique}}
   668        {{if .Properties}}
   669          <div class="panel panel-default">
   670            <div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
   671              <h4 class="panel-title">
   672                <a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapse{{$collapseIndex}}" aria-expanded="false" aria-controls="collapse{{$collapseIndex}}">
   673                   {{.Name}}{{range .OtherNames}}, {{.}}{{end}}
   674                </a>
   675              </h4>
   676            </div>
   677          </div>
   678          <div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
   679            <div class="panel-body">
   680              <p>{{.Text}}</p>
   681              {{range .OtherTexts}}<p>{{.}}</p>{{end}}
   682              {{template "properties" .Properties}}
   683            </div>
   684          </div>
   685        {{else}}
   686          <div>
   687            <h4>{{.Name}}{{range .OtherNames}}, {{.}}{{end}}</h4>
   688            <p>{{.Text}}</p>
   689            {{range .OtherTexts}}<p>{{.}}</p>{{end}}
   690            <p><i>Type: {{.Type}}</i></p>
   691            {{if .Default}}<p><i>Default: {{.Default}}</i></p>{{end}}
   692          </div>
   693        {{end}}
   694      {{end}}
   695    </div>
   696  {{end}}
   697  `
   698  )