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