index.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. 'use strict';
  2. const fs = require('fs');
  3. const sections = require('section-matter');
  4. const defaults = require('./lib/defaults');
  5. const stringify = require('./lib/stringify');
  6. const excerpt = require('./lib/excerpt');
  7. const engines = require('./lib/engines');
  8. const toFile = require('./lib/to-file');
  9. const parse = require('./lib/parse');
  10. const utils = require('./lib/utils');
  11. /**
  12. * Takes a string or object with `content` property, extracts
  13. * and parses front-matter from the string, then returns an object
  14. * with `data`, `content` and other [useful properties](#returned-object).
  15. *
  16. * ```js
  17. * const matter = require('gray-matter');
  18. * console.log(matter('---\ntitle: Home\n---\nOther stuff'));
  19. * //=> { data: { title: 'Home'}, content: 'Other stuff' }
  20. * ```
  21. * @param {Object|String} `input` String, or object with `content` string
  22. * @param {Object} `options`
  23. * @return {Object}
  24. * @api public
  25. */
  26. function matter(input, options) {
  27. if (input === '') {
  28. return { data: {}, content: input, excerpt: '', orig: input };
  29. }
  30. let file = toFile(input);
  31. const cached = matter.cache[file.content];
  32. if (!options) {
  33. if (cached) {
  34. file = Object.assign({}, cached);
  35. file.orig = cached.orig;
  36. return file;
  37. }
  38. // only cache if there are no options passed. if we cache when options
  39. // are passed, we would need to also cache options values, which would
  40. // negate any performance benefits of caching
  41. matter.cache[file.content] = file;
  42. }
  43. return parseMatter(file, options);
  44. }
  45. /**
  46. * Parse front matter
  47. */
  48. function parseMatter(file, options) {
  49. const opts = defaults(options);
  50. const open = opts.delimiters[0];
  51. const close = '\n' + opts.delimiters[1];
  52. let str = file.content;
  53. if (opts.language) {
  54. file.language = opts.language;
  55. }
  56. // get the length of the opening delimiter
  57. const openLen = open.length;
  58. if (!utils.startsWith(str, open, openLen)) {
  59. excerpt(file, opts);
  60. return file;
  61. }
  62. // if the next character after the opening delimiter is
  63. // a character from the delimiter, then it's not a front-
  64. // matter delimiter
  65. if (str.charAt(openLen) === open.slice(-1)) {
  66. return file;
  67. }
  68. // strip the opening delimiter
  69. str = str.slice(openLen);
  70. const len = str.length;
  71. // use the language defined after first delimiter, if it exists
  72. const language = matter.language(str, opts);
  73. if (language.name) {
  74. file.language = language.name;
  75. str = str.slice(language.raw.length);
  76. }
  77. // get the index of the closing delimiter
  78. let closeIndex = str.indexOf(close);
  79. if (closeIndex === -1) {
  80. closeIndex = len;
  81. }
  82. // get the raw front-matter block
  83. file.matter = str.slice(0, closeIndex);
  84. const block = file.matter.replace(/^\s*#[^\n]+/gm, '').trim();
  85. if (block === '') {
  86. file.isEmpty = true;
  87. file.empty = file.content;
  88. file.data = {};
  89. } else {
  90. // create file.data by parsing the raw file.matter block
  91. file.data = parse(file.language, file.matter, opts);
  92. }
  93. // update file.content
  94. if (closeIndex === len) {
  95. file.content = '';
  96. } else {
  97. file.content = str.slice(closeIndex + close.length);
  98. if (file.content[0] === '\r') {
  99. file.content = file.content.slice(1);
  100. }
  101. if (file.content[0] === '\n') {
  102. file.content = file.content.slice(1);
  103. }
  104. }
  105. excerpt(file, opts);
  106. if (opts.sections === true || typeof opts.section === 'function') {
  107. sections(file, opts.section);
  108. }
  109. return file;
  110. }
  111. /**
  112. * Expose engines
  113. */
  114. matter.engines = engines;
  115. /**
  116. * Stringify an object to YAML or the specified language, and
  117. * append it to the given string. By default, only YAML and JSON
  118. * can be stringified. See the [engines](#engines) section to learn
  119. * how to stringify other languages.
  120. *
  121. * ```js
  122. * console.log(matter.stringify('foo bar baz', {title: 'Home'}));
  123. * // results in:
  124. * // ---
  125. * // title: Home
  126. * // ---
  127. * // foo bar baz
  128. * ```
  129. * @param {String|Object} `file` The content string to append to stringified front-matter, or a file object with `file.content` string.
  130. * @param {Object} `data` Front matter to stringify.
  131. * @param {Object} `options` [Options](#options) to pass to gray-matter and [js-yaml].
  132. * @return {String} Returns a string created by wrapping stringified yaml with delimiters, and appending that to the given string.
  133. * @api public
  134. */
  135. matter.stringify = function(file, data, options) {
  136. if (typeof file === 'string') file = matter(file, options);
  137. return stringify(file, data, options);
  138. };
  139. /**
  140. * Synchronously read a file from the file system and parse
  141. * front matter. Returns the same object as the [main function](#matter).
  142. *
  143. * ```js
  144. * const file = matter.read('./content/blog-post.md');
  145. * ```
  146. * @param {String} `filepath` file path of the file to read.
  147. * @param {Object} `options` [Options](#options) to pass to gray-matter.
  148. * @return {Object} Returns [an object](#returned-object) with `data` and `content`
  149. * @api public
  150. */
  151. matter.read = function(filepath, options) {
  152. const str = fs.readFileSync(filepath, 'utf8');
  153. const file = matter(str, options);
  154. file.path = filepath;
  155. return file;
  156. };
  157. /**
  158. * Returns true if the given `string` has front matter.
  159. * @param {String} `string`
  160. * @param {Object} `options`
  161. * @return {Boolean} True if front matter exists.
  162. * @api public
  163. */
  164. matter.test = function(str, options) {
  165. return utils.startsWith(str, defaults(options).delimiters[0]);
  166. };
  167. /**
  168. * Detect the language to use, if one is defined after the
  169. * first front-matter delimiter.
  170. * @param {String} `string`
  171. * @param {Object} `options`
  172. * @return {Object} Object with `raw` (actual language string), and `name`, the language with whitespace trimmed
  173. */
  174. matter.language = function(str, options) {
  175. const opts = defaults(options);
  176. const open = opts.delimiters[0];
  177. if (matter.test(str)) {
  178. str = str.slice(open.length);
  179. }
  180. const language = str.slice(0, str.search(/\r?\n/));
  181. return {
  182. raw: language,
  183. name: language ? language.trim() : ''
  184. };
  185. };
  186. /**
  187. * Expose `matter`
  188. */
  189. matter.cache = {};
  190. matter.clearCache = () => (matter.cache = {});
  191. module.exports = matter;