1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18	"android/soong/android"
19	"bytes"
20	"html/template"
21	"io/ioutil"
22	"path/filepath"
23	"sort"
24
25	"github.com/google/blueprint/bootstrap"
26	"github.com/google/blueprint/bootstrap/bpdoc"
27)
28
29type perPackageTemplateData struct {
30	Name    string
31	Modules []moduleTypeTemplateData
32}
33
34type moduleTypeTemplateData struct {
35	Name       string
36	Synopsis   template.HTML
37	Properties []bpdoc.Property
38}
39
40// The properties in this map are displayed first, according to their rank.
41// TODO(jungjw): consider providing module type-dependent ranking
42var propertyRank = map[string]int{
43	"name":             0,
44	"src":              1,
45	"srcs":             2,
46	"exclude_srcs":     3,
47	"defaults":         4,
48	"host_supported":   5,
49	"device_supported": 6,
50}
51
52// For each module type, extract its documentation and convert it to the template data.
53func moduleTypeDocsToTemplates(moduleTypeList []*bpdoc.ModuleType) []moduleTypeTemplateData {
54	result := make([]moduleTypeTemplateData, 0)
55
56	// Combine properties from all PropertyStruct's and reorder them -- first the ones
57	// with rank, then the rest of the properties in alphabetic order.
58	for _, m := range moduleTypeList {
59		item := moduleTypeTemplateData{
60			Name:       m.Name,
61			Synopsis:   m.Text,
62			Properties: make([]bpdoc.Property, 0),
63		}
64		props := make([]bpdoc.Property, 0)
65		for _, propStruct := range m.PropertyStructs {
66			props = append(props, propStruct.Properties...)
67		}
68		sort.Slice(props, func(i, j int) bool {
69			if rankI, ok := propertyRank[props[i].Name]; ok {
70				if rankJ, ok := propertyRank[props[j].Name]; ok {
71					return rankI < rankJ
72				} else {
73					return true
74				}
75			}
76			if _, ok := propertyRank[props[j].Name]; ok {
77				return false
78			}
79			return props[i].Name < props[j].Name
80		})
81		// Eliminate top-level duplicates. TODO(jungjw): improve bpdoc to handle this.
82		previousPropertyName := ""
83		for _, prop := range props {
84			if prop.Name == previousPropertyName {
85				oldProp := &item.Properties[len(item.Properties)-1].Properties
86				bpdoc.CollapseDuplicateProperties(oldProp, &prop.Properties)
87			} else {
88				item.Properties = append(item.Properties, prop)
89			}
90			previousPropertyName = prop.Name
91		}
92		result = append(result, item)
93	}
94	sort.Slice(result, func(i, j int) bool { return result[i].Name < result[j].Name })
95	return result
96}
97
98func getPackages(ctx *android.Context, config interface{}) ([]*bpdoc.Package, error) {
99	moduleTypeFactories := android.ModuleTypeFactoriesForDocs()
100	return bootstrap.ModuleTypeDocs(ctx.Context, config, moduleTypeFactories)
101}
102
103func writeDocs(ctx *android.Context, config interface{}, filename string) error {
104	packages, err := getPackages(ctx, config)
105	if err != nil {
106		return err
107	}
108
109	// Produce the top-level, package list page first.
110	tmpl := template.Must(template.Must(template.New("file").Parse(packageListTemplate)).Parse(copyBaseUrl))
111	buf := &bytes.Buffer{}
112	err = tmpl.Execute(buf, packages)
113	if err == nil {
114		err = ioutil.WriteFile(filename, buf.Bytes(), 0666)
115	}
116
117	// Now, produce per-package module lists with detailed information, and a list
118	// of keywords.
119	keywordsTmpl := template.Must(template.New("file").Parse(keywordsTemplate))
120	keywordsBuf := &bytes.Buffer{}
121	for _, pkg := range packages {
122		// We need a module name getter/setter function because I couldn't
123		// find a way to keep it in a variable defined within the template.
124		currentModuleName := ""
125		tmpl := template.Must(
126			template.Must(template.New("file").Funcs(map[string]interface{}{
127				"setModule": func(moduleName string) string {
128					currentModuleName = moduleName
129					return ""
130				},
131				"getModule": func() string {
132					return currentModuleName
133				},
134			}).Parse(perPackageTemplate)).Parse(copyBaseUrl))
135		buf := &bytes.Buffer{}
136		modules := moduleTypeDocsToTemplates(pkg.ModuleTypes)
137		data := perPackageTemplateData{Name: pkg.Name, Modules: modules}
138		err = tmpl.Execute(buf, data)
139		if err != nil {
140			return err
141		}
142		pkgFileName := filepath.Join(filepath.Dir(filename), pkg.Name+".html")
143		err = ioutil.WriteFile(pkgFileName, buf.Bytes(), 0666)
144		if err != nil {
145			return err
146		}
147		err = keywordsTmpl.Execute(keywordsBuf, data)
148		if err != nil {
149			return err
150		}
151	}
152
153	// Write out list of keywords. This includes all module and property names, which is useful for
154	// building syntax highlighters.
155	keywordsFilename := filepath.Join(filepath.Dir(filename), "keywords.txt")
156	err = ioutil.WriteFile(keywordsFilename, keywordsBuf.Bytes(), 0666)
157
158	return err
159}
160
161// TODO(jungjw): Consider ordering by name.
162const (
163	packageListTemplate = `
164<html>
165<head>
166<title>Build Docs</title>
167<style>
168#main {
169  padding: 48px;
170}
171
172table{
173  table-layout: fixed;
174}
175
176td {
177  word-wrap:break-word;
178}
179
180/* The following entries are copied from source.android.com's css file. */
181td,td code {
182    color: #202124
183}
184
185th,th code {
186    color: #fff;
187    font: 500 16px/24px Roboto,sans-serif
188}
189
190td,table.responsive tr:not(.alt) td td:first-child,table.responsive td tr:not(.alt) td:first-child {
191    background: rgba(255,255,255,.95);
192    vertical-align: top
193}
194
195td,td code {
196    padding: 7px 8px 8px
197}
198
199tr {
200    border: 0;
201    background: #78909c;
202    border-top: 1px solid #cfd8dc
203}
204
205th,td {
206    border: 0;
207    margin: 0;
208    text-align: left
209}
210
211th {
212    height: 48px;
213    padding: 8px;
214    vertical-align: middle
215}
216
217table {
218    border: 0;
219    border-collapse: collapse;
220    border-spacing: 0;
221    font: 14px/20px Roboto,sans-serif;
222    margin: 16px 0;
223    width: 100%
224}
225
226h1 {
227    color: #80868b;
228    font: 300 34px/40px Roboto,sans-serif;
229    letter-spacing: -0.01em;
230    margin: 40px 0 20px
231}
232
233h1,h2,h3,h4,h5,h6 {
234    overflow: hidden;
235    padding: 0;
236    text-overflow: ellipsis
237}
238
239:link,:visited {
240    color: #039be5;
241    outline: 0;
242    text-decoration: none
243}
244
245body,html {
246    color: #202124;
247    font: 400 16px/24px Roboto,sans-serif;
248    -moz-osx-font-smoothing: grayscale;
249    -webkit-font-smoothing: antialiased;
250    height: 100%;
251    margin: 0;
252    -webkit-text-size-adjust: 100%;
253    -moz-text-size-adjust: 100%;
254    -ms-text-size-adjust: 100%;
255    text-size-adjust: 100%
256}
257
258html {
259    -webkit-box-sizing: border-box;
260    box-sizing: border-box
261}
262
263*,*::before,*::after {
264    -webkit-box-sizing: inherit;
265    box-sizing: inherit
266}
267
268body,div,dl,dd,form,img,input,figure,menu {
269    margin: 0;
270    padding: 0
271}
272</style>
273{{template "copyBaseUrl"}}
274</head>
275<body>
276<div id="main">
277<H1>Soong Modules Reference</H1>
278The latest versions of Android use the Soong build system, which greatly simplifies build
279configuration over the previous Make-based system. This site contains the generated reference
280files for the Soong build system.
281
282<table class="module_types" summary="Table of Soong module types sorted by package">
283  <thead>
284    <tr>
285      <th style="width:20%">Package</th>
286      <th style="width:80%">Module types</th>
287    </tr>
288  </thead>
289  <tbody>
290    {{range $pkg := .}}
291      <tr>
292        <td>{{.Path}}</td>
293        <td>
294        {{range $i, $mod := .ModuleTypes}}{{if $i}}, {{end}}<a href="{{$pkg.Name}}.html#{{$mod.Name}}">{{$mod.Name}}</a>{{end}}
295        </td>
296      </tr>
297    {{end}}
298  </tbody>
299</table>
300</div>
301</body>
302</html>
303`
304
305	perPackageTemplate = `
306<html>
307<head>
308<title>Build Docs</title>
309<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css">
310<style>
311.accordion,.simple{margin-left:1.5em;text-indent:-1.5em;margin-top:.25em}
312.collapsible{border-width:0 0 0 1;margin-left:.25em;padding-left:.25em;border-style:solid;
313  border-color:grey;display:none;}
314span.fixed{display: block; float: left; clear: left; width: 1em;}
315ul {
316	list-style-type: none;
317  margin: 0;
318  padding: 0;
319  width: 30ch;
320  background-color: #f1f1f1;
321  position: fixed;
322  height: 100%;
323  overflow: auto;
324}
325li a {
326  display: block;
327  color: #000;
328  padding: 8px 16px;
329  text-decoration: none;
330}
331
332li a.active {
333  background-color: #4CAF50;
334  color: white;
335}
336
337li a:hover:not(.active) {
338  background-color: #555;
339  color: white;
340}
341</style>
342{{template "copyBaseUrl"}}
343</head>
344<body>
345{{- /* Fixed sidebar with module types */ -}}
346<ul>
347<li><h3>{{.Name}} package</h3></li>
348{{range $moduleType := .Modules}}<li><a href="{{$.Name}}.html#{{$moduleType.Name}}">{{$moduleType.Name}}</a></li>
349{{end -}}
350</ul>
351{{/* Main panel with H1 section per module type */}}
352<div style="margin-left:30ch;padding:1px 16px;">
353{{range $moduleType := .Modules}}
354	{{setModule $moduleType.Name}}
355	<p>
356  <h2 id="{{$moduleType.Name}}">{{$moduleType.Name}}</h2>
357  {{if $moduleType.Synopsis }}{{$moduleType.Synopsis}}{{else}}<i>Missing synopsis</i>{{end}}
358  {{- /* Comma-separated list of module attributes' links module attributes */ -}}
359	<div class="breadcrumb">
360    {{range $i,$prop := $moduleType.Properties }}
361				{{ if gt $i 0 }},&nbsp;{{end -}}
362				<a href={{$.Name}}.html#{{getModule}}.{{$prop.Name}}>{{$prop.Name}}</a>
363		{{- end -}}
364  </div>
365	{{- /* Property description */ -}}
366	{{- template "properties" $moduleType.Properties -}}
367{{- end -}}
368
369{{define "properties" -}}
370  {{range .}}
371    {{if .Properties -}}
372      <div class="accordion"  id="{{getModule}}.{{.Name}}">
373        <span class="fixed">&#x2295</span><b>{{.Name}}</b>
374        {{- range .OtherNames -}}, {{.}}{{- end -}}
375      </div>
376      <div class="collapsible">
377        {{- .Text}} {{range .OtherTexts}}{{.}}{{end}}
378        {{template "properties" .Properties -}}
379      </div>
380    {{- else -}}
381      <div class="simple" id="{{getModule}}.{{.Name}}">
382        <span class="fixed">&nbsp;</span><b>{{.Name}} {{range .OtherNames}}, {{.}}{{end -}}</b>
383        <i>{{.Type}}</i>
384        {{- if .Text -}}{{if ne .Text "\n"}}, {{end}}{{.Text}}{{- end -}}
385        {{- with .OtherTexts -}}{{.}}{{- end -}}
386	{{- if .Default -}}<i>Default: {{.Default}}</i>{{- end -}}
387      </div>
388    {{- end}}
389  {{- end -}}
390{{- end -}}
391</div>
392<script>
393  accordions = document.getElementsByClassName('accordion');
394  for (i=0; i < accordions.length; ++i) {
395    accordions[i].addEventListener("click", function() {
396      var panel = this.nextElementSibling;
397      var child = this.firstElementChild;
398      if (panel.style.display === "block") {
399          panel.style.display = "none";
400          child.textContent = '\u2295';
401      } else {
402          panel.style.display = "block";
403          child.textContent = '\u2296';
404      }
405    });
406  }
407</script>
408</body>
409`
410
411	copyBaseUrl = `
412{{define "copyBaseUrl"}}
413<script type="text/javascript">
414window.addEventListener('message', (e) => {
415  if (e != null && e.data != null && e.data.type === "SET_BASE" && e.data.base != null) {
416    const existingBase = document.querySelector('base');
417    if (existingBase != null) {
418      existingBase.parentElement.removeChild(existingBase);
419    }
420
421    const base = document.createElement('base');
422    base.setAttribute('href', e.data.base);
423    document.head.appendChild(base);
424  }
425});
426</script>
427{{end}}
428`
429
430	keywordsTemplate = `
431{{range $moduleType := .Modules}}{{$moduleType.Name}}:{{range $property := $moduleType.Properties}}{{$property.Name}},{{end}}
432{{end}}
433`
434)
435