Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
charset = "utf-8"
end_of_line = lf
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"arrowParens": "avoid",
"trailingComma": "all"
}
65 changes: 53 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@

Adds meta data about a Markdown file to a Markdown file, formatted as [Front Matter](https://jekyllrb.com/docs/frontmatter/).

The following meta data is added:

- `lastModifiedAt` using one of the following heuristics:
1. the `vFile` has the property `data.lastModifiedAt` defined
1. if [`git`](https://git-scm.com/) exists, the commit time of the file
1. the `mtime` reported by Node's `stat` method.

## Installation

```sh
Expand Down Expand Up @@ -47,11 +40,38 @@ var example = vfile.readSync('example.md');

remark()
.use(frontmatter)
.use(metadata, { git: true })
.use(metadata, {
gitExcludeCommit: 'chore:',
metadata: {
// string
tag: 'remark-metadata',
// constant
gitCreated: metadata.GIT_CREATED_TIME,
gitUpdated: metadata.GIT_LAST_MODIFIED_TIME,
updated: metadata.LAST_MODIFIED_TIME,
// function
duration({ gitCreatedTime, modifiedTime }) {
return (
new Date(modifiedTime).getTime() - new Date(gitCreatedTime).getTime()
);
},
// object
title: {
value({ vFile }) {
return vFile.basename;
},
shouldUpdate(newValue, oldValue) {
if (oldValue === 'Example') {
return true;
}
return false;
},
},
},
})
.process(example, function (err, file) {
if (err) throw err;
console.log(String(file))
})
console.log(String(file));
});
```

Expand All @@ -61,7 +81,6 @@ This will output the following Markdown:
---
title: Example
lastModifiedDate: 'Tue, 28 Nov 2017 02:44:25 GMT'

---

# Example
Expand All @@ -75,4 +94,26 @@ If a file has no Front Matter, it will be added by this plugin.

The plugin has the following options:

- `git`: Enables determining modification dates using git (defaults: `true`)
- `gitExcludeCommit`: A regexp string to exclude commits when get the last modified time through git. This is useful to exclude commits of chore.
- `metadata`: An object describe each metadata.
- `metadata[key]`: A string value, or a constant, or a function that will be called with an object parameter and using the string it returns as the value, or an object contain options below.

```js
// the object parameter of function:
{
gitModifiedTime,
gitCreatedTime,
modifiedTime,
vFile,
oldFrontMatter,
}
```

- `metadata[key].value`: A string value, or a constant, or a function, like `metadata[key]`.
- `metadata[key].shouldUpdate`: A function will be called with old and new value of this metadata. The metadata will update, if this function return truthy.

## Constants

- `GIT_CREATED_TIME`: A value of metadata to set the created time of a markdown file. Will use the first commit time of Git.
- `GIT_LAST_MODIFIED_TIME`: A value of metadata to set the last modified time of a markdown file. Will use the last commit time of Git.
- `LAST_MODIFIED_TIME`: A value of metadata to set the last modified time of a markdown file. Will use the mtime of file.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
"vfile": "^2.2.0"
},
"scripts": {
"lint": "$(npm bin)/eslint src",
"lint": "eslint src",
"teardown": "rm -rf test/runtime/*",
"test": "npm run teardown && $(npm bin)/jest",
"test:coverage": "npm run teardown && $(npm bin)/jest --coverage --coverageReporters text text-summary",
"test": "npm run teardown && jest",
"test:coverage": "npm run teardown && jest --coverage --coverageReporters text text-summary",
"preversion": "npm run lint && npm run test",
"version": "version-changelog CHANGELOG.md && changelog-verify CHANGELOG.md && git add CHANGELOG.md"
},
Expand Down
186 changes: 151 additions & 35 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ function getMatter(ast) {
*
* @param {Object} frontmatterNode a MDAST-like node for Frontmatter.
* @param {Object} meta
* @param {Object} metadataConfig
*/
function writeMatter(frontmatterNode, meta) {
function writeMatter(frontmatterNode, meta, metadataConfig) {
const fm = {};

// parse any existing frontmatter
Expand All @@ -53,57 +54,157 @@ function writeMatter(frontmatterNode, meta) {
}

// merge in meta
Object.assign(fm, meta);
Object.entries(meta).forEach(([name, value]) => {
const { shouldUpdate } = metadataConfig[name];

if (typeof shouldUpdate !== 'function' || shouldUpdate(value, fm[name])) {
fm[name] = value;
}
});

// stringify
frontmatterNode.value = jsYaml.safeDump(fm).trim(); // eslint-disable-line no-param-reassign
}

/**
* Given the vFile, returns an object containing possible meta data:
* Get the modified time of a vFile.
*
* If git option is true return the last commit time of this vFile,
* otherwise return the mtime.
*
* @param {vFile} vFile
* @param {Object} options {git, gitLogGrep}
* @return {string}
*/
function getModifiedTime(vFile, { git, gitLogGrep }) {
if (git) {
const cmd = `git log -1 --format="%ad" ${gitLogGrep} -- "${vFile.path}"`;
let modified;
try {
modified = execSync(cmd, { encoding: 'utf-8' }).trim();
} catch (error) {
vFile.message(error, null, PLUGIN_NAME);
}

// New files that aren't committed yet will return nothing
if (modified) {
return new Date(modified).toUTCString();
}

return '';
}

try {
const stats = fs.statSync(vFile.path);
return new Date(stats.mtime).toUTCString();
} catch (error) /* istanbul ignore next */ {
vFile.message(error, null, PLUGIN_NAME);
}

return '';
}

/**
* Get the created (first commit) time of a vFile.
*
* @param {vFile} vFile
* @return {string}
*/
function getCreatedTime(vFile) {
const cmd = `git log --reverse --format="%ad" -- "${vFile.path}"`;
let created;
try {
[created] = execSync(cmd, { encoding: 'utf-8' }).trim().split('\n');
} catch (error) {
vFile.message(error, null, PLUGIN_NAME);
}

// New files that aren't committed yet will return nothing
if (created) {
return new Date(created).toUTCString();
}

return '';
}

/**
* Get the parameter of value function of metadata.
*
* - lastModifiedAt
* @param {Object} data {vFile, oldFrontMatter, gitLogGrep}
* @return {Object}
*/
function createValueFunctionParam({ vFile, oldFrontMatter, gitLogGrep }) {
const param = {
vFile,
oldFrontMatter,
};

Object.defineProperties(param, {
gitModifiedTime: {
get() {
return getModifiedTime(vFile, { git: true, gitLogGrep });
},
},
modifiedTime: {
get() {
return getModifiedTime(vFile, { git: false, gitLogGrep });
},
},
gitCreatedTime: {
get() {
return getCreatedTime(vFile);
},
},
});

return param;
}

/**
* Given the vFile, metadata returns an object containing possible meta data
*
* @todo extract this out, there may be other metadata to add and we don't want
* a mega function to handle it all.
* @param {vFile} vFile
* @param {boolean} hasGit
* @param {Object} options {gitLogGrep, oldFrontMatter}
* @param {Object} metadata
* @return {Object}
*/
function getMetadata(vFile, hasGit) {
function getMetadata(vFile, { gitLogGrep, oldFrontMatter }, metadataConfig) {
/* eslint-disable no-use-before-define */
const meta = {};
let isSet = false;

// Does the vFile already contain a date?
if (vFile.data && vFile.data.lastModifiedAt) {
meta.lastModifiedAt = vFile.data.lastModifiedAt;
isSet = true;
}
Object.entries(metadataConfig).forEach(([name, config]) => {
const value =
typeof config === 'string' || typeof config === 'function'
? config
: config.value;

// Do we have Git? We can get it way?
if (!isSet && hasGit) {
const cmd = `git log -1 --format="%ad" -- ${vFile.path}`;
const modified = execSync(cmd, { encoding: 'utf-8' }).trim();
if (value === metadata.GIT_LAST_MODIFIED_TIME) {
meta[name] = getModifiedTime(vFile, { git: true, gitLogGrep });
return;
}

// New files that aren't committed yet will return nothing
if (modified) {
meta.lastModifiedAt = new Date(modified).toUTCString();
isSet = true;
if (value === metadata.LAST_MODIFIED_TIME) {
meta[name] = getModifiedTime(vFile, { git: false, gitLogGrep });
return;
}
}

// Otherwise fallback to using the file's `mtime`.
if (!isSet) {
try {
const stats = fs.statSync(vFile.path);
meta.lastModifiedAt = new Date(stats.mtime).toUTCString();
isSet = true;
} catch (error) {
vFile.message(error, null, PLUGIN_NAME);
if (value === metadata.GIT_CREATED_TIME) {
meta[name] = getCreatedTime(vFile);
return;
}

if (typeof value === 'string') {
meta[name] = value;
return;
}
}

if (typeof value === 'function') {
meta[name] = value(createValueFunctionParam({ vFile, oldFrontMatter, gitLogGrep }));
}
});

return meta;
/* eslint-enable no-use-before-define */
}

/**
Expand All @@ -115,7 +216,11 @@ function getMetadata(vFile, hasGit) {
* @return {function}
*/
function metadata(options = {}) {
const hasGit = options.git !== undefined ? options.git : true;
const gitLogGrep =
typeof options.gitExcludeCommit === 'string'
? ` --grep="${options.gitExcludeCommit}" --invert-grep `
: '';
const metadataConfig = options.metadata || {};

/**
* @param {object} ast MDAST
Expand All @@ -128,10 +233,17 @@ function metadata(options = {}) {
const frontmatterNode = getMatter(ast);

// Get metadata
const meta = getMetadata(vFile, hasGit);
const meta = getMetadata(
vFile,
{
gitLogGrep,
oldFrontMatter: jsYaml.safeLoad(frontmatterNode.value),
},
metadataConfig,
);

// Write metadata (by reference)
writeMatter(frontmatterNode, meta);
writeMatter(frontmatterNode, meta, metadataConfig);

// If we don't have a Matter node in the AST, put it in.
if (!getMatterNode(ast)) {
Expand All @@ -146,4 +258,8 @@ function metadata(options = {}) {
};
}

metadata.GIT_LAST_MODIFIED_TIME = 'REMARK_METADATA_GIT_LAST_MODIFIED_TIME';
metadata.GIT_CREATED_TIME = 'REMARK_METADATA_GIT_CREATED_TIME';
metadata.LAST_MODIFIED_TIME = 'REMARK_METADATA_LAST_MODIFIED_TIME';

module.exports = metadata;
1 change: 1 addition & 0 deletions test/fixtures/existing.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: Lorem ipsum
author: Joe Blogs
lastModifiedAt: 'Thu, 22 Oct 2020 06:47:56 GMT'
---

# front matter
Expand Down
Empty file.
Loading