2 * Bootstrap Grunt task for parsing Less docstrings
3 * http://getbootstrap.com
4 * Copyright 2014 Twitter, Inc.
5 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
9 var Markdown = require('markdown-it');
11 function markdown2html(markdownString) {
12 var md = new Markdown();
14 // the slice removes the <p>...</p> wrapper output by Markdown processor
15 return md.render(markdownString.trim()).slice(3, -5);
21 //== This is a normal heading, which starts a section. Sections group variables together.
22 //## Optional description for the heading
24 //=== This is a subheading.
26 //** Optional description for the following variable. You **can** use Markdown in descriptions to discuss `<html>` stuff.
29 //-- This is a heading for a section whose variables shouldn't be customizable
31 All other lines are ignored completely.
35 var CUSTOMIZABLE_HEADING = /^[/]{2}={2}(.*)$/;
36 var UNCUSTOMIZABLE_HEADING = /^[/]{2}-{2}(.*)$/;
37 var SUBSECTION_HEADING = /^[/]{2}={3}(.*)$/;
38 var SECTION_DOCSTRING = /^[/]{2}#{2}(.+)$/;
39 var VAR_ASSIGNMENT = /^(@[a-zA-Z0-9_-]+):[ ]*([^ ;][^;]*);[ ]*$/;
40 var VAR_DOCSTRING = /^[/]{2}[*]{2}(.+)$/;
42 function Section(heading, customizable) {
43 this.heading = heading.trim();
44 this.id = this.heading.replace(/\s+/g, '-').toLowerCase();
45 this.customizable = customizable;
46 this.docstring = null;
47 this.subsections = [];
50 Section.prototype.addSubSection = function (subsection) {
51 this.subsections.push(subsection);
54 function SubSection(heading) {
55 this.heading = heading.trim();
56 this.id = this.heading.replace(/\s+/g, '-').toLowerCase();
60 SubSection.prototype.addVar = function (variable) {
61 this.variables.push(variable);
64 function VarDocstring(markdownString) {
65 this.html = markdown2html(markdownString);
68 function SectionDocstring(markdownString) {
69 this.html = markdown2html(markdownString);
72 function Variable(name, defaultValue) {
74 this.defaultValue = defaultValue;
75 this.docstring = null;
78 function Tokenizer(fileContent) {
79 this._lines = fileContent.split('\n');
80 this._next = undefined;
83 Tokenizer.prototype.unshift = function (token) {
84 if (this._next !== undefined) {
85 throw new Error('Attempted to unshift twice!');
90 Tokenizer.prototype._shift = function () {
91 // returning null signals EOF
92 // returning undefined means the line was ignored
93 if (this._next !== undefined) {
94 var result = this._next;
95 this._next = undefined;
98 if (this._lines.length <= 0) {
101 var line = this._lines.shift();
103 match = SUBSECTION_HEADING.exec(line);
104 if (match !== null) {
105 return new SubSection(match[1]);
107 match = CUSTOMIZABLE_HEADING.exec(line);
108 if (match !== null) {
109 return new Section(match[1], true);
111 match = UNCUSTOMIZABLE_HEADING.exec(line);
112 if (match !== null) {
113 return new Section(match[1], false);
115 match = SECTION_DOCSTRING.exec(line);
116 if (match !== null) {
117 return new SectionDocstring(match[1]);
119 match = VAR_DOCSTRING.exec(line);
120 if (match !== null) {
121 return new VarDocstring(match[1]);
123 var commentStart = line.lastIndexOf('//');
124 var varLine = (commentStart === -1) ? line : line.slice(0, commentStart);
125 match = VAR_ASSIGNMENT.exec(varLine);
126 if (match !== null) {
127 return new Variable(match[1], match[2]);
132 Tokenizer.prototype.shift = function () {
134 var result = this._shift();
135 if (result === undefined) {
142 function Parser(fileContent) {
143 this._tokenizer = new Tokenizer(fileContent);
146 Parser.prototype.parseFile = function () {
149 var section = this.parseSection();
150 if (section === null) {
151 if (this._tokenizer.shift() !== null) {
152 throw new Error('Unexpected unparsed section of file remains!');
156 sections.push(section);
160 Parser.prototype.parseSection = function () {
161 var section = this._tokenizer.shift();
162 if (section === null) {
165 if (!(section instanceof Section)) {
166 throw new Error('Expected section heading; got: ' + JSON.stringify(section));
168 var docstring = this._tokenizer.shift();
169 if (docstring instanceof SectionDocstring) {
170 section.docstring = docstring;
173 this._tokenizer.unshift(docstring);
175 this.parseSubSections(section);
180 Parser.prototype.parseSubSections = function (section) {
182 var subsection = this.parseSubSection();
183 if (subsection === null) {
184 if (section.subsections.length === 0) {
185 // Presume an implicit initial subsection
186 subsection = new SubSection('');
187 this.parseVars(subsection);
193 section.addSubSection(subsection);
196 if (section.subsections.length === 1 && !(section.subsections[0].heading) && section.subsections[0].variables.length === 0) {
197 // Ignore lone empty implicit subsection
198 section.subsections = [];
202 Parser.prototype.parseSubSection = function () {
203 var subsection = this._tokenizer.shift();
204 if (subsection instanceof SubSection) {
205 this.parseVars(subsection);
208 this._tokenizer.unshift(subsection);
212 Parser.prototype.parseVars = function (subsection) {
214 var variable = this.parseVar();
215 if (variable === null) {
218 subsection.addVar(variable);
222 Parser.prototype.parseVar = function () {
223 var docstring = this._tokenizer.shift();
224 if (!(docstring instanceof VarDocstring)) {
225 this._tokenizer.unshift(docstring);
228 var variable = this._tokenizer.shift();
229 if (variable instanceof Variable) {
230 variable.docstring = docstring;
233 this._tokenizer.unshift(variable);
238 module.exports = Parser;