1 | /*!
|
---|
2 | * Bootstrap Grunt task for parsing Less docstrings
|
---|
3 | * https://getbootstrap.com/
|
---|
4 | * Copyright 2014-2019 Twitter, Inc.
|
---|
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
---|
6 | */
|
---|
7 |
|
---|
8 | 'use strict';
|
---|
9 |
|
---|
10 | var Markdown = require('markdown-it');
|
---|
11 |
|
---|
12 | function markdown2html(markdownString) {
|
---|
13 | var md = new Markdown();
|
---|
14 |
|
---|
15 | // the slice removes the <p>...</p> wrapper output by Markdown processor
|
---|
16 | return md.render(markdownString.trim()).slice(3, -5);
|
---|
17 | }
|
---|
18 |
|
---|
19 |
|
---|
20 | /*
|
---|
21 | Mini-language:
|
---|
22 | //== This is a normal heading, which starts a section. Sections group variables together.
|
---|
23 | //## Optional description for the heading
|
---|
24 |
|
---|
25 | //=== This is a subheading.
|
---|
26 |
|
---|
27 | //** Optional description for the following variable. You **can** use Markdown in descriptions to discuss `<html>` stuff.
|
---|
28 | @foo: #fff;
|
---|
29 |
|
---|
30 | //-- This is a heading for a section whose variables shouldn't be customizable
|
---|
31 |
|
---|
32 | All other lines are ignored completely.
|
---|
33 | */
|
---|
34 |
|
---|
35 |
|
---|
36 | var CUSTOMIZABLE_HEADING = /^[/]{2}={2}(.*)$/;
|
---|
37 | var UNCUSTOMIZABLE_HEADING = /^[/]{2}-{2}(.*)$/;
|
---|
38 | var SUBSECTION_HEADING = /^[/]{2}={3}(.*)$/;
|
---|
39 | var SECTION_DOCSTRING = /^[/]{2}#{2}(.+)$/;
|
---|
40 | var VAR_ASSIGNMENT = /^(@[a-zA-Z0-9_-]+):[ ]*([^ ;][^;]*);[ ]*$/;
|
---|
41 | var VAR_DOCSTRING = /^[/]{2}[*]{2}(.+)$/;
|
---|
42 |
|
---|
43 | function Section(heading, customizable) {
|
---|
44 | this.heading = heading.trim();
|
---|
45 | this.id = this.heading.replace(/\s+/g, '-').toLowerCase();
|
---|
46 | this.customizable = customizable;
|
---|
47 | this.docstring = null;
|
---|
48 | this.subsections = [];
|
---|
49 | }
|
---|
50 |
|
---|
51 | Section.prototype.addSubSection = function (subsection) {
|
---|
52 | this.subsections.push(subsection);
|
---|
53 | };
|
---|
54 |
|
---|
55 | function SubSection(heading) {
|
---|
56 | this.heading = heading.trim();
|
---|
57 | this.id = this.heading.replace(/\s+/g, '-').toLowerCase();
|
---|
58 | this.variables = [];
|
---|
59 | }
|
---|
60 |
|
---|
61 | SubSection.prototype.addVar = function (variable) {
|
---|
62 | this.variables.push(variable);
|
---|
63 | };
|
---|
64 |
|
---|
65 | function VarDocstring(markdownString) {
|
---|
66 | this.html = markdown2html(markdownString);
|
---|
67 | }
|
---|
68 |
|
---|
69 | function SectionDocstring(markdownString) {
|
---|
70 | this.html = markdown2html(markdownString);
|
---|
71 | }
|
---|
72 |
|
---|
73 | function Variable(name, defaultValue) {
|
---|
74 | this.name = name;
|
---|
75 | this.defaultValue = defaultValue;
|
---|
76 | this.docstring = null;
|
---|
77 | }
|
---|
78 |
|
---|
79 | function Tokenizer(fileContent) {
|
---|
80 | this._lines = fileContent.split('\n');
|
---|
81 | this._next = undefined;
|
---|
82 | }
|
---|
83 |
|
---|
84 | Tokenizer.prototype.unshift = function (token) {
|
---|
85 | if (this._next !== undefined) {
|
---|
86 | throw new Error('Attempted to unshift twice!');
|
---|
87 | }
|
---|
88 | this._next = token;
|
---|
89 | };
|
---|
90 |
|
---|
91 | Tokenizer.prototype._shift = function () {
|
---|
92 | // returning null signals EOF
|
---|
93 | // returning undefined means the line was ignored
|
---|
94 | if (this._next !== undefined) {
|
---|
95 | var result = this._next;
|
---|
96 | this._next = undefined;
|
---|
97 | return result;
|
---|
98 | }
|
---|
99 | if (this._lines.length <= 0) {
|
---|
100 | return null;
|
---|
101 | }
|
---|
102 | var line = this._lines.shift();
|
---|
103 | var match = null;
|
---|
104 | match = SUBSECTION_HEADING.exec(line);
|
---|
105 | if (match !== null) {
|
---|
106 | return new SubSection(match[1]);
|
---|
107 | }
|
---|
108 | match = CUSTOMIZABLE_HEADING.exec(line);
|
---|
109 | if (match !== null) {
|
---|
110 | return new Section(match[1], true);
|
---|
111 | }
|
---|
112 | match = UNCUSTOMIZABLE_HEADING.exec(line);
|
---|
113 | if (match !== null) {
|
---|
114 | return new Section(match[1], false);
|
---|
115 | }
|
---|
116 | match = SECTION_DOCSTRING.exec(line);
|
---|
117 | if (match !== null) {
|
---|
118 | return new SectionDocstring(match[1]);
|
---|
119 | }
|
---|
120 | match = VAR_DOCSTRING.exec(line);
|
---|
121 | if (match !== null) {
|
---|
122 | return new VarDocstring(match[1]);
|
---|
123 | }
|
---|
124 | var commentStart = line.lastIndexOf('//');
|
---|
125 | var varLine = commentStart === -1 ? line : line.slice(0, commentStart);
|
---|
126 | match = VAR_ASSIGNMENT.exec(varLine);
|
---|
127 | if (match !== null) {
|
---|
128 | return new Variable(match[1], match[2]);
|
---|
129 | }
|
---|
130 | return undefined;
|
---|
131 | };
|
---|
132 |
|
---|
133 | Tokenizer.prototype.shift = function () {
|
---|
134 | while (true) {
|
---|
135 | var result = this._shift();
|
---|
136 | if (result === undefined) {
|
---|
137 | continue;
|
---|
138 | }
|
---|
139 | return result;
|
---|
140 | }
|
---|
141 | };
|
---|
142 |
|
---|
143 | function Parser(fileContent) {
|
---|
144 | this._tokenizer = new Tokenizer(fileContent);
|
---|
145 | }
|
---|
146 |
|
---|
147 | Parser.prototype.parseFile = function () {
|
---|
148 | var sections = [];
|
---|
149 | while (true) {
|
---|
150 | var section = this.parseSection();
|
---|
151 | if (section === null) {
|
---|
152 | if (this._tokenizer.shift() !== null) {
|
---|
153 | throw new Error('Unexpected unparsed section of file remains!');
|
---|
154 | }
|
---|
155 | return sections;
|
---|
156 | }
|
---|
157 | sections.push(section);
|
---|
158 | }
|
---|
159 | };
|
---|
160 |
|
---|
161 | Parser.prototype.parseSection = function () {
|
---|
162 | var section = this._tokenizer.shift();
|
---|
163 | if (section === null) {
|
---|
164 | return null;
|
---|
165 | }
|
---|
166 | if (!(section instanceof Section)) {
|
---|
167 | throw new Error('Expected section heading; got: ' + JSON.stringify(section));
|
---|
168 | }
|
---|
169 | var docstring = this._tokenizer.shift();
|
---|
170 | if (docstring instanceof SectionDocstring) {
|
---|
171 | section.docstring = docstring;
|
---|
172 | } else {
|
---|
173 | this._tokenizer.unshift(docstring);
|
---|
174 | }
|
---|
175 | this.parseSubSections(section);
|
---|
176 |
|
---|
177 | return section;
|
---|
178 | };
|
---|
179 |
|
---|
180 | Parser.prototype.parseSubSections = function (section) {
|
---|
181 | while (true) {
|
---|
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);
|
---|
188 | } else {
|
---|
189 | break;
|
---|
190 | }
|
---|
191 | }
|
---|
192 | section.addSubSection(subsection);
|
---|
193 | }
|
---|
194 |
|
---|
195 | if (section.subsections.length === 1 && !section.subsections[0].heading && section.subsections[0].variables.length === 0) {
|
---|
196 | // Ignore lone empty implicit subsection
|
---|
197 | section.subsections = [];
|
---|
198 | }
|
---|
199 | };
|
---|
200 |
|
---|
201 | Parser.prototype.parseSubSection = function () {
|
---|
202 | var subsection = this._tokenizer.shift();
|
---|
203 | if (subsection instanceof SubSection) {
|
---|
204 | this.parseVars(subsection);
|
---|
205 | return subsection;
|
---|
206 | }
|
---|
207 | this._tokenizer.unshift(subsection);
|
---|
208 | return null;
|
---|
209 | };
|
---|
210 |
|
---|
211 | Parser.prototype.parseVars = function (subsection) {
|
---|
212 | while (true) {
|
---|
213 | var variable = this.parseVar();
|
---|
214 | if (variable === null) {
|
---|
215 | return;
|
---|
216 | }
|
---|
217 | subsection.addVar(variable);
|
---|
218 | }
|
---|
219 | };
|
---|
220 |
|
---|
221 | Parser.prototype.parseVar = function () {
|
---|
222 | var docstring = this._tokenizer.shift();
|
---|
223 | if (!(docstring instanceof VarDocstring)) {
|
---|
224 | this._tokenizer.unshift(docstring);
|
---|
225 | docstring = null;
|
---|
226 | }
|
---|
227 | var variable = this._tokenizer.shift();
|
---|
228 | if (variable instanceof Variable) {
|
---|
229 | variable.docstring = docstring;
|
---|
230 | return variable;
|
---|
231 | }
|
---|
232 | this._tokenizer.unshift(variable);
|
---|
233 | return null;
|
---|
234 | };
|
---|
235 |
|
---|
236 |
|
---|
237 | module.exports = Parser;
|
---|