diff --git a/.verb.md b/.verb.md index ecda709..6d86fb4 100644 --- a/.verb.md +++ b/.verb.md @@ -55,7 +55,7 @@ Into an object like this: **Why did we create gray-matter in the first place?** -We created gray-matter after trying out other libraries that failed to meet our standards and requirements. +We created gray-matter after trying out other libraries that failed to meet our standards and requirements. Some libraries met most of the requirements, but _none met all of them_. @@ -71,7 +71,7 @@ Some libraries met most of the requirements, but _none met all of them_. * Have no problem reading YAML files directly * Have no problem with complex content, including **non-front-matter** fenced code blocks that contain examples of YAML front matter. Other parsers fail on this. * Support stringifying back to front-matter. This is useful for linting, updating properties, etc. -* Allow custom delimiters, when it's necessary for avoiding delimiter collision. +* Allow custom delimiters, when it's necessary for avoiding delimiter collision. * Should return an object with at least these three properties: - `data`: the parsed YAML front matter, as a JSON object - `content`: the contents as a string, without the front matter @@ -137,6 +137,7 @@ In addition, the following non-enumberable properties are added to the object to - `file.language` **{String}**: the front-matter language that was parsed. `yaml` is the default - `file.matter` **{String}**: the _raw_, un-parsed front-matter string - `file.stringify` **{Function}**: [stringify](#stringify) the file by converting `file.data` to a string in the given language, wrapping it in delimiters and prepending it to `file.content`. +- `file.delimiters` **{Array}**: The delimiters which surrounded the front-matter, stored as `[, ]`. Will be `null` if there was no front-matter. ## Run the examples @@ -275,7 +276,7 @@ Define custom engines for parsing and/or stringifying front-matter. **Engine format** -Engines may either be an object with `parse` and (optionally) `stringify` methods, or a function that will be used for parsing only. +Engines may either be an object with `parse` and (optionally) `stringify` method and `delimiters`, or a function that will be used for parsing only. **Examples** @@ -300,7 +301,7 @@ const file = matter(str, { engines: { toml: { parse: toml.parse.bind(toml), - + delimiters: '+++', // example of throwing an error to let users know stringifying is // not supported (a TOML stringifier might exist, this is just an example) stringify: function() { @@ -363,6 +364,24 @@ categories = "front matter toml" This is content ``` +If you provide the `delimiters` property to a custom engine, then gray-matter will use that to detect the language. For example using `+++` to detect TOML. + +```js +const str = `+++ +title = "My post" +tags = ["random"] ++++ +Content goes here. +`; +const file = matter(str, { + engines: { + toml: { + parse: toml.parse.bind(toml), + delimiters: '+++', + } + } +}); +``` ### options.delimiters diff --git a/examples/toml-custom.js b/examples/toml-custom.js new file mode 100644 index 0000000..35e5dff --- /dev/null +++ b/examples/toml-custom.js @@ -0,0 +1,26 @@ +const matter = require('..'); +const toml = require('toml'); + +/** + * Parse TOML front-matter + */ + +const str = [ + '+++', + 'title = "TOML"', + 'description = "Front matter"', + 'categories = ["front", "matter", "toml"]', + '+++', + 'This is content' +].join('\n'); + +const file = matter(str, { + engines: { + toml: { + parse: toml.parse.bind(toml), + delimiters: '+++' + } + } +}); + +console.log(file); diff --git a/gray-matter.d.ts b/gray-matter.d.ts index dec9c09..6ed5817 100644 --- a/gray-matter.d.ts +++ b/gray-matter.d.ts @@ -42,10 +42,11 @@ declare namespace matter { excerpt?: string orig: Buffer | I language: string + delimiters: [string, string] matter: string stringify(lang: string): string } - + /** * Stringify an object to YAML or the specified language, and * append it to the given string. By default, only YAML and JSON @@ -108,7 +109,7 @@ declare namespace matter { export function language>( str: string, options?: GrayMatterOption - ): { name: string; raw: string } + ): { name: string; raw: string; delimiters: null | [string, string] } } export = matter diff --git a/index.js b/index.js index 7d49331..6c83776 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ const fs = require('fs'); const sections = require('section-matter'); const defaults = require('./lib/defaults'); +const delimiters = require('./lib/delimiters'); const stringify = require('./lib/stringify'); const excerpt = require('./lib/excerpt'); const engines = require('./lib/engines'); @@ -56,39 +57,30 @@ function matter(input, options) { 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; + const language = matter.language(str, opts); + if (language.name) { + file.language = language.name; } - // 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)) { + if (!language.delimiters) { + excerpt(file, opts); return file; } + file.delimiters = language.delimiters; + + const close = '\n' + file.delimiters[1]; + // strip the opening delimiter - str = str.slice(openLen); + str = str.slice(language.raw.length); 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) { @@ -183,10 +175,10 @@ matter.read = function(filepath, options) { }; /** - * Returns true if the given `string` has front matter. + * Returns true if the given `string` has default front matter. * @param {String} `string` * @param {Object} `options` - * @return {Boolean} True if front matter exists. + * @return {Boolean} True if default front matter exists. * @api public */ @@ -205,15 +197,32 @@ matter.test = function(str, options) { matter.language = function(str, options) { const opts = defaults(options); const open = opts.delimiters[0]; + let raw, name, delims; - if (matter.test(str)) { - str = str.slice(open.length); + if (!matter.test(str, options)) { + return delimiters(str, options); + } + // 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(open.length) === open.slice(-1)) { + return { + raw: '', + name: '', + delimiters: null + }; } + str = str.slice(open.length); + name = str.slice(0, str.search(/\r?\n/)); + raw = open + name; + name = name.trim(); + delims = opts.delimiters; + delims[0] = open + name; - const language = str.slice(0, str.search(/\r?\n/)); return { - raw: language, - name: language ? language.trim() : '' + raw, + name, + delimiters: delims }; }; diff --git a/lib/delimiters.js b/lib/delimiters.js new file mode 100644 index 0000000..34d96c8 --- /dev/null +++ b/lib/delimiters.js @@ -0,0 +1,49 @@ +const defaults = require('./defaults'); +const utils = require('./utils'); + +module.exports = function(str, options) { + const opts = defaults(options); + const dlen = str.search(/\r?\n/); + let raw, name, delimiters; + if (dlen > 0) { + const customDelims = mapCustomDelimiters(opts); + raw = str.substr(0, dlen); + const firstLine = raw.trim(); + if (customDelims[firstLine]) { + name = customDelims[firstLine]; + delimiters = utils.arrayify(opts.engines[name].delimiters); + if (delimiters.length === 1) { + delimiters.push(delimiters[0]); + } + return { + raw: raw || '', + name: name || '', + delimiters + }; + } + } + + // No frontmatter + return { + raw: '', + name: '', + delimiters: null + }; +}; + +function mapCustomDelimiters(opts) { + const customDelims = {}; + for (const engine in opts.engines) { + if (opts.engines[engine].delimiters) { + const delims = utils.arrayify(opts.engines[engine].delimiters); + if (customDelims[delims[0]]) { + throw new Error('Another engine has already used delimiter: ' + delims[0]); + } + if (delims[0] === opts.delimiters[0]) { + throw new Error('Engine specific delimiters cannot match the default delimiters: ' + opts.delimiters[0]); + } + customDelims[delims[0]] = engine; + } + } + return customDelims; +} diff --git a/lib/stringify.js b/lib/stringify.js index b4c70a4..26a1301 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -33,8 +33,9 @@ module.exports = function(file, data, options) { } data = Object.assign({}, file.data, data); - const open = opts.delimiters[0]; - const close = opts.delimiters[1]; + file.delimiters = file.delimiters || []; + const open = file.delimiters[0] || opts.delimiters[0]; + const close = file.delimiters[1] || opts.delimiters[1]; const matter = engine.stringify(data, options).trim(); let buf = ''; diff --git a/lib/to-file.js b/lib/to-file.js index 799bb5d..047f6cf 100644 --- a/lib/to-file.js +++ b/lib/to-file.js @@ -27,6 +27,7 @@ module.exports = function(file) { // set non-enumerable properties on the file object utils.define(file, 'orig', utils.toBuffer(file.content)); utils.define(file, 'language', file.language || ''); + utils.define(file, 'delimiters', file.delimiters || ['', '']); utils.define(file, 'matter', file.matter || ''); utils.define(file, 'stringify', function(data, options) { if (options && options.language) { diff --git a/test/matter.language.js b/test/matter.language.js index 8a60897..cc9f0e8 100644 --- a/test/matter.language.js +++ b/test/matter.language.js @@ -13,31 +13,37 @@ var matter = require('..'); describe('.language', function() { it('should detect the name of the language to parse', function() { assert.deepEqual(matter.language('---\nfoo: bar\n---'), { - raw: '', - name: '' + raw: '---', + name: '', + delimiters: ['---', '---'] }); assert.deepEqual(matter.language('---js\nfoo: bar\n---'), { - raw: 'js', - name: 'js' + raw: '---js', + name: 'js', + delimiters: ['---js', '---'] }); assert.deepEqual(matter.language('---coffee\nfoo: bar\n---'), { - raw: 'coffee', - name: 'coffee' + raw: '---coffee', + name: 'coffee', + delimiters: ['---coffee', '---'] }); }); it('should work around whitespace', function() { assert.deepEqual(matter.language('--- \nfoo: bar\n---'), { - raw: ' ', - name: '' + raw: '--- ', + name: '', + delimiters: ['---', '---'] }); assert.deepEqual(matter.language('--- js \nfoo: bar\n---'), { - raw: ' js ', - name: 'js' + raw: '--- js ', + name: 'js', + delimiters: ['---js', '---'] }); assert.deepEqual(matter.language('--- coffee \nfoo: bar\n---'), { - raw: ' coffee ', - name: 'coffee' + raw: '--- coffee ', + name: 'coffee', + delimiters: ['---coffee', '---'] }); }); }); diff --git a/test/parse-toml.js b/test/parse-toml.js index 5dec0a5..c99635b 100644 --- a/test/parse-toml.js +++ b/test/parse-toml.js @@ -45,4 +45,19 @@ describe('parse TOML:', function() { matter('---toml\n[props\nuser = "jonschlinkert"\n---\nContent\n'); }); }); + + it('should auto-detect TOML with custom delimiters.', function() { + var actual = parse('+++\ntitle = "autodetect-TOML-custom-delims"\n[props]\nuser = "jonschlinkert"\n+++\nContent\n', { + engines: { + toml: { + parse: toml.parse.bind(toml), + delimiters: '+++' + } + } + }); + assert.equal(actual.data.title, 'autodetect-TOML-custom-delims'); + assert(actual.hasOwnProperty('data')); + assert(actual.hasOwnProperty('content')); + assert(actual.hasOwnProperty('orig')); + }); }); diff --git a/test/stringify.js b/test/stringify.js index 2747b46..7d88d80 100644 --- a/test/stringify.js +++ b/test/stringify.js @@ -51,6 +51,28 @@ describe('.stringify', function() { ].join('\n')); }); + it('should stringify a file object with custom delimiters', function() { + var file = { content: 'Name: {{name}}', data: {name: 'gray-matter'}, delimiters: ['+++', '+++'] }; + var actual = matter.stringify(file); + assert.equal(actual, [ + '+++', + 'name: gray-matter', + '+++', + 'Name: {{name}}\n' + ].join('\n')); + }); + + it('should stringify a file object with extra language info', function() { + var file = { content: 'Name: {{name}}', data: {name: 'gray-matter'}, delimiters: ['---toml', '---'] }; + var actual = matter.stringify(file); + assert.equal(actual, [ + '---toml', + 'name: gray-matter', + '---', + 'Name: {{name}}\n' + ].join('\n')); + }); + it('should stringify an excerpt', function() { var file = { content: 'Name: {{name}}', data: {name: 'gray-matter'} }; file.excerpt = 'This is an excerpt.';