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