./src/generate.js annotated source
Back to indexThis file contains the bulk of the logic for generating litterate pages. This file exports a function that the command-line utility calls with configurations.
4
5const fs = require('fs');
6const path = require('path');
Marked is our markdown parser
8const marked = require('marked');
9
This isn't optimal, but for now, we read the three template files into memory at the beginning, synchronously, so we can reuse them later.
12const INDEX_PAGE = fs.readFileSync(path.resolve(__dirname, '../templates/index.html'), 'utf8');
13const STYLES_CSS = fs.readFileSync(path.resolve(__dirname, '../templates/main.css'), 'utf8');
14const SOURCE_PAGE = fs.readFileSync(path.resolve(__dirname, '../templates/source.html'), 'utf8');
15
Helper function to wrap a given line of text into multiple lines,
with limit
characters per line.
18const wrapLine = (line, limit) => {
19 const len = line.length;
20 let result = '';
21 for (let countedChars = 0; countedChars < len; countedChars += limit) {
22 result += line.substr(countedChars, limit) + '\n';
23 }
24 return result;
25}
26
Helper function to scape characters that won't display in HTML correctly, like the very common
>
and <
and &
characters in code.
29const encodeHTML = code => {
30 return code.replace(/[\u00A0-\u9999<>&]/gim, i => {
31 return '&#' + i.codePointAt(0) + ';';
32 });
33}
34
Litterate uses a very, very minimal templating system that just wraps keywords
in {{curlyBraces}}
. We don't need anything complicated, and this allows us to be
lightweight and customizable when needed. This function populates a template with
given key-value pairs.
39const resolveTemplate = (templateContent, templateValues) => {
40 for (const [key, value] of Object.entries(templateValues)) {
41 templateContent = templateContent.replace(
42 new RegExp(`{{${key}}}`, 'g'),
43 value,
44 );
45 }
46 return templateContent;
47}
48
Function that maps a given source file to the path where its annotated version will be saved.
51const getOutputPathForSourcePath = (sourcePath, config) => {
52 return path.join(
53 config.outputDirectory,
54 sourcePath + '.html',
55 );
56}
57
Function to populate the index.html
page of the generated site with all the source
links, name/description, etc.
60const populateIndexPage = (sourceFiles, config) => {
61 const files = sourceFiles.map(sourcePath => {
62 const outputPath = getOutputPathForSourcePath(sourcePath, config);
63 return `<p class="sourceLink"><a href="${config.baseURL}${path.relative(config.outputDirectory, outputPath)}">${sourcePath}</a></p>`;
64 });
65 return resolveTemplate(INDEX_PAGE, {
66 title: config.name,
67 description: marked.parse(config.description),
68 sourcesList: files.join('\n'),
69 baseURL: config.baseURL,
70 });
71}
72
Given an array of source code lines, return an array of lines matched with any corresponding annotations and the line number from the source file.
75const linesToLinePairs = (lines, config) => {
76 const linePairs = [];
77 let docLine = '';
78
Shorthand function to markdown-process and optionally wrap source code lines.
81 const processCodeLine = codeLine => {
82 if (config.wrap !== 0) {
83 return wrapLine(encodeHTML(codeLine), config.wrap);
84 } else {
85 return encodeHTML(codeLine);
86 }
87 }
88
linesToLinePairs
works by having two arrays -- one of the annotation-lineNumber-source line
tuples in order, and another of the annotation lines counted so far for the next source line.
This takes the annotation, line number, and source line from the second array and pushes it
onto the first array, so we can move onto the next lines.
93 let inAnnotationComment = false;
94 const pushPair = (codeLine, lineNumber) => {
95 if (docLine) {
96 const lastLine = linePairs[linePairs.length - 1];
97 if (lastLine && lastLine[0]) {
98 linePairs.push(['', '', '']);
99 }
100 linePairs.push([marked.parse(docLine), processCodeLine(codeLine), lineNumber]);
101 } else {
102 linePairs.push(['', processCodeLine(codeLine), lineNumber]);
103 }
104 docLine = '';
105 }
106
Push the current annotation line onto the array of previous annotation lines, until we get to the next source code line.
109 const pushComment = line => {
110 if (line.trim().startsWith(config.annotationStartMark)) {
111 docLine = line.replace(config.annotationStartMark, '').trim();
112 } else {
113 docLine += '\n' + line.replace(config.annotationContinueMark, '').trim();
114 }
115 };
116
The main loop for this function.
118 lines.forEach((line, idx) => {
119 if (line.trim().startsWith(config.annotationStartMark)) {
120 inAnnotationComment = true;
121 pushComment(line);
122 } else if (line.trim().startsWith(config.annotationContinueMark)) {
123 if (inAnnotationComment) {
124 pushComment(line)
125 } else {
126 pushPair(line, idx + 1);
127 }
128 } else {
129 if (inAnnotationComment) {
130 inAnnotationComment = false;
131 }
132 pushPair(line, idx + 1);
133 }
134 });
135
136 return linePairs;
137}
138
This function is called for each source file, to process and save the Litterate version of the source file in the correct place.
141const createAndSavePage = (sourcePath, config) => {
142 const logErr = err => {
143 if (err) {
144 console.error(`Error writing ${sourcePath} annotated page: ${err}`);
145 }
146 }
147
148 return new Promise((res, _rej) => {
149 fs.readFile(sourcePath, 'utf8', (err, content) => {
150 if (err) {
151 logErr();
152 }
153
154 const sourceLines = linesToLinePairs(content.split('\n'), config).map(([doc, source, lineNumber]) => {
155 return `<div class="line"><div class="doc">${doc}</div><pre class="source javascript"><strong class="lineNumber">${lineNumber}</strong>${source}</pre></div>`;
156 }).join('\n');
157
158 const annotatedPage = resolveTemplate(SOURCE_PAGE, {
159 title: sourcePath,
160 lines: sourceLines,
161 baseURL: config.baseURL,
162 });
163 const outputFilePath = getOutputPathForSourcePath(sourcePath, config);
164 fs.mkdir(path.parse(outputFilePath).dir, {recursive: true}, err => {
165 if (err) {
166 logErr();
167 }
168
169 fs.writeFile(outputFilePath, annotatedPage, 'utf8', err => {
170 if (err) {
171 logErr();
172 }
173 res();
174 });
175 });
176 });
177 });
178}
179
This whole file exports this single function, which is called with a list of files to process, and the configuration options.
182const generateLitteratePages = (sourceFiles, config) => {
183 const {
184 outputDirectory,
185 } = config;
186
Write out index and main.css files
188 fs.mkdir(outputDirectory, {recursive: true}, err => {
189 if (err) {
190 console.error(`Unable to create ${outputDirectory} for documentation`);
191 }
192
193 fs.writeFile(
194 path.resolve(outputDirectory, 'index.html'),
195 populateIndexPage(sourceFiles, config),
196 'utf8', err => {
197 if (err) {
198 console.error(`Error encountered while writing index.html to disk: ${err}`);
199 }
200 },
201 );
202
203 fs.writeFile(path.resolve(outputDirectory, 'main.css'), STYLES_CSS, 'utf8', err => {
204 if (err) {
205 console.error(`Error encountered while writing main.css to disk: ${err}`);
206 }
207 });
208 });
209
Process source files that need to be annotated
211 for (const sourceFile of sourceFiles) {
212 createAndSavePage(sourceFile, config);
213 console.log(`Annotated ${sourceFile}`);
214 }
215}
216
217module.exports = {
218 generateLitteratePages,
219}
220