'use strict'; const fs = require('fs'); const sections = require('section-matter'); const defaults = require('./lib/defaults'); const stringify = require('./lib/stringify'); const excerpt = require('./lib/excerpt'); const engines = require('./lib/engines'); const toFile = require('./lib/to-file'); const parse = require('./lib/parse'); const utils = require('./lib/utils'); /** * Takes a string or object with `content` property, extracts * and parses front-matter from the string, then returns an object * with `data`, `content` and other [useful properties](#returned-object). * * ```js * const matter = require('gray-matter'); * console.log(matter('---\ntitle: Home\n---\nOther stuff')); * //=> { data: { title: 'Home'}, content: 'Other stuff' } * ``` * @param {Object|String} `input` String, or object with `content` string * @param {Object} `options` * @return {Object} * @api public */ function matter(input, options) { if (input === '') { return { data: {}, content: input, excerpt: '', orig: input }; } let file = toFile(input); const cached = matter.cache[file.content]; if (!options) { if (cached) { file = Object.assign({}, cached); file.orig = cached.orig; return file; } // only cache if there are no options passed. if we cache when options // are passed, we would need to also cache options values, which would // negate any performance benefits of caching matter.cache[file.content] = file; } return parseMatter(file, options); } /** * Parse front matter */ function parseMatter(file, options) { const opts = defaults(options); const open = opts.delimiters[0]; const close = '\n' + opts.delimiters[1]; let str = file.content; if (opts.language) { file.language = opts.language; } // get the length of the opening delimiter const openLen = open.length; if (!utils.startsWith(str, open, openLen)) { excerpt(file, opts); return file; } // if the next character after the opening delimiter is // a character from the delimiter, then it's not a front- // matter delimiter if (str.charAt(openLen) === open.slice(-1)) { return file; } // strip the opening delimiter str = str.slice(openLen); const len = str.length; // use the language defined after first delimiter, if it exists const language = matter.language(str, opts); if (language.name) { file.language = language.name; str = str.slice(language.raw.length); } // get the index of the closing delimiter let closeIndex = str.indexOf(close); if (closeIndex === -1) { closeIndex = len; } // get the raw front-matter block file.matter = str.slice(0, closeIndex); const block = file.matter.replace(/^\s*#[^\n]+/gm, '').trim(); if (block === '') { file.isEmpty = true; file.empty = file.content; file.data = {}; } else { // create file.data by parsing the raw file.matter block file.data = parse(file.language, file.matter, opts); } // update file.content if (closeIndex === len) { file.content = ''; } else { file.content = str.slice(closeIndex + close.length); if (file.content[0] === '\r') { file.content = file.content.slice(1); } if (file.content[0] === '\n') { file.content = file.content.slice(1); } } excerpt(file, opts); if (opts.sections === true || typeof opts.section === 'function') { sections(file, opts.section); } return file; } /** * Expose engines */ matter.engines = engines; /** * Stringify an object to YAML or the specified language, and * append it to the given string. By default, only YAML and JSON * can be stringified. See the [engines](#engines) section to learn * how to stringify other languages. * * ```js * console.log(matter.stringify('foo bar baz', {title: 'Home'})); * // results in: * // --- * // title: Home * // --- * // foo bar baz * ``` * @param {String|Object} `file` The content string to append to stringified front-matter, or a file object with `file.content` string. * @param {Object} `data` Front matter to stringify. * @param {Object} `options` [Options](#options) to pass to gray-matter and [js-yaml]. * @return {String} Returns a string created by wrapping stringified yaml with delimiters, and appending that to the given string. * @api public */ matter.stringify = function(file, data, options) { if (typeof file === 'string') file = matter(file, options); return stringify(file, data, options); }; /** * Synchronously read a file from the file system and parse * front matter. Returns the same object as the [main function](#matter). * * ```js * const file = matter.read('./content/blog-post.md'); * ``` * @param {String} `filepath` file path of the file to read. * @param {Object} `options` [Options](#options) to pass to gray-matter. * @return {Object} Returns [an object](#returned-object) with `data` and `content` * @api public */ matter.read = function(filepath, options) { const str = fs.readFileSync(filepath, 'utf8'); const file = matter(str, options); file.path = filepath; return file; }; /** * Returns true if the given `string` has front matter. * @param {String} `string` * @param {Object} `options` * @return {Boolean} True if front matter exists. * @api public */ matter.test = function(str, options) { return utils.startsWith(str, defaults(options).delimiters[0]); }; /** * Detect the language to use, if one is defined after the * first front-matter delimiter. * @param {String} `string` * @param {Object} `options` * @return {Object} Object with `raw` (actual language string), and `name`, the language with whitespace trimmed */ matter.language = function(str, options) { const opts = defaults(options); const open = opts.delimiters[0]; if (matter.test(str)) { str = str.slice(open.length); } const language = str.slice(0, str.search(/\r?\n/)); return { raw: language, name: language ? language.trim() : '' }; }; /** * Expose `matter` */ matter.cache = {}; matter.clearCache = () => (matter.cache = {}); module.exports = matter;