238 lines
5.9 KiB
JavaScript
238 lines
5.9 KiB
JavaScript
|
/*!
|
||
|
* Bootstrap Grunt task for parsing Less docstrings
|
||
|
* https://getbootstrap.com/
|
||
|
* Copyright 2014-2019 Twitter, Inc.
|
||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var Markdown = require('markdown-it');
|
||
|
|
||
|
function markdown2html(markdownString) {
|
||
|
var md = new Markdown();
|
||
|
|
||
|
// the slice removes the <p>...</p> wrapper output by Markdown processor
|
||
|
return md.render(markdownString.trim()).slice(3, -5);
|
||
|
}
|
||
|
|
||
|
|
||
|
/*
|
||
|
Mini-language:
|
||
|
//== This is a normal heading, which starts a section. Sections group variables together.
|
||
|
//## Optional description for the heading
|
||
|
|
||
|
//=== This is a subheading.
|
||
|
|
||
|
//** Optional description for the following variable. You **can** use Markdown in descriptions to discuss `<html>` stuff.
|
||
|
@foo: #fff;
|
||
|
|
||
|
//-- This is a heading for a section whose variables shouldn't be customizable
|
||
|
|
||
|
All other lines are ignored completely.
|
||
|
*/
|
||
|
|
||
|
|
||
|
var CUSTOMIZABLE_HEADING = /^[/]{2}={2}(.*)$/;
|
||
|
var UNCUSTOMIZABLE_HEADING = /^[/]{2}-{2}(.*)$/;
|
||
|
var SUBSECTION_HEADING = /^[/]{2}={3}(.*)$/;
|
||
|
var SECTION_DOCSTRING = /^[/]{2}#{2}(.+)$/;
|
||
|
var VAR_ASSIGNMENT = /^(@[a-zA-Z0-9_-]+):[ ]*([^ ;][^;]*);[ ]*$/;
|
||
|
var VAR_DOCSTRING = /^[/]{2}[*]{2}(.+)$/;
|
||
|
|
||
|
function Section(heading, customizable) {
|
||
|
this.heading = heading.trim();
|
||
|
this.id = this.heading.replace(/\s+/g, '-').toLowerCase();
|
||
|
this.customizable = customizable;
|
||
|
this.docstring = null;
|
||
|
this.subsections = [];
|
||
|
}
|
||
|
|
||
|
Section.prototype.addSubSection = function (subsection) {
|
||
|
this.subsections.push(subsection);
|
||
|
};
|
||
|
|
||
|
function SubSection(heading) {
|
||
|
this.heading = heading.trim();
|
||
|
this.id = this.heading.replace(/\s+/g, '-').toLowerCase();
|
||
|
this.variables = [];
|
||
|
}
|
||
|
|
||
|
SubSection.prototype.addVar = function (variable) {
|
||
|
this.variables.push(variable);
|
||
|
};
|
||
|
|
||
|
function VarDocstring(markdownString) {
|
||
|
this.html = markdown2html(markdownString);
|
||
|
}
|
||
|
|
||
|
function SectionDocstring(markdownString) {
|
||
|
this.html = markdown2html(markdownString);
|
||
|
}
|
||
|
|
||
|
function Variable(name, defaultValue) {
|
||
|
this.name = name;
|
||
|
this.defaultValue = defaultValue;
|
||
|
this.docstring = null;
|
||
|
}
|
||
|
|
||
|
function Tokenizer(fileContent) {
|
||
|
this._lines = fileContent.split('\n');
|
||
|
this._next = undefined;
|
||
|
}
|
||
|
|
||
|
Tokenizer.prototype.unshift = function (token) {
|
||
|
if (this._next !== undefined) {
|
||
|
throw new Error('Attempted to unshift twice!');
|
||
|
}
|
||
|
this._next = token;
|
||
|
};
|
||
|
|
||
|
Tokenizer.prototype._shift = function () {
|
||
|
// returning null signals EOF
|
||
|
// returning undefined means the line was ignored
|
||
|
if (this._next !== undefined) {
|
||
|
var result = this._next;
|
||
|
this._next = undefined;
|
||
|
return result;
|
||
|
}
|
||
|
if (this._lines.length <= 0) {
|
||
|
return null;
|
||
|
}
|
||
|
var line = this._lines.shift();
|
||
|
var match = null;
|
||
|
match = SUBSECTION_HEADING.exec(line);
|
||
|
if (match !== null) {
|
||
|
return new SubSection(match[1]);
|
||
|
}
|
||
|
match = CUSTOMIZABLE_HEADING.exec(line);
|
||
|
if (match !== null) {
|
||
|
return new Section(match[1], true);
|
||
|
}
|
||
|
match = UNCUSTOMIZABLE_HEADING.exec(line);
|
||
|
if (match !== null) {
|
||
|
return new Section(match[1], false);
|
||
|
}
|
||
|
match = SECTION_DOCSTRING.exec(line);
|
||
|
if (match !== null) {
|
||
|
return new SectionDocstring(match[1]);
|
||
|
}
|
||
|
match = VAR_DOCSTRING.exec(line);
|
||
|
if (match !== null) {
|
||
|
return new VarDocstring(match[1]);
|
||
|
}
|
||
|
var commentStart = line.lastIndexOf('//');
|
||
|
var varLine = commentStart === -1 ? line : line.slice(0, commentStart);
|
||
|
match = VAR_ASSIGNMENT.exec(varLine);
|
||
|
if (match !== null) {
|
||
|
return new Variable(match[1], match[2]);
|
||
|
}
|
||
|
return undefined;
|
||
|
};
|
||
|
|
||
|
Tokenizer.prototype.shift = function () {
|
||
|
while (true) {
|
||
|
var result = this._shift();
|
||
|
if (result === undefined) {
|
||
|
continue;
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function Parser(fileContent) {
|
||
|
this._tokenizer = new Tokenizer(fileContent);
|
||
|
}
|
||
|
|
||
|
Parser.prototype.parseFile = function () {
|
||
|
var sections = [];
|
||
|
while (true) {
|
||
|
var section = this.parseSection();
|
||
|
if (section === null) {
|
||
|
if (this._tokenizer.shift() !== null) {
|
||
|
throw new Error('Unexpected unparsed section of file remains!');
|
||
|
}
|
||
|
return sections;
|
||
|
}
|
||
|
sections.push(section);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Parser.prototype.parseSection = function () {
|
||
|
var section = this._tokenizer.shift();
|
||
|
if (section === null) {
|
||
|
return null;
|
||
|
}
|
||
|
if (!(section instanceof Section)) {
|
||
|
throw new Error('Expected section heading; got: ' + JSON.stringify(section));
|
||
|
}
|
||
|
var docstring = this._tokenizer.shift();
|
||
|
if (docstring instanceof SectionDocstring) {
|
||
|
section.docstring = docstring;
|
||
|
} else {
|
||
|
this._tokenizer.unshift(docstring);
|
||
|
}
|
||
|
this.parseSubSections(section);
|
||
|
|
||
|
return section;
|
||
|
};
|
||
|
|
||
|
Parser.prototype.parseSubSections = function (section) {
|
||
|
while (true) {
|
||
|
var subsection = this.parseSubSection();
|
||
|
if (subsection === null) {
|
||
|
if (section.subsections.length === 0) {
|
||
|
// Presume an implicit initial subsection
|
||
|
subsection = new SubSection('');
|
||
|
this.parseVars(subsection);
|
||
|
} else {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
section.addSubSection(subsection);
|
||
|
}
|
||
|
|
||
|
if (section.subsections.length === 1 && !section.subsections[0].heading && section.subsections[0].variables.length === 0) {
|
||
|
// Ignore lone empty implicit subsection
|
||
|
section.subsections = [];
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Parser.prototype.parseSubSection = function () {
|
||
|
var subsection = this._tokenizer.shift();
|
||
|
if (subsection instanceof SubSection) {
|
||
|
this.parseVars(subsection);
|
||
|
return subsection;
|
||
|
}
|
||
|
this._tokenizer.unshift(subsection);
|
||
|
return null;
|
||
|
};
|
||
|
|
||
|
Parser.prototype.parseVars = function (subsection) {
|
||
|
while (true) {
|
||
|
var variable = this.parseVar();
|
||
|
if (variable === null) {
|
||
|
return;
|
||
|
}
|
||
|
subsection.addVar(variable);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Parser.prototype.parseVar = function () {
|
||
|
var docstring = this._tokenizer.shift();
|
||
|
if (!(docstring instanceof VarDocstring)) {
|
||
|
this._tokenizer.unshift(docstring);
|
||
|
docstring = null;
|
||
|
}
|
||
|
var variable = this._tokenizer.shift();
|
||
|
if (variable instanceof Variable) {
|
||
|
variable.docstring = docstring;
|
||
|
return variable;
|
||
|
}
|
||
|
this._tokenizer.unshift(variable);
|
||
|
return null;
|
||
|
};
|
||
|
|
||
|
|
||
|
module.exports = Parser;
|