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/+/master:external/perfetto'; 24 25const ROOT_DIR = path.dirname(path.dirname(path.dirname(__dirname))); 26const DOCS_DIR = path.join(ROOT_DIR, 'docs'); 27 28let outDir = ''; 29let curMdFile = ''; 30let title = ''; 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 } 136 if (href.endsWith('.svg')) { 137 return `<object type="image/svg+xml" data="${href}"></object>` 138 } 139 return originalImgFn(href, title, text); 140} 141 142function renderParagraph(text) { 143 let cssClass = ''; 144 if (text.startsWith('NOTE:')) { 145 cssClass = 'note'; 146 } 147 else if (text.startsWith('TIP:')) { 148 cssClass = 'tip'; 149 } 150 else if (text.startsWith('TODO:') || text.startsWith('FIXME:')) { 151 cssClass = 'todo'; 152 } 153 else if (text.startsWith('WARNING:')) { 154 cssClass = 'warning'; 155 } 156 else if (text.startsWith('Summary:')) { 157 cssClass = 'summary'; 158 } 159 if (cssClass != '') { 160 cssClass = ` class="callout ${cssClass}"`; 161 } 162 return `<p${cssClass}>${text}</p>\n`; 163} 164 165function render(rawMarkdown) { 166 const renderer = new marked.Renderer(); 167 const originalLinkFn = renderer.link.bind(renderer); 168 const originalImgFn = renderer.image.bind(renderer); 169 renderer.link = (hr, ti, te) => renderLink(originalLinkFn, hr, ti, te); 170 renderer.image = (hr, ti, te) => renderImage(originalImgFn, hr, ti, te); 171 renderer.code = renderCode; 172 renderer.heading = renderHeading; 173 renderer.paragraph = renderParagraph; 174 175 return marked(rawMarkdown, {renderer: renderer}); 176} 177 178function main() { 179 const inFile = argv['i']; 180 const outFile = argv['o']; 181 outDir = argv['odir']; 182 const templateFile = argv['t']; 183 if (!outFile || !outDir) { 184 console.error( 185 'Usage: --odir site -o out.html [-i input.md] [-t templ.html]'); 186 process.exit(1); 187 } 188 curMdFile = inFile; 189 190 let markdownHtml = ''; 191 if (inFile) { 192 markdownHtml = render(fs.readFileSync(inFile, 'utf8')); 193 } 194 195 if (templateFile) { 196 // TODO rename nav.html to sitemap or something more mainstream. 197 const navFilePath = path.join(outDir, 'docs', '_nav.html'); 198 const fallbackTitle = 199 'Perfetto - System profiling, app tracing and trace analysis'; 200 const templateData = { 201 markdown: markdownHtml, 202 title: title ? `${title} - Perfetto Tracing Docs` : fallbackTitle, 203 fileName: '/' + outFile.split('/').slice(1).join('/'), 204 }; 205 if (fs.existsSync(navFilePath)) { 206 templateData['nav'] = fs.readFileSync(navFilePath, 'utf8'); 207 } 208 ejs.renderFile(templateFile, templateData, (err, html) => { 209 if (err) 210 throw err; 211 fs.writeFileSync(outFile, html); 212 process.exit(0); 213 }); 214 } else { 215 fs.writeFileSync(outFile, markdownHtml); 216 process.exit(0); 217 } 218} 219 220main(); 221