bs-lessdoc-parser.js 6.2 KB

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