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