• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2020 The Android Open Source Project
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
15const ejs = require('ejs');
16const marked = require('marked');
17const argv = require('yargs').argv
18const fs = require('fs-extra');
19const path = require('path');
20const hljs = require('highlight.js');
21
22const CS_BASE_URL =
23    'https://cs.android.com/android/platform/superproject/+/master:external/perfetto';
24
25const ROOT_DIR = path.dirname(path.dirname(path.dirname(__dirname)));
26const DOCS_DIR = path.join(ROOT_DIR, 'docs');
27
28let outDir = '';
29let curMdFile = '';
30let title = '';
31
32function hrefInDocs(href) {
33  if (href.match(/^(https?:)|^(mailto:)|^#/)) {
34    return undefined;
35  }
36  let pathFromRoot;
37  if (href.startsWith('/')) {
38    pathFromRoot = href;
39  } else {
40    curDocDir = '/' + path.relative(ROOT_DIR, path.dirname(curMdFile));
41    pathFromRoot = path.join(curDocDir, href);
42  }
43  if (pathFromRoot.startsWith('/docs/')) {
44    return pathFromRoot;
45  }
46  return undefined;
47}
48
49function assertNoDeadLink(relPathFromRoot) {
50  relPathFromRoot = relPathFromRoot.replace(/\#.*$/g, '');  // Remove #line.
51
52  // Skip check for build-time generated reference pages.
53  if (relPathFromRoot.endsWith('.autogen'))
54    return;
55
56  const fullPath = path.join(ROOT_DIR, relPathFromRoot);
57  if (!fs.existsSync(fullPath) && !fs.existsSync(fullPath + '.md')) {
58    const msg = `Dead link: ${relPathFromRoot} in ${curMdFile}`;
59    console.error(msg);
60    throw new Error(msg);
61  }
62}
63
64function renderHeading(text, level) {
65  // If the heading has an explicit ${#anchor}, use that. Otherwise infer the
66  // anchor from the text but only for h2 and h3. Note the right-hand-side TOC
67  // is dynamically generated from anchors (explicit or implicit).
68  if (level === 1 && !title) {
69    title = text;
70  }
71  let anchorId = '';
72  const explicitAnchor = /{#([\w-_.]+)}/.exec(text);
73  if (explicitAnchor) {
74    text = text.replace(explicitAnchor[0], '');
75    anchorId = explicitAnchor[1];
76  } else if (level >= 2 && level <= 3) {
77    anchorId = text.toLowerCase().replace(/[^\w]+/g, '-');
78    anchorId = anchorId.replace(/[-]+/g, '-');  // Drop consecutive '-'s.
79  }
80  let anchor = '';
81  if (anchorId) {
82    anchor = `<a name="${anchorId}" class="anchor" href="#${anchorId}"></a>`;
83  }
84  return `<h${level}>${anchor}${text}</h${level}>`;
85}
86
87function renderLink(originalLinkFn, href, title, text) {
88  if (href.startsWith('../')) {
89    throw new Error(
90        `Don\'t use relative paths in docs, always use /docs/xxx ` +
91        `or /src/xxx for both links to docs and code (${href})`)
92  }
93  const docsHref = hrefInDocs(href);
94  let sourceCodeLink = undefined;
95  if (docsHref !== undefined) {
96    // Check that the target doc exists. Skip the check on /reference/ files
97    // that are typically generated at build time.
98    assertNoDeadLink(docsHref);
99    href = docsHref.replace(/[.](md|autogen)\b/, '');
100    href = href.replace(/\/README$/, '/');
101  } else if (href.startsWith('/') && !href.startsWith('//')) {
102    // /tools/xxx -> github/tools/xxx.
103    sourceCodeLink = href;
104  }
105  if (sourceCodeLink !== undefined) {
106    // Fix up line anchors for GitHub link: #42 -> #L42.
107    sourceCodeLink = sourceCodeLink.replace(/#(\d+)$/g, '#L$1')
108    assertNoDeadLink(sourceCodeLink);
109    href = CS_BASE_URL + sourceCodeLink;
110  }
111  return originalLinkFn(href, title, text);
112}
113
114function renderCode(text, lang) {
115  if (lang === 'mermaid') {
116    return `<div class="mermaid">${text}</div>`;
117  }
118
119  let hlHtml = '';
120  if (lang) {
121    hlHtml = hljs.highlight(lang, text).value
122  } else {
123    hlHtml = hljs.highlightAuto(text).value
124  }
125  return `<code class="hljs code-block">${hlHtml}</code>`
126}
127
128function renderImage(originalImgFn, href, title, text) {
129  const docsHref = hrefInDocs(href);
130  if (docsHref !== undefined) {
131    const outFile = outDir + docsHref;
132    const outParDir = path.dirname(outFile);
133    fs.ensureDirSync(outParDir);
134    fs.copyFileSync(ROOT_DIR + docsHref, outFile);
135  }
136  if (href.endsWith('.svg')) {
137    return `<object type="image/svg+xml" data="${href}"></object>`
138  }
139  return originalImgFn(href, title, text);
140}
141
142function renderParagraph(text) {
143  let cssClass = '';
144  if (text.startsWith('NOTE:')) {
145    cssClass = 'note';
146  }
147   else if (text.startsWith('TIP:')) {
148    cssClass = 'tip';
149  }
150   else if (text.startsWith('TODO:') || text.startsWith('FIXME:')) {
151    cssClass = 'todo';
152  }
153   else if (text.startsWith('WARNING:')) {
154    cssClass = 'warning';
155  }
156   else if (text.startsWith('Summary:')) {
157    cssClass = 'summary';
158  }
159  if (cssClass != '') {
160    cssClass = ` class="callout ${cssClass}"`;
161  }
162  return `<p${cssClass}>${text}</p>\n`;
163}
164
165function render(rawMarkdown) {
166  const renderer = new marked.Renderer();
167  const originalLinkFn = renderer.link.bind(renderer);
168  const originalImgFn = renderer.image.bind(renderer);
169  renderer.link = (hr, ti, te) => renderLink(originalLinkFn, hr, ti, te);
170  renderer.image = (hr, ti, te) => renderImage(originalImgFn, hr, ti, te);
171  renderer.code = renderCode;
172  renderer.heading = renderHeading;
173  renderer.paragraph = renderParagraph;
174
175  return marked(rawMarkdown, {renderer: renderer});
176}
177
178function main() {
179  const inFile = argv['i'];
180  const outFile = argv['o'];
181  outDir = argv['odir'];
182  const templateFile = argv['t'];
183  if (!outFile || !outDir) {
184    console.error(
185        'Usage: --odir site -o out.html [-i input.md] [-t templ.html]');
186    process.exit(1);
187  }
188  curMdFile = inFile;
189
190  let markdownHtml = '';
191  if (inFile) {
192    markdownHtml = render(fs.readFileSync(inFile, 'utf8'));
193  }
194
195  if (templateFile) {
196    // TODO rename nav.html to sitemap or something more mainstream.
197    const navFilePath = path.join(outDir, 'docs', '_nav.html');
198    const fallbackTitle =
199        'Perfetto - System profiling, app tracing and trace analysis';
200    const templateData = {
201      markdown: markdownHtml,
202      title: title ? `${title} - Perfetto Tracing Docs` : fallbackTitle,
203      fileName: '/' + outFile.split('/').slice(1).join('/'),
204    };
205    if (fs.existsSync(navFilePath)) {
206      templateData['nav'] = fs.readFileSync(navFilePath, 'utf8');
207    }
208    ejs.renderFile(templateFile, templateData, (err, html) => {
209      if (err)
210        throw err;
211      fs.writeFileSync(outFile, html);
212      process.exit(0);
213    });
214  } else {
215    fs.writeFileSync(outFile, markdownHtml);
216    process.exit(0);
217  }
218}
219
220main();
221