Source: lib/kss_section.js

  1. 'use strict';
  2. const KssModifier = require('./kss_modifier'),
  3. KssParameter = require('./kss_parameter');
  4. /**
  5. * The `kss/lib/kss_section` module is normally accessed via the
  6. * [`KssSection()`]{@link module:kss.KssSection} class of the `kss` module:
  7. * ```
  8. * const KssSection = require('kss').KssSection;
  9. * ```
  10. * @private
  11. * @module kss/lib/kss_section
  12. */
  13. /**
  14. * A KssSection object represents a single section of a `KssStyleGuide`.
  15. *
  16. * A section of a style guide can be used for:
  17. * - a category of sub-sections, since sections are hierarchical
  18. * - a component in a design
  19. * - a mixin (or similar concept) of a CSS preprocessor
  20. *
  21. * This class is normally accessed via the [`kss`]{@link module:kss} module:
  22. * ```
  23. * const KssSection = require('kss').KssSection;
  24. * ```
  25. *
  26. * @alias module:kss.KssSection
  27. */
  28. class KssSection {
  29. /**
  30. * Creates a KssSection object and stores the given data.
  31. *
  32. * If passed an object, it will add the properties to the section.
  33. *
  34. * @param {Object} [data] An object of data.
  35. */
  36. constructor(data) {
  37. data = data || {};
  38. this.meta = {
  39. styleGuide: data.styleGuide || null,
  40. raw: data.raw || '',
  41. customPropertyNames: [],
  42. depth: data.depth || 0
  43. };
  44. this.data = {
  45. header: '',
  46. description: '',
  47. deprecated: false,
  48. experimental: false,
  49. reference: '',
  50. referenceNumber: '',
  51. referenceURI: '',
  52. weight: 0,
  53. markup: '',
  54. colors: [],
  55. source: {
  56. filename: '',
  57. path: '',
  58. line: ''
  59. },
  60. modifiers: [],
  61. parameters: []
  62. };
  63. // Loop through the given properties.
  64. for (let name in data) {
  65. // istanbul ignore else
  66. if (data.hasOwnProperty(name)) {
  67. // If the property is defined in this.data, add it via our API.
  68. if (this.data.hasOwnProperty(name)) {
  69. this[name](data[name]);
  70. // If the property isn't defined in meta or data, add a custom property.
  71. } else if (!this.meta.hasOwnProperty(name)) {
  72. this.custom(name, data[name]);
  73. }
  74. }
  75. }
  76. }
  77. /**
  78. * Return the `KssSection` as a JSON object.
  79. *
  80. * @returns {Object} A JSON object representation of the KssSection.
  81. */
  82. toJSON() {
  83. /* eslint-disable key-spacing */
  84. let returnObject = {
  85. header: this.header(),
  86. description: this.description(),
  87. deprecated: this.deprecated(),
  88. experimental: this.experimental(),
  89. reference: this.reference(),
  90. referenceNumber: this.referenceNumber(),
  91. referenceURI: this.referenceURI(),
  92. weight: this.weight(),
  93. colors: this.colors(),
  94. markup: this.markup(),
  95. source: this.source(),
  96. // Include meta as well.
  97. depth: this.depth()
  98. };
  99. /* eslint-enable key-spacing */
  100. returnObject.modifiers = this.modifiers().map(modifier => {
  101. return modifier.toJSON();
  102. });
  103. returnObject.parameters = this.parameters().map(parameter => {
  104. return parameter.toJSON();
  105. });
  106. // Add custom properties to the JSON object.
  107. for (let i = 0; i < this.meta.customPropertyNames.length; i++) {
  108. // istanbul ignore else
  109. if (typeof this.custom(this.meta.customPropertyNames[i]) !== 'undefined') {
  110. returnObject[this.meta.customPropertyNames[i]] = this.custom(this.meta.customPropertyNames[i]);
  111. }
  112. }
  113. return returnObject;
  114. }
  115. /**
  116. * Gets or sets the `KssStyleGuide` object this `KssSection` is associated with.
  117. *
  118. * If the `styleGuide` value is provided, the `KssStyleGuide` for this section
  119. * is set. Otherwise, the `KssStyleGuide` of the section is returned.
  120. *
  121. * @param {KssStyleGuide} [styleGuide] Optional. The `KssStyleGuide` that owns the
  122. * `KssSection`.
  123. * @returns {KssStyleGuide|KssSection} If styleGuide is given, the current
  124. * `KssSection` object is returned to allow chaining of methods. Otherwise,
  125. * the `KssStyleGuide` object the section belongs to is returned.
  126. */
  127. styleGuide(styleGuide) {
  128. if (typeof styleGuide === 'undefined') {
  129. return this.meta.styleGuide;
  130. }
  131. this.meta.styleGuide = styleGuide;
  132. // Tell the style guide about this section's custom property names.
  133. this.meta.styleGuide.customPropertyNames(this.customPropertyNames());
  134. // Allow chaining.
  135. return this;
  136. }
  137. /**
  138. * Gets or sets the header of the section, i.e. the first line in the description.
  139. *
  140. * If the `header` value is provided, the `header` for this section is set.
  141. * Otherwise, the `header` of the section is returned.
  142. *
  143. * @param {string} [header] Optional. The header of the section.
  144. * @returns {KssSection|string} If `header` is given, the `KssSection` object is
  145. * returned to allow chaining of methods. Otherwise, the header of the section
  146. * is returned.
  147. */
  148. header(header) {
  149. if (typeof header === 'undefined') {
  150. return this.data.header;
  151. }
  152. this.data.header = header;
  153. // Allow chaining.
  154. return this;
  155. }
  156. /**
  157. * Gets or sets the description of the section.
  158. *
  159. * If the `description` value is provided, the `description` for this section is
  160. * set. Otherwise, the `description` of the section is returned.
  161. *
  162. * @param {string} [description] Optional. The description of the section.
  163. * @returns {KssSection|string} If `description` is given, the `KssSection`
  164. * object is returned to allow chaining of methods. Otherwise, the description
  165. * of the section is returned.
  166. */
  167. description(description) {
  168. if (typeof description === 'undefined') {
  169. return this.data.description;
  170. }
  171. this.data.description = description;
  172. // Allow chaining.
  173. return this;
  174. }
  175. /**
  176. * Gets the list of custom properties of the section.
  177. *
  178. * Note that this method will return the actual custom properties set for this
  179. * section, and not all of the custom properties available for the entire style
  180. * guide. Use KssStyleGuide.customPropertyNames() for that list.
  181. *
  182. * @returns {string[]} An array of the section's custom property names.
  183. */
  184. customPropertyNames() {
  185. return this.meta.customPropertyNames;
  186. }
  187. /**
  188. * Gets or sets a custom property of the section.
  189. *
  190. * If the `value` is provided, the requested custom property of the section is
  191. * set. Otherwise, the section's custom property with the name specified in the
  192. * `name` parameter is returned.
  193. *
  194. * @param {string} name The name of the section's custom property.
  195. * @param {*} [value] Optional. The value of the section's custom property.
  196. * @returns {KssSection|*} If `value` is given, the `KssSection` object is
  197. * returned to allow chaining of methods. Otherwise, the section's custom
  198. * property, `name`, is returned.
  199. */
  200. custom(name, value) {
  201. if (typeof value === 'undefined') {
  202. /* eslint-disable no-undefined */
  203. return this.meta.customPropertyNames.indexOf(name) === -1 ? undefined : this.data[name];
  204. }
  205. if (this.styleGuide()) {
  206. this.styleGuide().customPropertyNames(name);
  207. }
  208. this.meta.customPropertyNames.push(name);
  209. this.data[name] = value;
  210. // Allow chaining.
  211. return this;
  212. }
  213. /**
  214. * Gets or sets the deprecated flag for the section.
  215. *
  216. * If the `deprecated` value is provided, the `deprecated` flag for this section
  217. * is set. Otherwise, the `deprecated` flag for the section is returned.
  218. *
  219. * @param {boolean} [deprecated] Optional. The deprecated flag for the section.
  220. * @returns {KssSection|boolean} If `deprecated` is given, the `KssSection`
  221. * object is returned to allow chaining of methods. Otherwise, the deprecated
  222. * flag for the section is returned.
  223. */
  224. deprecated(deprecated) {
  225. if (typeof deprecated === 'undefined') {
  226. return this.data.deprecated;
  227. }
  228. this.data.deprecated = !!deprecated;
  229. // Allow chaining.
  230. return this;
  231. }
  232. /**
  233. * Gets or sets the experimental flag for the section.
  234. *
  235. * If the `experimental` value is provided, the `experimental` flag for this
  236. * section is set. Otherwise, the `deprecated` flag for the section is returned.
  237. *
  238. * @param {boolean} [experimental] Optional. The experimental flag for the
  239. * section.
  240. * @returns {KssSection|boolean} If `experimental` is given, the `KssSection`
  241. * object is returned to allow chaining of methods. Otherwise, the
  242. * experimental flag for the section is returned.
  243. */
  244. experimental(experimental) {
  245. if (typeof experimental === 'undefined') {
  246. return this.data.experimental;
  247. }
  248. this.data.experimental = !!experimental;
  249. // Allow chaining.
  250. return this;
  251. }
  252. /**
  253. * Gets or sets the reference for the section.
  254. *
  255. * If the `reference` value is provided, the `reference` for this section is
  256. * set. Otherwise, the `reference` for the section is returned.
  257. *
  258. * @param {string} [reference] Optional. The reference of the section.
  259. * @returns {KssSection|string} If `reference` is given, the `KssSection` object
  260. * is returned to allow chaining of methods. Otherwise, the reference for the
  261. * section is returned.
  262. */
  263. reference(reference) {
  264. if (typeof reference === 'undefined') {
  265. return this.data.reference;
  266. }
  267. // @TODO: Tell the KssStyleGuide about the update.
  268. reference = reference.toString();
  269. // Normalize any " - " delimiters.
  270. reference = reference.replace(/\s+\-\s+/g, ' - ');
  271. // Remove trailing dot-zeros and periods.
  272. reference = reference.replace(/\.$|(\.0){1,}$/g, '');
  273. this.data.reference = reference;
  274. // Allow chaining.
  275. return this;
  276. }
  277. /**
  278. * Gets or sets a numeric reference number for the section.
  279. *
  280. * If the `referenceNumber` value is provided, the `referenceNumber` for this
  281. * section is set.
  282. *
  283. * If no parameters are given, this method returns a numeric reference number;
  284. * if the style guide's references are already numeric (e.g. 2, 2.1.3, 3.2),
  285. * then this method returns the same value as reference() does. Otherwise, an
  286. * auto-incremented reference number will be returned.
  287. *
  288. * @param {string} [referenceNumber] Optional. The auto-incremented reference
  289. * number of the section.
  290. * @returns {KssSection|string} If `referenceNumber` is given, the `KssSection`
  291. * object is returned to allow chaining of methods. Otherwise, the reference
  292. * number of the section is returned.
  293. */
  294. referenceNumber(referenceNumber) {
  295. if (typeof referenceNumber === 'undefined') {
  296. if (this.styleGuide() && this.styleGuide().hasNumericReferences()) {
  297. return this.data.reference;
  298. } else {
  299. return this.data.referenceNumber;
  300. }
  301. }
  302. this.data.referenceNumber = referenceNumber;
  303. // Allow chaining.
  304. return this;
  305. }
  306. /**
  307. * Gets or sets the reference of the section, encoded as a valid URI fragment.
  308. *
  309. * If the `referenceURI` value is provided, the `referenceURI` for this section
  310. * is set. Otherwise, the `referenceURI` of the section is returned.
  311. *
  312. * @param {string} [referenceURI] Optional. The referenceURI of the section.
  313. * @returns {KssSection|string} If `referenceURI` is given, the `KssSection`
  314. * object is returned to allow chaining of methods. Otherwise, the
  315. * referenceURI of the section is returned.
  316. */
  317. referenceURI(referenceURI) {
  318. if (typeof referenceURI === 'undefined') {
  319. if (!this.data.referenceURI) {
  320. this.data.referenceURI = encodeURI(
  321. this.reference()
  322. .replace(/ \- /g, '-')
  323. .replace(/[^\w-]+/g, '-')
  324. .toLowerCase()
  325. );
  326. }
  327. return this.data.referenceURI;
  328. }
  329. this.data.referenceURI = referenceURI;
  330. // Allow chaining.
  331. return this;
  332. }
  333. /**
  334. * Gets or sets the weight of the section.
  335. *
  336. * If the `weight` value is provided, the `weight` for this section is set.
  337. * Otherwise, the `weight` of the section is returned.
  338. *
  339. * @param {number} [weight] Optional. The weight of the section as an integer.
  340. * @returns {KssSection|number} If `weight` is given, the `KssSection` object
  341. * is returned to allow chaining of methods. Otherwise, the weight of the
  342. * section is returned.
  343. */
  344. weight(weight) {
  345. if (typeof weight === 'undefined') {
  346. return this.data.weight;
  347. }
  348. // @TODO: The weight needs to bubble-up to the KssStyleGuide weightMap.
  349. this.data.weight = weight;
  350. // Allow chaining.
  351. return this;
  352. }
  353. /**
  354. * Gets or sets the depth of the section.
  355. *
  356. * If the `depth` value is provided, the `depth` for this section is set.
  357. * Otherwise, the `depth` of the section is returned.
  358. *
  359. * @param {number} [depth] Optional. The depth of the section as a positive
  360. * integer.
  361. * @returns {KssSection|number} If `depth` is given, the `KssSection` object is
  362. * returned to allow chaining of methods. Otherwise, the depth of the section
  363. * is returned.
  364. */
  365. depth(depth) {
  366. if (typeof depth === 'undefined') {
  367. return this.meta.depth;
  368. }
  369. this.meta.depth = depth;
  370. // Allow chaining.
  371. return this;
  372. }
  373. /**
  374. * Gets or sets the markup of the section.
  375. *
  376. * If the `markup` value is provided, the `markup` for this section is set.
  377. * Otherwise, the `markup` of the section is returned.
  378. *
  379. * @param {string} [markup] Optional. The markup of the section.
  380. * @returns {KssSection|string|boolean} If `markup` is given, the `KssSection` object is
  381. * returned to allow chaining of methods. Otherwise, the markup of the section
  382. * is returned, or `false` if none.
  383. */
  384. markup(markup) {
  385. if (typeof markup === 'undefined') {
  386. return this.data.markup;
  387. }
  388. this.data.markup = markup;
  389. // Allow chaining.
  390. return this;
  391. }
  392. /**
  393. * Gets or sets the colors of the section.
  394. *
  395. * If the `colors` value is provided, the `colors` for this section is set.
  396. * Otherwise, the `colors` of the section is returned.
  397. *
  398. * @param {Array} [colors] Collection of color object.
  399. * @returns {KssSection|string|boolean} If `colors` is given, the `KssSection` object is
  400. * returned to allow chaining of methods. Otherwise, the colors of the section
  401. * is returned, or `false` if none.
  402. */
  403. colors(colors) {
  404. if (typeof colors === 'undefined') {
  405. return this.data.colors;
  406. }
  407. this.data.colors = colors;
  408. // Allow chaining.
  409. return this;
  410. }
  411. /**
  412. * Gets or sets the file information of the file where the section was
  413. * originally found.
  414. *
  415. * If the `source` parameter is provided, the `source` for this section is
  416. * set. Otherwise, the `source` of the section is returned.
  417. *
  418. * The `source` object contains the following information:
  419. * - filename: The name of the file.
  420. * - path: The full path of the file.
  421. * - line: The line number where the KSS comment is found.
  422. *
  423. * @param {{filename, path, line}} [source] The source file information where
  424. * the section was originally found.
  425. * @returns {KssSection|{filename, path, line}} If `source` is given, the
  426. * `KssSection` object is returned to allow chaining of methods. Otherwise,
  427. * the source of the section is returned.
  428. */
  429. source(source) {
  430. if (typeof source === 'undefined') {
  431. return this.data.source;
  432. }
  433. if (source.filename) {
  434. this.data.source.filename = source.filename;
  435. }
  436. if (source.path) {
  437. this.data.source.path = source.path;
  438. }
  439. if (source.line) {
  440. this.data.source.line = source.line;
  441. }
  442. // Allow chaining.
  443. return this;
  444. }
  445. /**
  446. * Gets the name of the file where the section was originally found.
  447. *
  448. * @returns {string} Returns the source file's path relative to the base path.
  449. */
  450. sourceFileName() {
  451. return this.data.source.filename;
  452. }
  453. /**
  454. * Gets the line number where the section was found in the original source file.
  455. *
  456. * @returns {string} Returns the source file's line number.
  457. */
  458. sourceLine() {
  459. return this.data.source.line;
  460. }
  461. /**
  462. * Gets or adds nested objects of the section.
  463. *
  464. * A common helper for `.modifiers()` and `.parameters()` methods.
  465. *
  466. * Different types of arguments for `properties` will yield different results:
  467. * - `Object|Array`: If the value is an array of objects or an object, the
  468. * `properties` are added to this section.
  469. * - `undefined`: Pass nothing to return all of the section's properties in an
  470. * array.
  471. * - `integer`: Use a 0-based index to return the section's Nth property.
  472. * - `string`: Use a string to return a specific modifier by name.
  473. *
  474. * @private
  475. * @param {string} propertyName The name of property in `KssSection`.
  476. * @param {Constructor} objectConstructor The constructor function for the type
  477. * of object the property is.
  478. * @param {*} [properties] Optional. The properties to set for the section.
  479. * @returns {*} If `properties` is given, the `KssSection` object is returned to
  480. * allow chaining of methods. Otherwise, the requested properties of the
  481. * section are returned.
  482. */
  483. _propertyHelper(propertyName, objectConstructor, properties) {
  484. if (typeof properties === 'undefined') {
  485. return this.data[propertyName];
  486. }
  487. // If we are given an object, assign the properties.
  488. if (typeof properties === 'object') {
  489. if (!(properties instanceof Array)) {
  490. properties = [properties];
  491. }
  492. properties.forEach(property => {
  493. let newProperty = (property instanceof objectConstructor) ? property : new objectConstructor(property);
  494. newProperty.section(this);
  495. this.data[propertyName].push(newProperty);
  496. });
  497. // Allow chaining.
  498. return this;
  499. }
  500. // Otherwise, we should search for the requested property.
  501. let query = properties,
  502. index = parseInt(query);
  503. if (typeof query === 'number' || typeof query === 'string' && query === (index + '')) {
  504. return (index < this.data[propertyName].length) ? this.data[propertyName][index] : false;
  505. // If the query can be converted to an integer, search by index instead.
  506. } else {
  507. // Otherwise, search for the property by name.
  508. for (let i = 0; i < this.data[propertyName].length; i++) {
  509. if (this.data[propertyName][i].name() === query) {
  510. return this.data[propertyName][i];
  511. }
  512. }
  513. }
  514. // No matching property found.
  515. return false;
  516. }
  517. /**
  518. * Gets or adds modifiers of the section.
  519. *
  520. * Different types of arguments will yield different results:
  521. * - `modifiers(Object|Array)`: If the value is an array of objects or an
  522. * object, the `modifiers` are added to this section.
  523. * - `modifiers()`: Pass nothing to return all of the section's modifiers in an
  524. * array.
  525. * - `modifiers(Integer)`: Use a 0-based index to return the section's Nth
  526. * modifier.
  527. * - `modifiers(String)`: Use a string to return a specific modifier by name.
  528. *
  529. * @param {*} [modifiers] Optional. The modifiers of the section.
  530. * @returns {KssSection|KssModifier|KssModifier[]|boolean} If `modifiers` is
  531. * given, the `KssSection` object is returned to allow chaining of methods.
  532. * Otherwise, the requested modifiers of the section are returned.
  533. */
  534. modifiers(modifiers) {
  535. return this._propertyHelper('modifiers', KssModifier, modifiers);
  536. }
  537. /**
  538. * Gets or adds parameters if the section is a CSS preprocessor function/mixin.
  539. *
  540. * Different types of arguments will yield different results:
  541. * - `parameters(Object|Array)`: If the value is an array of objects or an
  542. * object, the `parameters` are added to this section.
  543. * - `parameters()`: Pass nothing to return all of the section's parameters in
  544. * an array.
  545. * - `parameters(Integer)`: Use a 0-based index to return the section's Nth
  546. * parameter.
  547. * - `parameters(String)`: Use a string to return a specific parameter by name.
  548. *
  549. * @param {*} [parameters] Optional. The parameters of the section.
  550. * @returns {KssSection|KssParameter|KssParameter[]|boolean} If `parameters` is
  551. * given, the `KssSection` object is returned to allow chaining of methods.
  552. * Otherwise, the requested parameters of the section are returned.
  553. */
  554. parameters(parameters) {
  555. return this._propertyHelper('parameters', KssParameter, parameters);
  556. }
  557. }
  558. module.exports = KssSection;