[fa375fe] | 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;
|
---|