// Copyright 2017 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 main import ( "bytes" "html/template" "io/ioutil" "path/filepath" "sort" "android/soong/android" "github.com/google/blueprint/bootstrap" "github.com/google/blueprint/bootstrap/bpdoc" ) type perPackageTemplateData struct { Name string Modules []moduleTypeTemplateData } type moduleTypeTemplateData struct { Name string Synopsis template.HTML Properties []bpdoc.Property } // The properties in this map are displayed first, according to their rank. // TODO(jungjw): consider providing module type-dependent ranking var propertyRank = map[string]int{ "name": 0, "src": 1, "srcs": 2, "exclude_srcs": 3, "defaults": 4, "host_supported": 5, "device_supported": 6, } // For each module type, extract its documentation and convert it to the template data. func moduleTypeDocsToTemplates(moduleTypeList []*bpdoc.ModuleType) []moduleTypeTemplateData { result := make([]moduleTypeTemplateData, 0) // Combine properties from all PropertyStruct's and reorder them -- first the ones // with rank, then the rest of the properties in alphabetic order. for _, m := range moduleTypeList { item := moduleTypeTemplateData{ Name: m.Name, Synopsis: m.Text, Properties: make([]bpdoc.Property, 0), } props := make([]bpdoc.Property, 0) for _, propStruct := range m.PropertyStructs { props = append(props, propStruct.Properties...) } sort.Slice(props, func(i, j int) bool { if rankI, ok := propertyRank[props[i].Name]; ok { if rankJ, ok := propertyRank[props[j].Name]; ok { return rankI < rankJ } else { return true } } if _, ok := propertyRank[props[j].Name]; ok { return false } return props[i].Name < props[j].Name }) // Eliminate top-level duplicates. TODO(jungjw): improve bpdoc to handle this. previousPropertyName := "" for _, prop := range props { if prop.Name == previousPropertyName { oldProp := &item.Properties[len(item.Properties)-1].Properties bpdoc.CollapseDuplicateProperties(oldProp, &prop.Properties) } else { item.Properties = append(item.Properties, prop) } previousPropertyName = prop.Name } result = append(result, item) } sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name }) return result } func getPackages(ctx *android.Context) ([]*bpdoc.Package, error) { moduleTypeFactories := android.ModuleTypeFactoriesForDocs() return bootstrap.ModuleTypeDocs(ctx.Context, moduleTypeFactories) } func writeDocs(ctx *android.Context, filename string) error { packages, err := getPackages(ctx) if err != nil { return err } // Produce the top-level, package list page first. tmpl := template.Must(template.Must(template.New("file").Parse(packageListTemplate)).Parse(copyBaseUrl)) buf := &bytes.Buffer{} err = tmpl.Execute(buf, packages) if err == nil { err = ioutil.WriteFile(filename, buf.Bytes(), 0666) } // Now, produce per-package module lists with detailed information, and a list // of keywords. keywordsTmpl := template.Must(template.New("file").Parse(keywordsTemplate)) keywordsBuf := &bytes.Buffer{} for _, pkg := range packages { // We need a module name getter/setter function because I couldn't // find a way to keep it in a variable defined within the template. currentModuleName := "" tmpl := template.Must( template.Must(template.New("file").Funcs(map[string]interface{}{ "setModule": func(moduleName string) string { currentModuleName = moduleName return "" }, "getModule": func() string { return currentModuleName }, }).Parse(perPackageTemplate)).Parse(copyBaseUrl)) buf := &bytes.Buffer{} modules := moduleTypeDocsToTemplates(pkg.ModuleTypes) data := perPackageTemplateData{Name: pkg.Name, Modules: modules} err = tmpl.Execute(buf, data) if err != nil { return err } pkgFileName := filepath.Join(filepath.Dir(filename), pkg.Name+".html") err = ioutil.WriteFile(pkgFileName, buf.Bytes(), 0666) if err != nil { return err } err = keywordsTmpl.Execute(keywordsBuf, data) if err != nil { return err } } // Write out list of keywords. This includes all module and property names, which is useful for // building syntax highlighters. keywordsFilename := filepath.Join(filepath.Dir(filename), "keywords.txt") err = ioutil.WriteFile(keywordsFilename, keywordsBuf.Bytes(), 0666) return err } // TODO(jungjw): Consider ordering by name. const ( packageListTemplate = ` <html> <head> <title>Build Docs</title> <style> #main { padding: 48px; } table{ table-layout: fixed; } td { word-wrap:break-word; } /* The following entries are copied from source.android.com's css file. */ td,td code { color: #202124 } th,th code { color: #fff; font: 500 16px/24px Roboto,sans-serif } td,table.responsive tr:not(.alt) td td:first-child,table.responsive td tr:not(.alt) td:first-child { background: rgba(255,255,255,.95); vertical-align: top } td,td code { padding: 7px 8px 8px } tr { border: 0; background: #78909c; border-top: 1px solid #cfd8dc } th,td { border: 0; margin: 0; text-align: left } th { height: 48px; padding: 8px; vertical-align: middle } table { border: 0; border-collapse: collapse; border-spacing: 0; font: 14px/20px Roboto,sans-serif; margin: 16px 0; width: 100% } h1 { color: #80868b; font: 300 34px/40px Roboto,sans-serif; letter-spacing: -0.01em; margin: 40px 0 20px } h1,h2,h3,h4,h5,h6 { overflow: hidden; padding: 0; text-overflow: ellipsis } :link,:visited { color: #039be5; outline: 0; text-decoration: none } body,html { color: #202124; font: 400 16px/24px Roboto,sans-serif; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; height: 100%; margin: 0; -webkit-text-size-adjust: 100%; -moz-text-size-adjust: 100%; -ms-text-size-adjust: 100%; text-size-adjust: 100% } html { -webkit-box-sizing: border-box; box-sizing: border-box } *,*::before,*::after { -webkit-box-sizing: inherit; box-sizing: inherit } body,div,dl,dd,form,img,input,figure,menu { margin: 0; padding: 0 } </style> {{template "copyBaseUrl"}} </head> <body> <div id="main"> <H1>Soong Modules Reference</H1> The latest versions of Android use the Soong build system, which greatly simplifies build configuration over the previous Make-based system. This site contains the generated reference files for the Soong build system. <table class="module_types" summary="Table of Soong module types sorted by package"> <thead> <tr> <th style="width:20%">Package</th> <th style="width:80%">Module types</th> </tr> </thead> <tbody> {{range $pkg := .}} <tr> <td>{{.Path}}</td> <td> {{range $i, $mod := .ModuleTypes}}{{if $i}}, {{end}}<a href="{{$pkg.Name}}.html#{{$mod.Name}}">{{$mod.Name}}</a>{{end}} </td> </tr> {{end}} </tbody> </table> </div> </body> </html> ` perPackageTemplate = ` <html> <head> <title>Build Docs</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css"> <style> .accordion,.simple{margin-left:1.5em;text-indent:-1.5em;margin-top:.25em} .collapsible{border-width:0 0 0 1;margin-left:.25em;padding-left:.25em;border-style:solid; border-color:grey;display:none;} span.fixed{display: block; float: left; clear: left; width: 1em;} ul { list-style-type: none; margin: 0; padding: 0; width: 30ch; background-color: #f1f1f1; position: fixed; height: 100%; overflow: auto; } li a { display: block; color: #000; padding: 8px 16px; text-decoration: none; } li a.active { background-color: #4CAF50; color: white; } li a:hover:not(.active) { background-color: #555; color: white; } </style> {{template "copyBaseUrl"}} </head> <body> {{- /* Fixed sidebar with module types */ -}} <ul> <li><h3>{{.Name}} package</h3></li> {{range $moduleType := .Modules}}<li><a href="{{$.Name}}.html#{{$moduleType.Name}}">{{$moduleType.Name}}</a></li> {{end -}} </ul> {{/* Main panel with H1 section per module type */}} <div style="margin-left:30ch;padding:1px 16px;"> {{range $moduleType := .Modules}} {{setModule $moduleType.Name}} <p> <h2 id="{{$moduleType.Name}}">{{$moduleType.Name}}</h2> {{if $moduleType.Synopsis }}{{$moduleType.Synopsis}}{{else}}<i>Missing synopsis</i>{{end}} {{- /* Comma-separated list of module attributes' links module attributes */ -}} <div class="breadcrumb"> {{range $i,$prop := $moduleType.Properties }} {{ if gt $i 0 }}, {{end -}} <a href={{$.Name}}.html#{{getModule}}.{{$prop.Name}}>{{$prop.Name}}</a> {{- end -}} </div> {{- /* Property description */ -}} {{- template "properties" $moduleType.Properties -}} {{- end -}} {{define "properties" -}} {{range .}} {{if .Properties -}} <div class="accordion" id="{{getModule}}.{{.Name}}"> <span class="fixed">⊕</span><b>{{.Name}}</b> <i>{{.Type}}</i> {{- range .OtherNames -}}, {{.}}{{- end -}} </div> <div class="collapsible"> {{- .Text}} {{range .OtherTexts}}{{.}}{{end}} {{template "properties" .Properties -}} </div> {{- else -}} <div class="simple" id="{{getModule}}.{{.Name}}"> <span class="fixed"> </span><b>{{.Name}} {{range .OtherNames}}, {{.}}{{end -}}</b> <i>{{.Type}}</i> {{- if .Text -}}{{if ne .Text "\n"}}, {{end}}{{.Text}}{{- end -}} {{- with .OtherTexts -}}{{.}}{{- end -}} {{- if .Default -}}<i>Default: {{.Default}}</i>{{- end -}} </div> {{- end}} {{- end -}} {{- end -}} </div> <script> accordions = document.getElementsByClassName('accordion'); for (i=0; i < accordions.length; ++i) { accordions[i].addEventListener("click", function() { var panel = this.nextElementSibling; var child = this.firstElementChild; if (panel.style.display === "block") { panel.style.display = "none"; child.textContent = '\u2295'; } else { panel.style.display = "block"; child.textContent = '\u2296'; } }); } </script> </body> ` copyBaseUrl = ` {{define "copyBaseUrl"}} <script type="text/javascript"> window.addEventListener('message', (e) => { if (e != null && e.data != null && e.data.type === "SET_BASE" && e.data.base != null) { const existingBase = document.querySelector('base'); if (existingBase != null) { existingBase.parentElement.removeChild(existingBase); } const base = document.createElement('base'); base.setAttribute('href', e.data.base); document.head.appendChild(base); } }); </script> {{end}} ` keywordsTemplate = ` {{range $moduleType := .Modules}}{{$moduleType.Name}}:{{range $property := $moduleType.Properties}}{{$property.Name}},{{end}} {{end}} ` )