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