xref: /aosp_15_r20/external/perfetto/infra/perfetto.dev/src/markdown_render.js (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2020 The Android Open Source Project
2*6dbdd20aSAndroid Build Coastguard Worker//
3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*6dbdd20aSAndroid Build Coastguard Worker//
7*6dbdd20aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*6dbdd20aSAndroid Build Coastguard Worker//
9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License.
14*6dbdd20aSAndroid Build Coastguard Worker
15*6dbdd20aSAndroid Build Coastguard Workerconst ejs = require('ejs');
16*6dbdd20aSAndroid Build Coastguard Workerconst marked = require('marked');
17*6dbdd20aSAndroid Build Coastguard Workerconst argv = require('yargs').argv
18*6dbdd20aSAndroid Build Coastguard Workerconst fs = require('fs-extra');
19*6dbdd20aSAndroid Build Coastguard Workerconst path = require('path');
20*6dbdd20aSAndroid Build Coastguard Workerconst hljs = require('highlight.js');
21*6dbdd20aSAndroid Build Coastguard Worker
22*6dbdd20aSAndroid Build Coastguard Workerconst CS_BASE_URL =
23*6dbdd20aSAndroid Build Coastguard Worker    'https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto';
24*6dbdd20aSAndroid Build Coastguard Worker
25*6dbdd20aSAndroid Build Coastguard Workerconst ROOT_DIR = path.dirname(path.dirname(path.dirname(__dirname)));
26*6dbdd20aSAndroid Build Coastguard Worker
27*6dbdd20aSAndroid Build Coastguard Workerlet outDir = '';
28*6dbdd20aSAndroid Build Coastguard Workerlet curMdFile = '';
29*6dbdd20aSAndroid Build Coastguard Workerlet title = '';
30*6dbdd20aSAndroid Build Coastguard Workerlet depFileFd = undefined;
31*6dbdd20aSAndroid Build Coastguard Worker
32*6dbdd20aSAndroid Build Coastguard Workerfunction hrefInDocs(href) {
33*6dbdd20aSAndroid Build Coastguard Worker  if (href.match(/^(https?:)|^(mailto:)|^#/)) {
34*6dbdd20aSAndroid Build Coastguard Worker    return undefined;
35*6dbdd20aSAndroid Build Coastguard Worker  }
36*6dbdd20aSAndroid Build Coastguard Worker  let pathFromRoot;
37*6dbdd20aSAndroid Build Coastguard Worker  if (href.startsWith('/')) {
38*6dbdd20aSAndroid Build Coastguard Worker    pathFromRoot = href;
39*6dbdd20aSAndroid Build Coastguard Worker  } else {
40*6dbdd20aSAndroid Build Coastguard Worker    curDocDir = '/' + path.relative(ROOT_DIR, path.dirname(curMdFile));
41*6dbdd20aSAndroid Build Coastguard Worker    pathFromRoot = path.join(curDocDir, href);
42*6dbdd20aSAndroid Build Coastguard Worker  }
43*6dbdd20aSAndroid Build Coastguard Worker  if (pathFromRoot.startsWith('/docs/')) {
44*6dbdd20aSAndroid Build Coastguard Worker    return pathFromRoot;
45*6dbdd20aSAndroid Build Coastguard Worker  }
46*6dbdd20aSAndroid Build Coastguard Worker  return undefined;
47*6dbdd20aSAndroid Build Coastguard Worker}
48*6dbdd20aSAndroid Build Coastguard Worker
49*6dbdd20aSAndroid Build Coastguard Workerfunction assertNoDeadLink(relPathFromRoot) {
50*6dbdd20aSAndroid Build Coastguard Worker  relPathFromRoot = relPathFromRoot.replace(/\#.*$/g, '');  // Remove #line.
51*6dbdd20aSAndroid Build Coastguard Worker
52*6dbdd20aSAndroid Build Coastguard Worker  // Skip check for build-time generated reference pages.
53*6dbdd20aSAndroid Build Coastguard Worker  if (relPathFromRoot.endsWith('.autogen'))
54*6dbdd20aSAndroid Build Coastguard Worker    return;
55*6dbdd20aSAndroid Build Coastguard Worker
56*6dbdd20aSAndroid Build Coastguard Worker  const fullPath = path.join(ROOT_DIR, relPathFromRoot);
57*6dbdd20aSAndroid Build Coastguard Worker  if (!fs.existsSync(fullPath) && !fs.existsSync(fullPath + '.md')) {
58*6dbdd20aSAndroid Build Coastguard Worker    const msg = `Dead link: ${relPathFromRoot} in ${curMdFile}`;
59*6dbdd20aSAndroid Build Coastguard Worker    console.error(msg);
60*6dbdd20aSAndroid Build Coastguard Worker    throw new Error(msg);
61*6dbdd20aSAndroid Build Coastguard Worker  }
62*6dbdd20aSAndroid Build Coastguard Worker}
63*6dbdd20aSAndroid Build Coastguard Worker
64*6dbdd20aSAndroid Build Coastguard Workerfunction renderHeading(text, level) {
65*6dbdd20aSAndroid Build Coastguard Worker  // If the heading has an explicit ${#anchor}, use that. Otherwise infer the
66*6dbdd20aSAndroid Build Coastguard Worker  // anchor from the text but only for h2 and h3. Note the right-hand-side TOC
67*6dbdd20aSAndroid Build Coastguard Worker  // is dynamically generated from anchors (explicit or implicit).
68*6dbdd20aSAndroid Build Coastguard Worker  if (level === 1 && !title) {
69*6dbdd20aSAndroid Build Coastguard Worker    title = text;
70*6dbdd20aSAndroid Build Coastguard Worker  }
71*6dbdd20aSAndroid Build Coastguard Worker  let anchorId = '';
72*6dbdd20aSAndroid Build Coastguard Worker  const explicitAnchor = /{#([\w-_.]+)}/.exec(text);
73*6dbdd20aSAndroid Build Coastguard Worker  if (explicitAnchor) {
74*6dbdd20aSAndroid Build Coastguard Worker    text = text.replace(explicitAnchor[0], '');
75*6dbdd20aSAndroid Build Coastguard Worker    anchorId = explicitAnchor[1];
76*6dbdd20aSAndroid Build Coastguard Worker  } else if (level >= 2 && level <= 3) {
77*6dbdd20aSAndroid Build Coastguard Worker    anchorId = text.toLowerCase().replace(/[^\w]+/g, '-');
78*6dbdd20aSAndroid Build Coastguard Worker    anchorId = anchorId.replace(/[-]+/g, '-');  // Drop consecutive '-'s.
79*6dbdd20aSAndroid Build Coastguard Worker  }
80*6dbdd20aSAndroid Build Coastguard Worker  let anchor = '';
81*6dbdd20aSAndroid Build Coastguard Worker  if (anchorId) {
82*6dbdd20aSAndroid Build Coastguard Worker    anchor = `<a name="${anchorId}" class="anchor" href="#${anchorId}"></a>`;
83*6dbdd20aSAndroid Build Coastguard Worker  }
84*6dbdd20aSAndroid Build Coastguard Worker  return `<h${level}>${anchor}${text}</h${level}>`;
85*6dbdd20aSAndroid Build Coastguard Worker}
86*6dbdd20aSAndroid Build Coastguard Worker
87*6dbdd20aSAndroid Build Coastguard Workerfunction renderLink(originalLinkFn, href, title, text) {
88*6dbdd20aSAndroid Build Coastguard Worker  if (href.startsWith('../')) {
89*6dbdd20aSAndroid Build Coastguard Worker    throw new Error(
90*6dbdd20aSAndroid Build Coastguard Worker        `Don\'t use relative paths in docs, always use /docs/xxx ` +
91*6dbdd20aSAndroid Build Coastguard Worker        `or /src/xxx for both links to docs and code (${href})`)
92*6dbdd20aSAndroid Build Coastguard Worker  }
93*6dbdd20aSAndroid Build Coastguard Worker  const docsHref = hrefInDocs(href);
94*6dbdd20aSAndroid Build Coastguard Worker  let sourceCodeLink = undefined;
95*6dbdd20aSAndroid Build Coastguard Worker  if (docsHref !== undefined) {
96*6dbdd20aSAndroid Build Coastguard Worker    // Check that the target doc exists. Skip the check on /reference/ files
97*6dbdd20aSAndroid Build Coastguard Worker    // that are typically generated at build time.
98*6dbdd20aSAndroid Build Coastguard Worker    assertNoDeadLink(docsHref);
99*6dbdd20aSAndroid Build Coastguard Worker    href = docsHref.replace(/[.](md|autogen)\b/, '');
100*6dbdd20aSAndroid Build Coastguard Worker    href = href.replace(/\/README$/, '/');
101*6dbdd20aSAndroid Build Coastguard Worker  } else if (href.startsWith('/') && !href.startsWith('//')) {
102*6dbdd20aSAndroid Build Coastguard Worker    // /tools/xxx -> github/tools/xxx.
103*6dbdd20aSAndroid Build Coastguard Worker    sourceCodeLink = href;
104*6dbdd20aSAndroid Build Coastguard Worker  }
105*6dbdd20aSAndroid Build Coastguard Worker  if (sourceCodeLink !== undefined) {
106*6dbdd20aSAndroid Build Coastguard Worker    // Fix up line anchors for GitHub link: #42 -> #L42.
107*6dbdd20aSAndroid Build Coastguard Worker    sourceCodeLink = sourceCodeLink.replace(/#(\d+)$/g, '#L$1')
108*6dbdd20aSAndroid Build Coastguard Worker    assertNoDeadLink(sourceCodeLink);
109*6dbdd20aSAndroid Build Coastguard Worker    href = CS_BASE_URL + sourceCodeLink;
110*6dbdd20aSAndroid Build Coastguard Worker  }
111*6dbdd20aSAndroid Build Coastguard Worker  return originalLinkFn(href, title, text);
112*6dbdd20aSAndroid Build Coastguard Worker}
113*6dbdd20aSAndroid Build Coastguard Worker
114*6dbdd20aSAndroid Build Coastguard Workerfunction renderCode(text, lang) {
115*6dbdd20aSAndroid Build Coastguard Worker  if (lang === 'mermaid') {
116*6dbdd20aSAndroid Build Coastguard Worker    return `<div class="mermaid">${text}</div>`;
117*6dbdd20aSAndroid Build Coastguard Worker  }
118*6dbdd20aSAndroid Build Coastguard Worker
119*6dbdd20aSAndroid Build Coastguard Worker  let hlHtml = '';
120*6dbdd20aSAndroid Build Coastguard Worker  if (lang) {
121*6dbdd20aSAndroid Build Coastguard Worker    hlHtml = hljs.highlight(lang, text).value
122*6dbdd20aSAndroid Build Coastguard Worker  } else {
123*6dbdd20aSAndroid Build Coastguard Worker    hlHtml = hljs.highlightAuto(text).value
124*6dbdd20aSAndroid Build Coastguard Worker  }
125*6dbdd20aSAndroid Build Coastguard Worker  return `<code class="hljs code-block">${hlHtml}</code>`
126*6dbdd20aSAndroid Build Coastguard Worker}
127*6dbdd20aSAndroid Build Coastguard Worker
128*6dbdd20aSAndroid Build Coastguard Workerfunction renderImage(originalImgFn, href, title, text) {
129*6dbdd20aSAndroid Build Coastguard Worker  const docsHref = hrefInDocs(href);
130*6dbdd20aSAndroid Build Coastguard Worker  if (docsHref !== undefined) {
131*6dbdd20aSAndroid Build Coastguard Worker    const outFile = outDir + docsHref;
132*6dbdd20aSAndroid Build Coastguard Worker    const outParDir = path.dirname(outFile);
133*6dbdd20aSAndroid Build Coastguard Worker    fs.ensureDirSync(outParDir);
134*6dbdd20aSAndroid Build Coastguard Worker    fs.copyFileSync(ROOT_DIR + docsHref, outFile);
135*6dbdd20aSAndroid Build Coastguard Worker    if (depFileFd) {
136*6dbdd20aSAndroid Build Coastguard Worker      fs.write(depFileFd, ` ${ROOT_DIR + docsHref}`);
137*6dbdd20aSAndroid Build Coastguard Worker    }
138*6dbdd20aSAndroid Build Coastguard Worker  }
139*6dbdd20aSAndroid Build Coastguard Worker  if (href.endsWith('.svg')) {
140*6dbdd20aSAndroid Build Coastguard Worker    return `<object type="image/svg+xml" data="${href}"></object>`
141*6dbdd20aSAndroid Build Coastguard Worker  }
142*6dbdd20aSAndroid Build Coastguard Worker  return originalImgFn(href, title, text);
143*6dbdd20aSAndroid Build Coastguard Worker}
144*6dbdd20aSAndroid Build Coastguard Worker
145*6dbdd20aSAndroid Build Coastguard Workerfunction renderParagraph(text) {
146*6dbdd20aSAndroid Build Coastguard Worker  let cssClass = '';
147*6dbdd20aSAndroid Build Coastguard Worker  if (text.startsWith('NOTE:')) {
148*6dbdd20aSAndroid Build Coastguard Worker    cssClass = 'note';
149*6dbdd20aSAndroid Build Coastguard Worker  }
150*6dbdd20aSAndroid Build Coastguard Worker   else if (text.startsWith('TIP:')) {
151*6dbdd20aSAndroid Build Coastguard Worker    cssClass = 'tip';
152*6dbdd20aSAndroid Build Coastguard Worker  }
153*6dbdd20aSAndroid Build Coastguard Worker   else if (text.startsWith('TODO:') || text.startsWith('FIXME:')) {
154*6dbdd20aSAndroid Build Coastguard Worker    cssClass = 'todo';
155*6dbdd20aSAndroid Build Coastguard Worker  }
156*6dbdd20aSAndroid Build Coastguard Worker   else if (text.startsWith('WARNING:')) {
157*6dbdd20aSAndroid Build Coastguard Worker    cssClass = 'warning';
158*6dbdd20aSAndroid Build Coastguard Worker  }
159*6dbdd20aSAndroid Build Coastguard Worker   else if (text.startsWith('Summary:')) {
160*6dbdd20aSAndroid Build Coastguard Worker    cssClass = 'summary';
161*6dbdd20aSAndroid Build Coastguard Worker  }
162*6dbdd20aSAndroid Build Coastguard Worker  if (cssClass != '') {
163*6dbdd20aSAndroid Build Coastguard Worker    cssClass = ` class="callout ${cssClass}"`;
164*6dbdd20aSAndroid Build Coastguard Worker  }
165*6dbdd20aSAndroid Build Coastguard Worker
166*6dbdd20aSAndroid Build Coastguard Worker  // Rudimentary support of definition lists.
167*6dbdd20aSAndroid Build Coastguard Worker  var colonStart = text.search("\n:")
168*6dbdd20aSAndroid Build Coastguard Worker  if (colonStart != -1) {
169*6dbdd20aSAndroid Build Coastguard Worker    var key = text.substring(0, colonStart);
170*6dbdd20aSAndroid Build Coastguard Worker    var value = text.substring(colonStart + 2);
171*6dbdd20aSAndroid Build Coastguard Worker    return `<dl><dt><p>${key}</p></dt><dd><p>${value}</p></dd></dl>`
172*6dbdd20aSAndroid Build Coastguard Worker  }
173*6dbdd20aSAndroid Build Coastguard Worker
174*6dbdd20aSAndroid Build Coastguard Worker  return `<p${cssClass}>${text}</p>\n`;
175*6dbdd20aSAndroid Build Coastguard Worker}
176*6dbdd20aSAndroid Build Coastguard Worker
177*6dbdd20aSAndroid Build Coastguard Workerfunction render(rawMarkdown) {
178*6dbdd20aSAndroid Build Coastguard Worker  const renderer = new marked.Renderer();
179*6dbdd20aSAndroid Build Coastguard Worker  const originalLinkFn = renderer.link.bind(renderer);
180*6dbdd20aSAndroid Build Coastguard Worker  const originalImgFn = renderer.image.bind(renderer);
181*6dbdd20aSAndroid Build Coastguard Worker  renderer.link = (hr, ti, te) => renderLink(originalLinkFn, hr, ti, te);
182*6dbdd20aSAndroid Build Coastguard Worker  renderer.image = (hr, ti, te) => renderImage(originalImgFn, hr, ti, te);
183*6dbdd20aSAndroid Build Coastguard Worker  renderer.code = renderCode;
184*6dbdd20aSAndroid Build Coastguard Worker  renderer.heading = renderHeading;
185*6dbdd20aSAndroid Build Coastguard Worker  renderer.paragraph = renderParagraph;
186*6dbdd20aSAndroid Build Coastguard Worker
187*6dbdd20aSAndroid Build Coastguard Worker  return marked.marked.parse(rawMarkdown, {renderer: renderer});
188*6dbdd20aSAndroid Build Coastguard Worker}
189*6dbdd20aSAndroid Build Coastguard Worker
190*6dbdd20aSAndroid Build Coastguard Workerfunction main() {
191*6dbdd20aSAndroid Build Coastguard Worker  const inFile = argv['i'];
192*6dbdd20aSAndroid Build Coastguard Worker  const outFile = argv['o'];
193*6dbdd20aSAndroid Build Coastguard Worker  outDir = argv['odir'];
194*6dbdd20aSAndroid Build Coastguard Worker  depFile = argv['depfile'];
195*6dbdd20aSAndroid Build Coastguard Worker  const templateFile = argv['t'];
196*6dbdd20aSAndroid Build Coastguard Worker  if (!outFile || !outDir) {
197*6dbdd20aSAndroid Build Coastguard Worker    console.error(
198*6dbdd20aSAndroid Build Coastguard Worker        'Usage: --odir site -o out.html ' +
199*6dbdd20aSAndroid Build Coastguard Worker        '[-i input.md] [-t templ.html] ' +
200*6dbdd20aSAndroid Build Coastguard Worker        '[--depfile depfile.d]');
201*6dbdd20aSAndroid Build Coastguard Worker    process.exit(1);
202*6dbdd20aSAndroid Build Coastguard Worker  }
203*6dbdd20aSAndroid Build Coastguard Worker  curMdFile = inFile;
204*6dbdd20aSAndroid Build Coastguard Worker
205*6dbdd20aSAndroid Build Coastguard Worker  if (depFile) {
206*6dbdd20aSAndroid Build Coastguard Worker    const depFileDir = path.dirname(depFile);
207*6dbdd20aSAndroid Build Coastguard Worker    fs.ensureDirSync(depFileDir);
208*6dbdd20aSAndroid Build Coastguard Worker    depFileFd = fs.openSync(depFile, 'w');
209*6dbdd20aSAndroid Build Coastguard Worker    fs.write(depFileFd, `${outFile}:`);
210*6dbdd20aSAndroid Build Coastguard Worker  }
211*6dbdd20aSAndroid Build Coastguard Worker  let markdownHtml = '';
212*6dbdd20aSAndroid Build Coastguard Worker  if (inFile) {
213*6dbdd20aSAndroid Build Coastguard Worker    markdownHtml = render(fs.readFileSync(inFile, 'utf8'));
214*6dbdd20aSAndroid Build Coastguard Worker  }
215*6dbdd20aSAndroid Build Coastguard Worker
216*6dbdd20aSAndroid Build Coastguard Worker  if (templateFile) {
217*6dbdd20aSAndroid Build Coastguard Worker    // TODO rename nav.html to sitemap or something more mainstream.
218*6dbdd20aSAndroid Build Coastguard Worker    const navFilePath = path.join(outDir, 'docs', '_nav.html');
219*6dbdd20aSAndroid Build Coastguard Worker    const fallbackTitle =
220*6dbdd20aSAndroid Build Coastguard Worker        'Perfetto - System profiling, app tracing and trace analysis';
221*6dbdd20aSAndroid Build Coastguard Worker    const templateData = {
222*6dbdd20aSAndroid Build Coastguard Worker      markdown: markdownHtml,
223*6dbdd20aSAndroid Build Coastguard Worker      title: title ? `${title} - Perfetto Tracing Docs` : fallbackTitle,
224*6dbdd20aSAndroid Build Coastguard Worker      fileName: '/' + path.relative(outDir, outFile),
225*6dbdd20aSAndroid Build Coastguard Worker    };
226*6dbdd20aSAndroid Build Coastguard Worker    if (fs.existsSync(navFilePath)) {
227*6dbdd20aSAndroid Build Coastguard Worker      templateData['nav'] = fs.readFileSync(navFilePath, 'utf8');
228*6dbdd20aSAndroid Build Coastguard Worker    }
229*6dbdd20aSAndroid Build Coastguard Worker    ejs.renderFile(templateFile, templateData, (err, html) => {
230*6dbdd20aSAndroid Build Coastguard Worker      if (err)
231*6dbdd20aSAndroid Build Coastguard Worker        throw err;
232*6dbdd20aSAndroid Build Coastguard Worker      fs.writeFileSync(outFile, html);
233*6dbdd20aSAndroid Build Coastguard Worker      process.exit(0);
234*6dbdd20aSAndroid Build Coastguard Worker    });
235*6dbdd20aSAndroid Build Coastguard Worker  } else {
236*6dbdd20aSAndroid Build Coastguard Worker    fs.writeFileSync(outFile, markdownHtml);
237*6dbdd20aSAndroid Build Coastguard Worker    process.exit(0);
238*6dbdd20aSAndroid Build Coastguard Worker  }
239*6dbdd20aSAndroid Build Coastguard Worker}
240*6dbdd20aSAndroid Build Coastguard Worker
241*6dbdd20aSAndroid Build Coastguard Workermain();
242