aboutsummaryrefslogtreecommitdiff
path: root/infra/perfetto.dev/src/markdown_render.js
blob: ebb34d8667dd974f8bd46554a718ac79e5a61ab8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// Copyright (C) 2020 The Android Open Source Project
//
// 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.

const ejs = require('ejs');
const marked = require('marked');
const argv = require('yargs').argv
const fs = require('fs-extra');
const path = require('path');
const hljs = require('highlight.js');

const CS_BASE_URL =
    'https://cs.android.com/android/platform/superproject/+/master:external/perfetto';

const ROOT_DIR = path.dirname(path.dirname(path.dirname(__dirname)));
const DOCS_DIR = path.join(ROOT_DIR, 'docs');

let outDir = '';
let curMdFile = '';
let title = '';

function hrefInDocs(href) {
  if (href.match(/^(https?:)|^(mailto:)|^#/)) {
    return undefined;
  }
  let pathFromRoot;
  if (href.startsWith('/')) {
    pathFromRoot = href;
  } else {
    curDocDir = '/' + path.relative(ROOT_DIR, path.dirname(curMdFile));
    pathFromRoot = path.join(curDocDir, href);
  }
  if (pathFromRoot.startsWith('/docs/')) {
    return pathFromRoot;
  }
  return undefined;
}

function assertNoDeadLink(relPathFromRoot) {
  relPathFromRoot = relPathFromRoot.replace(/\#.*$/g, '');  // Remove #line.

  // Skip check for build-time generated reference pages.
  if (relPathFromRoot.endsWith('.autogen'))
    return;

  const fullPath = path.join(ROOT_DIR, relPathFromRoot);
  if (!fs.existsSync(fullPath) && !fs.existsSync(fullPath + '.md')) {
    const msg = `Dead link: ${relPathFromRoot} in ${curMdFile}`;
    console.error(msg);
    throw new Error(msg);
  }
}

function renderHeading(text, level) {
  // If the heading has an explicit ${#anchor}, use that. Otherwise infer the
  // anchor from the text but only for h2 and h3. Note the right-hand-side TOC
  // is dynamically generated from anchors (explicit or implicit).
  if (level === 1 && !title) {
    title = text;
  }
  let anchorId = '';
  const explicitAnchor = /{#([\w-_.]+)}/.exec(text);
  if (explicitAnchor) {
    text = text.replace(explicitAnchor[0], '');
    anchorId = explicitAnchor[1];
  } else if (level >= 2 && level <= 3) {
    anchorId = text.toLowerCase().replace(/[^\w]+/g, '-');
    anchorId = anchorId.replace(/[-]+/g, '-');  // Drop consecutive '-'s.
  }
  let anchor = '';
  if (anchorId) {
    anchor = `<a name="${anchorId}" class="anchor" href="#${anchorId}"></a>`;
  }
  return `<h${level}>${anchor}${text}</h${level}>`;
}

function renderLink(originalLinkFn, href, title, text) {
  if (href.startsWith('../')) {
    throw new Error(
        `Don\'t use relative paths in docs, always use /docs/xxx ` +
        `or /src/xxx for both links to docs and code (${href})`)
  }
  const docsHref = hrefInDocs(href);
  let sourceCodeLink = undefined;
  if (docsHref !== undefined) {
    // Check that the target doc exists. Skip the check on /reference/ files
    // that are typically generated at build time.
    assertNoDeadLink(docsHref);
    href = docsHref.replace(/[.](md|autogen)\b/, '');
    href = href.replace(/\/README$/, '/');
  } else if (href.startsWith('/') && !href.startsWith('//')) {
    // /tools/xxx -> github/tools/xxx.
    sourceCodeLink = href;
  }
  if (sourceCodeLink !== undefined) {
    // Fix up line anchors for GitHub link: #42 -> #L42.
    sourceCodeLink = sourceCodeLink.replace(/#(\d+)$/g, '#L$1')
    assertNoDeadLink(sourceCodeLink);
    href = CS_BASE_URL + sourceCodeLink;
  }
  return originalLinkFn(href, title, text);
}

function renderCode(text, lang) {
  if (lang === 'mermaid') {
    return `<div class="mermaid">${text}</div>`;
  }

  let hlHtml = '';
  if (lang) {
    hlHtml = hljs.highlight(lang, text).value
  } else {
    hlHtml = hljs.highlightAuto(text).value
  }
  return `<code class="hljs code-block">${hlHtml}</code>`
}

function renderImage(originalImgFn, href, title, text) {
  const docsHref = hrefInDocs(href);
  if (docsHref !== undefined) {
    const outFile = outDir + docsHref;
    const outParDir = path.dirname(outFile);
    fs.ensureDirSync(outParDir);
    fs.copyFileSync(ROOT_DIR + docsHref, outFile);
  }
  if (href.endsWith('.svg')) {
    return `<object type="image/svg+xml" data="${href}"></object>`
  }
  return originalImgFn(href, title, text);
}

function renderParagraph(text) {
  let cssClass = '';
  if (text.startsWith('NOTE:')) {
    cssClass = 'note';
  }
   else if (text.startsWith('TIP:')) {
    cssClass = 'tip';
  }
   else if (text.startsWith('TODO:') || text.startsWith('FIXME:')) {
    cssClass = 'todo';
  }
   else if (text.startsWith('WARNING:')) {
    cssClass = 'warning';
  }
   else if (text.startsWith('Summary:')) {
    cssClass = 'summary';
  }
  if (cssClass != '') {
    cssClass = ` class="callout ${cssClass}"`;
  }
  return `<p${cssClass}>${text}</p>\n`;
}

function render(rawMarkdown) {
  const renderer = new marked.Renderer();
  const originalLinkFn = renderer.link.bind(renderer);
  const originalImgFn = renderer.image.bind(renderer);
  renderer.link = (hr, ti, te) => renderLink(originalLinkFn, hr, ti, te);
  renderer.image = (hr, ti, te) => renderImage(originalImgFn, hr, ti, te);
  renderer.code = renderCode;
  renderer.heading = renderHeading;
  renderer.paragraph = renderParagraph;

  return marked(rawMarkdown, {renderer: renderer});
}

function main() {
  const inFile = argv['i'];
  const outFile = argv['o'];
  outDir = argv['odir'];
  const templateFile = argv['t'];
  if (!outFile || !outDir) {
    console.error(
        'Usage: --odir site -o out.html [-i input.md] [-t templ.html]');
    process.exit(1);
  }
  curMdFile = inFile;

  let markdownHtml = '';
  if (inFile) {
    markdownHtml = render(fs.readFileSync(inFile, 'utf8'));
  }

  if (templateFile) {
    // TODO rename nav.html to sitemap or something more mainstream.
    const navFilePath = path.join(outDir, 'docs', '_nav.html');
    const fallbackTitle =
        'Perfetto - System profiling, app tracing and trace analysis';
    const templateData = {
      markdown: markdownHtml,
      title: title ? `${title} - Perfetto Tracing Docs` : fallbackTitle,
      fileName: '/' + outFile.split('/').slice(1).join('/'),
    };
    if (fs.existsSync(navFilePath)) {
      templateData['nav'] = fs.readFileSync(navFilePath, 'utf8');
    }
    ejs.renderFile(templateFile, templateData, (err, html) => {
      if (err)
        throw err;
      fs.writeFileSync(outFile, html);
      process.exit(0);
    });
  } else {
    fs.writeFileSync(outFile, markdownHtml);
    process.exit(0);
  }
}

main();