├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── lib └── index.js ├── package.json └── test ├── fixtures ├── basic │ └── src │ │ └── test.md ├── delimiters-option │ └── src │ │ └── test.md ├── eval-option │ └── src │ │ └── test.md ├── lang-option │ └── src │ │ └── test.md ├── namespace-option │ └── src │ │ └── test.md └── parser-option │ └── src │ └── test.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/fixtures/*/build 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "iojs" 5 | - "4.0" 6 | script: 7 | - npm test 8 | - make lint-tests 9 | - make lint-code 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015, Ajedi32 4 | Copyright (c) 2014, Segment.io 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # Adds --harmony-generators flag when available/necessary 4 | # 5 | 6 | node ?= node 7 | node_flags ?= $(shell $(node) --v8-options | grep generators | cut -d ' ' -f 3) 8 | 9 | # 10 | # Binaries. 11 | # 12 | 13 | mocha = $(node) $(node_flags) ./node_modules/.bin/_mocha 14 | 15 | # 16 | # Targets. 17 | # 18 | 19 | # Install dependencies with npm. 20 | node_modules: package.json 21 | @npm install 22 | @touch node_modules # hack to fix mtime after npm installs 23 | 24 | # Run the tests. 25 | test: node_modules 26 | @$(mocha) 27 | 28 | # Run the tests in debugging mode. 29 | test-debug: node_modules 30 | @$(mocha) debug 31 | 32 | # Run jshint against the project 33 | lint-code: node_modules 34 | node_modules/.bin/jshint lib/ 35 | lint-tests: node_modules 36 | node_modules/.bin/jshint test/ 37 | lint: lint-code lint-tests 38 | 39 | # 40 | # Phonies. 41 | # 42 | 43 | .PHONY: test 44 | .PHONY: test-debug 45 | .PHONY: lint-code 46 | .PHONY: lint-tests 47 | .PHONY: lint 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # metalsmith-matters [![Build Status](https://travis-ci.org/Ajedi32/metalsmith-matters.svg)](https://travis-ci.org/Ajedi32/metalsmith-matters) 2 | 3 | A [Metalsmith](https://github.com/segmentio/metalsmith) plugin to read file 4 | metadata from frontmatter. Supports all frontmatter formats supported by the 5 | [gray-matter](https://github.com/jonschlinkert/gray-matter#optionslang) 6 | library (including YAML, JSON, TOML, CSON, and more). 7 | 8 | "But wait!" you say. "Doesn't Metalsmith already have frontmatter parsing 9 | built-in"? Well, yes. For now anyway. This plugin is an attempt to extract that 10 | functionality out of the Metalsmith core. Eventually the goal is to have 11 | frontmatter parsing removed from the Metalsmith core entirely in favor of this 12 | plugin or a similar one (see 13 | [segmentio/metalsmith#157](https://github.com/segmentio/metalsmith/issues/157)). 14 | 15 | Even in the absence of that change though, this plugin has several key 16 | advantages over the built-in frontmatter parsing in Metalsmith. Firstly, it's 17 | composable with other plugins. For example, you could use it with 18 | [`metalsmith-branch`](https://github.com/ericgj/metalsmith-branch) to turn on 19 | frontmatter parsing for only a subset of files, or you could place it *after* 20 | [`metalsmith-s3`](https://github.com/mwishek/metalsmith-s3) in the plugin list 21 | so that it parses frontmatter in downloaded files. 22 | 23 | Secondly, this plugin provides a way to pass options to the underlying 24 | frontmatter parsing library, 25 | [gray-matter](https://github.com/jonschlinkert/gray-matter). This allows you to 26 | do things like [change the delimiter used to separate frontmatter from the rest 27 | of the file](https://github.com/jonschlinkert/gray-matter#optionsdelims), or 28 | [set TOML as the default frontmatter 29 | format](https://github.com/jonschlinkert/gray-matter#optionslang). 30 | 31 | Thirdly, this plugin provides an additional feature allowing parsed frontmatter 32 | to be automatically namespaced under a single key (e.g. `page.keys` instead of 33 | `keys`). 34 | 35 | These are all things you can't do with Metalsmith's built-in frontmatter 36 | parsing. 37 | 38 | 39 | ## Installation 40 | 41 | npm install --save metalsmith-matters 42 | 43 | ## CLI Usage 44 | 45 | After installing metalsmith-matters, simply add the `metalsmith-matters` key to 46 | the plugins in your `metalsmith.json` file. Be sure to include it *before* any 47 | plugins which need to use the metadata in your frontmatter, or which expect as 48 | input files with no frontmatter header. Generally speaking, this means that 49 | metalsmith-matters should be the first plugin in the list. 50 | 51 | ```javascript 52 | { 53 | "frontmatter": false, // Disable built-in frontmatter parsing (recommended) 54 | "plugins": { 55 | "metalsmith-matters": { 56 | // Options 57 | } 58 | // Other plugins... 59 | } 60 | } 61 | ``` 62 | 63 | ## JavaScript Usage 64 | 65 | After installing metalsmith-matters, you can require `metalsmith-matters` in 66 | your code, then call the exported value to initialize the plugin and pass the 67 | result to `Metalsmith.use` (just as you would with any other Metalsmith plugin). 68 | Again, be sure to use metalsmith-matters *before* any plugins which need to use 69 | the metadata defined your frontmatter, or which expect as input files with no 70 | frontmatter header. Generally speaking, this means that metalsmith-matters 71 | should be the first plugin in the list. 72 | 73 | ```javascript 74 | var frontmatter = require('metalsmith-matters'); 75 | 76 | Metalsmith(__dirname) 77 | .frontmatter(false) // Disable built-in frontmatter parsing (recommended) 78 | .use(frontmatter({ 79 | // Options 80 | })) 81 | .use(/* Other plugins... */) 82 | .build(function(err) { 83 | if (err) throw err; 84 | }); 85 | ``` 86 | 87 | ## Options 88 | 89 | ### `namespace` 90 | 91 | Type: `string` 92 | Default: `undefined` 93 | 94 | Namespace all parsed data under a single key. 95 | 96 | #### Example 97 | 98 | Your frontmatter: 99 | ```` markdown 100 | --- 101 | title: My title 102 | author: Joe Writer 103 | --- 104 | ```` 105 | 106 | In the JavaScript: 107 | ```` javascript 108 | // new Metalsmith… 109 | .use(frontmatter({ namespace: 'page' })) 110 | // …build() 111 | ```` 112 | 113 | Now in your HTML, you can access the metadata like this (example with 114 | Handlebars): 115 | 116 | ```` html 117 |

{{ page.title }}

118 |

Author: {{ page.author }}

119 | ```` 120 | 121 | ### gray-matter Options 122 | 123 | metalsmith-matters also supports all the options supported by 124 | [`gray-matter`](https://github.com/jonschlinkert/gray-matter), 125 | metalsmith-matters' underlying frontmatter parsing library (it's the same one 126 | used by the Metalsmith core). These include: 127 | 128 | * `parser` 129 | * `eval` 130 | * `lang` 131 | * `delims` 132 | 133 | For details on how to use these options, see the 134 | [documentation for `gray-matter`](https://github.com/jonschlinkert/gray-matter#options). 135 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var matter = require('gray-matter'); 2 | var utf8 = require('is-utf8'); 3 | var each = require('metalsmith-each'); 4 | 5 | module.exports = frontmatter; 6 | 7 | // Not needed in Node 4.0+ 8 | if (!Object.assign) Object.assign = require('object-assign'); 9 | 10 | /** 11 | * Assign metadata in `file` based on the YAML frontmatter in `file.contents`. 12 | * 13 | * @param {Object} file The Metalsmith file object to extract frontmatter from 14 | * @param {string} filePath The path to the file represented by `file` 15 | * @param {Object} options Options for the extraction routine 16 | * @param {Object} grayMatterOptions Options for gray-matter 17 | */ 18 | 19 | function extractFrontmatter(file, filePath, options, grayMatterOptions){ 20 | if (utf8(file.contents)) { 21 | var parsed; 22 | 23 | try { 24 | parsed = matter(file.contents.toString(), grayMatterOptions); 25 | } catch (e) { 26 | var errMsg = 'Invalid frontmatter in file'; 27 | if (filePath !== undefined) errMsg += ": " + filePath; 28 | var err = new Error(errMsg); 29 | err.code = 'invalid_frontmatter'; 30 | err.cause = e; 31 | throw err; 32 | } 33 | 34 | if (options.hasOwnProperty('namespace')){ 35 | var nsData = {}; nsData[options.namespace] = parsed.data; 36 | Object.assign(file, nsData); 37 | } else { 38 | Object.assign(file, parsed.data); 39 | } 40 | file.contents = new Buffer(parsed.content); 41 | } 42 | } 43 | 44 | /** 45 | * Metalsmith plugin to extract metadata from frontmatter in file contents. 46 | * 47 | * @param {Object} options - `Namespace` is used, the rest is passed on to `gray-matter` 48 | * @return {Function} 49 | */ 50 | function frontmatter(opts){ 51 | var options = {}, grayMatterOptions = {}; 52 | Object.keys(opts || {}).forEach(function(key){ 53 | if (key === 'namespace'){ 54 | options[key] = opts[key]; 55 | } else { 56 | grayMatterOptions[key] = opts[key]; 57 | } 58 | }); 59 | 60 | return each(function(file, filePath){ 61 | extractFrontmatter(file, filePath, options, grayMatterOptions); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "metalsmith-matters", 3 | "version": "1.2.0", 4 | "description": "A Metalsmith plugin to read file metadata from frontmatter", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "make test", 8 | "lint": "jshint lib/ && jshint test/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Ajedi32/metalsmith-matters.git" 13 | }, 14 | "keywords": [ 15 | "metalsmith" 16 | ], 17 | "author": "Andrew Meyer", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/Ajedi32/metalsmith-matters/issues" 21 | }, 22 | "homepage": "https://github.com/Ajedi32/metalsmith-matters#readme", 23 | "dependencies": { 24 | "gray-matter": "^2.0.1", 25 | "is-utf8": "^0.2.0", 26 | "metalsmith-each": "^0.1.1", 27 | "object-assign": "^4.0.1" 28 | }, 29 | "devDependencies": { 30 | "jshint": "^2.9.2", 31 | "metalsmith": "^2.0.1", 32 | "mocha": "^2.3.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/fixtures/basic/src/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | someKey: value 3 | --- 4 | # Header 5 | 6 | Content 7 | -------------------------------------------------------------------------------- /test/fixtures/delimiters-option/src/test.md: -------------------------------------------------------------------------------- 1 | ~~~ 2 | someKey: value 3 | ~~~ 4 | # Header 5 | 6 | Content 7 | -------------------------------------------------------------------------------- /test/fixtures/eval-option/src/test.md: -------------------------------------------------------------------------------- 1 | ---javascript 2 | someKey: function() { return "value"; }() 3 | --- 4 | # Header 5 | 6 | Content 7 | -------------------------------------------------------------------------------- /test/fixtures/lang-option/src/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | someKey: function() { return "value"; }() 3 | --- 4 | # Header 5 | 6 | Content 7 | -------------------------------------------------------------------------------- /test/fixtures/namespace-option/src/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | someKey: value 3 | --- 4 | # Header 5 | 6 | Content 7 | -------------------------------------------------------------------------------- /test/fixtures/parser-option/src/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | ({ 3 | someKey: function() { return "value"; }() 4 | }) 5 | --- 6 | # Header 7 | 8 | Content 9 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Metalsmith = require('metalsmith'); 3 | var frontmatter = require('../lib'); 4 | 5 | describe('metalsmith-matters', function(){ 6 | it('should add metadata based on the frontmatter in the file', function(done){ 7 | Metalsmith('test/fixtures/basic') 8 | .frontmatter(false) 9 | .use(frontmatter()) 10 | .build(function(err, files){ 11 | if (err) return done(err); 12 | assert.equal(files["test.md"].someKey, "value"); 13 | done(); 14 | }); 15 | }); 16 | 17 | it('should remove frontmatter from the file contents', function(done){ 18 | Metalsmith('test/fixtures/basic') 19 | .frontmatter(false) 20 | .use(frontmatter()) 21 | .build(function(err, files){ 22 | if (err) return done(err); 23 | assert.equal(files["test.md"].contents.toString(), "# Header\n\nContent\n"); 24 | done(); 25 | }); 26 | }); 27 | 28 | describe('options', function(){ 29 | describe('namespace', function(){ 30 | it('should namespace metadata', function(done){ 31 | Metalsmith('test/fixtures/namespace-option') 32 | .frontmatter(false) 33 | .use(frontmatter({ namespace: 'myNamespace' })) 34 | .build(function(err, files){ 35 | if (err) return done(err); 36 | assert.equal(files["test.md"].myNamespace.someKey, "value"); 37 | assert.equal(files["test.md"].someKey, undefined); 38 | done(); 39 | }); 40 | }); 41 | }); 42 | 43 | // gray-matter options 44 | describe('delims', function(){ 45 | it('should set the delimiters used for frontmatter', function(done) { 46 | Metalsmith('test/fixtures/delimiters-option') 47 | .frontmatter(false) 48 | .use(frontmatter({delims: ['~~~', '~~~']})) 49 | .build(function(err, files){ 50 | if (err) return done(err); 51 | assert.equal(files["test.md"].someKey, "value"); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | describe('parser', function(){ 57 | it('should set the parser used for frontmatter', function(done) { 58 | Metalsmith('test/fixtures/parser-option') 59 | .frontmatter(false) 60 | /* jshint evil:true */ 61 | .use(frontmatter({parser: function(str) { return eval(str); }})) 62 | .build(function(err, files){ 63 | if (err) return done(err); 64 | assert.equal(files["test.md"].someKey, "value"); 65 | done(); 66 | }); 67 | }); 68 | }); 69 | describe('eval', function(){ 70 | describe('when true', function(){ 71 | it('should allow parsing executable frontmatter', function(done) { 72 | Metalsmith('test/fixtures/eval-option') 73 | .frontmatter(false) 74 | .use(frontmatter({eval: true})) 75 | .build(function(err, files){ 76 | if (err) return done(err); 77 | assert.equal(files["test.md"].someKey, "value"); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | describe('when false', function(){ 83 | it('should not allow parsing executable frontmatter', function(done) { 84 | Metalsmith('test/fixtures/eval-option') 85 | .frontmatter(false) 86 | .use(frontmatter({eval: false})) 87 | .build(function(err, files){ 88 | if (err) return done(err); 89 | assert.equal(files["test.md"].someKey, undefined); 90 | done(); 91 | }); 92 | }); 93 | }); 94 | }); 95 | describe('lang', function(){ 96 | it('should set the language used for frontmatter', function(done) { 97 | Metalsmith('test/fixtures/lang-option') 98 | .frontmatter(false) 99 | .use(frontmatter({lang: 'javascript', eval: true})) 100 | .build(function(err, files){ 101 | if (err) return done(err); 102 | assert.equal(files["test.md"].someKey, "value"); 103 | done(); 104 | }); 105 | }); 106 | }); 107 | }); 108 | }); 109 | --------------------------------------------------------------------------------