puppeteer/test/doclint/MDOutline.js
Andrey Lushnikov 62cacbe5f5 Implement documentation linter (#47)
This patch implements documentation linter, which leaves under `test/doclint`
folder.

The documentation linter works like this:
1. Parse javascript source code with esprima and construct a "documentation" out of source code
2. Generate HTML out of `api.md` and traverse the HTML with puppeteer.
3. Make sure javascript aligns nicely with HTML

The documentation linter adds the following commands:
- `yarn doc` - to test that documentation covers all the relevant apis
- `yarn generate-toc` - to update the table-of-contents for the `api.md`
2017-07-07 19:36:45 +03:00

86 lines
2.6 KiB
JavaScript

const Documentation = require('./Documentation');
const commonmark = require('commonmark');
const Browser = require('../../lib/Browser');
class MDOutline {
constructor(text) {
this.classes = [];
this.html = MDOutline.compile(text);
}
static compile(text) {
const reader = new commonmark.Parser();
const parsed = reader.parse(text);
const writer = new commonmark.HtmlRenderer();
const html = writer.render(parsed);
return html;
}
async collectHeadings() {
const browser = new Browser({args: ['--no-sandbox']});
const page = await browser.newPage();
await page.setContent(this.html);
this.headings = await page.evaluate(getTOCHeadings);
await browser.close();
}
buildClasses() {
const headings = this.headings;
const classHeading = /^class: (\w+)$/;
const constructorRegex = /^new (\w+)\((.*)\)$/;
const methodRegex = /^(\w+)\.(\w+)\((.*)\)$/;
let currentClassName = null;
let currentClassMethods = [];
for (const heading of Object.keys(headings)) {
let match = heading.match(classHeading);
currentClassName = match[1];
for (const title of headings[heading]) {
let className = null;
let methodName = null;
if (constructorRegex.test(title)) {
let match = title.match(constructorRegex);
className = match[1];
methodName = 'constructor';
} else if (methodRegex.test(title)) {
let match = title.match(methodRegex);
className = match[1];
methodName = match[2];
}
if (!currentClassName || !className || !methodName || className.toLowerCase() !== currentClassName.toLowerCase()) {
console.warn('failed to process header as method: ' + heading);
continue;
}
let method = new Documentation.Method(methodName);
currentClassMethods.push(method);
}
flushClassIfNeeded.call(this);
}
function flushClassIfNeeded() {
if (currentClassName === null)
return;
this.classes.push(new Documentation.Class(currentClassName, currentClassMethods));
currentClassName = null;
currentClassMethods = [];
}
}
}
function getTOCHeadings(){
const headings = {};
document.querySelectorAll('h3').forEach((domainEl, i) => {
const methods = [];
let currElem = domainEl;
while ((currElem = currElem.nextElementSibling) && !currElem.matches('h3')) {
if (currElem.matches('h4'))
methods.push(currElem.textContent);
}
headings[domainEl.textContent] = methods;
});
return headings;
}
module.exports = MDOutline;