xref: /aosp_15_r20/external/perfetto/infra/perfetto.dev/src/markdown_render.js (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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/main/+/main:external/perfetto';
24
25const ROOT_DIR = path.dirname(path.dirname(path.dirname(__dirname)));
26
27let outDir = '';
28let curMdFile = '';
29let title = '';
30let depFileFd = undefined;
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    if (depFileFd) {
136      fs.write(depFileFd, ` ${ROOT_DIR + docsHref}`);
137    }
138  }
139  if (href.endsWith('.svg')) {
140    return `<object type="image/svg+xml" data="${href}"></object>`
141  }
142  return originalImgFn(href, title, text);
143}
144
145function renderParagraph(text) {
146  let cssClass = '';
147  if (text.startsWith('NOTE:')) {
148    cssClass = 'note';
149  }
150   else if (text.startsWith('TIP:')) {
151    cssClass = 'tip';
152  }
153   else if (text.startsWith('TODO:') || text.startsWith('FIXME:')) {
154    cssClass = 'todo';
155  }
156   else if (text.startsWith('WARNING:')) {
157    cssClass = 'warning';
158  }
159   else if (text.startsWith('Summary:')) {
160    cssClass = 'summary';
161  }
162  if (cssClass != '') {
163    cssClass = ` class="callout ${cssClass}"`;
164  }
165
166  // Rudimentary support of definition lists.
167  var colonStart = text.search("\n:")
168  if (colonStart != -1) {
169    var key = text.substring(0, colonStart);
170    var value = text.substring(colonStart + 2);
171    return `<dl><dt><p>${key}</p></dt><dd><p>${value}</p></dd></dl>`
172  }
173
174  return `<p${cssClass}>${text}</p>\n`;
175}
176
177function render(rawMarkdown) {
178  const renderer = new marked.Renderer();
179  const originalLinkFn = renderer.link.bind(renderer);
180  const originalImgFn = renderer.image.bind(renderer);
181  renderer.link = (hr, ti, te) => renderLink(originalLinkFn, hr, ti, te);
182  renderer.image = (hr, ti, te) => renderImage(originalImgFn, hr, ti, te);
183  renderer.code = renderCode;
184  renderer.heading = renderHeading;
185  renderer.paragraph = renderParagraph;
186
187  return marked.marked.parse(rawMarkdown, {renderer: renderer});
188}
189
190function main() {
191  const inFile = argv['i'];
192  const outFile = argv['o'];
193  outDir = argv['odir'];
194  depFile = argv['depfile'];
195  const templateFile = argv['t'];
196  if (!outFile || !outDir) {
197    console.error(
198        'Usage: --odir site -o out.html ' +
199        '[-i input.md] [-t templ.html] ' +
200        '[--depfile depfile.d]');
201    process.exit(1);
202  }
203  curMdFile = inFile;
204
205  if (depFile) {
206    const depFileDir = path.dirname(depFile);
207    fs.ensureDirSync(depFileDir);
208    depFileFd = fs.openSync(depFile, 'w');
209    fs.write(depFileFd, `${outFile}:`);
210  }
211  let markdownHtml = '';
212  if (inFile) {
213    markdownHtml = render(fs.readFileSync(inFile, 'utf8'));
214  }
215
216  if (templateFile) {
217    // TODO rename nav.html to sitemap or something more mainstream.
218    const navFilePath = path.join(outDir, 'docs', '_nav.html');
219    const fallbackTitle =
220        'Perfetto - System profiling, app tracing and trace analysis';
221    const templateData = {
222      markdown: markdownHtml,
223      title: title ? `${title} - Perfetto Tracing Docs` : fallbackTitle,
224      fileName: '/' + path.relative(outDir, outFile),
225    };
226    if (fs.existsSync(navFilePath)) {
227      templateData['nav'] = fs.readFileSync(navFilePath, 'utf8');
228    }
229    ejs.renderFile(templateFile, templateData, (err, html) => {
230      if (err)
231        throw err;
232      fs.writeFileSync(outFile, html);
233      process.exit(0);
234    });
235  } else {
236    fs.writeFileSync(outFile, markdownHtml);
237    process.exit(0);
238  }
239}
240
241main();
242