| // 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))); |
| |
| 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}"`; |
| } |
| |
| // Rudimentary support of definition lists. |
| var colonStart = text.search("\n:") |
| if (colonStart != -1) { |
| var key = text.substring(0, colonStart); |
| var value = text.substring(colonStart + 2); |
| return `<dl><dt><p>${key}</p></dt><dd><p>${value}</p></dd></dl>` |
| } |
| |
| 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: '/' + path.relative(outDir, outFile), |
| }; |
| 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(); |