A markdown proofing platform for individuals, teams, and organizations.
Install into your project:
> npm install markdown-proofing
Now, create a .markdown-proofing
JSON file in the root of your project. Here's a simple one using a preset to get started:
{
"presets": ["technical-blog"]
}
Now, run it (may require global npm installation to run directly at command line):
> markdown-proofing ./file1.md
Next, you could wire it up in your package.json
as part of your build (or, perhaps as a lint step, you decide!).
This section is centered around Jekyll, but may work with other static site generators, too!
Jekyll itself doesn't use npm. But, markdown-proofing can still be used!
First, check if a package.json
file exists in the repository root. If one does, great! If not, no problem -- simply create one using npm init
.
After package.json
exists, run npm install markdown-proofing --save-dev
. This assumes you won't need this package available in your production environment.
Then, add or modify the package.json
test
script:
"scripts": {
"test": "markdown-proofing _posts/*.md"
},
Adjust the above as necessary if the posts live in a different place, or if you use a different file extension.
Now, use npm test
to run markdown-proofing on the posts!
Configuration is specified in JSON.
By default .markdown-proofing
located in the root of the project is used. You can optionally specify a different file using the -c
/ --configuration
flags, if you'd like.
The configuration can be as simple as:
{
"presets": ["technical-blog"]
}
Or, a bit more complex:
{
"presets": [],
"analyzers": [
"require-oxford-commas",
"sensitivity",
"sentiment",
"spelling",
"statistics",
"write-good"
],
"rules": {
"missing-oxford-commas": "error",
"sensitivity": "warning",
"sentiment-score": "info",
"sentiment-comparative-score": "info",
"spelling-error": "error",
"statistics-flesch-kincaid-grade-level": "info, warning > 12",
"statistics-flesch-kincaid-reading-ease": "warning <= 40",
"write-good": "info"
}
}
The SpellingAnalyzer
implements markdown-spellcheck. This package uses a .spelling
file for permitting unrecognized text. Only global words are currently supported, though (no file specific words).
markdown-spellcheck also includes an interactive CLI, which you can use to interactively fix spelling and update the .spelling
file as necessary. You may find this useful.
There are two core concepts: Analyzers and Rules.
- Analyzers process the text!
- Analyzers parse a markdown text string and return an
AnalyzerResult
, which includes a collection ofAnalyzerMessage
objects.AnalyzerMessage
's are{ type: String, text: String, line: Number, column: Number }
.
- There are useful analyzers built-in and ready for use. Custom analyzers are supported as well.
- Analyzers parse a markdown text string and return an
- Rules react to analyzers!
- These react to the output of the configured analyzers. Without any rules, the output of the analyzers is not surfaced to the user or applied in any way. So, you'll need some rules!
- Rules are in the format of
'{{message-type}}': '{{condition}}'
.- Example rule:
'statistics-word-count': 'info'
- Example rule:
'statistics-flesch-kincaid-reading-ease': 'warning < 40'
- Example rule:
- There are four types of rules:
info
,warning
,error
, andnone
.info
signals to add this to any output. It should show up in build results and any place where messages should be visible.warning
is a standard warning, it shouldn't fail a build.error
violations should result in a build failure.none
is used to override a preset.
- Rules can have an optional condition, which is applied as
warning < 40
-- it's awarning
only when the value is less than40
.- This is useful for statistics and other numerical outputs from analyzers.
You can use the following to integrate with Jekyll:
package.json
"scripts": {
"test": "markdown-proofing _posts/*.md",
"proof": "markdown-proofing --color --no-throw --",
"proof:all": "npm run test -- --no-throw",
"proof:latest": "node proofScripts.js latest _posts/*.md",
"proof:changed": "node proofScripts.js git-index-and-uncommitted _posts/*.md",
"proof:index": "node proofScripts.js git-index _posts/*.md",
"proof:uncommitted": "node proofScripts.js git-uncommitted _posts/*.md"
},
"devDependencies": {
"shelljs": "^0.7.0"
}
proofScripts.js
require('shelljs/global');
const arg = process.argv[2];
if (!arg) {
echo('Please specify a proofing instruction argument.');
exit(1);
}
const filePattern = process.argv[3];
if (!filePattern) {
echo('Please specify the file pattern as the last argument.');
exit(1);
}
if (arg === 'latest') {
const orderedPosts = ls('-l', filePattern)
.filter(x => x.name)
.map(x => x.name)
.sort((a, b) => b.localeCompare(a));
if (orderedPosts.length > 0) {
const latestPost = orderedPosts[0];
proof(latestPost);
}
}
else if (arg === 'git-index') {
gitProof(`git diff --name-only --cached ${filePattern}`);
}
else if (arg === 'git-uncommitted') {
gitProof(`git diff --name-only --diff-filter=ACMRT ${filePattern}`);
}
else if (arg === 'git-index-and-uncommitted') {
gitProof([
`git diff --name-only --diff-filter=ACMRT ${filePattern}`,
`git diff --name-only --cached ${filePattern}`
]);
}
function gitProof(gitDiffPatterns) {
if (!which('git')) {
echo('This script requires git.');
exit(1);
}
if (!Array.isArray(gitDiffPatterns)) {
gitDiffPatterns = [ gitDiffPatterns ];
}
const files = [];
gitDiffPatterns.forEach(x => {
const gitDiffOutput = exec(x, { silent: true });
if (gitDiffOutput.stderr) {
throw new Error(gitDiffOutput.stderr);
}
if (gitDiffOutput.stdout) {
files.push(gitDiffOutput.stdout);
}
});
if (files.length === 0) {
echo('No files to proof.');
exit(0);
}
proof(files);
}
function proof(files) {
if (!Array.isArray(files)) {
files = [ files ];
}
const cmd = `npm run proof ${files.join(' ')}`;
if (exec(cmd).code !== 0) {
echo(`Error while running: ${cmd}`);
exit(1);
}
}
import AnalyzerResult from 'markdown-proofing/analyzer-result'; // TODO: Change this import if this is not correct
export default class MyCustomAnalyzerAnalyzer {
analyze(str) {
const result = new AnalyzerResult();
// As part of the logic, optionally add one or more messages:
result.addMessage('my-custom-analyzer-message-type', 'some-value');
// The return value can be an `AnalyzerResult`
// or a promise to return an `AnalyzerResult`.
return result;
}
}
Then, simply wire it up in configuration as an analyzers
array item:
{
"presets": [
],
"analyzers": [
"path/to/custom-analyzer"
],
"rules": {
"custom-analyzer-message": "info"
}
}
Ritter Insurance Marketing
MIT
Contributions are highly welcome! However, before making large changes that may be outside the scope of this project, we may want to discuss it in an issue prior to opening a pull request.
If you construct an analyzer useful to you and/or your team/company and it could be useful for others, we'd appreciate a pull request to incorporate it into the project!