./src/generate.js annotated source

Back to index

        

This 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