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:
Oliver Eyton-Williams
2019-06-21 15:22:14 +02:00
committed by mrugesh
parent b8bdbc7dc8
commit 9de68bd4a7
18 changed files with 1444 additions and 750 deletions

View 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
}

View 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);

View 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;

View 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.
}

View 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>

View 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>

View 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
View 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
View 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
View 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;

View 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;
}
});
};

View 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
});
}
});
}
};