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