blob: 8fe5cc9fc87f72d34c0fd11b6a5115584ec7680f [file] [log] [blame]
Primiano Tuccie70df3f2020-05-21 19:48:08 +01001// 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)));
Primiano Tuccie70df3f2020-05-21 19:48:08 +010026
27let outDir = '';
28let curMdFile = '';
29let title = '';
30
31function hrefInDocs(href) {
32 if (href.match(/^(https?:)|^(mailto:)|^#/)) {
33 return undefined;
34 }
35 let pathFromRoot;
36 if (href.startsWith('/')) {
37 pathFromRoot = href;
38 } else {
39 curDocDir = '/' + path.relative(ROOT_DIR, path.dirname(curMdFile));
40 pathFromRoot = path.join(curDocDir, href);
41 }
42 if (pathFromRoot.startsWith('/docs/')) {
43 return pathFromRoot;
44 }
45 return undefined;
46}
47
48function assertNoDeadLink(relPathFromRoot) {
49 relPathFromRoot = relPathFromRoot.replace(/\#.*$/g, ''); // Remove #line.
50
51 // Skip check for build-time generated reference pages.
52 if (relPathFromRoot.endsWith('.autogen'))
53 return;
54
55 const fullPath = path.join(ROOT_DIR, relPathFromRoot);
56 if (!fs.existsSync(fullPath) && !fs.existsSync(fullPath + '.md')) {
57 const msg = `Dead link: ${relPathFromRoot} in ${curMdFile}`;
58 console.error(msg);
59 throw new Error(msg);
60 }
61}
62
63function renderHeading(text, level) {
64 // If the heading has an explicit ${#anchor}, use that. Otherwise infer the
65 // anchor from the text but only for h2 and h3. Note the right-hand-side TOC
66 // is dynamically generated from anchors (explicit or implicit).
67 if (level === 1 && !title) {
68 title = text;
69 }
70 let anchorId = '';
71 const explicitAnchor = /{#([\w-_.]+)}/.exec(text);
72 if (explicitAnchor) {
73 text = text.replace(explicitAnchor[0], '');
74 anchorId = explicitAnchor[1];
75 } else if (level >= 2 && level <= 3) {
76 anchorId = text.toLowerCase().replace(/[^\w]+/g, '-');
77 anchorId = anchorId.replace(/[-]+/g, '-'); // Drop consecutive '-'s.
78 }
79 let anchor = '';
80 if (anchorId) {
81 anchor = `<a name="${anchorId}" class="anchor" href="#${anchorId}"></a>`;
82 }
83 return `<h${level}>${anchor}${text}</h${level}>`;
84}
85
86function renderLink(originalLinkFn, href, title, text) {
87 if (href.startsWith('../')) {
88 throw new Error(
89 `Don\'t use relative paths in docs, always use /docs/xxx ` +
90 `or /src/xxx for both links to docs and code (${href})`)
91 }
92 const docsHref = hrefInDocs(href);
93 let sourceCodeLink = undefined;
94 if (docsHref !== undefined) {
95 // Check that the target doc exists. Skip the check on /reference/ files
96 // that are typically generated at build time.
97 assertNoDeadLink(docsHref);
98 href = docsHref.replace(/[.](md|autogen)\b/, '');
99 href = href.replace(/\/README$/, '/');
100 } else if (href.startsWith('/') && !href.startsWith('//')) {
101 // /tools/xxx -> github/tools/xxx.
102 sourceCodeLink = href;
103 }
104 if (sourceCodeLink !== undefined) {
105 // Fix up line anchors for GitHub link: #42 -> #L42.
106 sourceCodeLink = sourceCodeLink.replace(/#(\d+)$/g, '#L$1')
107 assertNoDeadLink(sourceCodeLink);
108 href = CS_BASE_URL + sourceCodeLink;
109 }
110 return originalLinkFn(href, title, text);
111}
112
113function renderCode(text, lang) {
114 if (lang === 'mermaid') {
115 return `<div class="mermaid">${text}</div>`;
116 }
117
118 let hlHtml = '';
119 if (lang) {
120 hlHtml = hljs.highlight(lang, text).value
121 } else {
122 hlHtml = hljs.highlightAuto(text).value
123 }
124 return `<code class="hljs code-block">${hlHtml}</code>`
125}
126
127function renderImage(originalImgFn, href, title, text) {
128 const docsHref = hrefInDocs(href);
129 if (docsHref !== undefined) {
130 const outFile = outDir + docsHref;
131 const outParDir = path.dirname(outFile);
132 fs.ensureDirSync(outParDir);
133 fs.copyFileSync(ROOT_DIR + docsHref, outFile);
134 }
135 if (href.endsWith('.svg')) {
136 return `<object type="image/svg+xml" data="${href}"></object>`
137 }
138 return originalImgFn(href, title, text);
139}
140
141function renderParagraph(text) {
142 let cssClass = '';
143 if (text.startsWith('NOTE:')) {
144 cssClass = 'note';
145 }
146  else if (text.startsWith('TIP:')) {
147 cssClass = 'tip';
148 }
149  else if (text.startsWith('TODO:') || text.startsWith('FIXME:')) {
150 cssClass = 'todo';
151 }
152  else if (text.startsWith('WARNING:')) {
153 cssClass = 'warning';
154 }
155  else if (text.startsWith('Summary:')) {
156 cssClass = 'summary';
157 }
158 if (cssClass != '') {
159 cssClass = ` class="callout ${cssClass}"`;
160 }
161 return `<p${cssClass}>${text}</p>\n`;
162}
163
164function render(rawMarkdown) {
165 const renderer = new marked.Renderer();
166 const originalLinkFn = renderer.link.bind(renderer);
167 const originalImgFn = renderer.image.bind(renderer);
168 renderer.link = (hr, ti, te) => renderLink(originalLinkFn, hr, ti, te);
169 renderer.image = (hr, ti, te) => renderImage(originalImgFn, hr, ti, te);
170 renderer.code = renderCode;
171 renderer.heading = renderHeading;
172 renderer.paragraph = renderParagraph;
173
174 return marked(rawMarkdown, {renderer: renderer});
175}
176
177function main() {
178 const inFile = argv['i'];
179 const outFile = argv['o'];
180 outDir = argv['odir'];
181 const templateFile = argv['t'];
182 if (!outFile || !outDir) {
183 console.error(
184 'Usage: --odir site -o out.html [-i input.md] [-t templ.html]');
185 process.exit(1);
186 }
187 curMdFile = inFile;
188
189 let markdownHtml = '';
190 if (inFile) {
191 markdownHtml = render(fs.readFileSync(inFile, 'utf8'));
192 }
193
194 if (templateFile) {
195 // TODO rename nav.html to sitemap or something more mainstream.
196 const navFilePath = path.join(outDir, 'docs', '_nav.html');
197 const fallbackTitle =
198 'Perfetto - System profiling, app tracing and trace analysis';
199 const templateData = {
200 markdown: markdownHtml,
201 title: title ? `${title} - Perfetto Tracing Docs` : fallbackTitle,
Primiano Tuccicde1a8c2021-02-15 19:18:10 +0100202 fileName: '/' + path.relative(outDir, outFile),
Primiano Tuccie70df3f2020-05-21 19:48:08 +0100203 };
204 if (fs.existsSync(navFilePath)) {
205 templateData['nav'] = fs.readFileSync(navFilePath, 'utf8');
206 }
207 ejs.renderFile(templateFile, templateData, (err, html) => {
208 if (err)
209 throw err;
210 fs.writeFileSync(outFile, html);
211 process.exit(0);
212 });
213 } else {
214 fs.writeFileSync(outFile, markdownHtml);
215 process.exit(0);
216 }
217}
218
219main();