feat: Add rule checking Prism languages
The linter now checks that fences have languages and that those languages are supported by PrismJS. The linter has been extended over the guide with its own set of rules that only validate code fences.
This commit is contained in:
committed by
mrugesh
parent
b8bdbc7dc8
commit
9de68bd4a7
8
tools/lint-guide/.guidelintrc.js
Normal file
8
tools/lint-guide/.guidelintrc.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// JS rather than JSON so comments can be included
|
||||
|
||||
module.exports = {
|
||||
"default": false, // the guide has a more permissive set of rules
|
||||
"MD031": true, // fenced blocks do need surrounding by newlines
|
||||
"MD040": true, // code fence languages should be specificed
|
||||
"prism-langs": true, // code fence languages should be supported by PrismJS
|
||||
}
|
27
tools/lint-guide/gulpfile.js
Normal file
27
tools/lint-guide/gulpfile.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const gulp = require('gulp');
|
||||
const through2 = require('through2');
|
||||
|
||||
const { testedLangs } = require('../../curriculum/utils');
|
||||
const lintMarkdown = require('./lint-guide');
|
||||
|
||||
/**
|
||||
* Tasks
|
||||
**/
|
||||
|
||||
function lint() {
|
||||
return gulp.src(globLangs(testedLangs()), { read: false }).pipe(
|
||||
through2.obj(function obj(file, enc, next) {
|
||||
lintMarkdown(file, next);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper functions
|
||||
**/
|
||||
|
||||
function globLangs(langs) {
|
||||
return langs.map(lang => `../../guide/${lang}/**/*.md`);
|
||||
}
|
||||
|
||||
gulp.task('lint', lint);
|
12
tools/lint-guide/lint-guide.js
Normal file
12
tools/lint-guide/lint-guide.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const lintRules = require('./.guidelintrc');
|
||||
const linter = require('../linter');
|
||||
const argv = require('yargs').argv;
|
||||
|
||||
const isMDRE = /.*\.md$/;
|
||||
|
||||
const lint = linter(lintRules);
|
||||
|
||||
const files = argv._.filter(arg => isMDRE.test(arg));
|
||||
files.forEach(path => lint({ path: path }));
|
||||
|
||||
module.exports = lint;
|
11
tools/lint/.markdownlintrc.js
Normal file
11
tools/lint/.markdownlintrc.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// JS rather than JSON so comments can be included
|
||||
|
||||
module.exports = {
|
||||
"default": true, // include all rules, with exceptions below
|
||||
"MD002": false, // first heading should not be a top level heading
|
||||
"MD013": false, // lines can be any length
|
||||
"MD022": false, // headings don't need surrounding by newlines
|
||||
"MD031": true, // fenced blocks do need surrounding by newlines
|
||||
"MD033": false, // inline html is required
|
||||
"whitespace": false // extra whitespace is ignored, so we don't enforce it.
|
||||
}
|
38
tools/lint/fixtures/badFencing.md
Normal file
38
tools/lint/fixtures/badFencing.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
id: ''
|
||||
title: ''
|
||||
challengeType: 0
|
||||
videoUrl: ''
|
||||
---
|
||||
|
||||
## Description
|
||||
<section id='description'>
|
||||
</section>
|
||||
|
||||
## Instructions
|
||||
<section id='instructions'>
|
||||
</section>
|
||||
|
||||
## Tests
|
||||
<section id='tests'>
|
||||
```yml
|
||||
tests:
|
||||
- text: text
|
||||
testString: testString
|
||||
|
||||
```
|
||||
</section>
|
||||
|
||||
## Challenge Seed
|
||||
<section id='challengeSeed'>
|
||||
|
||||
<div id='html-seed'>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
## Solution
|
||||
<section id='solution'>
|
||||
</section>
|
40
tools/lint/fixtures/badYML.md
Normal file
40
tools/lint/fixtures/badYML.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: ''
|
||||
title: ''
|
||||
challengeType: 0
|
||||
videoUrl: ''
|
||||
---
|
||||
|
||||
## Description
|
||||
<section id='description'>
|
||||
</section>
|
||||
|
||||
## Instructions
|
||||
<section id='instructions'>
|
||||
</section>
|
||||
|
||||
## Tests
|
||||
<section id='tests'>
|
||||
|
||||
```yml
|
||||
tests:
|
||||
- text: text
|
||||
testString: testString
|
||||
|
||||
```
|
||||
|
||||
</section>
|
||||
|
||||
## Challenge Seed
|
||||
<section id='challengeSeed'>
|
||||
|
||||
<div id='html-seed'>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
## Solution
|
||||
<section id='solution'>
|
||||
</section>
|
40
tools/lint/fixtures/good.md
Normal file
40
tools/lint/fixtures/good.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
id: ''
|
||||
title: ''
|
||||
challengeType: 0
|
||||
videoUrl: ''
|
||||
---
|
||||
|
||||
## Description
|
||||
<section id='description'>
|
||||
</section>
|
||||
|
||||
## Instructions
|
||||
<section id='instructions'>
|
||||
</section>
|
||||
|
||||
## Tests
|
||||
<section id='tests'>
|
||||
|
||||
```yml
|
||||
tests:
|
||||
- text: text
|
||||
testString: testString
|
||||
|
||||
```
|
||||
|
||||
</section>
|
||||
|
||||
## Challenge Seed
|
||||
<section id='challengeSeed'>
|
||||
|
||||
<div id='html-seed'>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
## Solution
|
||||
<section id='solution'>
|
||||
</section>
|
12
tools/lint/lint.js
Normal file
12
tools/lint/lint.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const lintRules = require('./.markdownlintrc');
|
||||
const linter = require('../linter');
|
||||
const argv = require('yargs').argv;
|
||||
|
||||
const isMDRE = /.*\.md$/;
|
||||
|
||||
const lint = linter(lintRules);
|
||||
|
||||
const files = argv._.filter(arg => isMDRE.test(arg));
|
||||
files.forEach(path => lint({ path: path }));
|
||||
|
||||
module.exports = lint;
|
57
tools/lint/lint.test.js
Normal file
57
tools/lint/lint.test.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/* global describe it expect jest beforeEach */
|
||||
const path = require('path');
|
||||
|
||||
const lint = require('./lint');
|
||||
|
||||
describe('markdown linter', () => {
|
||||
let good = { path: path.join(__dirname, './fixtures/good.md') };
|
||||
let badYML = { path: path.join(__dirname, './fixtures/badYML.md') };
|
||||
let badFencing = { path: path.join(__dirname, './fixtures/badFencing.md') };
|
||||
beforeEach(() => {
|
||||
console.log = jest.fn();
|
||||
// the linter signals that a file failed by setting
|
||||
// exitCode to 1, so it needs (re)setting to 0
|
||||
process.exitCode = 0;
|
||||
});
|
||||
afterEach(() => {
|
||||
process.exitCode = 0;
|
||||
});
|
||||
|
||||
it('should pass `good` markdown', done => {
|
||||
function callback() {
|
||||
expect(process.exitCode).toBe(0);
|
||||
done();
|
||||
}
|
||||
lint(good, callback);
|
||||
});
|
||||
|
||||
it('should fail invalid YML blocks', done => {
|
||||
function callback() {
|
||||
expect(process.exitCode).toBe(1);
|
||||
done();
|
||||
}
|
||||
lint(badYML, callback);
|
||||
});
|
||||
|
||||
it('should fail when code fences are not surrounded by newlines', done => {
|
||||
function callback() {
|
||||
expect(process.exitCode).toBe(1);
|
||||
done();
|
||||
}
|
||||
lint(badFencing, callback);
|
||||
});
|
||||
|
||||
it('should write to the console describing the problem', done => {
|
||||
function callback() {
|
||||
const expected =
|
||||
// eslint-disable-next-line max-len
|
||||
'fixtures/badYML.md: 19: yaml-linter YAML code blocks must be valid [bad indentation of a mapping entry at line 3, column 17:\n testString: testString\n ^] [Context: "```yml"]';
|
||||
expect(console.log.mock.calls.length).toBe(1);
|
||||
expect(console.log.mock.calls[0][0]).toEqual(
|
||||
expect.stringContaining(expected)
|
||||
);
|
||||
done();
|
||||
}
|
||||
lint(badYML, callback);
|
||||
});
|
||||
});
|
25
tools/linter/index.js
Normal file
25
tools/linter/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const markdownlint = require('markdownlint');
|
||||
|
||||
const lintPrism = require('./markdown-prism');
|
||||
const lintYAML = require('./markdown-yaml');
|
||||
|
||||
function linter(rules) {
|
||||
const lint = (file, next) => {
|
||||
const options = {
|
||||
files: [file.path],
|
||||
config: rules,
|
||||
customRules: [lintYAML, lintPrism]
|
||||
};
|
||||
markdownlint(options, function callback(err, result) {
|
||||
const resultString = (result || '').toString();
|
||||
if (resultString) {
|
||||
process.exitCode = 1;
|
||||
console.log(resultString);
|
||||
}
|
||||
if (next) next(err, file);
|
||||
});
|
||||
};
|
||||
return lint;
|
||||
}
|
||||
|
||||
module.exports = linter;
|
44
tools/linter/markdown-prism.js
Normal file
44
tools/linter/markdown-prism.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const components = require(`prismjs/components`);
|
||||
|
||||
module.exports = {
|
||||
names: ['prism-langs'],
|
||||
description: 'Code block languages should be supported by PrismJS',
|
||||
tags: ['prism'],
|
||||
function: function rule(params, onError) {
|
||||
params.tokens
|
||||
.filter(param => param.type === 'fence')
|
||||
.forEach(codeBlock => {
|
||||
// whitespace around the language is ignored by the parser, as is case:
|
||||
const baseLang = codeBlock.info.trim().toLowerCase();
|
||||
const lang = getBaseLanguageName(baseLang);
|
||||
// Rule MD040 checks if the block has a language, so this rule only
|
||||
// comes into play if a language has been specified.
|
||||
if (baseLang && !lang) {
|
||||
onError({
|
||||
lineNumber: codeBlock.lineNumber,
|
||||
detail: `\'${baseLang}\' is not recognised.`
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* This is the method used by https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-remark-prismjs/src/load-prism-language.js
|
||||
*/
|
||||
|
||||
// Get the real name of a language given it or an alias
|
||||
const getBaseLanguageName = nameOrAlias => {
|
||||
if (components.languages[nameOrAlias]) {
|
||||
return nameOrAlias;
|
||||
}
|
||||
return Object.keys(components.languages).find(language => {
|
||||
const { alias } = components.languages[language];
|
||||
if (!alias) return false;
|
||||
if (Array.isArray(alias)) {
|
||||
return alias.includes(nameOrAlias);
|
||||
} else {
|
||||
return alias === nameOrAlias;
|
||||
}
|
||||
});
|
||||
};
|
24
tools/linter/markdown-yaml.js
Normal file
24
tools/linter/markdown-yaml.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
module.exports = {
|
||||
names: ['yaml-linter'],
|
||||
description: 'YAML code blocks should be valid',
|
||||
tags: ['yaml'],
|
||||
function: function rule(params, onError) {
|
||||
params.tokens
|
||||
.filter(param => param.type === 'fence')
|
||||
.filter(param => param.info === 'yml' || param.info === 'yaml')
|
||||
// TODO since the parser only looks for yml, should we reject yaml blocks?
|
||||
.forEach(codeBlock => {
|
||||
try {
|
||||
yaml.safeLoad(codeBlock.content);
|
||||
} catch (e) {
|
||||
onError({
|
||||
lineNumber: codeBlock.lineNumber,
|
||||
detail: e.message,
|
||||
context: codeBlock.line
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user