Add documentation for new metadata
This commit is contained in:
parent
95830e0750
commit
585e4f4d74
|
|
@ -1,41 +1,36 @@
|
|||
var pi = require('pipe-iterators'),
|
||||
xtend = require('xtend');
|
||||
|
||||
// a function similar to python's update
|
||||
// no need to copy as first dict always starts from a fresh one
|
||||
// also allow an undefined d2
|
||||
dictUpdate = function(d1, d2) {
|
||||
if (d2)
|
||||
for (var k in d2)
|
||||
d1[k] = d2[k]
|
||||
}
|
||||
|
||||
xtend = require('xtend'),
|
||||
path = require('path');
|
||||
|
||||
module.exports = function(meta) {
|
||||
|
||||
return pi.map(function(item) {
|
||||
var newItem = {};
|
||||
var path = item.relative.split("/");
|
||||
var l = path.length;
|
||||
// split by path.sep for Windows compatibility
|
||||
var parts = item.relative.split(path.sep).filter(Boolean);
|
||||
// we have exactly as many directories to consider
|
||||
// as there are elements in path
|
||||
for (var i=0; i<l; i++) {
|
||||
var subpath = path.slice(0,i).join("/");
|
||||
// add a trailing '/' for all but the first one
|
||||
if (subpath) subpath += "/";
|
||||
// as there are elements in parts
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var subpath = parts.slice(0, i).join('/');
|
||||
// add a trailing '/' for all but the first one, which is just '*'
|
||||
if (i > 0) {
|
||||
subpath += '/';
|
||||
}
|
||||
// each of these corresponds to a directory
|
||||
subpath += "*";
|
||||
//console.log("index i=" + i + " subpath=" + subpath);
|
||||
dictUpdate(newItem, meta[subpath]);
|
||||
subpath += '*';
|
||||
// console.log('index i=' + i + ' subpath=' + subpath);
|
||||
if (typeof meta[subpath] === 'object') {
|
||||
newItem = xtend(newItem, meta[subpath]);
|
||||
}
|
||||
}
|
||||
// as far as the file itself, we remove trailing .md
|
||||
var subpath = item.relative.replace(/.md$/, '');
|
||||
//console.log("last=" + subpath);
|
||||
dictUpdate(newItem, meta[subpath]);
|
||||
newItem = xtend(newItem, meta[subpath]);
|
||||
|
||||
// finally inject the data from the file itself
|
||||
dictUpdate(newItem, item)
|
||||
// set title from first heading if otherwise missing
|
||||
newItem = xtend(newItem, item);
|
||||
// set title from first heading if otherwise missing
|
||||
if (!newItem.title && newItem.headings && newItem.headings[0]) {
|
||||
newItem.title = newItem.headings[0].text;
|
||||
}
|
||||
|
|
|
|||
36
readme.md
36
readme.md
|
|
@ -6,6 +6,7 @@ Looking for something to generate a blog from Markdown files? Check out [ghost-r
|
|||
|
||||
## Features
|
||||
|
||||
- `v3.0` changes how the optional `meta.json` file works, adding support for setting per-directory and global metadata values (see the section further down).
|
||||
- `v2.4` adds better handling for when the same header text is used multiple times in the same file.
|
||||
- `v2.3` adds one new feature: header hover anchor links. When you hover over a header, a hover anchor link appears to the side of the header. Clicking on that link or copying its URL produces a link to that specific location on the page. All built-in layouts support this feature by default.
|
||||
- `v2.2` added Windows support (!)
|
||||
|
|
@ -261,7 +262,7 @@ Note the usage of the "triple-stash", e.g. `{{{` here. The technical reason for
|
|||
</ul>
|
||||
```
|
||||
|
||||
### `meta.json`
|
||||
### `meta.json` (new behavior in 3.x)
|
||||
|
||||
If you want to apply additional metadata to all Markdown files in a particular folder, you can add a file named `meta.json` to the root of the input folder.
|
||||
|
||||
|
|
@ -269,32 +270,49 @@ For example, if you run `generate-md --input foo`, the `meta.json` file should b
|
|||
|
||||
(Note: in `v1.x`, `meta.json` was read from `process.cwd()`, e.g. the folder from which you ran `generate-md`).
|
||||
|
||||
`meta.json` will be read, and the values in it are added to the regular metadata. The values from `meta.json` act like default values, e.g. the per-file metadata section values will override the values from `meta.json`.
|
||||
Metadata handling has changed in v3.0.0. The metadata is now applied by sequentially merging keys which represent paths. This allows you to set default values for all of the files and then override those values for each subdirectory in `meta.json`
|
||||
|
||||
The keys in `meta.json` should refer to the file or folder names relative to the `--input`. See the bottom of the section for more examples.
|
||||
The keys in meta.json represent file paths relative to the root of the input directory. Each file will be rendered with the merged metadata. The merge proceeds as follows:
|
||||
|
||||
- Start with an empty object
|
||||
- Read the `*` key in `meta.json`
|
||||
- Take split the pathname of the current file relative to the input directory by the path separator (`/` in Linux/OSX and `\\` in Windows; note that the key lookup will always use `/` on all platforms). For example, if the filename is `./input/a/b/c.md` and the input directory is `./input`, then the path components would be `a`, `b`.
|
||||
- Concatenate the components one by one and look for keys that end with the concatenated path + `/*`. For example, for `./input/a/b/c.md`, the keys will be `a/*`, `a/b/*`.
|
||||
- Merge the metadata values from the keys in order of specificity, e.g. starting with the values under the `*` key, then `a/*`, then `a/b/*`.
|
||||
- Look for a key that matches the full relative file name without the extension. e.g. `a/b/c`, and merge that in.
|
||||
- Read the file, and overwrite the metadata values with the values set in the file.
|
||||
- Finally, if the title property is still not set, automatically set using the first heading in the markdown file.
|
||||
|
||||
For example, a `./input/meta.json` file like this:
|
||||
|
||||
````json
|
||||
{
|
||||
"foo": {
|
||||
"repoUrl": "https://github.com/mixu/markdown-styles"
|
||||
"*": {
|
||||
"repoUrl": "DEFAULT"
|
||||
}
|
||||
"foo/*": {
|
||||
"repoUrl": "MORE SPECIFIC"
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
would make the metadata value `{{repoUrl}}` available in the template, for all files that are in the directory `./input/foo`. If any markdown file in `./input/foo/` defines a metadata value called `repoUrl`, then that value will override the value from `meta.json`.
|
||||
would make the metadata value `{{repoUrl}}` available in the template for all input files to `DEFAULT` except for input files in `./input/foo/`. For `./input/foo` and all subdirectories, `repoUrl` would be set to `MORE SPECIFIC`.
|
||||
|
||||
If any markdown file in `./input/foo/` defines a metadata value called `repoUrl`, then that value will override the value from `meta.json`.
|
||||
|
||||
Here are a couple of additional examples:
|
||||
|
||||
| meta.json content | `{{key}}` is available in: |
|
||||
|-----------------------------------|-----------------------------------------
|
||||
| `{ "foo": {"key": "value" }}` | `./input/foo.md`, `./input/foo/*`
|
||||
| `{ "abc/bar": {"key": "value" }}` | `./input/abc/bar/*`
|
||||
| `{ "*": {"key": "value" }}` | all input files
|
||||
| `{ "foo": {"key": "value" }}` | `./input/foo.md`
|
||||
| `{ "foo/*": {"key": "value" }}` | `./input/foo/*` and subdirs
|
||||
| `{ "foo/bar": {"key": "value" }}` | `./input/foo/bar.md`
|
||||
| `{ "foo/bar/*": {"key": "value" }}` | `./input/foo/bar/*` and subdirs
|
||||
|
||||
### API
|
||||
|
||||
It exists, and uses the same options as `generate-md`. Docs TODO, see `bin/generate-md` and `test/api.test.js` for now.
|
||||
It exists, and uses the same options as `generate-md`. Docs TODO, see `bin/generate-md` and `test/api.test.gjs` for now.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
|
|
|
|||
|
|
@ -71,17 +71,21 @@ describe('integration tests', function() {
|
|||
it('reads and scopes the meta.json based on the path relative to target directory', function(done) {
|
||||
var dir = fixture.dir({
|
||||
'meta.json': JSON.stringify({
|
||||
foo: { pn: 'value-from-key-foo' },
|
||||
'abc/bar': { pn: 'value-from-key-abc/bar' }
|
||||
'*': { cascade: 'value-from-*' },
|
||||
foo: { cascade: 'value-from-key-foo' },
|
||||
'foo/*': { cascade: 'value-from-key-foo-*' },
|
||||
'abc/bar': { cascade: 'value-from-key-abc/bar' },
|
||||
'abc/bar/baz/*': { cascade: 'value-from-key-abc-bar-baz-*' }
|
||||
}),
|
||||
'foo.md': 'pn: aaa\nbase: keep\n---\nfoo.md', // projectName foo
|
||||
'foo/bar.md': 'pn: bbb\n---\nfoo/bar.md', // projectName foo
|
||||
'abc/bar.md': 'pn: ccc\n---\nabc/bar.md', // projectName abc
|
||||
'abc/bar/baz.md': 'pn: ddd\n---\nabc/bar/baz.md' // projectName abc/bar
|
||||
'foo.md': 'file: foo.md\nbase: keep\n---\nfoo.md', // components: *, foo
|
||||
'foo/bar.md': 'file: foo/bar.md\n---\nfoo/bar.md', // components: *, foo, bar
|
||||
'abc/bar.md': 'file: abc/bar.md\n---\nabc/bar.md', // components: *, abc, bar
|
||||
'abc/bar/baz.md': 'file: abc/bar/baz.md\n---\nabc/bar/baz.md', // components: *, abc, bar, baz
|
||||
'abc/bar/baz/foo.md': 'file: abc/bar/baz/foo.md\n---\nabc/bar/baz.md' // components: *, abc, bar, baz, foo
|
||||
});
|
||||
|
||||
var layoutDir = fixture.dir({
|
||||
'page.html': '"{{pn}}","{{base}}"\n{{> content}}'
|
||||
'page.html': '"{{file}}","{{cascade}}","{{base}}"\n{{> content}}'
|
||||
});
|
||||
var out = fixture.dirname();
|
||||
|
||||
|
|
@ -91,19 +95,23 @@ describe('integration tests', function() {
|
|||
layout: layoutDir
|
||||
}, function() {
|
||||
assert.equal(fs.readFileSync(out + '/foo.html', 'utf8'), [
|
||||
'"value-from-key-foo","keep"',
|
||||
'"foo.md","value-from-key-foo","keep"',
|
||||
'<p>foo.md</p>\n'
|
||||
].join('\n'));
|
||||
assert.equal(fs.readFileSync(out + '/foo/bar.html', 'utf8'), [
|
||||
'"value-from-key-foo",""',
|
||||
'"foo/bar.md","value-from-key-foo-*",""',
|
||||
'<p>foo/bar.md</p>\n'
|
||||
].join('\n'));
|
||||
assert.equal(fs.readFileSync(out + '/abc/bar.html', 'utf8'), [
|
||||
'"ccc",""',
|
||||
'"abc/bar.md","value-from-key-abc/bar",""',
|
||||
'<p>abc/bar.md</p>\n'
|
||||
].join('\n'));
|
||||
assert.equal(fs.readFileSync(out + '/abc/bar/baz.html', 'utf8'), [
|
||||
'"value-from-key-abc/bar",""',
|
||||
'"abc/bar/baz.md","value-from-*",""',
|
||||
'<p>abc/bar/baz.md</p>\n'
|
||||
].join('\n'));
|
||||
assert.equal(fs.readFileSync(out + '/abc/bar/baz/foo.html', 'utf8'), [
|
||||
'"abc/bar/baz/foo.md","value-from-key-abc-bar-baz-*",""',
|
||||
'<p>abc/bar/baz.md</p>\n'
|
||||
].join('\n'));
|
||||
done();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
var assert = require('assert'),
|
||||
pi = require('pipe-iterators'),
|
||||
mergeMeta = require('../lib/merge-meta');
|
||||
|
||||
describe('merge meta', function() {
|
||||
|
||||
/*
|
||||
|
||||
The keys in meta.json represent file paths relative to the root of the input directory.
|
||||
Each file will be rendered with the merged metadata. The merge proceeds as follows:
|
||||
|
||||
- Start with an empty object
|
||||
- Read the `*` key in `meta.json`
|
||||
- Take split the pathname of the current file relative to the input directory
|
||||
by the path separator (`/` in Linux/OSX and `\\` in Windows;
|
||||
note that the key lookup will always use `/` on all platforms).
|
||||
For example, if the filename is `./input/a/b/c.md` and the input directory is `./input`,
|
||||
then the path components would be `a`, `b`.
|
||||
- Concatenate the components one by one and look for keys that end with
|
||||
the concatenated path + `/*`. For example, for `./input/a/b/c.md`,
|
||||
the keys will be `a/*`, `a/b/*`.
|
||||
- Merge the metadata values from the keys in order of specificity,
|
||||
e.g. starting with the values under the `*` key, then `a/*`, then `a/b/*`.
|
||||
- Look for a key that matches the full relative file name without the extension.
|
||||
e.g. `a/b/c`, and merge that in.
|
||||
- Read the file, and overwrite the metadata values with the values set in the file.
|
||||
- Finally, if the title property is still not set, automatically set using the
|
||||
first heading in the markdown file.
|
||||
*/
|
||||
|
||||
it('reads the * key', function(done) {
|
||||
pi.fromArray([{ relative: '/a/b/c.md' }])
|
||||
.pipe(mergeMeta({
|
||||
'*': {
|
||||
'*': '*',
|
||||
a: '*'
|
||||
},
|
||||
}))
|
||||
.pipe(pi.toArray(function(results) {
|
||||
assert.deepEqual(results, [
|
||||
{
|
||||
relative: '/a/b/c.md',
|
||||
'*': '*',
|
||||
a: '*'
|
||||
}
|
||||
]);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
it('reads the * key and the a/* key and merges', function(done) {
|
||||
pi.fromArray([{ relative: '/a/b/c.md' }])
|
||||
.pipe(mergeMeta({
|
||||
'*': {
|
||||
'*': '*',
|
||||
a: '*'
|
||||
},
|
||||
'a/*': {
|
||||
a: 'a'
|
||||
},
|
||||
}))
|
||||
.pipe(pi.toArray(function(results) {
|
||||
assert.deepEqual(results, [
|
||||
{
|
||||
relative: '/a/b/c.md',
|
||||
'*': '*',
|
||||
a: 'a'
|
||||
}
|
||||
]);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
it('reads the keys *, a/*, a/b/* and merges', function(done) {
|
||||
pi.fromArray([{ relative: '/a/b/c.md' }])
|
||||
.pipe(mergeMeta({
|
||||
'*': {
|
||||
'*': '*',
|
||||
a: '*',
|
||||
b: '*'
|
||||
},
|
||||
'a/*': {
|
||||
a: 'a',
|
||||
b: 'a'
|
||||
},
|
||||
'a/b/*': {
|
||||
b: 'b'
|
||||
},
|
||||
}))
|
||||
.pipe(pi.toArray(function(results) {
|
||||
assert.deepEqual(results, [
|
||||
{
|
||||
relative: '/a/b/c.md',
|
||||
'*': '*',
|
||||
a: 'a',
|
||||
b: 'b'
|
||||
}
|
||||
]);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
it('reads the keys *, a/*, a/b/*, the file\'s metadata and merges', function(done) {
|
||||
pi.fromArray([{
|
||||
relative: '/a/b/c.md',
|
||||
file: 'file',
|
||||
}])
|
||||
.pipe(mergeMeta({
|
||||
'*': {
|
||||
'*': '*',
|
||||
a: '*',
|
||||
b: '*',
|
||||
file: '*'
|
||||
},
|
||||
'a/*': {
|
||||
a: 'a',
|
||||
b: 'a',
|
||||
file: 'a'
|
||||
},
|
||||
'a/b/*': {
|
||||
b: 'b',
|
||||
file: 'b'
|
||||
},
|
||||
}))
|
||||
.pipe(pi.toArray(function(results) {
|
||||
assert.deepEqual(results, [
|
||||
{
|
||||
relative: '/a/b/c.md',
|
||||
'*': '*',
|
||||
a: 'a',
|
||||
b: 'b',
|
||||
file: 'file'
|
||||
}
|
||||
]);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
it('reads all of the metadata keys above, and then auto-detects the title key from the headings', function(done) {
|
||||
pi.fromArray([{
|
||||
relative: '/a/b/c.md',
|
||||
file: 'file',
|
||||
headings: [ { text: 'heading-name' } ],
|
||||
}])
|
||||
.pipe(mergeMeta({
|
||||
'*': {
|
||||
'*': '*',
|
||||
a: '*',
|
||||
b: '*',
|
||||
file: '*'
|
||||
},
|
||||
'a/*': {
|
||||
a: 'a',
|
||||
b: 'a',
|
||||
file: 'a'
|
||||
},
|
||||
'a/b/*': {
|
||||
b: 'b',
|
||||
file: 'b'
|
||||
},
|
||||
}))
|
||||
.pipe(pi.toArray(function(results) {
|
||||
assert.deepEqual(results, [
|
||||
{
|
||||
relative: '/a/b/c.md',
|
||||
'*': '*',
|
||||
a: 'a',
|
||||
b: 'b',
|
||||
file: 'file',
|
||||
title: 'heading-name',
|
||||
headings: [ { text: 'heading-name' } ],
|
||||
}
|
||||
]);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
it('does not error out when one of the keys in the merge is not an object', function(done) {
|
||||
pi.fromArray([{ relative: '/a/b/c.md' }])
|
||||
.pipe(mergeMeta({
|
||||
'*': 'not an object',
|
||||
'a/*': true,
|
||||
'a/b/*': undefined,
|
||||
}))
|
||||
.pipe(pi.toArray(function(results) {
|
||||
assert.deepEqual(results, [
|
||||
{
|
||||
relative: '/a/b/c.md'
|
||||
}
|
||||
]);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ var assert = require('assert'),
|
|||
|
||||
describe('set output path', function() {
|
||||
|
||||
it('can set a basic output path', function(done) {
|
||||
it('can set a basic output path for files', function(done) {
|
||||
pi.fromArray([{ path: '/input/bar.md' }, { path: '/input/baz.md' }])
|
||||
.pipe(setOutputPath({
|
||||
input: '/input',
|
||||
|
|
@ -15,12 +15,12 @@ describe('set output path', function() {
|
|||
assert.deepEqual(results, [
|
||||
{
|
||||
path: '/output/bar.html',
|
||||
projectName: 'bar',
|
||||
relative: 'bar.md',
|
||||
assetsRelative: 'assets'
|
||||
},
|
||||
{
|
||||
path: '/output/baz.html',
|
||||
projectName: 'baz',
|
||||
relative: 'baz.md',
|
||||
assetsRelative: 'assets'
|
||||
}
|
||||
]);
|
||||
|
|
@ -28,5 +28,30 @@ describe('set output path', function() {
|
|||
}));
|
||||
});
|
||||
|
||||
it('can set a basic output path for subdirectories', function(done) {
|
||||
pi.fromArray([{ path: '/input/bar/baz.md' }, { path: '/input/a/b/c.md' }])
|
||||
.pipe(setOutputPath({
|
||||
input: '/input',
|
||||
output: '/output',
|
||||
assetDir: '/output/assets'
|
||||
}))
|
||||
.pipe(pi.toArray(function(results) {
|
||||
assert.deepEqual(results, [
|
||||
{
|
||||
path: '/output/bar/baz.html',
|
||||
relative: 'bar/baz.md',
|
||||
assetsRelative: '../assets'
|
||||
},
|
||||
{
|
||||
path: '/output/a/b/c.html',
|
||||
relative: 'a/b/c.md',
|
||||
assetsRelative: '../../assets'
|
||||
}
|
||||
]);
|
||||
done();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue