infra: add perfetto.dev site
Merge the sources of the new website @ perfetto.dev
Change-Id: Ie3bc59fa3f42a41360bf07118d2ad8e12f409660
diff --git a/infra/perfetto.dev/src/markdown_render.js b/infra/perfetto.dev/src/markdown_render.js
new file mode 100644
index 0000000..ebb34d8
--- /dev/null
+++ b/infra/perfetto.dev/src/markdown_render.js
@@ -0,0 +1,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();