bs-lessdoc-parser.js 5.9 KB

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